package web import ( "html" "io" "log/slog" "net/http" "net/http/httptest" "net/url" "regexp" "strings" "testing" "gardomatic.kleiax.de/lib/client" "github.com/go-playground/form/v4" ) type webHandlerTransport struct { handler http.Handler } func (transport webHandlerTransport) RoundTrip(request *http.Request) (*http.Response, error) { recorder := httptest.NewRecorder() transport.handler.ServeHTTP(recorder, request) response := recorder.Result() response.Request = request return response, nil } func newAPIBackedTestApplication(t *testing.T, handler http.Handler) *application { t.Helper() templateCache, err := newTemplateCache() if err != nil { t.Fatal(err) } apiClient, err := client.New("https://api.example", client.WithHTTPClient(&http.Client{Transport: webHandlerTransport{handler: handler}})) if err != nil { t.Fatal(err) } return &application{ config: Config{SessionCookieName: "gardomatic_session", CookieSecure: false}, logger: slog.New(slog.NewTextHandler(io.Discard, nil)), templateCache: templateCache, formDecoder: form.NewDecoder(), apiClient: apiClient, } } func TestSettingsKeepsSelectedGarden(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/session": _, _ = w.Write([]byte(`{"user":{"id":7,"name":"Alice","email":"alice@example.com","activated":true,"permissions":["gardens:create"]}}`)) case "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodGet, "/settings?garden=3", nil) response := httptest.NewRecorder() app.routes().ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusOK, response.Body.String()) } body := response.Body.String() if !strings.Contains(body, "href='/g/3'>Hinterhof") || !strings.Contains(body, "href='/settings?garden=3'") { t.Fatalf("selected garden was not kept in settings: %s", body) } } func TestHealthAndPrivacyPagesArePublic(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/session": w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"you must be authenticated"}`)) case "/v1/healthcheck": _, _ = w.Write([]byte(`{"status":"available","server_time":"2026-09-11T10:00:00Z","system_info":{"environment":"test","version":"v1"}}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) for _, test := range []struct { path string want string }{ {"/healtcheck", "Serverzeit"}, {"/datenschutz", "Verarbeitete Daten"}, } { request := httptest.NewRequest(http.MethodGet, test.path, nil) response := httptest.NewRecorder() app.routes().ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), test.want) { t.Errorf("GET %s: status=%d, missing %q: %s", test.path, response.Code, test.want, response.Body.String()) } } } func TestPinboardRequestsOnlyPinboardEntries(t *testing.T) { requestedType := "" apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/session": _, _ = w.Write([]byte(`{"user":{"id":7,"name":"Alice","email":"alice@example.com","activated":true,"permissions":["gardens:create"]}}`)) case "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) case "/v1/gardens/3/journal": requestedType = r.URL.Query().Get("type") _, _ = w.Write([]byte(`{"journal_entries":[{"id":8,"garden_id":3,"entry_type":"pinboard","title":"Sitzecke","body":"Bank bauen"}]}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodGet, "/g/3/pinboard", nil) response := httptest.NewRecorder() app.routes().ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusOK, response.Body.String()) } if requestedType != client.JournalEntryTypePinboard { t.Fatalf("entry type: got %q, want %q", requestedType, client.JournalEntryTypePinboard) } if body := response.Body.String(); !strings.Contains(body, "Sitzecke") || !strings.Contains(body, "/g/3/pinboard/edit/8") { t.Fatalf("pinboard entry missing: %s", body) } } func TestGardenPageUsesIncomingSession(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/session": cookie, err := r.Cookie("gardomatic_session") if err != nil || cookie.Value != "browser-session" { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"you must be authenticated"}`)) return } http.SetCookie(w, &http.Cookie{Name: "gardomatic_session", Value: "browser-session", Path: "/", HttpOnly: true, MaxAge: 1800}) _, _ = w.Write([]byte(`{"user":{"id":7,"name":"Alice","email":"alice@example.com","activated":true,"permissions":["gardens:create"]}}`)) case "/v1/gardens": _, _ = w.Write([]byte(`{"gardens":[{"id":3,"name":"Hinterhof","description":"Gemüse"}]}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodGet, "/gardens", nil) request.AddCookie(&http.Cookie{Name: "gardomatic_session", Value: "browser-session"}) response := httptest.NewRecorder() app.routes().ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusOK, response.Body.String()) } var refreshedSession *http.Cookie for _, cookie := range response.Result().Cookies() { if cookie.Name == "gardomatic_session" { refreshedSession = cookie break } } if refreshedSession == nil || refreshedSession.Value != "browser-session" || refreshedSession.MaxAge != 1800 { t.Errorf("refreshed session cookie was not forwarded: got %+v", refreshedSession) } if body := response.Body.String(); !strings.Contains(body, "Hinterhof") || !strings.Contains(body, "Alice") { t.Errorf("page does not contain garden and user: %s", body) } body := response.Body.String() if !strings.Contains(body, `href='/gardens/new'`) { t.Errorf("page does not link to the garden creation page: %s", body) } for _, want := range []string{"class='user-menu'", `href='/gardens'`, `href='/account'`, ">Nutzer ", "Abmelden"} { if !strings.Contains(body, want) { t.Errorf("user menu does not contain %q: %s", want, body) } } if strings.Contains(body, `