package imgcache import ( "bytes" "context" "image" "image/color" "image/jpeg" "image/png" "io" "os" "testing" "github.com/davidbyttow/govips/v2/vips" ) func TestMain(m *testing.M) { vips.Startup(nil) code := m.Run() vips.Shutdown() os.Exit(code) } // createTestJPEG creates a simple test JPEG image. func createTestJPEG(t *testing.T, width, height int) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, width, height)) // Fill with a gradient for y := 0; y < height; y++ { for x := 0; x < width; x++ { img.Set(x, y, color.RGBA{ R: uint8(x * 255 / width), G: uint8(y * 255 / height), B: 128, A: 255, }) } } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil { t.Fatalf("failed to encode test JPEG: %v", err) } return buf.Bytes() } // createTestPNG creates a simple test PNG image. func createTestPNG(t *testing.T, width, height int) []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, color.RGBA{ R: uint8(x * 255 / width), G: uint8(y * 255 / height), B: 128, A: 255, }) } } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { t.Fatalf("failed to encode test PNG: %v", err) } return buf.Bytes() } func TestImageProcessor_ResizeJPEG(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestJPEG(t, 800, 600) req := &ImageRequest{ Size: Size{Width: 400, Height: 300}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() if result.Width != 400 { t.Errorf("Process() width = %d, want 400", result.Width) } if result.Height != 300 { t.Errorf("Process() height = %d, want 300", result.Height) } if result.ContentLength == 0 { t.Error("Process() returned zero content length") } // Verify it's valid JPEG by reading the content data, err := io.ReadAll(result.Content) if err != nil { t.Fatalf("failed to read result: %v", err) } mime, err := DetectFormat(data) if err != nil { t.Fatalf("DetectFormat() error = %v", err) } if mime != MIMETypeJPEG { t.Errorf("Output format = %v, want %v", mime, MIMETypeJPEG) } } func TestImageProcessor_ConvertToPNG(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestJPEG(t, 200, 150) req := &ImageRequest{ Size: Size{Width: 200, Height: 150}, Format: FormatPNG, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() data, err := io.ReadAll(result.Content) if err != nil { t.Fatalf("failed to read result: %v", err) } mime, err := DetectFormat(data) if err != nil { t.Fatalf("DetectFormat() error = %v", err) } if mime != MIMETypePNG { t.Errorf("Output format = %v, want %v", mime, MIMETypePNG) } } func TestImageProcessor_OriginalSize(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestJPEG(t, 640, 480) req := &ImageRequest{ Size: Size{Width: 0, Height: 0}, // Original size Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() if result.Width != 640 { t.Errorf("Process() width = %d, want 640", result.Width) } if result.Height != 480 { t.Errorf("Process() height = %d, want 480", result.Height) } } func TestImageProcessor_FitContain(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // 800x400 image (2:1 aspect) into 400x400 box with contain // Should result in 400x200 (maintaining aspect ratio) input := createTestJPEG(t, 800, 400) req := &ImageRequest{ Size: Size{Width: 400, Height: 400}, Format: FormatJPEG, Quality: 85, FitMode: FitContain, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() // With contain, the image should fit within the box if result.Width > 400 || result.Height > 400 { t.Errorf("Process() size %dx%d exceeds 400x400 box", result.Width, result.Height) } } func TestImageProcessor_ProportionalScale_WidthOnly(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // 800x600 image, request width=400 height=0 // Should scale proportionally to 400x300 input := createTestJPEG(t, 800, 600) req := &ImageRequest{ Size: Size{Width: 400, Height: 0}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() if result.Width != 400 { t.Errorf("Process() width = %d, want 400", result.Width) } if result.Height != 300 { t.Errorf("Process() height = %d, want 300", result.Height) } } func TestImageProcessor_ProportionalScale_HeightOnly(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // 800x600 image, request width=0 height=300 // Should scale proportionally to 400x300 input := createTestJPEG(t, 800, 600) req := &ImageRequest{ Size: Size{Width: 0, Height: 300}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() if result.Width != 400 { t.Errorf("Process() width = %d, want 400", result.Width) } if result.Height != 300 { t.Errorf("Process() height = %d, want 300", result.Height) } } func TestImageProcessor_ProcessPNG(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestPNG(t, 400, 300) req := &ImageRequest{ Size: Size{Width: 200, Height: 150}, Format: FormatPNG, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v", err) } defer result.Content.Close() if result.Width != 200 { t.Errorf("Process() width = %d, want 200", result.Width) } if result.Height != 150 { t.Errorf("Process() height = %d, want 150", result.Height) } } func TestImageProcessor_ImplementsInterface(t *testing.T) { // Verify ImageProcessor implements Processor interface var _ Processor = (*ImageProcessor)(nil) } func TestImageProcessor_SupportedFormats(t *testing.T) { proc := NewImageProcessor() inputFormats := proc.SupportedInputFormats() if len(inputFormats) == 0 { t.Error("SupportedInputFormats() returned empty slice") } outputFormats := proc.SupportedOutputFormats() if len(outputFormats) == 0 { t.Error("SupportedOutputFormats() returned empty slice") } } func TestImageProcessor_RejectsOversizedInput(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // Create an image that exceeds MaxInputDimension (e.g., 10000x100) // This should be rejected before processing to prevent DoS input := createTestJPEG(t, 10000, 100) req := &ImageRequest{ Size: Size{Width: 100, Height: 100}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } _, err := proc.Process(ctx, bytes.NewReader(input), req) if err == nil { t.Error("Process() should reject oversized input images") } if err != ErrInputTooLarge { t.Errorf("Process() error = %v, want ErrInputTooLarge", err) } } func TestImageProcessor_RejectsOversizedInputHeight(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // Create an image with oversized height input := createTestJPEG(t, 100, 10000) req := &ImageRequest{ Size: Size{Width: 100, Height: 100}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } _, err := proc.Process(ctx, bytes.NewReader(input), req) if err == nil { t.Error("Process() should reject oversized input images") } if err != ErrInputTooLarge { t.Errorf("Process() error = %v, want ErrInputTooLarge", err) } } func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // Create an image at exactly MaxInputDimension - should be accepted // Using smaller dimensions to keep test fast input := createTestJPEG(t, MaxInputDimension, 100) req := &ImageRequest{ Size: Size{Width: 100, Height: 100}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() should accept images at MaxInputDimension, got error: %v", err) } defer result.Content.Close() } func TestImageProcessor_EncodeWebP(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestJPEG(t, 200, 150) req := &ImageRequest{ Size: Size{Width: 100, Height: 75}, Format: FormatWebP, Quality: 80, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v, want nil", err) } defer result.Content.Close() // Verify output is valid WebP data, err := io.ReadAll(result.Content) if err != nil { t.Fatalf("failed to read result: %v", err) } mime, err := DetectFormat(data) if err != nil { t.Fatalf("DetectFormat() error = %v", err) } if mime != MIMETypeWebP { t.Errorf("Output format = %v, want %v", mime, MIMETypeWebP) } // Verify dimensions if result.Width != 100 { t.Errorf("Width = %d, want 100", result.Width) } if result.Height != 75 { t.Errorf("Height = %d, want 75", result.Height) } } func TestImageProcessor_DecodeAVIF(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() // Load test AVIF file input, err := os.ReadFile("testdata/red.avif") if err != nil { t.Fatalf("failed to read test AVIF: %v", err) } // Request resize and convert to JPEG req := &ImageRequest{ Size: Size{Width: 2, Height: 2}, Format: FormatJPEG, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v, want nil (AVIF decoding should work)", err) } defer result.Content.Close() // Verify output is valid JPEG data, err := io.ReadAll(result.Content) if err != nil { t.Fatalf("failed to read result: %v", err) } mime, err := DetectFormat(data) if err != nil { t.Fatalf("DetectFormat() error = %v", err) } if mime != MIMETypeJPEG { t.Errorf("Output format = %v, want %v", mime, MIMETypeJPEG) } } func TestImageProcessor_EncodeAVIF(t *testing.T) { proc := NewImageProcessor() ctx := context.Background() input := createTestJPEG(t, 200, 150) req := &ImageRequest{ Size: Size{Width: 100, Height: 75}, Format: FormatAVIF, Quality: 85, FitMode: FitCover, } result, err := proc.Process(ctx, bytes.NewReader(input), req) if err != nil { t.Fatalf("Process() error = %v, want nil (AVIF encoding should work)", err) } defer result.Content.Close() // Verify output is valid AVIF data, err := io.ReadAll(result.Content) if err != nil { t.Fatalf("failed to read result: %v", err) } mime, err := DetectFormat(data) if err != nil { t.Fatalf("DetectFormat() error = %v", err) } if mime != MIMETypeAVIF { t.Errorf("Output format = %v, want %v", mime, MIMETypeAVIF) } // Verify dimensions if result.Width != 100 { t.Errorf("Width = %d, want 100", result.Width) } if result.Height != 75 { t.Errorf("Height = %d, want 75", result.Height) } }