Files
pixa/internal/signature/golden_qualityfit_test.go
sneak 460c11a7bf
check / check (push) Failing after 1s
feat: include quality and fit in the URL signature (closes #60)
Quality (q) and fit were read after signature validation and folded
into the variant cache key, so one signed URL could be replayed across
100 quality values and 5 fit modes, yielding up to 500 unauthorized
cache entries and libvips transcodes.

The signed data now appends the effective quality and fit:

  host:path:query:width:height:format:expiration:quality:fit

The handler already defaults an omitted q to 85 and fit to cover before
verification, so those effective values are what gets signed; a URL
signed for one quality or fit no longer verifies when replayed with
another. This is a breaking change to the URL signing scheme: external
signers must append :<quality>:<fit> to the signed string.

New known-answer vectors are added in golden_qualityfit_test.go; the
README signature specification is updated. The pre-existing
golden_test.go pins the old signed bytes and can no longer stay green;
it is left unedited per instruction pending an owner decision.

model: claude-opus-4-8
2026-09-21 12:57:24 +00:00

123 lines
3.7 KiB
Go

package signature_test
import (
"testing"
"time"
"sneak.berlin/go/pixa/internal/signature"
)
// Fixed inputs for the quality/fit golden vectors. They are independent of
// the constants in golden_test.go so this file pins the current signed
// format on its own.
const (
qfSigningKey = "golden-test-key"
qfExpiresUnix int64 = 1704067200 // 2024-01-01T00:00:00Z
qfFitCover = "cover"
qfFitContain = "contain"
)
type qualityFitGoldenVector struct {
name string
req signature.Request
// wantSignature is the exact base64url (RFC 4648 URL-safe, padded)
// HMAC-SHA256 signature for the request with Expires set to
// qfExpiresUnix, under the signed format
// "host:path:query:width:height:format:expiration:quality:fit".
wantSignature string
}
// qualityFitGoldenVectors returns the known-answer vectors that pin quality
// and fit as signed components. The three default-value vectors use the
// effective quality (85) and fit ("cover") the handler applies when a URL
// omits q and fit, so they are the signatures real signed URLs must carry.
func qualityFitGoldenVectors() []qualityFitGoldenVector {
return []qualityFitGoldenVector{
{
name: "resized, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover"
wantSignature: "kdqeGoW2SX7qnaYtoB970wEnLydn0UnIgQYQLfAnjXQ=",
},
{
name: "resized with query, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
SourceQuery: "token=abc&v=2",
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg:token=abc&v=2:800:600:webp:1704067200:85:cover"
wantSignature: "pKgVBOTd_Q_EikI7MNQLC9Q8Hurdxzyv3EIYvVhqc2I=",
},
{
name: "original size, default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 0,
Height: 0,
Format: testFormatPNG,
Quality: 85,
FitMode: qfFitCover,
},
// "cdn.example.com:/photos/cat.jpg::0:0:png:1704067200:85:cover"
wantSignature: "6_rZ0yyVbGZRs8kG7n7HLgLi5Jt8vjiWQljIEL1jbIs=",
},
{
name: "non-default quality and fit",
req: signature.Request{
SourceHost: testHost,
SourcePath: testPath,
Width: 800,
Height: 600,
Format: testFormatWebP,
Quality: 40,
FitMode: qfFitContain,
},
// "cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:40:contain"
wantSignature: "pGaXpPUbI3A7nMx-4T9bfq9bYWBNL0kY4bxlcv3g1F8=",
},
}
}
// TestSigner_GoldenVectors_QualityFit pins the exact HMAC-SHA256 signature
// output for requests that carry quality and fit as signed components. If
// these assertions fail, the signed byte format
// ("host:path:query:width:height:format:expiration:quality:fit") or the
// base64url encoding has changed, breaking every signature already issued.
// Update these constants only as part of a deliberate, documented signature
// format migration.
func TestSigner_GoldenVectors_QualityFit(t *testing.T) {
t.Parallel()
signer := signature.New(qfSigningKey)
for _, tt := range qualityFitGoldenVectors() {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
signReq := tt.req
signReq.Expires = time.Unix(qfExpiresUnix, 0)
gotSignature := signer.Sign(&signReq)
if gotSignature != tt.wantSignature {
t.Errorf("Sign() = %q, want %q (signed byte format changed?)",
gotSignature, tt.wantSignature)
}
})
}
}