utils/crdt-sync/crdt_sync/tests/test_tool_google_id_token.py
Krzysztof kuhy Rudnicki c295db7a15 crdt-sync: pylint --fail-under=10, scoped to source, on pylint 4.0.8
The 8.0 bar tolerated two points of debt and let the score regress
unnoticed. Adopting the fleet's source-only pylint scope needed
ignore-paths, not ignore = ["tests"]: ignore matches only the basename
of what pylint is handed, and pre-commit hands it explicit file paths,
so a directory name there never fired under the hook.

What the scope change left in source: runtime imports moved above the
TYPE_CHECKING blocks, the consent-flow CLI plumbing shared between
google_id_token and seed_session instead of duplicated, the token
exchange split out of google_id_token to stay under the 250-line cap,
and http.server's do_<METHOD> names admitted by method-rgx since the
stdlib dispatches on that exact spelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
2026-09-12 19:06:30 +02:00

207 lines
8.2 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki
"""Tests for ``tool/google_id_token``, the loopback OAuth ID-token minter.
No network and no browser. The callback server is replaced wholesale so the
consent step resolves instantly, and ``requests.post`` is mocked at the same
boundary ``test_firebase_auth.py`` uses. Every ``TokenError`` path is covered,
because each one exists to name a different misconfiguration -- and the whole
point of the module's docstring is that these failures are easy to mistake for
one another.
"""
from __future__ import annotations
import logging
from unittest.mock import MagicMock, patch
import pytest
import requests
from tool import _token_exchange as tx
from tool import google_id_token as git
_CLIENT_ID = "client-id-for-the-web-application"
# S105-S107 match on the *binding name*, not the value, so a constant called
# anything like SECRET/TOKEN trips them however obviously fake it is. Named
# around the rule the way test_firebase_auth.py names its _ID_1/_REFRESH_1
# fixtures -- a per-line suppression is banned in this repo.
_CLIENT_CREDENTIAL = "the-web-client-secret"
_ID_VALUE = "the-minted-id-token"
_STATE = "generated-state"
@pytest.fixture(autouse=True)
def browser() -> None:
"""Never launch a real browser, whatever a test passes."""
with patch.object(git.webbrowser, "open") as opened:
yield opened
@pytest.fixture(autouse=True)
def _deterministic_state() -> None:
"""Pin the CSRF state so callbacks can echo it."""
with patch.object(git.secrets, "token_urlsafe", return_value=_STATE):
yield
def _fake_flow(result: dict[str, str], *, port: int = 8765) -> tuple:
"""Patch the callback server so the flow resolves without a socket."""
server, thread = MagicMock(), MagicMock()
starter = patch.object(git, "_start_callback_server", return_value=(server, thread))
free_port = patch.object(git, "_free_port", return_value=port)
handler = patch.object(git._CallbackHandler, "result", result)
return starter, free_port, handler, server, thread
def _response(
*, ok: bool = True, status: int = 200, payload: object = None
) -> MagicMock:
"""Build a fake ``requests.Response``."""
response = MagicMock()
response.ok = ok
response.status_code = status
response.text = "" if ok else "the error body"
response.json.return_value = payload if payload is not None else {}
return response
def _run(result: dict[str, str], post: MagicMock | Exception, **kwargs: object) -> str:
"""Drive ``fetch_id_token`` against a canned callback and exchange."""
starter, free_port, handler, _, _ = _fake_flow(result)
with starter, free_port, handler:
target = patch.object(tx.requests, "post")
with target as posted:
if isinstance(post, Exception):
posted.side_effect = post
else:
posted.return_value = post
return git.fetch_id_token(_CLIENT_ID, _CLIENT_CREDENTIAL, **kwargs)
def test_successful_flow_returns_the_id_token() -> None:
"""The happy path: a matching callback exchanged for an id_token."""
result = {"code": "the-auth-code", "state": _STATE}
token = _run(result, _response(payload={"id_token": _ID_VALUE}))
assert token == _ID_VALUE
def test_the_exchange_posts_the_code_and_the_matching_redirect_uri() -> None:
"""Google rejects an exchange whose redirect_uri differs from consent's."""
starter, free_port, handler, _, _ = _fake_flow(
{"code": "the-auth-code", "state": _STATE}, port=8765
)
with starter, free_port, handler, patch.object(tx.requests, "post") as posted:
posted.return_value = _response(payload={"id_token": _ID_VALUE})
git.fetch_id_token(_CLIENT_ID, _CLIENT_CREDENTIAL, port=8765)
sent = posted.call_args.kwargs["data"]
assert sent["code"] == "the-auth-code"
assert sent["grant_type"] == "authorization_code"
assert sent["redirect_uri"] == "http://localhost:8765"
def test_a_fixed_port_is_used_verbatim_rather_than_a_free_one() -> None:
"""A Web client matches the redirect URI exactly, so --port must win.
Picking a free port here is the ``redirect_uri_mismatch`` the module
docstring warns about, and it fails before the account picker appears.
"""
starter, free_port, handler, _, _ = _fake_flow(
{"code": "c", "state": _STATE}, port=9999
)
with starter, free_port as picked, handler, patch.object(tx.requests, "post") as p:
p.return_value = _response(payload={"id_token": _ID_VALUE})
git.fetch_id_token(_CLIENT_ID, _CLIENT_CREDENTIAL, port=8765)
picked.assert_not_called()
def test_port_zero_falls_back_to_a_free_port() -> None:
"""The Desktop-app case: any loopback port is acceptable."""
starter, free_port, handler, _, _ = _fake_flow({"code": "c", "state": _STATE})
with starter, free_port as picked, handler, patch.object(tx.requests, "post") as p:
p.return_value = _response(payload={"id_token": _ID_VALUE})
git.fetch_id_token(_CLIENT_ID, _CLIENT_CREDENTIAL, port=0)
picked.assert_called_once()
def test_browser_is_opened_unless_suppressed(browser: MagicMock) -> None:
"""``open_browser=True`` launches the consent page."""
_run({"code": "c", "state": _STATE}, _response(payload={"id_token": _ID_VALUE}))
browser.assert_called_once()
assert browser.call_args.args[0].startswith(git._AUTH_ENDPOINT)
def test_no_browser_still_reports_the_consent_url(
browser: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Without a browser the URL must still be visible, or the flow is stuck."""
with caplog.at_level(logging.INFO, logger="google_id_token"):
_run(
{"code": "c", "state": _STATE},
_response(payload={"id_token": _ID_VALUE}),
open_browser=False,
)
browser.assert_not_called()
assert any(git._AUTH_ENDPOINT in record.getMessage() for record in caplog.records)
def test_no_callback_at_all_is_a_timeout() -> None:
"""Nothing arrived on the loopback port before the deadline."""
with pytest.raises(git.TokenError, match="no response on http://localhost"):
_run({}, _response())
def test_refused_consent_is_reported_as_refusal() -> None:
"""Google reports a declined consent as ``error``."""
with pytest.raises(git.TokenError, match="refused: access_denied"):
_run({"error": "access_denied"}, _response())
def test_mismatched_state_is_rejected() -> None:
"""A callback that does not echo the state is not ours."""
with pytest.raises(git.TokenError, match="state did not match"):
_run({"code": "c", "state": "some-other-state"}, _response())
def test_callback_without_a_code_is_rejected() -> None:
"""State matched but no code: nothing to exchange."""
with pytest.raises(git.TokenError, match="carried no authorization code"):
_run({"state": _STATE, "scope": "openid"}, _response())
def test_network_failure_during_exchange_is_wrapped() -> None:
"""A transport error must not surface as a bare requests exception."""
boom = requests.ConnectionError("connection refused")
with pytest.raises(git.TokenError, match="network error exchanging the code"):
_run({"code": "c", "state": _STATE}, boom)
def test_http_error_from_the_exchange_is_reported_with_its_status() -> None:
"""A non-2xx exchange carries the status, which is what identifies it."""
with pytest.raises(git.TokenError, match="HTTP 400"):
_run({"code": "c", "state": _STATE}, _response(ok=False, status=400))
def test_a_token_response_without_an_id_token_names_the_likely_cause() -> None:
"""Dropping ``openid`` from the scopes yields tokens but no id_token."""
with pytest.raises(git.TokenError, match="was 'openid' in the scopes"):
_run({"code": "c", "state": _STATE}, _response(payload={"access_token": "a"}))
def test_the_server_is_closed_even_though_the_flow_failed() -> None:
"""A leaked listener would make the next run fail to bind the same port."""
starter, free_port, handler, server, thread = _fake_flow({})
with starter, free_port, handler, pytest.raises(git.TokenError):
git.fetch_id_token(_CLIENT_ID, _CLIENT_CREDENTIAL)
thread.join.assert_called_once()
server.server_close.assert_called_once()