package handlers import ( "image/jpeg" "log/slog" "net/http" "net/http/httptest" "testing" "time" "sneak.berlin/go/pixa/internal/encurl" ) // requireServedPhoto requires that rec answers 200 with the JPEG at photoPath // on signedHost at the 50x50 that encPhotoURL and the generator tests ask for. func requireServedPhoto(t *testing.T, rec *httptest.ResponseRecorder) { t.Helper() if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body %q", rec.Code, http.StatusOK, rec.Body.String()) } contentType := rec.Header().Get("Content-Type") if contentType != "image/jpeg" { t.Errorf("Content-Type = %q, want image/jpeg", contentType) } img, err := jpeg.DecodeConfig(rec.Body) if err != nil { t.Fatalf("body is not a JPEG: %v", err) } if img.Width != 50 || img.Height != 50 { t.Errorf("image is %dx%d, want 50x50", img.Width, img.Height) } } // TestHandleImageEnc_ValidToken_ServesImage verifies that a token made with // the signing key serves the image it asks for. func TestHandleImageEnc_ValidToken_ServesImage(t *testing.T) { t.Parallel() h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler)) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequestWithContext( t.Context(), http.MethodGet, encPhotoURL(t, h), nil)) requireServedPhoto(t, rec) } // TestHandleImageEnc_RejectedToken verifies that a token that has expired // answers 410, and that a token with one character changed, a token cut // short, and a token made with another signing key answer 400. The server // would serve the photo for a token it accepted. func TestHandleImageEnc_RejectedToken(t *testing.T) { t.Parallel() h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler)) photo := encurl.Payload{ SourceHost: signedHost, SourcePath: photoPath, Width: 50, Height: 50, } valid, err := h.encGen.Generate(&photo) if err != nil { t.Fatalf("Generate() error = %v", err) } expiredPhoto := photo expiredPhoto.ExpiresAt = time.Now().Add(-time.Minute).Unix() expired, err := h.encGen.Generate(&expiredPhoto) if err != nil { t.Fatalf("Generate() error = %v", err) } otherGen, err := encurl.NewGenerator("another-signing-key-fedcba9876543210") if err != nil { t.Fatalf("encurl.NewGenerator() error = %v", err) } otherKey, err := otherGen.Generate(&photo) if err != nil { t.Fatalf("Generate() error = %v", err) } // Changing a character in the middle always changes the decoded bytes; // the last character of unpadded base64 can carry unused bits. middle := len(valid) / 2 replacement := "A" if valid[middle] == 'A' { replacement = "B" } changed := valid[:middle] + replacement + valid[middle+1:] tests := []struct { name string token string wantStatus int }{ {"expired", expired, http.StatusGone}, {"one character changed", changed, http.StatusBadRequest}, {"cut short", valid[:middle], http.StatusBadRequest}, {"another signing key", otherKey, http.StatusBadRequest}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() rec := getEncToken(srv, tt.token) t.Logf("GET /v1/e/%s/img.jpg: %d %s", tt.token, rec.Code, rec.Body) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } }) } }