TDD: This test expects WebP encoding to succeed. It currently fails because WebP encoding is not implemented (returns ErrUnsupportedOutputFormat). The test will pass once we add the gen2brain/webp library.
381 lines
8.7 KiB
Go
381 lines
8.7 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
// 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_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_RejectsUnsupportedOutputFormat_AVIF(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,
|
|
}
|
|
|
|
_, err := proc.Process(ctx, bytes.NewReader(input), req)
|
|
if err == nil {
|
|
t.Fatal("Process() should return error for unsupported AVIF encoding")
|
|
}
|
|
|
|
if !errors.Is(err, ErrUnsupportedOutputFormat) {
|
|
t.Errorf("Process() error = %v, want ErrUnsupportedOutputFormat", err)
|
|
}
|
|
}
|