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}