import sqlite3 import tempfile import unittest from pathlib import Path from unittest.mock import patch from fastapi.testclient import TestClient from music_server.app import create_app from music_server.services.token_service import TokenService from tests.support import auth_headers, issue_access_token class AuthRouteTests(unittest.TestCase): def _prepare_catalog_db(self, db_path: Path) -> None: conn = sqlite3.connect(db_path) conn.execute( """ create table catalog_track_files ( song_id integer not null, quality_label text not null, ext text not null, file_size_bytes integer, backend_type text not null, backend_name text not null, locator text not null, public_url text, status text not null, is_primary integer not null ) """ ) conn.executemany( """ insert into catalog_track_files ( song_id, quality_label, ext, file_size_bytes, backend_type, backend_name, locator, public_url, status, is_primary ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ (101, "super", "flac", 100, "object_storage", "cdn", "101.flac", None, "active", 1), (101, "standard", "mp3", 80, "object_storage", "cdn", "101.mp3", None, "active", 0), (102, "standard", "mp3", 90, "object_storage", "cdn", "102.mp3", None, "inactive", 1), (103, "standard", "mp3", 90, "object_storage", "cdn", "103.mp3", None, "active", 1), ], ) conn.commit() conn.close() def test_token_status_active_returns_playable_song_count(self): with tempfile.TemporaryDirectory() as tmpdir: player_db_path = Path(tmpdir) / "player.db" catalog_db_path = Path(tmpdir) / "catalog_read.db" self._prepare_catalog_db(catalog_db_path) with patch.dict( "os.environ", { "PLAYER_DB_PATH": str(player_db_path), "CATALOG_DB_PATH": str(catalog_db_path), }, clear=False, ): client = TestClient(create_app()) response = client.get( "/auth/v1/token-status", headers=auth_headers(player_db_path), ) self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["valid"]) self.assertEqual("active", payload["status"]) self.assertEqual(2, payload["playableSongCount"]) def test_token_status_client_id_missing_uses_body_status_not_401(self): with tempfile.TemporaryDirectory() as tmpdir: player_db_path = Path(tmpdir) / "player.db" catalog_db_path = Path(tmpdir) / "catalog_read.db" self._prepare_catalog_db(catalog_db_path) token = issue_access_token(player_db_path) with patch.dict( "os.environ", { "PLAYER_DB_PATH": str(player_db_path), "CATALOG_DB_PATH": str(catalog_db_path), }, clear=False, ): client = TestClient(create_app()) response = client.get( "/auth/v1/token-status", headers=auth_headers( player_db_path, token=token, include_client_id=False, ), ) self.assertEqual(200, response.status_code) payload = response.json() self.assertFalse(payload["valid"]) self.assertEqual("client_id_missing", payload["status"]) self.assertIsNone(payload["playableSongCount"]) def test_token_status_rejects_missing_bearer(self): client = TestClient(create_app()) response = client.get("/auth/v1/token-status") self.assertEqual(401, response.status_code) def test_token_status_allows_missing_bearer_when_auth_disabled(self): with tempfile.TemporaryDirectory() as tmpdir: player_db_path = Path(tmpdir) / "player.db" catalog_db_path = Path(tmpdir) / "catalog_read.db" self._prepare_catalog_db(catalog_db_path) with patch.dict( "os.environ", { "PLAYER_DB_PATH": str(player_db_path), "CATALOG_DB_PATH": str(catalog_db_path), "MUSIC_SERVER_DISABLE_AUTH": "1", }, clear=False, ): client = TestClient(create_app()) response = client.get("/auth/v1/token-status") self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["valid"]) self.assertEqual("active", payload["status"]) self.assertEqual("auth_disabled", payload["source"]) self.assertEqual(2, payload["playableSongCount"]) def test_token_status_active_degrades_when_catalog_track_files_table_missing(self): with tempfile.TemporaryDirectory() as tmpdir: player_db_path = Path(tmpdir) / "player.db" catalog_db_path = Path(tmpdir) / "catalog_read.db" sqlite3.connect(catalog_db_path).close() with patch.dict( "os.environ", { "PLAYER_DB_PATH": str(player_db_path), "CATALOG_DB_PATH": str(catalog_db_path), }, clear=False, ): client = TestClient(create_app(), raise_server_exceptions=False) response = client.get( "/auth/v1/token-status", headers=auth_headers(player_db_path), ) self.assertEqual(200, response.status_code) payload = response.json() self.assertTrue(payload["valid"]) self.assertEqual("active", payload["status"]) self.assertIsNone(payload["playableSongCount"]) def test_auth_headers_reuses_same_token_by_default_for_same_db(self): with tempfile.TemporaryDirectory() as tmpdir: player_db_path = Path(tmpdir) / "player.db" first = auth_headers(player_db_path) second = auth_headers(player_db_path) self.assertEqual(first["Authorization"], second["Authorization"]) service = TokenService(str(player_db_path)) self.assertEqual(1, len(service.list_tokens(include_revoked=True))) if __name__ == "__main__": unittest.main()