package server import ( "bytes" "encoding/json" "image" "image/png" "io" "net/http" "net/http/httptest" "path/filepath" "testing" "music-server/internal/backend" "music-server/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func pngImage(t *testing.T) []byte { t.Helper() var buf bytes.Buffer require.NoError(t, png.Encode(&buf, image.NewRGBA(image.Rect(0, 0, 2, 3)))) return buf.Bytes() } // newFakeAniList answers AniList lookups by ID: ID 42 has a cover, others do not. func newFakeAniList(t *testing.T) string { img := pngImage(t) var server *httptest.Server server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { _, _ = w.Write(img) return } var req struct { Variables struct { ID int `json:"id"` } `json:"variables"` } _ = json.NewDecoder(r.Body).Decode(&req) media := any(nil) if req.Variables.ID == 42 { media = map[string]any{"id": 42, "coverImage": map[string]any{"extraLarge": server.URL + "/cover.png"}} } _ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"Media": media}}) })) t.Cleanup(server.Close) return server.URL } func withCovers(t *testing.T) func(*config.Config) { aniList := newFakeAniList(t) dir := filepath.Join(t.TempDir(), "covers") return func(c *config.Config) { c.CoversPath = dir c.AniListURL = aniList } } func (e *testEnv) put(path, contentType string, body []byte) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPut, path, bytes.NewReader(body)) req.Header.Set("Content-Type", contentType) rec := httptest.NewRecorder() e.handler.ServeHTTP(rec, req) return rec } func TestCoverUploadAndGet(t *testing.T) { e := newTestEnv(t, withCovers(t)) song := e.addSong("Game A", "Song", "x") path := "/api/v1/soundtracks/" + song.SoundtrackUuid.String() + "/cover" assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, path).Code, "no cover yet") img := pngImage(t) rec := e.put(path, "image/png", img) require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String()) rec = e.do(http.MethodGet, path) assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "image/png", rec.Header().Get("Content-Type")) assert.Equal(t, img, rec.Body.Bytes()) assert.NotEmpty(t, rec.Header().Get("Last-Modified")) var soundtrack backend.Soundtrack require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/soundtracks/"+song.SoundtrackUuid.String()).Body.Bytes(), &soundtrack)) assert.NotNil(t, soundtrack.CoverModified) assert.Equal(t, http.StatusBadRequest, e.put(path, "image/png", []byte("not an image")).Code) big := append(pngImage(t), make([]byte, backend.MaxCoverBytes)...) assert.Equal(t, http.StatusRequestEntityTooLarge, e.put(path, "image/png", big).Code) assert.Equal(t, http.StatusNotFound, e.put("/api/v1/soundtracks/00000000-0000-0000-0000-000000000000/cover", "image/png", img).Code) } func TestCoverPickFromSource(t *testing.T) { e := newTestEnv(t, withCovers(t)) song := e.addSong("Game A", "Song", "x") path := "/api/v1/soundtracks/" + song.SoundtrackUuid.String() + "/cover" rec := e.doBody(http.MethodPut, path+"/source", `{"source":"anilist","id":"42"}`) require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String()) assert.Equal(t, http.StatusOK, e.do(http.MethodGet, path).Code) assert.Equal(t, http.StatusNotFound, e.doBody(http.MethodPut, path+"/source", `{"source":"anilist","id":"7"}`).Code) assert.Equal(t, http.StatusBadRequest, e.doBody(http.MethodPut, path+"/source", `{"source":"steamgriddb","id":"7"}`).Code, "SteamGridDB needs an API key") assert.Equal(t, http.StatusUnprocessableEntity, e.doBody(http.MethodPut, path+"/source", `{"source":"igdb","id":"7"}`).Code) } func TestCoverDelete(t *testing.T) { e := newTestEnv(t, withCovers(t)) song := e.addSong("Game A", "Song", "x") path := "/api/v1/soundtracks/" + song.SoundtrackUuid.String() + "/cover" require.Equal(t, http.StatusNoContent, e.put(path, "image/png", pngImage(t)).Code) assert.Equal(t, http.StatusNoContent, e.do(http.MethodDelete, path).Code) assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, path).Code) } func TestCoversDisabled(t *testing.T) { e := newTestEnv(t) song := e.addSong("Game A", "Song", "x") path := "/api/v1/soundtracks/" + song.SoundtrackUuid.String() + "/cover" assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, path).Code) assert.Equal(t, http.StatusServiceUnavailable, e.put(path, "image/png", pngImage(t)).Code) assert.Equal(t, http.StatusServiceUnavailable, e.do(http.MethodDelete, path).Code) rec := e.doBody(http.MethodPut, path+"/source", `{"source":"anilist","id":"42"}`) assert.Equal(t, http.StatusServiceUnavailable, rec.Code) body, _ := io.ReadAll(rec.Body) assert.Contains(t, string(body), "not enabled") } func TestCoverWritesRequireToken(t *testing.T) { e := newTestEnv(t, withAuth("secret"), withCovers(t)) song := e.addSong("Game A", "Song", "x") path := "/api/v1/soundtracks/" + song.SoundtrackUuid.String() + "/cover" assert.Equal(t, http.StatusUnauthorized, e.put(path, "image/png", pngImage(t)).Code) assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodDelete, path).Code) assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, path).Code) }