all repos — weimar @ master

Unnamed repository; edit this file 'description' to name the repository.

internal/storage/processing_test.go (view raw)

  1package storage
  2
  3import (
  4	"bytes"
  5	"image"
  6	"image/color"
  7	"image/jpeg"
  8	"image/png"
  9	"testing"
 10)
 11
 12func TestDetectContentType(t *testing.T) {
 13	tests := []struct {
 14		name string
 15		data []byte
 16		want string
 17	}{
 18		{"JPEG", []byte{0xFF, 0xD8, 0xFF, 0xE0}, "image/jpeg"},
 19		{"PNG", []byte{0x89, 0x50, 0x4E, 0x47}, "image/png"},
 20		{"GIF", []byte{0x47, 0x49, 0x46, 0x38}, "image/gif"},
 21		{"WebP", []byte{0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50}, "image/webp"},
 22		{"BMP", []byte{0x42, 0x4D}, "image/bmp"},
 23		{"MP4", []byte{0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70}, "video/mp4"},
 24		{"WebM", []byte{0x1A, 0x45, 0xDF, 0xA3}, "video/webm"},
 25		{"unknown", []byte{0x00, 0x01, 0x02}, "application/octet-stream"},
 26		{"empty", []byte{}, "application/octet-stream"},
 27	}
 28	for _, tt := range tests {
 29		t.Run(tt.name, func(t *testing.T) {
 30			got := DetectContentType(tt.data)
 31			if got != tt.want {
 32				t.Fatalf("DetectContentType = %q, want %q", got, tt.want)
 33			}
 34		})
 35	}
 36}
 37
 38func TestExtensionFromMIME(t *testing.T) {
 39	if ext := ExtensionFromMIME("image/jpeg"); ext != "jpg" {
 40		t.Fatalf("jpg extension mismatch: %s", ext)
 41	}
 42	if ext := ExtensionFromMIME("image/png"); ext != "png" {
 43		t.Fatalf("png extension mismatch: %s", ext)
 44	}
 45	if ext := ExtensionFromMIME("image/gif"); ext != "gif" {
 46		t.Fatalf("gif extension mismatch: %s", ext)
 47	}
 48	if ext := ExtensionFromMIME("video/mp4"); ext != "mp4" {
 49		t.Fatalf("mp4 extension mismatch: %s", ext)
 50	}
 51	if ext := ExtensionFromMIME("application/octet-stream"); ext != "bin" {
 52		t.Fatalf("bin extension mismatch: %s", ext)
 53	}
 54}
 55
 56func TestIsImage(t *testing.T) {
 57	if !IsImage("image/jpeg") {
 58		t.Fatal("expected jpeg to be image")
 59	}
 60	if IsImage("video/mp4") {
 61		t.Fatal("expected mp4 to not be image")
 62	}
 63}
 64
 65func TestIsVideo(t *testing.T) {
 66	if !IsVideo("video/mp4") {
 67		t.Fatal("expected mp4 to be video")
 68	}
 69	if IsVideo("image/jpeg") {
 70		t.Fatal("expected jpeg to not be video")
 71	}
 72}
 73
 74func createTestJPEG(t *testing.T, w, h int) []byte {
 75	t.Helper()
 76	img := image.NewRGBA(image.Rect(0, 0, w, h))
 77	for y := range h {
 78		for x := range w {
 79			img.Set(x, y, color.RGBA{uint8(x % 256), uint8(y % 256), 128, 255})
 80		}
 81	}
 82	var buf bytes.Buffer
 83	if err := jpeg.Encode(&buf, img, nil); err != nil {
 84		t.Fatalf("encode test jpeg: %v", err)
 85	}
 86	return buf.Bytes()
 87}
 88
 89func TestGenerateThumbnail_JPEG(t *testing.T) {
 90	data := createTestJPEG(t, 800, 600)
 91	thumb, err := GenerateThumbnail(data, ThumbnailMaxDimension)
 92	if err != nil {
 93		t.Fatalf("GenerateThumbnail: %v", err)
 94	}
 95	if len(thumb) == 0 {
 96		t.Fatal("expected non-empty thumbnail")
 97	}
 98
 99	// Decode and verify dimensions.
100	img, _, err := image.Decode(bytes.NewReader(thumb))
101	if err != nil {
102		t.Fatalf("decode thumbnail: %v", err)
103	}
104	bounds := img.Bounds()
105	if bounds.Dx() > ThumbnailMaxDimension || bounds.Dy() > ThumbnailMaxDimension {
106		t.Fatalf("thumbnail too large: %dx%d", bounds.Dx(), bounds.Dy())
107	}
108}
109
110func TestGenerateThumbnail_SmallImage(t *testing.T) {
111	data := createTestJPEG(t, 50, 50)
112	thumb, err := GenerateThumbnail(data, ThumbnailMaxDimension)
113	if err != nil {
114		t.Fatalf("GenerateThumbnail: %v", err)
115	}
116	if len(thumb) == 0 {
117		t.Fatal("expected non-empty thumbnail for small image")
118	}
119}
120
121func TestGenerateThumbnail_PNG(t *testing.T) {
122	img := image.NewRGBA(image.Rect(0, 0, 400, 300))
123	var buf bytes.Buffer
124	png.Encode(&buf, img)
125	thumb, err := GenerateThumbnail(buf.Bytes(), ThumbnailMaxDimension)
126	if err != nil {
127		t.Fatalf("GenerateThumbnail PNG: %v", err)
128	}
129	if len(thumb) == 0 {
130		t.Fatal("expected non-empty thumbnail")
131	}
132}
133
134func TestGenerateThumbnail_InvalidFormat(t *testing.T) {
135	_, err := GenerateThumbnail([]byte("not an image"), ThumbnailMaxDimension)
136	if err == nil {
137		t.Fatal("expected error for invalid image data")
138	}
139}
140
141func TestThumbnailPath(t *testing.T) {
142	if got := ThumbnailPath("ab/cd/ef/photo.jpg"); got != "ab/cd/ef/photo_thumb.jpg" {
143		t.Fatalf("ThumbnailPath = %q", got)
144	}
145	if got := ThumbnailPath("single.jpg"); got != "single_thumb.jpg" {
146		t.Fatalf("ThumbnailPath = %q", got)
147	}
148}
149
150func TestDetectContentType_RealJPEG(t *testing.T) {
151	data := createTestJPEG(t, 100, 100)
152	mime := DetectContentType(data)
153	if mime != "image/jpeg" {
154		t.Fatalf("mime = %q, want image/jpeg", mime)
155	}
156}
157
158func TestStripEXIF_NoEXIF(t *testing.T) {
159	data := createTestJPEG(t, 100, 100)
160	cleaned, err := StripEXIF(data)
161	if err != nil {
162		t.Fatalf("StripEXIF: %v", err)
163	}
164	if len(cleaned) == 0 {
165		t.Fatal("expected non-empty cleaned data")
166	}
167}
168
169func TestDecodeImage(t *testing.T) {
170	data := createTestJPEG(t, 640, 480)
171	cfg, _, err := DecodeImage(data)
172	if err != nil {
173		t.Fatalf("DecodeImage: %v", err)
174	}
175	if cfg.Bounds().Dx() != 640 {
176		t.Fatalf("width = %d, want 640", cfg.Bounds().Dx())
177	}
178	if cfg.Bounds().Dy() != 480 {
179		t.Fatalf("height = %d, want 480", cfg.Bounds().Dy())
180	}
181}
182
183func TestDecodeImage_Invalid(t *testing.T) {
184	_, _, err := DecodeImage([]byte("not an image"))
185	if err == nil {
186		t.Fatal("expected error for invalid image")
187	}
188}
189
190func TestFFmpegAvailable(t *testing.T) {
191	FFmpegAvailable()
192}
193
194func TestComputeInnateTags(t *testing.T) {
195	tests := []struct {
196		name     string
197		mimeType string
198		width    int
199		height   int
200		want     []string
201	}{
202		{
203			name:     "JPEG image",
204			mimeType: "image/jpeg",
205			width:    800,
206			height:   600,
207			want:     []string{"image"},
208		},
209		{
210			name:     "GIF",
211			mimeType: "image/gif",
212			width:    800,
213			height:   600,
214			want:     []string{"image", "gif"},
215		},
216		{
217			name:     "lowres image",
218			mimeType: "image/jpeg",
219			width:    100,
220			height:   100,
221			want:     []string{"image", "lowres"},
222		},
223		{
224			name:     "highres image",
225			mimeType: "image/jpeg",
226			width:    2560,
227			height:   1440,
228			want:     []string{"image", "highres"},
229		},
230		{
231			name:     "both lowres and highres (gif small)",
232			mimeType: "image/gif",
233			width:    50,
234			height:   50,
235			want:     []string{"image", "gif", "lowres"},
236		},
237		{
238			name:     "video",
239			mimeType: "video/mp4",
240			width:    1920,
241			height:   1080,
242			want:     []string{"video"},
243		},
244		{
245			name:     "PNG highres",
246			mimeType: "image/png",
247			width:    3840,
248			height:   2160,
249			want:     []string{"image", "highres"},
250		},
251	}
252
253	for _, tt := range tests {
254		t.Run(tt.name, func(t *testing.T) {
255			got := ComputeInnateTags(tt.mimeType, tt.width, tt.height, "")
256			if len(got) != len(tt.want) {
257				t.Fatalf("got %v, want %v", got, tt.want)
258			}
259			for i := range tt.want {
260				if got[i] != tt.want[i] {
261					t.Fatalf("got[%d] = %q, want %q; full: got=%v want=%v", i, got[i], tt.want[i], got, tt.want)
262				}
263			}
264		})
265	}
266}
267
268func TestComputeInnateTags_VideoDuration(t *testing.T) {
269	tests := []struct {
270		name     string
271		mimeType string
272		duration float64
273		want     []string
274	}{
275		{
276			name:     "short video",
277			mimeType: "video/mp4",
278			duration: 5,
279			want:     []string{"video", "<10 sec"},
280		},
281		{
282			name:     "long video",
283			mimeType: "video/webm",
284			duration: 30,
285			want:     []string{"video", ">10 sec"},
286		},
287		{
288			name:     "exactly 10 seconds - no duration tag",
289			mimeType: "video/mp4",
290			duration: 10,
291			want:     []string{"video"},
292		},
293	}
294
295	for _, tt := range tests {
296		t.Run(tt.name, func(t *testing.T) {
297			// Override GetVideoDuration to return our test value.
298			orig := getVideoDuration
299			getVideoDuration = func(string) float64 { return tt.duration }
300			defer func() { getVideoDuration = orig }()
301
302			got := ComputeInnateTags(tt.mimeType, 640, 480, "/fake/path.mp4")
303			if len(got) != len(tt.want) {
304				t.Fatalf("got %v, want %v", got, tt.want)
305			}
306			for i := range tt.want {
307				if got[i] != tt.want[i] {
308					t.Fatalf("got[%d] = %q, want %q; full: got=%v want=%v", i, got[i], tt.want[i], got, tt.want)
309				}
310			}
311		})
312	}
313}
314
315func TestComputeInnateTags_EmptyPathNoPanic(t *testing.T) {
316	// Passing empty videoPath should not cause issues for images.
317	tags := ComputeInnateTags("image/jpeg", 800, 600, "")
318	if len(tags) != 1 || tags[0] != "image" {
319		t.Fatalf("got %v, want [image]", tags)
320	}
321}
322
323func TestComputeInnateTags_VideoEmptyPath(t *testing.T) {
324	// Videos with empty path should get no duration tags.
325	tags := ComputeInnateTags("video/mp4", 640, 480, "")
326	if len(tags) != 1 || tags[0] != "video" {
327		t.Fatalf("got %v, want [video]", tags)
328	}
329}