check / check (push) Canceled after 0s
The image request now holds only its source and its transform, exactly what the variant key covers. The /v1/image/ handler checks the allowlist and verifies the signature itself with Signer.Verify(req, sig, expires). Both image handlers keep the URL's expiry as a local value for Cache-Control. The image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is gone. The fetcher takes the source and builds its URL with Source.URL from its own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat returns the format it chooses instead of writing into the request. The signed URL format and every route's answers are unchanged. Model: opus-5-5
128 lines
3.3 KiB
Go
128 lines
3.3 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, time.Time{})
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
expired, err := h.encGen.Generate(&photo, time.Now().Add(-time.Minute))
|
|
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, time.Time{})
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|