"""Unit: host Claude auth extraction.""" from __future__ import annotations import json import tempfile import unittest from datetime import datetime, timezone from pathlib import Path from bot_bottle.contrib.claude.claude_auth import ( claude_auth_path, claude_host_access_token, ) from bot_bottle.log import Die class TestClaudeHostAccessToken(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory(prefix="bb-claude-auth.") self.home = Path(self.tmp.name) self.cred_dir = self.home / ".claude" self.cred_dir.mkdir() self.auth_path = self.cred_dir / ".credentials.json" def tearDown(self): self.tmp.cleanup() def _write(self, payload: dict) -> None: # type: ignore self.auth_path.write_text(json.dumps(payload)) def test_auth_path_uses_home_env(self): self.assertEqual( self.auth_path, claude_auth_path({"HOME": str(self.home)}), ) def test_returns_access_token(self): key = "sk-ant-oat01-real-key" self._write({"claudeAiOauth": {"accessToken": key}}) out = claude_host_access_token({"HOME": str(self.home)}) self.assertEqual(key, out) def test_missing_auth_file_dies(self): with self.assertRaises(Die): # Use a home with no .credentials.json empty_home = self.home / "empty" empty_home.mkdir() claude_host_access_token({"HOME": str(empty_home)}) def test_missing_claude_ai_oauth_dies(self): self._write({"hasCompletedOnboarding": True}) with self.assertRaises(Die): claude_host_access_token({"HOME": str(self.home)}) def test_missing_access_token_dies(self): self._write({"claudeAiOauth": {"expiresAt": 2000000000000}}) with self.assertRaises(Die): claude_host_access_token({"HOME": str(self.home)}) def test_empty_access_token_dies(self): self._write({"claudeAiOauth": {"accessToken": ""}}) with self.assertRaises(Die): claude_host_access_token({"HOME": str(self.home)}) def test_expired_token_dies(self): # expiresAt is in milliseconds; 1000000 ms = year 1970 self._write({ "claudeAiOauth": { "accessToken": "sk-ant-oat01-x", "expiresAt": 1000000, }, }) with self.assertRaises(Die): claude_host_access_token( {"HOME": str(self.home)}, now=datetime(2026, 1, 1, tzinfo=timezone.utc), ) def test_future_expiry_is_accepted(self): key = "sk-ant-oat01-y" # 2000000000000 ms = ~year 2033 self._write({ "claudeAiOauth": { "accessToken": key, "expiresAt": 2000000000000, }, }) out = claude_host_access_token( {"HOME": str(self.home)}, now=datetime(2026, 1, 1, tzinfo=timezone.utc), ) self.assertEqual(key, out) def test_absent_expiry_field_is_accepted(self): key = "sk-ant-oat01-z" self._write({"claudeAiOauth": {"accessToken": key}}) out = claude_host_access_token({"HOME": str(self.home)}) self.assertEqual(key, out) def test_non_json_file_dies(self): self.auth_path.write_text("not json {{{") with self.assertRaises(Die): claude_host_access_token({"HOME": str(self.home)}) def test_json_array_root_dies(self): self.auth_path.write_text("[]") with self.assertRaises(Die): claude_host_access_token({"HOME": str(self.home)}) def test_extra_fields_in_credentials_are_ignored(self): key = "sk-ant-oat01-real" self._write({ "claudeAiOauth": { "accessToken": key, "refreshToken": "sk-ant-ort01-secret", "scopes": ["user:inference", "user:profile"], "expiresAt": 2000000000000, }, }) out = claude_host_access_token({"HOME": str(self.home)}) self.assertEqual(key, out) if __name__ == "__main__": unittest.main()