131 lines
3.8 KiB
Python
131 lines
3.8 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.core.app_config import get_app_config
|
||
|
|
from app.core.config import settings
|
||
|
|
from app.core.errors import ServiceUnavailableError
|
||
|
|
from app.features.auth.google import verify_google_id_token
|
||
|
|
from app.features.auth.service_errors import AuthError
|
||
|
|
|
||
|
|
|
||
|
|
def _configure_remote_verifier(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
monkeypatch.setattr(
|
||
|
|
get_app_config().deploy,
|
||
|
|
"google_oauth_client_ids",
|
||
|
|
"web-client",
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
settings,
|
||
|
|
"google_token_verifier_url",
|
||
|
|
"https://verifier.example/api/verify-google-token",
|
||
|
|
)
|
||
|
|
monkeypatch.setattr(settings, "google_token_verifier_secret", "shared-secret")
|
||
|
|
monkeypatch.setattr(settings, "google_token_verifier_timeout_seconds", 3.0)
|
||
|
|
|
||
|
|
|
||
|
|
def test_remote_google_verifier_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
_configure_remote_verifier(monkeypatch)
|
||
|
|
calls: list[dict[str, Any]] = []
|
||
|
|
|
||
|
|
def fake_post(
|
||
|
|
url: str,
|
||
|
|
*,
|
||
|
|
json: dict[str, str],
|
||
|
|
headers: dict[str, str],
|
||
|
|
timeout: float,
|
||
|
|
) -> httpx.Response:
|
||
|
|
calls.append(
|
||
|
|
{
|
||
|
|
"url": url,
|
||
|
|
"json": json,
|
||
|
|
"headers": headers,
|
||
|
|
"timeout": timeout,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return httpx.Response(
|
||
|
|
200,
|
||
|
|
json={
|
||
|
|
"subject": "google-sub",
|
||
|
|
"email": "Person@Example.com",
|
||
|
|
"email_verified": True,
|
||
|
|
"name": "Google Person",
|
||
|
|
"picture": "https://example.com/avatar.jpg",
|
||
|
|
"audience": "web-client",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
monkeypatch.setattr("app.features.auth.google.httpx.post", fake_post)
|
||
|
|
|
||
|
|
identity = verify_google_id_token("id-token")
|
||
|
|
|
||
|
|
assert identity.subject == "google-sub"
|
||
|
|
assert identity.email == "person@example.com"
|
||
|
|
assert identity.audience == "web-client"
|
||
|
|
assert calls == [
|
||
|
|
{
|
||
|
|
"url": "https://verifier.example/api/verify-google-token",
|
||
|
|
"json": {"id_token": "id-token"},
|
||
|
|
"headers": {
|
||
|
|
"Accept": "application/json",
|
||
|
|
"Authorization": "Bearer shared-secret",
|
||
|
|
},
|
||
|
|
"timeout": 3.0,
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_remote_google_verifier_rejects_invalid_token(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
_configure_remote_verifier(monkeypatch)
|
||
|
|
|
||
|
|
def fake_post(*_args: object, **_kwargs: object) -> httpx.Response:
|
||
|
|
return httpx.Response(401, json={"error": "INVALID_TOKEN"})
|
||
|
|
|
||
|
|
monkeypatch.setattr("app.features.auth.google.httpx.post", fake_post)
|
||
|
|
|
||
|
|
with pytest.raises(AuthError) as exc_info:
|
||
|
|
verify_google_id_token("bad-token")
|
||
|
|
|
||
|
|
assert exc_info.value.code == "INVALID_TOKEN"
|
||
|
|
|
||
|
|
|
||
|
|
def test_remote_google_verifier_rejects_audience_mismatch(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
_configure_remote_verifier(monkeypatch)
|
||
|
|
|
||
|
|
def fake_post(*_args: object, **_kwargs: object) -> httpx.Response:
|
||
|
|
return httpx.Response(
|
||
|
|
200,
|
||
|
|
json={
|
||
|
|
"subject": "google-sub",
|
||
|
|
"email": "person@example.com",
|
||
|
|
"email_verified": True,
|
||
|
|
"name": "",
|
||
|
|
"picture": None,
|
||
|
|
"audience": "other-client",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
monkeypatch.setattr("app.features.auth.google.httpx.post", fake_post)
|
||
|
|
|
||
|
|
with pytest.raises(AuthError) as exc_info:
|
||
|
|
verify_google_id_token("id-token")
|
||
|
|
|
||
|
|
assert exc_info.value.code == "INVALID_TOKEN"
|
||
|
|
|
||
|
|
|
||
|
|
def test_remote_google_verifier_requires_shared_secret(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
_configure_remote_verifier(monkeypatch)
|
||
|
|
monkeypatch.setattr(settings, "google_token_verifier_secret", "")
|
||
|
|
|
||
|
|
with pytest.raises(ServiceUnavailableError):
|
||
|
|
verify_google_id_token("id-token")
|