diff --git a/internal/api/home.go b/internal/api/home.go new file mode 100644 index 00000000..d1284e2a --- /dev/null +++ b/internal/api/home.go @@ -0,0 +1,55 @@ +package api + +import ( + "net/http" + + "git.fabledsword.com/bvandeusen/minstrel/internal/auth" + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" + "git.fabledsword.com/bvandeusen/minstrel/internal/recommendation" +) + +// handleGetHome implements GET /api/home. Returns five sections in one +// payload; sections that have no rows render as []. 500 on any underlying +// DB failure — the SPA should render either empty states or full content, +// not a half-broken page. +func (h *handlers) handleGetHome(w http.ResponseWriter, r *http.Request) { + user, ok := auth.UserFromContext(r.Context()) + if !ok { + writeErr(w, http.StatusUnauthorized, "unauthorized", "authentication required") + return + } + data, err := recommendation.HomeData(r.Context(), h.pool, user.ID) + if err != nil { + h.logger.Error("api: home data", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "failed to load home") + return + } + + out := HomePayload{ + RecentlyAddedAlbums: make([]AlbumRef, 0, len(data.RecentlyAddedAlbums)), + RediscoverAlbums: make([]AlbumRef, 0, len(data.RediscoverAlbums)), + RediscoverArtists: make([]ArtistRef, 0, len(data.RediscoverArtists)), + MostPlayedTracks: make([]TrackRef, 0, len(data.MostPlayedTracks)), + LastPlayedArtists: make([]ArtistRef, 0, len(data.LastPlayedArtists)), + } + for _, row := range data.RecentlyAddedAlbums { + out.RecentlyAddedAlbums = append(out.RecentlyAddedAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0)) + } + for _, row := range data.RediscoverAlbums { + out.RediscoverAlbums = append(out.RediscoverAlbums, albumRefFrom(row.Album, row.ArtistName, 0, 0)) + } + for _, row := range data.RediscoverArtists { + out.RediscoverArtists = append(out.RediscoverArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID)) + } + for _, row := range data.MostPlayedTracks { + out.MostPlayedTracks = append(out.MostPlayedTracks, trackRefFrom(row.Track, row.AlbumTitle, row.ArtistName)) + } + for _, row := range data.LastPlayedArtists { + out.LastPlayedArtists = append(out.LastPlayedArtists, artistRefFromCovered(row.Artist, int(row.AlbumCount), row.CoverAlbumID)) + } + writeJSON(w, http.StatusOK, out) +} + +// Compile-time guard that dbq has the row types we need; if the SQL is +// regenerated with a different name, this fails fast at build time. +var _ = dbq.ListMostPlayedTracksForUserRow{}.Track diff --git a/internal/api/home_test.go b/internal/api/home_test.go new file mode 100644 index 00000000..2f953f9b --- /dev/null +++ b/internal/api/home_test.go @@ -0,0 +1,79 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" +) + +func newHomeRouter(h *handlers) chi.Router { + r := chi.NewRouter() + r.Get("/api/home", h.handleGetHome) + return r +} + +func doGetHome(h *handlers, user dbq.User) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodGet, "/api/home", nil) + req = withUser(req, user) + w := httptest.NewRecorder() + newHomeRouter(h).ServeHTTP(w, req) + return w +} + +func TestHome_EmptyForNewUser(t *testing.T) { + h, pool := testHandlers(t) + user := seedUser(t, pool, "alice", "pw", false) + + w := doGetHome(h, user) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String()) + } + var got HomePayload + if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { + t.Fatalf("decode: %v", err) + } + // All slices must be non-nil so the SPA branches consistently on + // length rather than dealing with null. + if got.RecentlyAddedAlbums == nil || got.RediscoverAlbums == nil || + got.RediscoverArtists == nil || got.MostPlayedTracks == nil || + got.LastPlayedArtists == nil { + t.Errorf("payload has nil slice: %+v", got) + } + if len(got.RecentlyAddedAlbums) != 0 { + t.Errorf("RecentlyAddedAlbums len = %d, want 0", len(got.RecentlyAddedAlbums)) + } +} + +func TestHome_RecentlyAddedSurfacesAlbums(t *testing.T) { + h, pool := testHandlers(t) + user := seedUser(t, pool, "alice", "pw", false) + q := dbq.New(pool) + artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{ + Name: "ArtistOne", SortName: "ArtistOne", + }) + _, _ = q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{ + Title: "Newest", SortTitle: "Newest", ArtistID: artist.ID, + }) + + w := doGetHome(h, user) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + var got HomePayload + _ = json.Unmarshal(w.Body.Bytes(), &got) + if len(got.RecentlyAddedAlbums) != 1 { + t.Fatalf("len = %d, want 1", len(got.RecentlyAddedAlbums)) + } + if got.RecentlyAddedAlbums[0].Title != "Newest" { + t.Errorf("title = %q, want Newest", got.RecentlyAddedAlbums[0].Title) + } + if got.RecentlyAddedAlbums[0].ArtistName != "ArtistOne" { + t.Errorf("artist_name = %q, want ArtistOne", got.RecentlyAddedAlbums[0].ArtistName) + } +}