package media import ( "bytes" "encoding/json" "image" "image/jpeg" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" ) func init() { gin.SetMode(gin.TestMode) } func newTestHandler(t *testing.T) (*MediaHandler, string) { t.Helper() dir := t.TempDir() return NewMediaHandler(dir), dir } func testJPEGBytes(t *testing.T) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 100, 60)) var buf bytes.Buffer if err := jpeg.Encode(&buf, img, nil); err != nil { t.Fatalf("encode jpeg: %v", err) } return buf.Bytes() } func multipartUpload(t *testing.T, field, filename string, data []byte) *http.Request { t.Helper() var body bytes.Buffer w := multipart.NewWriter(&body) fw, err := w.CreateFormFile(field, filename) if err != nil { t.Fatalf("create form file: %v", err) } fw.Write(data) w.Close() req, _ := http.NewRequest(http.MethodPost, "/admin/upload/image", &body) req.Header.Set("Content-Type", w.FormDataContentType()) return req } // TestUploadValid verifies a valid JPEG upload returns success with a /media/ URL. func TestUploadValid(t *testing.T) { h, dir := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = multipartUpload(t, "image", "photo.jpg", testJPEGBytes(t)) h.HandleUpload(c) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { t.Fatalf("parse response: %v", err) } if resp["success"] != float64(1) { t.Fatalf("expected success=1, got %v", resp) } file, ok := resp["file"].(map[string]interface{}) if !ok { t.Fatal("expected file object in response") } url, _ := file["url"].(string) if len(url) < 7 || url[:7] != "/media/" { t.Fatalf("expected /media/ URL, got %q", url) } entries, _ := os.ReadDir(dir) if len(entries) == 0 { t.Fatal("expected original to be written to storage dir") } } // TestUploadInvalidMIME rejects non-image content. func TestUploadInvalidMIME(t *testing.T) { h, _ := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = multipartUpload(t, "image", "evil.txt", []byte("not an image at all")) h.HandleUpload(c) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", rr.Code) } } // TestUploadWrongField rejects request missing the "image" field. func TestUploadWrongField(t *testing.T) { h, _ := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = multipartUpload(t, "file", "photo.jpg", testJPEGBytes(t)) h.HandleUpload(c) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", rr.Code) } } // TestServeCacheHit serves a pre-existing cache file directly. func TestServeCacheHit(t *testing.T) { h, dir := newTestHandler(t) if err := os.WriteFile(filepath.Join(dir, "abc123.webp"), []byte("FAKE_WEBP"), 0644); err != nil { t.Fatal(err) } rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = httptest.NewRequest(http.MethodGet, "/media/abc123.webp", nil) c.Params = gin.Params{{Key: "filepath", Value: "/abc123.webp"}} h.HandleServe(c) if rr.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rr.Code) } if ct := rr.Header().Get("Content-Type"); ct != "image/webp" { t.Fatalf("expected image/webp, got %q", ct) } } // TestServeNotFound returns 404 when no original exists. func TestServeNotFound(t *testing.T) { h, _ := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = httptest.NewRequest(http.MethodGet, "/media/missing.webp", nil) c.Params = gin.Params{{Key: "filepath", Value: "/missing.webp"}} h.HandleServe(c) if rr.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", rr.Code) } } // TestServeInvalidWidth returns 400 for non-positive width. func TestServeInvalidWidth(t *testing.T) { h, _ := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = httptest.NewRequest(http.MethodGet, "/media/abc.webp?w=0", nil) c.Params = gin.Params{{Key: "filepath", Value: "/abc.webp"}} h.HandleServe(c) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", rr.Code) } } // TestServeUnknownFormat returns 404 for unrecognised extensions. func TestServeUnknownFormat(t *testing.T) { h, _ := newTestHandler(t) rr := httptest.NewRecorder() c, _ := gin.CreateTestContext(rr) c.Request = httptest.NewRequest(http.MethodGet, "/media/abc.png", nil) c.Params = gin.Params{{Key: "filepath", Value: "/abc.png"}} h.HandleServe(c) if rr.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", rr.Code) } } // TestCacheKey verifies the cache filename scheme. func TestCacheKey(t *testing.T) { cases := []struct { base, format string width int want string }{ {"uuid1", "webp", 0, "uuid1.webp"}, {"uuid1", "webp", 800, "uuid1_800w.webp"}, {"uuid1", "jpeg", 0, "uuid1.jpg"}, {"uuid1", "jpeg", 400, "uuid1_400w.jpg"}, } for _, tc := range cases { got := cacheKey(tc.base, tc.width, tc.format) if got != tc.want { t.Errorf("cacheKey(%q, %d, %q) = %q, want %q", tc.base, tc.width, tc.format, got, tc.want) } } }