package encurl import ( "testing" "time" "sneak.berlin/go/pixa/internal/imgcache" ) func TestGenerator_GenerateAndParse(t *testing.T) { gen, err := NewGenerator("test-signing-key-12345") if err != nil { t.Fatalf("NewGenerator() error = %v", err) } payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", SourceQuery: "v=2", Width: 800, Height: 600, Format: imgcache.FormatWebP, Quality: 90, FitMode: imgcache.FitContain, ExpiresAt: time.Now().Add(time.Hour).Unix(), } token, err := gen.Generate(payload) if err != nil { t.Fatalf("Generate() error = %v", err) } if token == "" { t.Fatal("Generate() returned empty token") } parsed, err := gen.Parse(token) if err != nil { t.Fatalf("Parse() error = %v", err) } // Verify all fields if parsed.SourceHost != payload.SourceHost { t.Errorf("SourceHost = %q, want %q", parsed.SourceHost, payload.SourceHost) } if parsed.SourcePath != payload.SourcePath { t.Errorf("SourcePath = %q, want %q", parsed.SourcePath, payload.SourcePath) } if parsed.SourceQuery != payload.SourceQuery { t.Errorf("SourceQuery = %q, want %q", parsed.SourceQuery, payload.SourceQuery) } if parsed.Width != payload.Width { t.Errorf("Width = %d, want %d", parsed.Width, payload.Width) } if parsed.Height != payload.Height { t.Errorf("Height = %d, want %d", parsed.Height, payload.Height) } if parsed.Format != payload.Format { t.Errorf("Format = %q, want %q", parsed.Format, payload.Format) } if parsed.Quality != payload.Quality { t.Errorf("Quality = %d, want %d", parsed.Quality, payload.Quality) } if parsed.FitMode != payload.FitMode { t.Errorf("FitMode = %q, want %q", parsed.FitMode, payload.FitMode) } if parsed.ExpiresAt != payload.ExpiresAt { t.Errorf("ExpiresAt = %d, want %d", parsed.ExpiresAt, payload.ExpiresAt) } } func TestGenerator_Parse_Expired(t *testing.T) { gen, _ := NewGenerator("test-signing-key-12345") payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", ExpiresAt: time.Now().Add(-time.Hour).Unix(), // Already expired } token, err := gen.Generate(payload) 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 err != ErrExpired { t.Errorf("Parse() error = %v, want %v", err, ErrExpired) } } func TestGenerator_Parse_InvalidToken(t *testing.T) { gen, _ := 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) { gen, _ := NewGenerator("test-signing-key-12345") payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", ExpiresAt: time.Now().Add(time.Hour).Unix(), } token, _ := gen.Generate(payload) // 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) { gen1, _ := NewGenerator("signing-key-1") gen2, _ := NewGenerator("signing-key-2") payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", ExpiresAt: time.Now().Add(time.Hour).Unix(), } token, _ := gen1.Generate(payload) _, err := gen2.Parse(token) if err == nil { t.Error("Parse() should fail with different signing key") } } func TestPayload_ToImageRequest(t *testing.T) { payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", SourceQuery: "v=2", Width: 800, Height: 600, Format: imgcache.FormatWebP, Quality: 90, FitMode: imgcache.FitContain, ExpiresAt: time.Now().Add(time.Hour).Unix(), } req := payload.ToImageRequest() if req.SourceHost != payload.SourceHost { t.Errorf("SourceHost = %q, want %q", req.SourceHost, payload.SourceHost) } if req.SourcePath != payload.SourcePath { t.Errorf("SourcePath = %q, want %q", req.SourcePath, payload.SourcePath) } if req.SourceQuery != payload.SourceQuery { t.Errorf("SourceQuery = %q, want %q", req.SourceQuery, payload.SourceQuery) } if req.Size.Width != payload.Width { t.Errorf("Width = %d, want %d", req.Size.Width, payload.Width) } if req.Size.Height != payload.Height { t.Errorf("Height = %d, want %d", req.Size.Height, payload.Height) } if req.Format != payload.Format { t.Errorf("Format = %q, want %q", req.Format, payload.Format) } if req.Quality != payload.Quality { t.Errorf("Quality = %d, want %d", req.Quality, payload.Quality) } if req.FitMode != payload.FitMode { t.Errorf("FitMode = %q, want %q", req.FitMode, payload.FitMode) } } func TestPayload_ToImageRequest_Defaults(t *testing.T) { // Payload with only required fields - should get defaults payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", ExpiresAt: time.Now().Add(time.Hour).Unix(), } req := payload.ToImageRequest() if req.Format != DefaultFormat { t.Errorf("Format = %q, want default %q", req.Format, DefaultFormat) } if req.Quality != DefaultQuality { t.Errorf("Quality = %d, want default %d", req.Quality, DefaultQuality) } if req.FitMode != DefaultFitMode { t.Errorf("FitMode = %q, want default %q", req.FitMode, DefaultFitMode) } } func TestFromImageRequest(t *testing.T) { req := &imgcache.ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", SourceQuery: "v=2", Size: imgcache.Size{Width: 800, Height: 600}, Format: imgcache.FormatWebP, Quality: 90, FitMode: imgcache.FitContain, } expiresAt := time.Now().Add(time.Hour) payload := FromImageRequest(req, expiresAt) if payload.SourceHost != req.SourceHost { t.Errorf("SourceHost = %q, want %q", payload.SourceHost, req.SourceHost) } if payload.SourcePath != req.SourcePath { t.Errorf("SourcePath = %q, want %q", payload.SourcePath, req.SourcePath) } if payload.Width != req.Size.Width { t.Errorf("Width = %d, want %d", payload.Width, req.Size.Width) } if payload.ExpiresAt != expiresAt.Unix() { t.Errorf("ExpiresAt = %d, want %d", payload.ExpiresAt, expiresAt.Unix()) } } func TestFromImageRequest_OmitsDefaults(t *testing.T) { // Request with default values - payload should omit them for smaller encoding req := &imgcache.ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", Format: DefaultFormat, Quality: DefaultQuality, FitMode: DefaultFitMode, } payload := FromImageRequest(req, time.Now().Add(time.Hour)) // These should be zero/empty because they match defaults if payload.Format != "" { t.Errorf("Format should be empty for default, got %q", payload.Format) } if payload.Quality != 0 { t.Errorf("Quality should be 0 for default, got %d", payload.Quality) } if payload.FitMode != "" { t.Errorf("FitMode should be empty for default, got %q", payload.FitMode) } } func TestGenerator_TokenIsURLSafe(t *testing.T) { gen, _ := NewGenerator("test-signing-key-12345") payload := &Payload{ SourceHost: "cdn.example.com", SourcePath: "/images/photo.jpg", ExpiresAt: time.Now().Add(time.Hour).Unix(), } token, _ := gen.Generate(payload) // 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) } } }