One image request type instead of five (closes #307)
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 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
This commit is contained in:
2026-10-21 12:27:29 +00:00
parent 385c0ca76f
commit fece3b69c9
68 changed files with 2217 additions and 1978 deletions
+140 -106
View File
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"sneak.berlin/go/pixa/internal/imagerequest"
"sneak.berlin/go/pixa/internal/signature"
)
@@ -26,14 +27,17 @@ func TestSigner_Sign(t *testing.T) {
signer := signature.New("test-secret-key")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "",
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Unix(1704067200, 0), // Fixed timestamp for reproducibility
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
Query: "",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0), // Fixed timestamp for reproducibility
}
sig1 := signer.Sign(req)
@@ -51,14 +55,17 @@ func TestSigner_Sign(t *testing.T) {
}
// Different input should produce different signature
req2 := &signature.Request{
SourceHost: testHost,
SourcePath: "/photos/dog.jpg", // Different path
SourceQuery: "",
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Unix(1704067200, 0),
req2 := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: "/photos/dog.jpg", // Different path
Query: "",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0),
}
sig3 := signer.Sign(req2)
@@ -69,20 +76,23 @@ func TestSigner_Sign(t *testing.T) {
// validVerifyRequest returns a fully-populated request that verifies
// successfully once signed.
func validVerifyRequest() *signature.Request {
return &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Now().Add(1 * time.Hour),
func validVerifyRequest() *imagerequest.Request {
return &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
}
type verifyCase struct {
name string
setup func() *signature.Request
setup func() *imagerequest.Request
wantErr error
}
@@ -90,7 +100,7 @@ func verifyCases(signer *signature.Signer) []verifyCase {
return []verifyCase{
{
name: "valid signature",
setup: func() *signature.Request {
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = signer.Sign(req)
@@ -100,7 +110,7 @@ func verifyCases(signer *signature.Signer) []verifyCase {
},
{
name: "expired signature",
setup: func() *signature.Request {
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Expires = time.Now().Add(-1 * time.Hour)
req.Signature = signer.Sign(req)
@@ -111,7 +121,7 @@ func verifyCases(signer *signature.Signer) []verifyCase {
},
{
name: "invalid signature",
setup: func() *signature.Request {
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = "invalid-signature"
@@ -121,7 +131,7 @@ func verifyCases(signer *signature.Signer) []verifyCase {
},
{
name: "missing expiration",
setup: func() *signature.Request {
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Expires = time.Time{}
req.Signature = "some-signature"
@@ -132,10 +142,10 @@ func verifyCases(signer *signature.Signer) []verifyCase {
},
{
name: "tampered request",
setup: func() *signature.Request {
setup: func() *imagerequest.Request {
req := validVerifyRequest()
req.Signature = signer.Sign(req)
req.SourcePath = "/photos/secret.jpg"
req.Source.Path = "/photos/secret.jpg"
return req
},
@@ -173,7 +183,7 @@ func TestSigner_Verify(t *testing.T) {
type tamperCase struct {
name string
tamper func(r *signature.Request)
tamper func(r *imagerequest.Request)
}
// exactMatchTamperCases mutates one signed component per case; every
@@ -182,59 +192,59 @@ func exactMatchTamperCases() []tamperCase {
return []tamperCase{
{
name: "parent domain does not match subdomain",
tamper: func(r *signature.Request) { r.SourceHost = "example.com" },
tamper: func(r *imagerequest.Request) { r.Source.Host = "example.com" },
},
{
name: "subdomain does not match parent domain",
tamper: func(r *signature.Request) { r.SourceHost = "images.cdn.example.com" },
tamper: func(r *imagerequest.Request) { r.Source.Host = "images.cdn.example.com" },
},
{
name: "sibling subdomain does not match",
tamper: func(r *signature.Request) { r.SourceHost = "images.example.com" },
tamper: func(r *imagerequest.Request) { r.Source.Host = "images.example.com" },
},
{
name: "host with suffix appended does not match",
tamper: func(r *signature.Request) { r.SourceHost = testHost + ".evil.com" },
tamper: func(r *imagerequest.Request) { r.Source.Host = testHost + ".evil.com" },
},
{
name: "host with prefix does not match",
tamper: func(r *signature.Request) { r.SourceHost = "evilcdn.example.com" },
tamper: func(r *imagerequest.Request) { r.Source.Host = "evilcdn.example.com" },
},
{
name: "different path does not match",
tamper: func(r *signature.Request) { r.SourcePath = "/photos/dog.jpg" },
tamper: func(r *imagerequest.Request) { r.Source.Path = "/photos/dog.jpg" },
},
{
name: "path suffix does not match",
tamper: func(r *signature.Request) { r.SourcePath = testPath + "/extra" },
tamper: func(r *imagerequest.Request) { r.Source.Path = testPath + "/extra" },
},
{
name: "path prefix does not match",
tamper: func(r *signature.Request) { r.SourcePath = "/other" + testPath },
tamper: func(r *imagerequest.Request) { r.Source.Path = "/other" + testPath },
},
{
name: "different query does not match",
tamper: func(r *signature.Request) { r.SourceQuery = "token=xyz" },
tamper: func(r *imagerequest.Request) { r.Source.Query = "token=xyz" },
},
{
name: "added query does not match empty query",
tamper: func(r *signature.Request) { r.SourceQuery = "extra=1" },
tamper: func(r *imagerequest.Request) { r.Source.Query = "extra=1" },
},
{
name: "removed query does not match",
tamper: func(r *signature.Request) { r.SourceQuery = "" },
tamper: func(r *imagerequest.Request) { r.Source.Query = "" },
},
{
name: "different width does not match",
tamper: func(r *signature.Request) { r.Width = 801 },
tamper: func(r *imagerequest.Request) { r.Transform.Size.Width = 801 },
},
{
name: "different height does not match",
tamper: func(r *signature.Request) { r.Height = 601 },
tamper: func(r *imagerequest.Request) { r.Transform.Size.Height = 601 },
},
{
name: "different format does not match",
tamper: func(r *signature.Request) { r.Format = testFormatPNG },
tamper: func(r *imagerequest.Request) { r.Transform.Format = testFormatPNG },
},
}
}
@@ -248,15 +258,18 @@ func TestSigner_Verify_ExactMatchOnly(t *testing.T) {
signer := signature.New("test-secret-key")
// Base request that we'll sign, then tamper with individual fields.
baseReq := func() *signature.Request {
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "token=abc",
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Now().Add(1 * time.Hour),
baseReq := func() *imagerequest.Request {
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
Query: "token=abc",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
req.Signature = signer.Sign(req)
@@ -309,14 +322,17 @@ func TestSigner_Sign_ExactHostInData(t *testing.T) {
sigs := make(map[string]string)
for _, host := range hosts {
req := &signature.Request{
SourceHost: host,
SourcePath: testPath,
SourceQuery: "",
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Unix(1704067200, 0),
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: host,
Path: testPath,
Query: "",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Unix(1704067200, 0),
}
sig := signer.Sign(req)
@@ -334,13 +350,16 @@ func TestSigner_DifferentKeys(t *testing.T) {
signer1 := signature.New("secret-key-1")
signer2 := signature.New("secret-key-2")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Expires: time.Now().Add(1 * time.Hour),
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
Expires: time.Now().Add(1 * time.Hour),
}
// Sign with key 1
@@ -364,13 +383,16 @@ func TestGenerateSignedURL(t *testing.T) {
signer := signature.New("test-secret-key")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "",
Width: 800,
Height: 600,
Format: testFormatWebP,
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
Query: "",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
}
ttl := 1 * time.Hour
@@ -405,12 +427,15 @@ func TestGenerateSignedURL_OrigSize(t *testing.T) {
signer := signature.New("test-secret-key")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 0, // Original size
Height: 0,
Format: testFormatPNG,
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 0, Height: 0}, // Original size
Format: testFormatPNG,
},
}
path, _, _ := signer.GenerateSignedURL(req, time.Hour)
@@ -426,13 +451,16 @@ func TestGenerateSignedURL_WithQueryString(t *testing.T) {
signer := signature.New("test-secret-key-for-testing!")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "token=abc&v=2",
Width: 800,
Height: 600,
Format: testFormatWebP,
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
Query: "token=abc&v=2",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
}
path, _, _ := signer.GenerateSignedURL(req, time.Hour)
@@ -460,12 +488,15 @@ func TestGenerateSignedURL_WithoutQueryString(t *testing.T) {
signer := signature.New("test-secret-key-for-testing!")
req := &signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
req := &imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: testPath,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
},
}
path, _, _ := signer.GenerateSignedURL(req, time.Hour)
@@ -485,16 +516,19 @@ func TestGenerateSignedURL_ReadmeQueryExample(t *testing.T) {
signer := signature.New("example-signing-key-for-documentation")
req := signature.Request{
SourceHost: testHost,
SourcePath: "/img",
SourceQuery: "url=https://origin.example.com/my%20cat.jpg",
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: testFitCover,
Expires: time.Unix(1704067200, 0),
req := imagerequest.Request{
Source: imagerequest.Source{
Host: testHost,
Path: "/img",
Query: "url=https://origin.example.com/my%20cat.jpg",
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: 800, Height: 600},
Format: testFormatWebP,
Quality: 85,
FitMode: testFitCover,
},
Expires: time.Unix(1704067200, 0),
}
const wantSig = "37aFa9GSVCZM1kRY9SzMOk2fOg_M7Wt5gKNR4xM-FYA="