package imgcache import ( "bytes" "context" "database/sql" "image" "image/color" "image/gif" "image/jpeg" "image/png" "io/fs" "testing" "testing/fstest" "time" "github.com/prometheus/client_golang/prometheus" "sneak.berlin/go/pixa/internal/database" "sneak.berlin/go/pixa/internal/httpfetcher" "sneak.berlin/go/pixa/internal/imageprocessor" ) // Shared test data literals, extracted as constants for goconst. const ( testHostCDN = "cdn.example.com" testHostExample = "example.com" testPathCat = "/photos/cat.jpg" testContentTypeJPEG = "image/jpeg" ) // TestFixtures contains paths to test files in the mock filesystem. type TestFixtures struct { // Valid image files GoodHostJPEG string // valid JPEG on GoodHost GoodHostPNG string // valid PNG on GoodHost GoodHostGIF string // valid GIF on GoodHost OtherHostJPEG string // valid JPEG on OtherHost OtherHostPNG string // valid PNG on OtherHost // 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 // a host the mock fetcher serves OtherHost string // a second host the mock fetcher serves } // 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 := range height { for x := range width { img.Set(x, y, c) } } var buf bytes.Buffer err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 85}) if 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 := range height { for x := range width { img.Set(x, y, c) } } var buf bytes.Buffer err := png.Encode(&buf, img) if 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 := range height { for x := range width { img.SetColorIndex(x, y, 0) } } var buf bytes.Buffer err := gif.Encode(&buf, img, nil) if 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) (*Service, *TestFixtures) { t.Helper() mockFS, fixtures := NewTestFS(t) // Create temp directory for cache tmpDir := t.TempDir() // Create in-memory database db := setupServiceTestDB(t) cache, err := NewCache(db, CacheConfig{ StateDir: tmpDir, NegativeTTL: 5 * time.Minute, MaxBytes: 1 << 30, }, prometheus.NewRegistry()) if err != nil { t.Fatalf("failed to create cache: %v", err) } svc, err := NewService(&ServiceConfig{ Cache: cache, Fetcher: httpfetcher.NewMock(mockFS), Processor: imageprocessor.New(imageprocessor.Params{ MaxInputBytes: 50 << 20, MaxConcurrentProcessing: 2, }), MaxResponseSize: 50 << 20, }) 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 err = database.ApplyMigrations(context.Background(), db, nil) if err != nil { t.Fatalf("failed to apply migrations: %v", err) } return db }