package handlers import ( "context" "log/slog" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/imgcache" ) // newEncTestServer builds a router serving the encrypted-URL route with a // generator seeded by the shared test signing key. The image service is left // nil: these tests exercise validation that rejects a token before any image // is fetched, so the handler must never reach the service. func newEncTestServer(t *testing.T) (*encurl.Generator, http.Handler) { t.Helper() encGen, err := encurl.NewGenerator(testSigningKey) if err != nil { t.Fatalf("encurl.NewGenerator() error = %v", err) } h := &Handlers{ log: slog.New(slog.DiscardHandler), encGen: encGen, } r := chi.NewRouter() r.Get("/v1/e/{token}/*", h.HandleImageEnc()) return encGen, r } // getEncToken issues a GET for the given token and returns the recorder. func getEncToken(srv http.Handler, token string) *httptest.ResponseRecorder { req := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/v1/e/"+token+"/img.jpg", nil) rec := httptest.NewRecorder() srv.ServeHTTP(rec, req) return rec } // TestHandleImageEnc_OverLimitDimension_Returns400 verifies that a decrypted // token requesting a dimension beyond MaxDimension is rejected with 400 // instead of reaching the image processor and libvips. func TestHandleImageEnc_OverLimitDimension_Returns400(t *testing.T) { t.Parallel() encGen, srv := newEncTestServer(t) token, err := encGen.Generate(&encurl.Payload{ SourceHost: "cdn.example.com", SourcePath: "/photo.jpg", Width: 100000, Height: 100000, }) if err != nil { t.Fatalf("Generate() error = %v", err) } rec := getEncToken(srv, token) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } } // TestHandleImageEnc_InvalidFitMode_Returns400 verifies that a decrypted token // carrying an unrecognized fit mode is rejected with 400 rather than surfacing // as a 500 from the image processor's default branch. func TestHandleImageEnc_InvalidFitMode_Returns400(t *testing.T) { t.Parallel() encGen, srv := newEncTestServer(t) token, err := encGen.Generate(&encurl.Payload{ SourceHost: "cdn.example.com", SourcePath: "/photo.jpg", Width: 800, Height: 600, FitMode: imgcache.FitMode("bogus"), }) if err != nil { t.Fatalf("Generate() error = %v", err) } rec := getEncToken(srv, token) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } } // TestHandleImageEnc_IfNoneMatch_Returns304 verifies that an image served // through an encrypted URL carries an ETag, and that a request whose // If-None-Match is that ETag is answered 304 Not Modified with no body. func TestHandleImageEnc_IfNoneMatch_Returns304(t *testing.T) { t.Parallel() h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler)) target := encPhotoURL(t, h) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequestWithContext( t.Context(), http.MethodGet, target, nil)) etag := rec.Header().Get("ETag") t.Logf("GET: %d, ETag %q", rec.Code, etag) if rec.Code != http.StatusOK || etag == "" { t.Fatalf("GET: status = %d, ETag = %q, want %d and an ETag", rec.Code, etag, http.StatusOK) } req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, target, nil) req.Header.Set("If-None-Match", etag) rec = httptest.NewRecorder() srv.ServeHTTP(rec, req) t.Logf("GET with If-None-Match: %d, %d body bytes", rec.Code, rec.Body.Len()) if rec.Code != http.StatusNotModified || rec.Body.Len() != 0 { t.Errorf("status = %d with %d body bytes, want %d with none", rec.Code, rec.Body.Len(), http.StatusNotModified) } } // TestHandleImageEnc_HEAD_ReturnsHeadersOnly verifies that HEAD on an // encrypted URL is answered 200 with the headers GET sends and no body. func TestHandleImageEnc_HEAD_ReturnsHeadersOnly(t *testing.T) { t.Parallel() h, _ := newSignedHostServer(t, slog.New(slog.DiscardHandler)) r := chi.NewRouter() r.Head("/v1/e/{token}/*", h.HandleImageEnc()) rec := httptest.NewRecorder() r.ServeHTTP(rec, httptest.NewRequestWithContext( t.Context(), http.MethodHead, encPhotoURL(t, h), nil)) t.Logf("HEAD: %d, headers %v, %d body bytes", rec.Code, rec.Header(), rec.Body.Len()) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } for _, name := range []string{ "Content-Type", "Content-Length", "Cache-Control", "ETag", } { if rec.Header().Get(name) == "" { t.Errorf("HEAD response has no %s", name) } } if rec.Body.Len() != 0 { t.Errorf("HEAD response body has %d bytes, want none", rec.Body.Len()) } }