package recommendation import ( "context" "testing" "time" "github.com/jackc/pgx/v5/pgtype" ) func TestHomeData_NewUser_AllSectionsEmpty(t *testing.T) { pool := newPool(t) user := seedUser(t, pool, "home-newuser") got, err := HomeData(context.Background(), pool, user.ID) if err != nil { t.Fatalf("HomeData: %v", err) } if got == nil { t.Fatal("HomeData returned nil payload") } if len(got.RecentlyAddedAlbums) != 0 { t.Errorf("RecentlyAddedAlbums len = %d, want 0", len(got.RecentlyAddedAlbums)) } if len(got.RediscoverAlbums) != 0 { t.Errorf("RediscoverAlbums len = %d, want 0", len(got.RediscoverAlbums)) } if len(got.RediscoverArtists) != 0 { t.Errorf("RediscoverArtists len = %d, want 0", len(got.RediscoverArtists)) } if len(got.MostPlayedTracks) != 0 { t.Errorf("MostPlayedTracks len = %d, want 0", len(got.MostPlayedTracks)) } if len(got.LastPlayedArtists) != 0 { t.Errorf("LastPlayedArtists len = %d, want 0", len(got.LastPlayedArtists)) } } // Compile-time assert that pgtype.UUID is a valid map key — used by the // service when de-duping rediscover fallback rows. If this changes, the // service needs to switch to keying by string. var _ map[pgtype.UUID]struct{} func TestHomeData_RecentlyAdded_OrderedByCreatedAt(t *testing.T) { pool := newPool(t) user := seedUser(t, pool, "home-recents") a1 := seedArtist(t, pool, "ArtistA", "") // Three albums, oldest first — created_at increases naturally with each insert. al1 := seedAlbumForArtist(t, pool, a1.ID, "Older") al2 := seedAlbumForArtist(t, pool, a1.ID, "Middle") al3 := seedAlbumForArtist(t, pool, a1.ID, "Newest") got, err := HomeData(context.Background(), pool, user.ID) if err != nil { t.Fatalf("HomeData: %v", err) } if len(got.RecentlyAddedAlbums) != 3 { t.Fatalf("len = %d, want 3", len(got.RecentlyAddedAlbums)) } if got.RecentlyAddedAlbums[0].Album.ID != al3.ID || got.RecentlyAddedAlbums[1].Album.ID != al2.ID || got.RecentlyAddedAlbums[2].Album.ID != al1.ID { t.Errorf("order = [%s, %s, %s], want newest→oldest", got.RecentlyAddedAlbums[0].Album.Title, got.RecentlyAddedAlbums[1].Album.Title, got.RecentlyAddedAlbums[2].Album.Title) } } func TestHomeData_MostPlayed_RanksByCount_HonorsQuarantine(t *testing.T) { pool := newPool(t) user := seedUser(t, pool, "home-mostplayed") artist := seedArtist(t, pool, "ArtistB", "") album := seedAlbumForArtist(t, pool, artist.ID, "AlbumB") tHigh := seedTrackOnAlbum(t, pool, album.ID, artist.ID, "High") tLow := seedTrackOnAlbum(t, pool, album.ID, artist.ID, "Low") tQuar := seedTrackOnAlbum(t, pool, album.ID, artist.ID, "Quarantined") now := time.Now() for i := 0; i < 3; i++ { insertPlayEvent(t, pool, user.ID, tHigh.ID, now.Add(-time.Duration(i)*time.Minute)) } insertPlayEvent(t, pool, user.ID, tLow.ID, now) for i := 0; i < 5; i++ { insertPlayEvent(t, pool, user.ID, tQuar.ID, now) } if _, err := pool.Exec(context.Background(), `INSERT INTO lidarr_quarantine (user_id, track_id, reason) VALUES ($1, $2, 'bad_rip')`, user.ID, tQuar.ID, ); err != nil { t.Fatalf("insert quarantine: %v", err) } got, err := HomeData(context.Background(), pool, user.ID) if err != nil { t.Fatalf("HomeData: %v", err) } if len(got.MostPlayedTracks) != 2 { t.Fatalf("len = %d, want 2 (quarantined excluded)", len(got.MostPlayedTracks)) } if got.MostPlayedTracks[0].Track.ID != tHigh.ID || got.MostPlayedTracks[1].Track.ID != tLow.ID { t.Errorf("ranking wrong: got [%s, %s], want [High, Low]", got.MostPlayedTracks[0].Track.Title, got.MostPlayedTracks[1].Track.Title) } } func TestHomeData_RediscoverAlbums_FallbackWhenSparse(t *testing.T) { pool := newPool(t) user := seedUser(t, pool, "home-rediscover") artist := seedArtist(t, pool, "ArtistC", "") // Like one album NOW (won't satisfy >30d eligibility) — should still // surface via fallback because primary returns 0 rows. al := seedAlbumForArtist(t, pool, artist.ID, "RecentLike") if _, err := pool.Exec(context.Background(), `INSERT INTO general_likes_albums (user_id, album_id, liked_at) VALUES ($1, $2, now())`, user.ID, al.ID, ); err != nil { t.Fatalf("insert like: %v", err) } got, err := HomeData(context.Background(), pool, user.ID) if err != nil { t.Fatalf("HomeData: %v", err) } if len(got.RediscoverAlbums) != 1 { t.Fatalf("len = %d, want 1 (from fallback)", len(got.RediscoverAlbums)) } if got.RediscoverAlbums[0].Album.ID != al.ID { t.Errorf("got %s, want %s", got.RediscoverAlbums[0].Album.Title, al.Title) } }