"""provider OAuth(PKCE) 서비스 단위 테스트 — 실제 네트워크 호출 없음.""" import unittest from types import SimpleNamespace from unittest.mock import patch from app.services import provider_oauth as svc class _FakeResponse: def __init__(self, payload, status_code=200): self._payload = payload self.status_code = status_code def raise_for_status(self): if self.status_code >= 400: import httpx raise httpx.HTTPStatusError("err", request=None, response=None) def json(self): return self._payload class _FakeAsyncClient: def __init__(self, payload): self._payload = payload self.captured = {} async def __aenter__(self): return self async def __aexit__(self, *exc): return None async def post(self, url, **kwargs): self.captured = {"url": url, **kwargs} return _FakeResponse(self._payload) class StartOAuthTest(unittest.TestCase): def setUp(self): svc._PENDING.clear() def tearDown(self): svc._PENDING.clear() def test_unsupported_provider_rejected(self): with self.assertRaises(svc.ProviderCredentialError): svc.start_oauth("codex", "admin@example.com") def test_claude_authorize_url_contains_pkce_and_state(self): result = svc.start_oauth("claude", "admin@example.com") self.assertIn("https://claude.ai/oauth/authorize", result["authorize_url"]) self.assertIn("code_challenge_method=S256", result["authorize_url"]) self.assertIn(f"&state={result['state']}", result["authorize_url"]) self.assertIn("user%3Ainference", result["authorize_url"]) self.assertIn(result["state"], svc._PENDING) def test_openrouter_authorize_url_is_headless(self): result = svc.start_oauth("openrouter", "admin@example.com") self.assertIn("https://openrouter.ai/auth?", result["authorize_url"]) self.assertIn("code_challenge_method=S256", result["authorize_url"]) self.assertNotIn("callback_url", result["authorize_url"]) def test_pending_expires(self): import time result = svc.start_oauth("claude", "admin@example.com") attempt = svc._PENDING[result["state"]] stale = svc._PendingOAuth( provider=attempt.provider, code_verifier=attempt.code_verifier, created_by=attempt.created_by, created_at=time.time() - svc.OAUTH_PENDING_TTL_SECONDS - 1, ) svc._PENDING[result["state"]] = stale with self.assertRaises(svc.ProviderCredentialError): svc._pop_pending("claude", result["state"]) class FinishOAuthTest(unittest.IsolatedAsyncioTestCase): def setUp(self): svc._PENDING.clear() def tearDown(self): svc._PENDING.clear() async def test_claude_code_state_exchange_and_save(self): start = svc.start_oauth("claude", "admin@example.com") fake = _FakeAsyncClient({"access_token": "sk-ant-oat-token-abc", "expires_in": 31536000}) with patch.object(svc.httpx, "AsyncClient", return_value=fake), patch.object( svc, "save_credential" ) as save, patch.object(svc, "push_credentials_to_gateway") as push: save.return_value = SimpleNamespace( provider="claude", token_hint="…oat", auth_kind="oauth_token", updated_by="admin@example.com", updated_at=1.0, ) push.return_value = {"synced": True} result = await svc.finish_oauth( "claude", f"auth-code-123#{start['state']}", start["state"], "admin@example.com" ) self.assertEqual(result["stored"].auth_kind, "oauth_token") sent = fake.captured["json"] self.assertEqual(sent["grant_type"], "authorization_code") self.assertEqual(sent["code"], "auth-code-123") self.assertIn("code_verifier", sent) self.assertEqual(fake.captured["url"], svc._CLAUDE_OAUTH["token_url"]) async def test_state_mismatch_rejected(self): start = svc.start_oauth("claude", "admin@example.com") with self.assertRaises(svc.ProviderCredentialError): await svc.finish_oauth("claude", "code#other-state", start["state"], "a@b.com") async def test_openrouter_code_exchange_returns_api_key(self): start = svc.start_oauth("openrouter", "admin@example.com") fake = _FakeAsyncClient({"key": "sk-or-v1-oauth-key"}) with patch.object(svc.httpx, "AsyncClient", return_value=fake), patch.object( svc, "save_credential" ) as save, patch.object(svc, "push_credentials_to_gateway") as push: save.return_value = SimpleNamespace( provider="openrouter", token_hint="…key", auth_kind="api_key", updated_by="admin@example.com", updated_at=1.0, ) push.return_value = {"synced": True} result = await svc.finish_oauth( "openrouter", "or-code-xyz", start["state"], "admin@example.com" ) self.assertEqual(result["stored"].auth_kind, "api_key") self.assertEqual(fake.captured["url"], svc._OPENROUTER_OAUTH["exchange_url"]) self.assertEqual(fake.captured["json"]["code"], "or-code-xyz") self.assertEqual(fake.captured["json"]["code_challenge_method"], "S256") if __name__ == "__main__": unittest.main()