Files
pixa/internal/encurl/encurl_test.go
T
clawbot 77d55abb7e
check / check (push) Canceled after 0s
Derive every key from the signing key in one place (closes #315)
seal.DeriveKeys derives the session hash and block keys, the encrypted URL
key and the CSRF key from the signing key, with their salts declared beside
it, and returns them as one seal.Keys. internal/providers calls it once at
startup and provides the value to the fx graph. The session manager, the
encrypted URL generator and the CSRF protection each take their own key.
session.NewManager, encurl.NewGenerator, newCSRFProtect and handlers.New
return no error, as nothing in them can fail now. Each key is derived as
before, with seal.DeriveKey and the same salt. The signing key itself stays
the HMAC key of signed URLs and the login password. README.md says under
signing_key what it protects.

Model: opus-5-5
2026-10-26 14:10:20 +01:00

256 lines
6.1 KiB
Go

package encurl_test
import (
"errors"
"testing"
"time"
"sneak.berlin/go/pixa/internal/encurl"
"sneak.berlin/go/pixa/internal/imagerequest"
"sneak.berlin/go/pixa/internal/seal"
)
// Shared test fixture strings.
const (
testSourceHost = "cdn.example.com"
testSourcePath = "/images/photo.jpg"
testSourceQuery = "v=2"
)
func TestGenerator_GenerateAndParse(t *testing.T) {
t.Parallel()
keys, err := seal.DeriveKeys("test-signing-key-12345")
if err != nil {
t.Fatalf("seal.DeriveKeys() error = %v", err)
}
gen := encurl.NewGenerator(keys.EncryptedURLKey)
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()
keys, _ := seal.DeriveKeys("test-signing-key-12345")
gen := encurl.NewGenerator(keys.EncryptedURLKey)
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()
keys, _ := seal.DeriveKeys("test-signing-key-12345")
gen := encurl.NewGenerator(keys.EncryptedURLKey)
_, _, 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()
keys, _ := seal.DeriveKeys("test-signing-key-12345")
gen := encurl.NewGenerator(keys.EncryptedURLKey)
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()
keys1, _ := seal.DeriveKeys("signing-key-1")
keys2, _ := seal.DeriveKeys("signing-key-2")
gen1 := encurl.NewGenerator(keys1.EncryptedURLKey)
gen2 := encurl.NewGenerator(keys2.EncryptedURLKey)
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()
keys, _ := seal.DeriveKeys("test-signing-key-12345")
gen := encurl.NewGenerator(keys.EncryptedURLKey)
// 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()
keys, _ := seal.DeriveKeys("test-signing-key-12345")
gen := encurl.NewGenerator(keys.EncryptedURLKey)
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)
}
}
}