Files
pixa/internal/handlers/imageenc_token_internal_test.go
T
sneak fece3b69c9
check / check (push) Waiting to run
One image request type instead of five (closes #307)
The new package internal/imagerequest holds the request type, its two
parts Source and Transform, the Format and Fit types, the dimension and
fit checks, the /v1/image/ path parsing and the variant key; it imports
no other package of pixa. imgcache.ParsedURL, imgcache.ImageRequest,
signature.Request, imageprocessor.Request, the second Size, format and
fit types and the conversions between them are gone. The image
processor takes a Transform only. encurl keeps a private struct with the
token's CBOR field names, so tokens made before still decode. VariantKey
moves with the function that makes it. Tests follow the moved and
renamed code; the tests of the removed conversions go with them.

Model: opus-5-5
2026-10-21 12:27:29 +00:00

131 lines
3.4 KiB
Go

package handlers
import (
"image/jpeg"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"time"
"sneak.berlin/go/pixa/internal/encurl"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// 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 := imagerequest.Request{
Source: imagerequest.Source{
Host: signedHost,
Path: photoPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 50, Height: 50},
},
}
valid, err := h.encGen.Generate(&photo)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
expiredPhoto := photo
expiredPhoto.Expires = time.Now().Add(-time.Minute)
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)
}
})
}
}