package subsonic import ( "database/sql" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "git.noroutes.net/NoRoutes/vibetunes/internal/database" "git.noroutes.net/NoRoutes/vibetunes/internal/metadataenrich" ) // withEnrichClient temporarily swaps the package-level enrichClient for // a test double and restores it afterward, since tests run // sequentially in this package. func withEnrichClient(t *testing.T, c *metadataenrich.Client) { t.Helper() orig := enrichClient enrichClient = c t.Cleanup(func() { enrichClient = orig }) } func TestArtistInfo2EnrichedWhenEnabled(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) artist, err := database.FindOrCreateArtist(db, "Test Artist", "", "mbid-artist-1") if err != nil { t.Fatal(err) } var calls int32 mb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&calls, 1) json.NewEncoder(w).Encode(map[string]any{"disambiguation": "a real band"}) })) defer mb.Close() withEnrichClient(t, &metadataenrich.Client{MusicBrainzBaseURL: mb.URL}) s.Config.EnableExternalMetadata = true srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", artist.ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getArtistInfo2.view", q) j := decodeJSON(t, resp) info, ok := j["artistInfo2"].(map[string]any) if !ok || info["biography"] != "a real band" { t.Fatalf("artistInfo2 = %v, want biography from the mocked MusicBrainz response", j) } if info["musicBrainzId"] != "mbid-artist-1" { t.Errorf("musicBrainzId = %v", info["musicBrainzId"]) } // A second lookup must hit the cache, not MusicBrainz again. getPath(t, srv, "/rest/getArtistInfo2.view", q) if got := atomic.LoadInt32(&calls); got != 1 { t.Errorf("MusicBrainz was called %d times, want 1 (second lookup should be served from cache)", got) } } func TestArtistInfo2NotEnrichedWhenDisabled(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) artist, err := database.FindOrCreateArtist(db, "Test Artist", "", "mbid-artist-2") if err != nil { t.Fatal(err) } called := false mb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true json.NewEncoder(w).Encode(map[string]any{"disambiguation": "should never be fetched"}) })) defer mb.Close() withEnrichClient(t, &metadataenrich.Client{MusicBrainzBaseURL: mb.URL}) // EnableExternalMetadata left false (the default): must never // call out to the network, matching "must work completely offline". srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", artist.ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getArtistInfo2.view", q) j := decodeJSON(t, resp) info := j["artistInfo2"].(map[string]any) if _, has := info["biography"]; has { t.Errorf("biography should be absent when external metadata is disabled, got %v", info) } if called { t.Error("MusicBrainz must never be called when external metadata is disabled") } } func TestArtistInfo2NoMusicBrainzIDNeverCallsNetwork(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) artist, err := database.FindOrCreateArtist(db, "Untagged Artist", "", "") if err != nil { t.Fatal(err) } called := false mb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true })) defer mb.Close() withEnrichClient(t, &metadataenrich.Client{MusicBrainzBaseURL: mb.URL}) s.Config.EnableExternalMetadata = true srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", artist.ID) q.Set("f", "json") getPath(t, srv, "/rest/getArtistInfo2.view", q) if called { t.Error("must never look up an artist with no locally-tagged MusicBrainz ID") } } func TestArtistInfo2NetworkFailureLeavesFieldsBlank(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) artist, err := database.FindOrCreateArtist(db, "Test Artist", "", "mbid-artist-3") if err != nil { t.Fatal(err) } mb := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer mb.Close() withEnrichClient(t, &metadataenrich.Client{MusicBrainzBaseURL: mb.URL}) s.Config.EnableExternalMetadata = true srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", artist.ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getArtistInfo2.view", q) j := decodeJSON(t, resp) if j["status"] != nil && j["status"] != "ok" { t.Fatalf("a MusicBrainz failure must not fail the whole response: %v", j) } info := j["artistInfo2"].(map[string]any) if _, has := info["biography"]; has { t.Errorf("biography should be absent after a fetch failure, got %v", info) } } func TestAlbumInfo2EnrichedCoverArt(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) album, err := database.FindOrCreateAlbum(db, "Test Album", sql.NullString{}, "", sql.NullInt64{}, sql.NullInt64{}, "", "mbid-release-1") if err != nil { t.Fatal(err) } caa := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{ "images": []map[string]any{ {"front": true, "image": "http://example.com/full.jpg", "thumbnails": map[string]string{ "small": "http://example.com/small.jpg", "large": "http://example.com/large.jpg", }}, }, }) })) defer caa.Close() withEnrichClient(t, &metadataenrich.Client{CoverArtArchiveBaseURL: caa.URL}) s.Config.EnableExternalMetadata = true srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", album.ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getAlbumInfo2.view", q) j := decodeJSON(t, resp) info, ok := j["albumInfo"].(map[string]any) if !ok || info["largeImageUrl"] != "http://example.com/full.jpg" { t.Fatalf("albumInfo = %v, want cover art from the mocked Cover Art Archive response", j) } }