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, `