soulsync/tests/test_profile_spotify_resolution.py
BoulderBadgeDad e8bd9c8018 Profiles: per-profile Spotify self-auth (shared app) + My Accounts modal + read wiring
First service of the per-profile playlist-auth feature. Each profile connects
its OWN Spotify account through the shared (admin's) app, getting its own token;
used for that profile's playlist reads. Admin + unconnected profiles + all
background workers keep using the global/admin client — fully non-regressive.

- Shared-app OAuth: get_spotify_client_for_profile + the /auth/spotify init &
  callback now use the GLOBAL app creds (falling back from any legacy per-profile
  app creds) with the profile's own token cache, and show_dialog=true forces the
  account chooser so a user can't silently inherit the admin's Spotify session.
  The builder gates on the profile's own token cache existing — no cache → global.
- My Accounts modal (new, all-profile-accessible via the profile bar): one-click
  Connect/Disconnect Spotify + connection status (account name). GET
  /api/profiles/me/connections + POST .../spotify/disconnect; admin's Spotify is
  read-only here (managed in Settings).
- Wired the request-scoped reads to the per-profile client: the playlist LIST,
  the playlist TRACKS view, liked-songs count, and user info — so a connected
  user sees and opens THEIR OWN (incl. private) playlists, not the admin's.

Tests: builder falls back to the global client for admin/None/unconnected (the
non-regression guarantee); connections status reports unconnected; admin
disconnect rejected. 124 profile/spotify/gate/integrity tests pass.

Still on the global account (next step): sync/download jobs run in background
workers with no profile context — stamping the requesting profile onto the job
is the remaining wiring. Other services (Tidal/Deezer/Qobuz/Last.fm/ListenBrainz)
follow this same pattern.
2026-06-10 12:21:17 -07:00

29 lines
1.1 KiB
Python

"""Per-profile Spotify client resolution falls back safely (shared-app model).
The builder must return the GLOBAL client for admin (profile 1) and for any
non-admin profile that hasn't connected its own Spotify (no token cache) — so
background workers and existing users are unaffected. A per-profile client only
appears once that profile has its own .spotify_cache_profile_<id>.
"""
from __future__ import annotations
import os
from core.metadata import registry
def test_admin_and_none_use_global_client():
g = registry.get_spotify_client()
assert registry.get_spotify_client_for_profile(1) is g
assert registry.get_spotify_client_for_profile(None) is g
def test_unconnected_profile_falls_back_to_global(tmp_path, monkeypatch):
# A non-admin profile with no token cache must resolve to the global client.
g = registry.get_spotify_client()
# ensure no stray cache file for this id
pid = 987654
cache = f"config/.spotify_cache_profile_{pid}"
assert not os.path.exists(cache)
assert registry.get_spotify_client_for_profile(pid) is g