Split internal/imgcache into the image proxy and the cache (closes #317)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The new package internal/imageproxy holds Proxy, which was imgcache.Service, with its Config, ImageResponse, CacheStatus and errors. imageproxy.New makes and registers the counters of upstream fetches and transforms. internal/imgcache holds the cache only, with its hit and miss counters, and imgcache.CacheConfig is imgcache.Config. The cache reads and writes each kind it keeps with one pair of methods: Variant and PutVariant, Source and PutSource, Failure and PutFailure. Source replaces LookupSource followed by GetSourceContent. imgcache.go and module.go are gone. The proxy's tests moved with it. The cache's tests check that the reconciliation pass removes an empty or cut file and its row. The proxy's tests remove them by hand, then check that Get fetches or converts again. Model: opus-5-5
This commit was merged in pull request #334.
This commit is contained in:
@@ -2,98 +2,19 @@ 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"
|
||||
testHostCDN = "cdn.example.com"
|
||||
testHostExample = "example.com"
|
||||
testPathCat = "/photos/cat.jpg"
|
||||
)
|
||||
|
||||
// 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()
|
||||
@@ -114,104 +35,3 @@ func generateTestJPEG(t *testing.T, width, height int, c color.Color) []byte {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user