diff --git a/internal/handlers/imageenc_internal_test.go b/internal/handlers/imageenc_internal_test.go index be2db22..d4f8c6b 100644 --- a/internal/handlers/imageenc_internal_test.go +++ b/internal/handlers/imageenc_internal_test.go @@ -96,3 +96,70 @@ func TestHandleImageEnc_InvalidFitMode_Returns400(t *testing.T) { 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()) + } +} diff --git a/internal/server/routes_internal_test.go b/internal/server/routes_internal_test.go new file mode 100644 index 0000000..f072a3d --- /dev/null +++ b/internal/server/routes_internal_test.go @@ -0,0 +1,27 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// TestEncryptedImageRouteAnswersHEAD verifies that HEAD on the encrypted image +// route reaches its handler, as GET does, instead of being answered 405 Method +// Not Allowed. The handler refuses a token it cannot decrypt with 400, so that +// status shows the request got through. +func TestEncryptedImageRouteAnswersHEAD(t *testing.T) { + t.Parallel() + + s := newTestServer(t) + + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequestWithContext( + t.Context(), http.MethodHead, encryptedImagePath, nil)) + t.Logf("status %d", rec.Code) + + if rec.Code != http.StatusBadRequest { + t.Errorf("status = %d, want %d from the encrypted image handler", + rec.Code, http.StatusBadRequest) + } +}