o2o-site-AEO/solution/backend/tests/test_google_identity.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

128 lines
4.6 KiB
Python

"""구글 ID 토큰 검증 — 서명·수신자(aud)·발급자(iss)·이메일 인증."""
import time
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from jose import jwk
from jose import jwt as jose_jwt
from services.external import google_identity
from services.external.google_identity import GoogleNotConfigured, GoogleTokenInvalid, verify_id_token
_KID = "test-key-1"
_CLIENT_ID = "our-app.apps.googleusercontent.com"
@pytest.fixture(scope="module")
def keypair():
"""테스트 전용 RSA 키 → (서명용 PEM, 구글 JWKS 를 흉내 낸 공개키 묶음)."""
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
private_pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
).decode()
public_pem = key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode()
entry = {k: (v.decode() if isinstance(v, bytes) else v) for k, v in jwk.construct(public_pem, "RS256").to_dict().items()}
entry["kid"] = _KID
return private_pem, {"keys": [entry]}
@pytest.fixture(autouse=True)
def google_configured(monkeypatch, keypair):
"""client_id 를 채우고 JWKS 조회를 테스트 키로 바꾼다."""
_, jwks = keypair
monkeypatch.setattr(google_identity.google_oauth_config, "client_id", _CLIENT_ID)
async def _fetch():
return jwks
monkeypatch.setattr(google_identity, "_fetch_jwks", _fetch)
monkeypatch.setattr(google_identity, "_jwks", None)
monkeypatch.setattr(google_identity, "_jwks_at", 0.0)
def _token(keypair, **overrides) -> str:
private_pem, _ = keypair
claims = {
"iss": "https://accounts.google.com",
"aud": _CLIENT_ID,
"sub": "1234567890",
"email": "boss@example.com",
"email_verified": True,
"name": "김사장",
"exp": int(time.time()) + 600,
"iat": int(time.time()),
}
claims.update(overrides)
return jose_jwt.encode(claims, private_pem, algorithm="RS256", headers={"kid": _KID})
async def test_valid_token_yields_identity(keypair):
"""검증: 우리 client_id 로 발급된 정상 토큰."""
account = await verify_id_token(_token(keypair))
assert account.sub == "1234567890"
assert account.email == "boss@example.com"
assert account.name == "김사장"
async def test_token_for_another_app_is_refused(keypair):
"""검증: 서명·발급자는 진짜인데 aud 가 **다른 서비스**인 토큰."""
with pytest.raises(GoogleTokenInvalid):
await verify_id_token(_token(keypair, aud="someone-else.apps.googleusercontent.com"))
async def test_token_from_another_issuer_is_refused(keypair):
"""검증: 우리 aud 를 달고 있지만 iss 가 구글이 아닌 토큰."""
with pytest.raises(GoogleTokenInvalid):
await verify_id_token(_token(keypair, iss="https://evil.example.com"))
async def test_expired_token_is_refused(keypair):
"""검증: 만료된 토큰."""
with pytest.raises(GoogleTokenInvalid):
await verify_id_token(_token(keypair, exp=int(time.time()) - 10))
async def test_unverified_email_is_refused(keypair):
"""검증: email_verified=false."""
with pytest.raises(GoogleTokenInvalid):
await verify_id_token(_token(keypair, email_verified=False))
async def test_tampered_signature_is_refused(keypair):
"""검증: 본문을 바꾼 토큰(서명 불일치)."""
head, payload, sig = _token(keypair).split(".")
other = _token(keypair, sub="9999999999").split(".")[1]
with pytest.raises(GoogleTokenInvalid):
await verify_id_token(f"{head}.{other}.{sig}")
async def test_unknown_kid_refetches_keys_once(keypair, monkeypatch):
"""검증: 캐시에 없는 kid(키 회전 직후)."""
private_pem, jwks = keypair
calls = {"n": 0}
async def _fetch():
calls["n"] += 1
# 첫 호출은 우리 kid 가 없는(=낡은) 묶음을 준다.
return {"keys": []} if calls["n"] == 1 else jwks
monkeypatch.setattr(google_identity, "_fetch_jwks", _fetch)
account = await verify_id_token(_token(keypair))
assert account.sub == "1234567890"
assert calls["n"] == 2
async def test_missing_client_id_disables_google_login(monkeypatch, keypair):
"""검증: GOOGLE_CLIENT_ID 가 비어 있을 때."""
monkeypatch.setattr(google_identity.google_oauth_config, "client_id", "")
with pytest.raises(GoogleNotConfigured):
await verify_id_token(_token(keypair))