package encurl_test import ( "errors" "testing" "time" "sneak.berlin/go/pixa/internal/encurl" "sneak.berlin/go/pixa/internal/imgcache" ) // 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) } payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, SourceQuery: testSourceQuery, 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) { t.Parallel() gen, _ := encurl.NewGenerator("test-signing-key-12345") payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, 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 !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") payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, 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) { t.Parallel() gen1, _ := encurl.NewGenerator("signing-key-1") gen2, _ := encurl.NewGenerator("signing-key-2") payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, 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) { t.Parallel() payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, SourceQuery: testSourceQuery, 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) { t.Parallel() // Payload with only required fields - should get defaults payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, ExpiresAt: time.Now().Add(time.Hour).Unix(), } req := payload.ToImageRequest() if req.Format != encurl.DefaultFormat { t.Errorf("Format = %q, want default %q", req.Format, encurl.DefaultFormat) } if req.Quality != encurl.DefaultQuality { t.Errorf("Quality = %d, want default %d", req.Quality, encurl.DefaultQuality) } if req.FitMode != encurl.DefaultFitMode { t.Errorf("FitMode = %q, want default %q", req.FitMode, encurl.DefaultFitMode) } } func TestFromImageRequest(t *testing.T) { t.Parallel() req := &imgcache.ImageRequest{ SourceHost: testSourceHost, SourcePath: testSourcePath, SourceQuery: testSourceQuery, Size: imgcache.Size{Width: 800, Height: 600}, Format: imgcache.FormatWebP, Quality: 90, FitMode: imgcache.FitContain, } expiresAt := time.Now().Add(time.Hour) payload := encurl.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) { t.Parallel() // Request with default values - payload should omit them for // smaller encoding req := &imgcache.ImageRequest{ SourceHost: testSourceHost, SourcePath: testSourcePath, Format: encurl.DefaultFormat, Quality: encurl.DefaultQuality, FitMode: encurl.DefaultFitMode, } payload := encurl.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) { t.Parallel() gen, _ := encurl.NewGenerator("test-signing-key-12345") payload := &encurl.Payload{ SourceHost: testSourceHost, SourcePath: testSourcePath, 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) } } }