누락된 OAuth 콜백 코드 파싱과 계정 metadata 헬퍼 구현
Some checks failed
API contract / OpenAPI type drift (push) Failing after 48s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 48s
This commit is contained in:
parent
7dce818788
commit
fefca4743e
2 changed files with 252 additions and 0 deletions
|
|
@ -1,5 +1,7 @@
|
|||
"""provider OAuth(PKCE) 서비스 단위 테스트 — 실제 네트워크 호출 없음."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
|
@ -7,6 +9,11 @@ from unittest.mock import patch
|
|||
from app.services import provider_oauth as svc
|
||||
|
||||
|
||||
def _b64url(claims: dict) -> str:
|
||||
raw = json.dumps(claims).encode("utf-8")
|
||||
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, payload, status_code=200):
|
||||
self._payload = payload
|
||||
|
|
@ -38,6 +45,25 @@ class _FakeAsyncClient:
|
|||
return _FakeResponse(self._payload)
|
||||
|
||||
|
||||
class _SequencedAsyncClient:
|
||||
"""호출 순서대로 준비한 응답을 돌려주고 호출을 기록하는 가짜 클라이언트."""
|
||||
|
||||
def __init__(self, payloads):
|
||||
self._payloads = list(payloads)
|
||||
self.calls = []
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return None
|
||||
|
||||
async def post(self, url, **kwargs):
|
||||
self.calls.append({"url": url, **kwargs})
|
||||
payload = self._payloads.pop(0) if self._payloads else {}
|
||||
return _FakeResponse(payload)
|
||||
|
||||
|
||||
class StartOAuthTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
svc._PENDING.clear()
|
||||
|
|
@ -94,6 +120,52 @@ class StartOAuthTest(unittest.TestCase):
|
|||
svc._pop_pending("claude", result["state"])
|
||||
|
||||
|
||||
class PasteAndMetadataTest(unittest.TestCase):
|
||||
"""콜백 붙여넣기 파싱과 제공자 metadata 추출 헬퍼."""
|
||||
|
||||
def test_extract_code_from_callback_url(self):
|
||||
url = (
|
||||
"http://localhost:1455/auth/callback?state=s1&iss=https://accounts.google.com"
|
||||
"&code=4/0AXqlp5_x-3&scope=email%20profile"
|
||||
)
|
||||
self.assertEqual(svc._extract_code_from_paste(url), "4/0AXqlp5_x-3")
|
||||
|
||||
def test_extract_code_accepts_bare_and_quoted_values(self):
|
||||
self.assertEqual(svc._extract_code_from_paste(" 4/0AX "), "4/0AX")
|
||||
self.assertEqual(svc._extract_code_from_paste('"4/0AX"'), "4/0AX")
|
||||
|
||||
def test_extract_code_percent_decodes(self):
|
||||
self.assertEqual(svc._extract_code_from_paste("code=4%2F0A%20x&state=s"), "4/0A x")
|
||||
|
||||
def test_extract_code_rejects_useless_paste(self):
|
||||
for value in ("", " ", "http://localhost:1455/auth/callback?state=only"):
|
||||
with self.assertRaises(svc.ProviderCredentialError):
|
||||
svc._extract_code_from_paste(value)
|
||||
|
||||
def test_chatgpt_account_id_reads_namespace_claim(self):
|
||||
claims = {"https://api.openai.com/auth": {"chatgpt_account_id": "acct-1"}}
|
||||
token = "header." + _b64url(claims) + ".signature"
|
||||
self.assertEqual(svc._chatgpt_account_id(token), "acct-1")
|
||||
|
||||
def test_chatgpt_account_id_returns_none_without_claim(self):
|
||||
self.assertIsNone(svc._chatgpt_account_id(None))
|
||||
self.assertIsNone(svc._chatgpt_account_id("not-a-jwt"))
|
||||
self.assertIsNone(svc._chatgpt_account_id("h." + _b64url({"sub": "x"}) + ".s"))
|
||||
|
||||
def test_default_tier_prefers_is_default_flag(self):
|
||||
loaded = {
|
||||
"allowedTiers": [{"id": "free-tier"}, {"id": "paid-tier", "isDefault": True}],
|
||||
"currentTier": {"id": "current-tier"},
|
||||
}
|
||||
self.assertEqual(svc._default_tier_id(loaded), "paid-tier")
|
||||
|
||||
def test_default_tier_falls_back_to_current_tier(self):
|
||||
self.assertEqual(
|
||||
svc._default_tier_id({"currentTier": {"id": "current-tier"}}), "current-tier"
|
||||
)
|
||||
self.assertEqual(svc._default_tier_id({}), "")
|
||||
|
||||
|
||||
class FinishOAuthTest(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
svc._PENDING.clear()
|
||||
|
|
@ -147,5 +219,58 @@ class FinishOAuthTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertEqual(fake.captured["json"]["code_challenge_method"], "S256")
|
||||
|
||||
|
||||
async def test_antigravity_project_id_reuses_existing_project(self):
|
||||
fake = _SequencedAsyncClient([{"cloudaicompanionProject": "proj-existing"}])
|
||||
with patch.object(svc.httpx, "AsyncClient", return_value=fake):
|
||||
self.assertEqual(await svc._antigravity_project_id("ya29-token"), "proj-existing")
|
||||
self.assertEqual(len(fake.calls), 1)
|
||||
self.assertEqual(
|
||||
fake.calls[0]["url"], "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
|
||||
)
|
||||
|
||||
async def test_antigravity_project_id_requires_a_tier(self):
|
||||
fake = _SequencedAsyncClient([{}])
|
||||
with patch.object(svc.httpx, "AsyncClient", return_value=fake):
|
||||
with self.assertRaises(svc.ProviderCredentialError):
|
||||
await svc._antigravity_project_id("ya29-token")
|
||||
|
||||
async def test_agy_callback_url_paste_onboards_code_assist_project(self):
|
||||
start = svc.start_oauth("agy", "admin@example.com")
|
||||
fake = _SequencedAsyncClient(
|
||||
[
|
||||
{"access_token": "ya29-token", "refresh_token": "1//refresh"},
|
||||
{"allowedTiers": [{"id": "legacy-tier", "isDefault": True}]},
|
||||
{"done": True, "response": {"cloudaicompanionProject": "proj-123"}},
|
||||
]
|
||||
)
|
||||
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="agy", token_hint="…ken", auth_kind="oauth_token",
|
||||
updated_by="admin@example.com", updated_at=1.0,
|
||||
)
|
||||
push.return_value = {"synced": True}
|
||||
callback = (
|
||||
f"http://localhost:1455/auth/callback?state={start['state']}"
|
||||
"&code=4/0AXqlp5_x-3&scope=email%20profile"
|
||||
)
|
||||
result = await svc.finish_oauth("agy", callback, start["state"], "admin@example.com")
|
||||
|
||||
self.assertEqual(result["stored"].auth_kind, "oauth_token")
|
||||
self.assertEqual(fake.calls[0]["url"], svc._AGY_OAUTH["token_url"])
|
||||
self.assertEqual(fake.calls[0]["data"]["code"], "4/0AXqlp5_x-3")
|
||||
self.assertEqual(
|
||||
fake.calls[1]["url"], "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
|
||||
)
|
||||
self.assertEqual(
|
||||
fake.calls[2]["url"], "https://cloudcode-pa.googleapis.com/v1internal:onboardUser"
|
||||
)
|
||||
self.assertEqual(fake.calls[2]["json"]["tierId"], "legacy-tier")
|
||||
self.assertEqual(save.call_args.kwargs["token"], "ya29-token")
|
||||
self.assertEqual(save.call_args.kwargs["refresh_token"], "1//refresh")
|
||||
self.assertEqual(save.call_args.kwargs["extra"], {"project_id": "proj-123"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue