package imgcache import ( "bytes" "database/sql" "image" "image/color" "image/gif" "image/jpeg" "image/png" "io/fs" "testing" "testing/fstest" "time" "sneak.berlin/go/pixa/internal/database" ) // TestFixtures contains paths to test files in the mock filesystem. type TestFixtures struct { // Valid image files GoodHostJPEG string // whitelisted host, valid JPEG GoodHostPNG string // whitelisted host, valid PNG GoodHostGIF string // whitelisted host, valid GIF OtherHostJPEG string // non-whitelisted host, valid JPEG OtherHostPNG string // non-whitelisted host, valid PNG // Invalid/edge case files InvalidFile string // file with wrong magic bytes EmptyFile string // zero-byte file TextFile string // text file masquerading as image // Hostnames GoodHost string // whitelisted hostname OtherHost string // non-whitelisted hostname } // DefaultFixtures returns the standard test fixture paths. func DefaultFixtures() *TestFixtures { return &TestFixtures{ GoodHostJPEG: "goodhost.example.com/images/photo.jpg", GoodHostPNG: "goodhost.example.com/images/logo.png", GoodHostGIF: "goodhost.example.com/images/animation.gif", OtherHostJPEG: "otherhost.example.com/uploads/image.jpg", OtherHostPNG: "otherhost.example.com/uploads/icon.png", InvalidFile: "goodhost.example.com/images/fake.jpg", EmptyFile: "goodhost.example.com/images/empty.jpg", TextFile: "goodhost.example.com/images/text.png", GoodHost: "goodhost.example.com", OtherHost: "otherhost.example.com", } } // NewTestFS creates a mock filesystem with test images. func NewTestFS(t *testing.T) (fs.FS, *TestFixtures) { t.Helper() fixtures := DefaultFixtures() // Generate test images jpegData := generateTestJPEG(t, 100, 100, color.RGBA{255, 0, 0, 255}) pngData := generateTestPNG(t, 100, 100, color.RGBA{0, 255, 0, 255}) gifData := generateTestGIF(t, 100, 100, color.RGBA{0, 0, 255, 255}) // Create the mock filesystem mockFS := fstest.MapFS{ // Good host files fixtures.GoodHostJPEG: &fstest.MapFile{Data: jpegData}, fixtures.GoodHostPNG: &fstest.MapFile{Data: pngData}, fixtures.GoodHostGIF: &fstest.MapFile{Data: gifData}, // Other host files fixtures.OtherHostJPEG: &fstest.MapFile{Data: jpegData}, fixtures.OtherHostPNG: &fstest.MapFile{Data: pngData}, // Invalid files fixtures.InvalidFile: &fstest.MapFile{Data: []byte("not a real image file content")}, fixtures.EmptyFile: &fstest.MapFile{Data: []byte{}}, fixtures.TextFile: &fstest.MapFile{Data: []byte("This is just text, not a PNG")}, } return mockFS, fixtures } // generateTestJPEG creates a minimal valid JPEG image. func generateTestJPEG(t *testing.T, width, height int, c color.Color) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { for x := 0; x < width; x++ { img.Set(x, y, c) } } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 85}); err != nil { t.Fatalf("failed to encode test JPEG: %v", err) } return buf.Bytes() } // generateTestPNG creates a minimal valid PNG image. func generateTestPNG(t *testing.T, width, height int, c color.Color) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { for x := 0; x < width; x++ { img.Set(x, y, c) } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { t.Fatalf("failed to encode test PNG: %v", err) } return buf.Bytes() } // generateTestGIF creates a minimal valid GIF image. func generateTestGIF(t *testing.T, width, height int, c color.Color) []byte { t.Helper() img := image.NewPaletted(image.Rect(0, 0, width, height), []color.Color{c, color.White}) for y := 0; y < height; y++ { for x := 0; x < width; x++ { img.SetColorIndex(x, y, 0) } } var buf bytes.Buffer if err := gif.Encode(&buf, img, nil); err != nil { t.Fatalf("failed to encode test GIF: %v", err) } return buf.Bytes() } // SetupTestService creates a Service with mock fetcher for testing. func SetupTestService(t *testing.T, opts ...TestServiceOption) (*Service, *TestFixtures) { t.Helper() mockFS, fixtures := NewTestFS(t) cfg := &testServiceConfig{ whitelist: []string{fixtures.GoodHost}, signingKey: "", } for _, opt := range opts { opt(cfg) } // Create temp directory for cache tmpDir := t.TempDir() // Create in-memory database db := setupServiceTestDB(t) cache, err := NewCache(db, CacheConfig{ StateDir: tmpDir, CacheTTL: time.Hour, NegativeTTL: 5 * time.Minute, HotCacheSize: 100, HotCacheEnabled: true, }) if err != nil { t.Fatalf("failed to create cache: %v", err) } svc, err := NewService(&ServiceConfig{ Cache: cache, Fetcher: NewMockFetcher(mockFS), SigningKey: cfg.signingKey, Whitelist: cfg.whitelist, }) if err != nil { t.Fatalf("failed to create service: %v", err) } return svc, fixtures } // setupServiceTestDB creates an in-memory SQLite database for testing // using the production schema. func setupServiceTestDB(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatalf("failed to open test db: %v", err) } // Use the real production schema via migrations if err := database.ApplyMigrations(db); err != nil { t.Fatalf("failed to apply migrations: %v", err) } return db } type testServiceConfig struct { whitelist []string signingKey string } // TestServiceOption configures the test service. type TestServiceOption func(*testServiceConfig) // WithWhitelist sets the whitelist for the test service. func WithWhitelist(hosts ...string) TestServiceOption { return func(c *testServiceConfig) { c.whitelist = hosts } } // WithSigningKey sets the signing key for the test service. func WithSigningKey(key string) TestServiceOption { return func(c *testServiceConfig) { c.signingKey = key } } // WithNoWhitelist removes all whitelisted hosts. func WithNoWhitelist() TestServiceOption { return func(c *testServiceConfig) { c.whitelist = nil } }