package api import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "github.com/go-chi/chi/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/auth" "git.fabledsword.com/bvandeusen/minstrel/internal/catalogue" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/musicbrainz" "git.fabledsword.com/bvandeusen/minstrel/internal/wanted" ) func doWantedReq(t *testing.T, h *handlers, path string, user dbq.User) *httptest.ResponseRecorder { t.Helper() r := chi.NewRouter() r.Route("/api/admin", func(admin chi.Router) { admin.Use(auth.RequireAdmin()) admin.Get("/wanted", h.handleListWants) admin.Get("/wanted/{id}/tracks", h.handleWantTracks) }) w := httptest.NewRecorder() r.ServeHTTP(w, withUser(httptest.NewRequest(http.MethodGet, path, nil), user)) return w } func seedWant(t *testing.T, h *handlers, group, artistMBID, artist, title, date string) dbq.Want { t.Helper() w, err := wanted.WantWith(context.Background(), dbq.New(h.pool), wanted.WantParams{ GroupMBID: group, ArtistMBID: artistMBID, ArtistName: artist, Title: title, PrimaryType: "Album", FirstReleaseDate: date, Reason: wanted.ReasonFollow, }) if err != nil { t.Fatal(err) } return w } func listWanted(t *testing.T, h *handlers, user dbq.User, q string) wantsPage { t.Helper() w := doWantedReq(t, h, "/api/admin/wanted?"+url.Values{"q": {q}}.Encode(), user) if w.Code != http.StatusOK { t.Fatalf("status = %d; body = %s", w.Code, w.Body.String()) } var page wantsPage if err := json.Unmarshal(w.Body.Bytes(), &page); err != nil { t.Fatal(err) } return page } // The list runs like the library (#5572): artists A–Z by the library's sort // name where the library holds the artist, each artist's albums oldest // first; q narrows it to an artist or an album. func TestListWants_ArtistsAToZThenTheirAlbumsByDate(t *testing.T) { h, _ := testHandlersWithClientFn(t) admin := seedUser(t, h.pool, "admin", "pw", true) ctx := context.Background() beatles := "a0000000-0000-4000-8000-000000000002" if _, err := dbq.New(h.pool).UpsertArtist(ctx, dbq.UpsertArtistParams{Name: "The Beatles", SortName: "Beatles, The", Mbid: &beatles}); err != nil { t.Fatal(err) } seedWant(t, h, "g0000000-0000-4000-8000-000000000001", "a0000000-0000-4000-8000-000000000003", "Zeds", "Zed Album", "2001") seedWant(t, h, "g0000000-0000-4000-8000-000000000002", beatles, "The Beatles", "Abbey Road", "1969-09-26") seedWant(t, h, "g0000000-0000-4000-8000-000000000003", beatles, "The Beatles", "Please Please Me", "1963-03-22") seedWant(t, h, "g0000000-0000-4000-8000-000000000004", "a0000000-0000-4000-8000-000000000001", "Camel", "Moonmadness", "1976") page := listWanted(t, h, admin, "") var got []string for _, it := range page.Items { got = append(got, it.ArtistName+"/"+it.Title) } want := "[The Beatles/Please Please Me The Beatles/Abbey Road Camel/Moonmadness Zeds/Zed Album]" if fmt.Sprint(got) != want { t.Errorf("order = %v, want %s", got, want) } if page.Items[0].ArtistID == "" || page.Items[2].ArtistID != "" { t.Errorf("artist ids = %q, %q: only an artist the library holds has one", page.Items[0].ArtistID, page.Items[2].ArtistID) } page = listWanted(t, h, admin, "BEATLES") if len(page.Items) != 2 || page.Total != 2 { t.Errorf("q=BEATLES: %d items, total %d; want the two Beatles albums", len(page.Items), page.Total) } page = listWanted(t, h, admin, "moon") if len(page.Items) != 1 || page.Items[0].Title != "Moonmadness" || page.Total != 1 { t.Errorf("q=moon: %+v, total %d", page.Items, page.Total) } if page = listWanted(t, h, admin, "nothing like it"); len(page.Items) != 0 || page.Total != 0 { t.Errorf("no match: %d items, total %d", len(page.Items), page.Total) } } // fakeEditions is a MusicBrainz holding one group with two editions, the // shorter official one the standard, or 503 when down is set. func fakeEditions(t *testing.T, group string, down bool) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if down { w.WriteHeader(http.StatusServiceUnavailable) return } switch { case r.URL.Path == "/release-group/"+group: _, _ = fmt.Fprintf(w, `{"id": %q, "title": "Moonmadness", "primary-type": "Album", "first-release-date": "1976"}`, group) case r.URL.Path == "/release" && r.URL.Query().Get("release-group") == group: _, _ = fmt.Fprint(w, `{"release-count": 2, "releases": [ {"id": "r-deluxe", "title": "Moonmadness", "status": "Official", "date": "2009", "country": "GB", "media": [{"position": 1, "format": "CD", "tracks": [ {"position": 1, "number": "1", "title": "Aristillus", "length": 120000, "recording": {"id": "rec-1"}}, {"position": 2, "number": "2", "title": "Song Within a Song", "length": 433000, "recording": {"id": "rec-2"}}, {"position": 3, "number": "3", "title": "Bonus", "length": 200000, "recording": {"id": "rec-3"}}]}]}, {"id": "r-orig", "title": "Moonmadness", "status": "Official", "date": "1976-03", "country": "GB", "media": [{"position": 1, "format": "12\" Vinyl", "tracks": [ {"position": 1, "number": "A1", "title": "Aristillus", "length": 120000, "recording": {"id": "rec-1"}}, {"position": 2, "number": "A2", "title": "Song Within a Song", "length": 433000, "recording": {"id": "rec-2"}}]}]}]}`) default: w.WriteHeader(http.StatusNotFound) } })) t.Cleanup(srv.Close) return srv } func wantTracks(t *testing.T, h *handlers, user dbq.User, id string) (int, wantTracksView) { t.Helper() w := doWantedReq(t, h, "/api/admin/wanted/"+id+"/tracks", user) var out wantTracksView if w.Code == http.StatusOK { if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatal(err) } } return w.Code, out } // An album not yet held shows MusicBrainz's standard edition: the shortest // official one. func TestWantTracks_StandardEditionFromMusicBrainz(t *testing.T) { const group = "g0000000-0000-4000-8000-000000000010" for _, down := range []bool{false, true} { h, pool := testHandlersWithClientFn(t) admin := seedUser(t, h.pool, "admin", "pw", true) h.catalogue = catalogue.New(pool, &musicbrainz.Client{BaseURL: fakeEditions(t, group, down).URL, Pace: &musicbrainz.Pacer{}, Retries: -1}, h.logger) w := seedWant(t, h, group, "a0000000-0000-4000-8000-000000000001", "Camel", "Moonmadness", "1976") code, out := wantTracks(t, h, admin, uuidToString(w.ID)) if down { if code != http.StatusServiceUnavailable { t.Errorf("MusicBrainz down: status %d, want 503", code) } continue } if code != http.StatusOK || out.Source != "musicbrainz" || len(out.Tracks) != 2 { t.Fatalf("status %d, %+v: want the two-track original", code, out) } if out.Edition != "1976 · 12\" Vinyl · GB" { t.Errorf("edition = %q", out.Edition) } if tr := out.Tracks[1]; tr.Disc != 1 || tr.Number != 2 || tr.Title != "Song Within a Song" || tr.LengthMs != 433000 { t.Errorf("track 2 = %+v", tr) } } } // A held album shows the library's own tracks, a lost file marked. func TestWantTracks_HeldAlbumShowsTheLibrarysTracks(t *testing.T) { h, _ := testHandlersWithClientFn(t) admin := seedUser(t, h.pool, "admin", "pw", true) ctx := context.Background() q := dbq.New(h.pool) mbid := "a0000000-0000-4000-8000-000000000001" artist, err := q.UpsertArtist(ctx, dbq.UpsertArtistParams{Name: "Camel", SortName: "Camel", Mbid: &mbid}) if err != nil { t.Fatal(err) } album, err := q.UpsertAlbum(ctx, dbq.UpsertAlbumParams{Title: "Moonmadness", SortTitle: "Moonmadness", ArtistID: artist.ID}) if err != nil { t.Fatal(err) } for i, title := range []string{"Aristillus", "Song Within a Song"} { num := int32(i + 1) if _, err := q.UpsertTrack(ctx, dbq.UpsertTrackParams{ Title: title, AlbumID: album.ID, ArtistID: artist.ID, TrackNumber: &num, DurationMs: 120000, FilePath: fmt.Sprintf("/music/camel/%02d.flac", i+1), FileSize: 1, FileFormat: "flac", }); err != nil { t.Fatal(err) } } if _, err := h.pool.Exec(ctx, `UPDATE tracks SET missing_since = now() WHERE title = 'Aristillus'`); err != nil { t.Fatal(err) } w := seedWant(t, h, "g0000000-0000-4000-8000-000000000011", mbid, "Camel", "Moonmadness", "1976") if err := q.FulfillWant(ctx, dbq.FulfillWantParams{ID: w.ID, AlbumID: album.ID}); err != nil { t.Fatal(err) } code, out := wantTracks(t, h, admin, uuidToString(w.ID)) if code != http.StatusOK || out.Source != "library" || len(out.Tracks) != 2 { t.Fatalf("status %d, %+v", code, out) } if !out.Tracks[0].Missing || out.Tracks[1].Missing || out.Tracks[1].Number != 2 { t.Errorf("tracks = %+v: the first is missing, the second on disk", out.Tracks) } }