package storage import ( "bytes" "image" "image/color" "image/jpeg" "image/png" "testing" ) func TestDetectContentType(t *testing.T) { tests := []struct { name string data []byte want string }{ {"JPEG", []byte{0xFF, 0xD8, 0xFF, 0xE0}, "image/jpeg"}, {"PNG", []byte{0x89, 0x50, 0x4E, 0x47}, "image/png"}, {"GIF", []byte{0x47, 0x49, 0x46, 0x38}, "image/gif"}, {"WebP", []byte{0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50}, "image/webp"}, {"BMP", []byte{0x42, 0x4D}, "image/bmp"}, {"MP4", []byte{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70}, "video/mp4"}, {"WebM", []byte{0x1A, 0x45, 0xDF, 0xA3}, "video/webm"}, {"unknown", []byte{0x00, 0x01, 0x02}, "application/octet-stream"}, {"empty", []byte{}, "application/octet-stream"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := DetectContentType(tt.data) if got != tt.want { t.Fatalf("DetectContentType = %q, want %q", got, tt.want) } }) } } func TestExtensionFromMIME(t *testing.T) { if ext := ExtensionFromMIME("image/jpeg"); ext != "jpg" { t.Fatalf("jpg extension mismatch: %s", ext) } if ext := ExtensionFromMIME("image/png"); ext != "png" { t.Fatalf("png extension mismatch: %s", ext) } if ext := ExtensionFromMIME("image/gif"); ext != "gif" { t.Fatalf("gif extension mismatch: %s", ext) } if ext := ExtensionFromMIME("video/mp4"); ext != "mp4" { t.Fatalf("mp4 extension mismatch: %s", ext) } if ext := ExtensionFromMIME("application/octet-stream"); ext != "bin" { t.Fatalf("bin extension mismatch: %s", ext) } } func TestIsImage(t *testing.T) { if !IsImage("image/jpeg") { t.Fatal("expected jpeg to be image") } if IsImage("video/mp4") { t.Fatal("expected mp4 to not be image") } } func TestIsVideo(t *testing.T) { if !IsVideo("video/mp4") { t.Fatal("expected mp4 to be video") } if IsVideo("image/jpeg") { t.Fatal("expected jpeg to not be video") } } func createTestJPEG(t *testing.T, w, h int) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := range h { for x := range w { img.Set(x, y, color.RGBA{uint8(x % 256), uint8(y % 256), 128, 255}) } } var buf bytes.Buffer if err := jpeg.Encode(&buf, img, nil); err != nil { t.Fatalf("encode test jpeg: %v", err) } return buf.Bytes() } func TestGenerateThumbnail_JPEG(t *testing.T) { data := createTestJPEG(t, 800, 600) thumb, err := GenerateThumbnail(data, ThumbnailMaxDimension) if err != nil { t.Fatalf("GenerateThumbnail: %v", err) } if len(thumb) == 0 { t.Fatal("expected non-empty thumbnail") } // Decode and verify dimensions. img, _, err := image.Decode(bytes.NewReader(thumb)) if err != nil { t.Fatalf("decode thumbnail: %v", err) } bounds := img.Bounds() if bounds.Dx() > ThumbnailMaxDimension || bounds.Dy() > ThumbnailMaxDimension { t.Fatalf("thumbnail too large: %dx%d", bounds.Dx(), bounds.Dy()) } } func TestGenerateThumbnail_SmallImage(t *testing.T) { data := createTestJPEG(t, 50, 50) thumb, err := GenerateThumbnail(data, ThumbnailMaxDimension) if err != nil { t.Fatalf("GenerateThumbnail: %v", err) } if len(thumb) == 0 { t.Fatal("expected non-empty thumbnail for small image") } } func TestGenerateThumbnail_PNG(t *testing.T) { img := image.NewRGBA(image.Rect(0, 0, 400, 300)) var buf bytes.Buffer png.Encode(&buf, img) thumb, err := GenerateThumbnail(buf.Bytes(), ThumbnailMaxDimension) if err != nil { t.Fatalf("GenerateThumbnail PNG: %v", err) } if len(thumb) == 0 { t.Fatal("expected non-empty thumbnail") } } func TestGenerateThumbnail_InvalidFormat(t *testing.T) { _, err := GenerateThumbnail([]byte("not an image"), ThumbnailMaxDimension) if err == nil { t.Fatal("expected error for invalid image data") } } func TestThumbnailPath(t *testing.T) { if got := ThumbnailPath("ab/cd/ef/photo.jpg"); got != "ab/cd/ef/photo_thumb.jpg" { t.Fatalf("ThumbnailPath = %q", got) } if got := ThumbnailPath("single.jpg"); got != "single_thumb.jpg" { t.Fatalf("ThumbnailPath = %q", got) } } func TestDetectContentType_RealJPEG(t *testing.T) { data := createTestJPEG(t, 100, 100) mime := DetectContentType(data) if mime != "image/jpeg" { t.Fatalf("mime = %q, want image/jpeg", mime) } } func TestStripEXIF_NoEXIF(t *testing.T) { data := createTestJPEG(t, 100, 100) cleaned, err := StripEXIF(data) if err != nil { t.Fatalf("StripEXIF: %v", err) } if len(cleaned) == 0 { t.Fatal("expected non-empty cleaned data") } } func TestDecodeImage(t *testing.T) { data := createTestJPEG(t, 640, 480) cfg, _, err := DecodeImage(data) if err != nil { t.Fatalf("DecodeImage: %v", err) } if cfg.Bounds().Dx() != 640 { t.Fatalf("width = %d, want 640", cfg.Bounds().Dx()) } if cfg.Bounds().Dy() != 480 { t.Fatalf("height = %d, want 480", cfg.Bounds().Dy()) } } func TestDecodeImage_Invalid(t *testing.T) { _, _, err := DecodeImage([]byte("not an image")) if err == nil { t.Fatal("expected error for invalid image") } } func TestFFmpegAvailable(t *testing.T) { FFmpegAvailable() } func TestComputeInnateTags(t *testing.T) { tests := []struct { name string mimeType string width int height int want []string }{ { name: "JPEG image", mimeType: "image/jpeg", width: 800, height: 600, want: []string{"image"}, }, { name: "GIF", mimeType: "image/gif", width: 800, height: 600, want: []string{"image", "gif"}, }, { name: "lowres image", mimeType: "image/jpeg", width: 100, height: 100, want: []string{"image", "lowres"}, }, { name: "highres image", mimeType: "image/jpeg", width: 2560, height: 1440, want: []string{"image", "highres"}, }, { name: "both lowres and highres (gif small)", mimeType: "image/gif", width: 50, height: 50, want: []string{"image", "gif", "lowres"}, }, { name: "video", mimeType: "video/mp4", width: 1920, height: 1080, want: []string{"video"}, }, { name: "PNG highres", mimeType: "image/png", width: 3840, height: 2160, want: []string{"image", "highres"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ComputeInnateTags(tt.mimeType, tt.width, tt.height, "") if len(got) != len(tt.want) { t.Fatalf("got %v, want %v", got, tt.want) } for i := range tt.want { if got[i] != tt.want[i] { t.Fatalf("got[%d] = %q, want %q; full: got=%v want=%v", i, got[i], tt.want[i], got, tt.want) } } }) } } func TestComputeInnateTags_VideoDuration(t *testing.T) { tests := []struct { name string mimeType string duration float64 want []string }{ { name: "short video", mimeType: "video/mp4", duration: 5, want: []string{"video", "<10 sec"}, }, { name: "long video", mimeType: "video/webm", duration: 30, want: []string{"video", ">10 sec"}, }, { name: "exactly 10 seconds - no duration tag", mimeType: "video/mp4", duration: 10, want: []string{"video"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Override GetVideoDuration to return our test value. orig := getVideoDuration getVideoDuration = func(string) float64 { return tt.duration } defer func() { getVideoDuration = orig }() got := ComputeInnateTags(tt.mimeType, 640, 480, "/fake/path.mp4") if len(got) != len(tt.want) { t.Fatalf("got %v, want %v", got, tt.want) } for i := range tt.want { if got[i] != tt.want[i] { t.Fatalf("got[%d] = %q, want %q; full: got=%v want=%v", i, got[i], tt.want[i], got, tt.want) } } }) } } func TestComputeInnateTags_EmptyPathNoPanic(t *testing.T) { // Passing empty videoPath should not cause issues for images. tags := ComputeInnateTags("image/jpeg", 800, 600, "") if len(tags) != 1 || tags[0] != "image" { t.Fatalf("got %v, want [image]", tags) } } func TestComputeInnateTags_VideoEmptyPath(t *testing.T) { // Videos with empty path should get no duration tags. tags := ComputeInnateTags("video/mp4", 640, 480, "") if len(tags) != 1 || tags[0] != "video" { t.Fatalf("got %v, want [video]", tags) } }