package handlers import ( "log/slog" "net/http" "net/url" "strings" "testing" "github.com/go-chi/chi/v5" "sneak.berlin/go/pixa/internal/config" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/session" ) // Form field names and a throwaway source image URL for the body-limit // tests. const ( sourceURLField = "url" testSourceURL = "https://example.com/a.jpg" ) // newBodyLimitTestRouter mirrors the production wiring for the form POST // routes (see server.SetupRoutes): LimitBody sits in front of the CSRF // middleware, which sits in front of the handlers. maxBytes is the body // cap under test, so a test can trip the limit with a small body. func newBodyLimitTestRouter( t *testing.T, maxBytes int64, ) (*Handlers, http.Handler) { t.Helper() cfg := &config.Config{SigningKey: testSigningKey, Debug: true} sessMgr, err := session.NewManager(testSigningKey) if err != nil { t.Fatalf("session.NewManager() error = %v", err) } encGen, err := encurl.NewGenerator(testSigningKey) if err != nil { t.Fatalf("encurl.NewGenerator() error = %v", err) } protect, err := newCSRFProtect(testSigningKey, cfg.Debug) if err != nil { t.Fatalf("newCSRFProtect() error = %v", err) } h := &Handlers{ log: slog.New(slog.DiscardHandler), config: cfg, sessMgr: sessMgr, encGen: encGen, csrfProtect: protect, } r := chi.NewRouter() r.Group(func(r chi.Router) { r.Use(h.LimitBody(maxBytes)) r.Use(h.CSRF()) r.Get("/", h.HandleRoot()) r.Post("/", h.HandleRoot()) r.Post("/generate", h.HandleGenerateURL()) }) return h, r } // TestOversizedLoginPostRejectedBeforeCSRF is the core regression: an // oversized POST / carrying an otherwise valid CSRF cookie and token must // be rejected with 413. If the body limit ran after CSRF, the truncated // body would read as a missing token and return 403; if it ran after the // handler, a valid token would return 303. Getting 413 proves the limit // fires before CSRF parses the form. func TestOversizedLoginPostRejectedBeforeCSRF(t *testing.T) { t.Parallel() _, srv := newBodyLimitTestRouter(t, 16) cookies, token := csrfCredentials(t, srv, nil) rec := postForm(srv, "/", cookies, url.Values{ loginKeyField: {testSigningKey}, csrfTokenField: {token}, }) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("oversized POST / status = %d, want %d", rec.Code, http.StatusRequestEntityTooLarge) } } // TestOversizedGeneratePostRejectedBeforeCSRF is the same regression for // POST /generate, which also parses a form behind CSRF. func TestOversizedGeneratePostRejectedBeforeCSRF(t *testing.T) { t.Parallel() h, srv := newBodyLimitTestRouter(t, 16) sessionCookie := newSessionCookie(t, h) cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie}) cookies = append(cookies, sessionCookie) rec := postForm(srv, "/generate", cookies, url.Values{ sourceURLField: {testSourceURL}, csrfTokenField: {token}, }) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("oversized POST /generate status = %d, want %d", rec.Code, http.StatusRequestEntityTooLarge) } } // TestWithinLimitLoginPostSucceeds verifies the limit does not disturb a // normal request: under the production cap, a valid login still parses and // establishes a session (303). This guards against the body limit // consuming or corrupting the form the CSRF check and handler depend on. func TestWithinLimitLoginPostSucceeds(t *testing.T) { t.Parallel() _, srv := newBodyLimitTestRouter(t, MaxFormBytes) cookies, token := csrfCredentials(t, srv, nil) rec := postForm(srv, "/", cookies, url.Values{ loginKeyField: {testSigningKey}, csrfTokenField: {token}, }) if rec.Code != http.StatusSeeOther { t.Fatalf("within-limit POST / status = %d, want %d", rec.Code, http.StatusSeeOther) } var authed bool for _, c := range rec.Result().Cookies() { if c.Name == session.CookieName && c.Value != "" { authed = true } } if !authed { t.Error("within-limit valid login did not set a session cookie") } } // TestWithinLimitGeneratePostSucceeds is the same non-regression check for // POST /generate. func TestWithinLimitGeneratePostSucceeds(t *testing.T) { t.Parallel() h, srv := newBodyLimitTestRouter(t, MaxFormBytes) sessionCookie := newSessionCookie(t, h) cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie}) cookies = append(cookies, sessionCookie) rec := postForm(srv, "/generate", cookies, url.Values{ sourceURLField: {testSourceURL}, "format": {"jpeg"}, csrfTokenField: {token}, }) if rec.Code != http.StatusOK { t.Fatalf("within-limit POST /generate status = %d, want %d", rec.Code, http.StatusOK) } if !strings.Contains(rec.Body.String(), "/v1/e/") { t.Error("within-limit generate response did not contain a generated URL") } }