check / check (push) Canceled after 0s
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 short CBOR field names. 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
169 lines
4.6 KiB
Go
169 lines
4.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/davidbyttow/govips/v2/vips"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/signature"
|
|
)
|
|
|
|
// requireJPEGXL requires that rec answers 200 with a JPEG XL image.
|
|
func requireJPEGXL(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())
|
|
}
|
|
|
|
if got := rec.Header().Get("Content-Type"); got != jxlType {
|
|
t.Errorf("Content-Type = %q, want %s", got, jxlType)
|
|
}
|
|
|
|
if got := vips.DetermineImageType(rec.Body.Bytes()); got != vips.ImageTypeJXL {
|
|
t.Errorf("body is %s, want jxl", vips.ImageTypes[got])
|
|
}
|
|
}
|
|
|
|
// TestImageWithoutFormat_ServesJPEGXL verifies that a /v1/image/ URL whose
|
|
// last segment is a size with no format, 50x50 or orig, answers JPEG XL.
|
|
func TestImageWithoutFormat_ServesJPEGXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
route := newImageRoute(t, newPhotoFetcher(t, allowlistedHost))
|
|
|
|
for _, size := range []string{"50x50", "orig"} {
|
|
target := "/v1/image/" + allowlistedHost + photoPath + "/" + size
|
|
requireJPEGXL(t, sendGet(t, route, target))
|
|
}
|
|
}
|
|
|
|
// TestImageWithoutFormat_SignedAsJXL verifies that a /v1/image/ URL with no
|
|
// format is signed as jxl: the signature made for the URL ending in .jxl is
|
|
// accepted for the same URL without .jxl.
|
|
func TestImageWithoutFormat_SignedAsJXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
route := newImageRoute(t, newPhotoFetcher(t, signedHost))
|
|
expires := time.Now().Add(time.Hour)
|
|
|
|
sig := signature.New(testSigningKey).Sign(&imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: signedHost,
|
|
Path: photoPath,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 50, Height: 50},
|
|
Format: imagerequest.FormatJXL,
|
|
Quality: encurl.DefaultQuality,
|
|
FitMode: imagerequest.FitCover,
|
|
},
|
|
Expires: expires,
|
|
})
|
|
query := fmt.Sprintf("?sig=%s&exp=%d", sig, expires.Unix())
|
|
|
|
for _, size := range []string{"50x50.jxl", "50x50"} {
|
|
target := "/v1/image/" + signedHost + photoPath + "/" + size + query
|
|
requireJPEGXL(t, sendGet(t, route, target))
|
|
}
|
|
}
|
|
|
|
// TestImageEncWithoutFormat_ServesJPEGXL verifies that an encrypted URL whose
|
|
// token holds no format answers JPEG XL.
|
|
func TestImageEncWithoutFormat_ServesJPEGXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
|
|
|
token, err := h.encGen.Generate(&imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: signedHost,
|
|
Path: photoPath,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 50, Height: 50},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
requireJPEGXL(t, getEncToken(srv, token))
|
|
}
|
|
|
|
// TestGeneratorPage_SelectsJPEGXL verifies that the generator page's format
|
|
// choice is JPEG XL until another is chosen.
|
|
func TestGeneratorPage_SelectsJPEGXL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h, srv := newCSRFTestRouter(t)
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(newSessionCookie(t, h))
|
|
|
|
rec := httptest.NewRecorder()
|
|
srv.ServeHTTP(rec, req)
|
|
|
|
if !strings.Contains(rec.Body.String(), `<option value="jxl" selected>`) {
|
|
t.Errorf("generator page does not select JPEG XL: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGeneratePost_NoFormat_MakesJPEGXLURL verifies that the generator form
|
|
// sent with an empty format field, or with none, makes a URL whose name ends
|
|
// in .jxl and which answers JPEG XL.
|
|
func TestGeneratePost_NoFormat_MakesJPEGXLURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
photo := url.Values{
|
|
sourceURLField: {"https://" + signedHost + photoPath},
|
|
widthField: {"50"},
|
|
heightField: {"50"},
|
|
}
|
|
|
|
emptyFormat := maps.Clone(photo)
|
|
emptyFormat.Set(formatField, "")
|
|
|
|
for name, form := range map[string]url.Values{
|
|
"empty format field": emptyFormat,
|
|
"no format field": photo,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, imageSrv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
|
|
|
rec := generatePost(t, form)
|
|
|
|
match := generatedURLPattern.FindStringSubmatch(rec.Body.String())
|
|
if match == nil {
|
|
t.Fatalf("generator page shows no URL: %d %s",
|
|
rec.Code, rec.Body.String())
|
|
}
|
|
|
|
t.Logf("generated URL path: %s", match[1])
|
|
|
|
if !strings.HasSuffix(match[1], "/img.jxl") {
|
|
t.Errorf("generated URL %s does not end in /img.jxl", match[1])
|
|
}
|
|
|
|
imageRec := httptest.NewRecorder()
|
|
imageSrv.ServeHTTP(imageRec, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, match[1], nil))
|
|
|
|
requireJPEGXL(t, imageRec)
|
|
})
|
|
}
|
|
}
|