import io import runpy import tempfile import unittest from contextlib import redirect_stdout from pathlib import Path from unittest.mock import patch from music_server.services.token_service import TokenService class TokenCliTests(unittest.TestCase): def test_issue_token_prints_plaintext_token_and_expiry(self): with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "player.db" with patch.dict("os.environ", {"PLAYER_DB_PATH": str(db_path)}, clear=False): with patch("sys.argv", ["issue_token", "--days", "90", "--label", "iphone16"]): buffer = io.StringIO() with redirect_stdout(buffer): runpy.run_module("music_server.tools.issue_token", run_name="__main__") output = buffer.getvalue() self.assertIn("token_id=", output) self.assertIn("token=", output) self.assertIn("expires_at=", output) def test_unbind_and_revoke_commands_mutate_service_state(self): with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "player.db" service = TokenService(str(db_path)) issued = service.issue_token(days=90, label="android") service.authenticate(issued.plaintext_token, "client-a", "Pixel") with patch.dict("os.environ", {"PLAYER_DB_PATH": str(db_path)}, clear=False): with patch("sys.argv", ["unbind_token", "--token-id", issued.token_id]): runpy.run_module("music_server.tools.unbind_token", run_name="__main__") with patch( "sys.argv", ["revoke_token", "--token-id", issued.token_id, "--reason", "replaced"], ): runpy.run_module("music_server.tools.revoke_token", run_name="__main__") listed = service.list_tokens(include_revoked=True) self.assertIsNone(listed[0]["bound_client_id"]) self.assertEqual("replaced", listed[0]["revoked_reason"])