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
246 lines
5.7 KiB
Go
246 lines
5.7 KiB
Go
package encurl_test
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/encurl"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// Shared test fixture strings.
|
|
const (
|
|
testSourceHost = "cdn.example.com"
|
|
testSourcePath = "/images/photo.jpg"
|
|
testSourceQuery = "v=2"
|
|
)
|
|
|
|
func TestGenerator_GenerateAndParse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, err := encurl.NewGenerator("test-signing-key-12345")
|
|
if err != nil {
|
|
t.Fatalf("NewGenerator() error = %v", err)
|
|
}
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
Query: testSourceQuery,
|
|
},
|
|
Transform: imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 800, Height: 600},
|
|
Format: imagerequest.FormatWebP,
|
|
Quality: 90,
|
|
FitMode: imagerequest.FitContain,
|
|
},
|
|
}
|
|
expires := time.Now().Add(time.Hour)
|
|
|
|
token, err := gen.Generate(req, expires)
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
if token == "" {
|
|
t.Fatal("Generate() returned empty token")
|
|
}
|
|
|
|
parsed, parsedExpires, err := gen.Parse(token)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
// Verify all fields
|
|
if parsed.Source.Host != req.Source.Host {
|
|
t.Errorf("SourceHost = %q, want %q", parsed.Source.Host, req.Source.Host)
|
|
}
|
|
|
|
if parsed.Source.Path != req.Source.Path {
|
|
t.Errorf("SourcePath = %q, want %q", parsed.Source.Path, req.Source.Path)
|
|
}
|
|
|
|
if parsed.Source.Query != req.Source.Query {
|
|
t.Errorf("SourceQuery = %q, want %q", parsed.Source.Query, req.Source.Query)
|
|
}
|
|
|
|
if parsed.Transform.Size.Width != req.Transform.Size.Width {
|
|
t.Errorf("Width = %d, want %d",
|
|
parsed.Transform.Size.Width, req.Transform.Size.Width)
|
|
}
|
|
|
|
if parsed.Transform.Size.Height != req.Transform.Size.Height {
|
|
t.Errorf("Height = %d, want %d",
|
|
parsed.Transform.Size.Height, req.Transform.Size.Height)
|
|
}
|
|
|
|
if parsed.Transform.Format != req.Transform.Format {
|
|
t.Errorf("Format = %q, want %q",
|
|
parsed.Transform.Format, req.Transform.Format)
|
|
}
|
|
|
|
if parsed.Transform.Quality != req.Transform.Quality {
|
|
t.Errorf("Quality = %d, want %d",
|
|
parsed.Transform.Quality, req.Transform.Quality)
|
|
}
|
|
|
|
if parsed.Transform.FitMode != req.Transform.FitMode {
|
|
t.Errorf("FitMode = %q, want %q",
|
|
parsed.Transform.FitMode, req.Transform.FitMode)
|
|
}
|
|
|
|
if parsedExpires.Unix() != expires.Unix() {
|
|
t.Errorf("ExpiresAt = %d, want %d", parsedExpires.Unix(), expires.Unix())
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_Expired(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, _ := encurl.NewGenerator("test-signing-key-12345")
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, err := gen.Generate(req, time.Now().Add(-time.Hour)) // Already expired
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
_, _, err = gen.Parse(token)
|
|
if err == nil {
|
|
t.Error("Parse() should fail for expired token")
|
|
}
|
|
|
|
if !errors.Is(err, encurl.ErrExpired) {
|
|
t.Errorf("Parse() error = %v, want %v", err, encurl.ErrExpired)
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_InvalidToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, _ := encurl.NewGenerator("test-signing-key-12345")
|
|
|
|
_, _, err := gen.Parse("not-a-valid-token")
|
|
if err == nil {
|
|
t.Error("Parse() should fail for invalid token")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_TamperedToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, _ := encurl.NewGenerator("test-signing-key-12345")
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
// Tamper with the token
|
|
tampered := []byte(token)
|
|
if len(tampered) > 10 {
|
|
tampered[10] ^= 0x01
|
|
}
|
|
|
|
_, _, err := gen.Parse(string(tampered))
|
|
if err == nil {
|
|
t.Error("Parse() should fail for tampered token")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_WrongKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen1, _ := encurl.NewGenerator("signing-key-1")
|
|
gen2, _ := encurl.NewGenerator("signing-key-2")
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen1.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
_, _, err := gen2.Parse(token)
|
|
if err == nil {
|
|
t.Error("Parse() should fail with different signing key")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_Parse_Defaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, _ := encurl.NewGenerator("test-signing-key-12345")
|
|
|
|
// Request with only required fields - should get defaults
|
|
token, err := gen.Generate(&imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}, time.Now().Add(time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("Generate() error = %v", err)
|
|
}
|
|
|
|
req, _, err := gen.Parse(token)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if req.Transform.Format != encurl.DefaultFormat {
|
|
t.Errorf("Format = %q, want default %q",
|
|
req.Transform.Format, encurl.DefaultFormat)
|
|
}
|
|
|
|
if req.Transform.Quality != encurl.DefaultQuality {
|
|
t.Errorf("Quality = %d, want default %d",
|
|
req.Transform.Quality, encurl.DefaultQuality)
|
|
}
|
|
|
|
if req.Transform.FitMode != encurl.DefaultFitMode {
|
|
t.Errorf("FitMode = %q, want default %q",
|
|
req.Transform.FitMode, encurl.DefaultFitMode)
|
|
}
|
|
}
|
|
|
|
func TestGenerator_TokenIsURLSafe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gen, _ := encurl.NewGenerator("test-signing-key-12345")
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{
|
|
Host: testSourceHost,
|
|
Path: testSourcePath,
|
|
},
|
|
}
|
|
|
|
token, _ := gen.Generate(req, time.Now().Add(time.Hour))
|
|
|
|
// Check that token only contains URL-safe characters
|
|
for _, c := range token {
|
|
isURLSafe := (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
c == '-' || c == '_'
|
|
if !isURLSafe {
|
|
t.Errorf("Token contains non-URL-safe character: %q", c)
|
|
}
|
|
}
|
|
}
|