mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 22:47:14 +01:00
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
5.1 KiB
Python
157 lines
5.1 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Tests for ``tool/verify_desktop_client``: the PKCE loopback + Firebase check.
|
|
|
|
No browser, no network, no socket: the consent step, the code exchange and
|
|
Firebase's REST endpoint are all faked at the module boundary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import contextlib
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from crdt_sync._autherrors import FirebaseAuthError
|
|
from crdt_sync._config import FirebaseConfig
|
|
from tool import verify_desktop_client as vdc
|
|
from tool._token_exchange import TokenError, _exchange_code
|
|
from tool.desktop_client import DesktopClient
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
_HELD = "stored-client-credential"
|
|
_CLIENT = DesktopClient("1-abc.apps.googleusercontent.com", _HELD, "kuhy-syncs")
|
|
_CONFIG = FirebaseConfig("api", "https://db", "kuhy-syncs", "uid-1", "me@example.com")
|
|
_ID_VALUE = (
|
|
"header."
|
|
+ base64.urlsafe_b64encode(json.dumps({"aud": _CLIENT.client_id}).encode())
|
|
.decode()
|
|
.rstrip("=")
|
|
+ ".sig"
|
|
)
|
|
|
|
|
|
def test_audience_of_reads_the_aud_claim() -> None:
|
|
assert vdc.audience_of(_ID_VALUE) == _CLIENT.client_id
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"bad", ["", "a.b", "a.!!!.c", "a." + base64.b64encode(b"[]").decode() + ".c"]
|
|
)
|
|
def test_audience_of_survives_garbage(bad: str) -> None:
|
|
assert vdc.audience_of(bad) == "?"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("message", "needle"),
|
|
[
|
|
(
|
|
"failed: HTTP 400 (INVALID_IDP_RESPONSE : id_token audience mismatch)",
|
|
"Safelist",
|
|
),
|
|
("signed in as the wrong account: Google resolved to uid 'x'", "login_hint"),
|
|
("anything else", ""),
|
|
],
|
|
)
|
|
def test_hint_for(message: str, needle: str) -> None:
|
|
hint = vdc.hint_for(message)
|
|
assert (needle in hint) if needle else hint == ""
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _flow(result: dict[str, str]) -> Iterator[MagicMock]:
|
|
"""Fake the socket, the callback and the exchange; yield the exchange."""
|
|
with (
|
|
patch.object(vdc, "_free_port", return_value=5555),
|
|
patch.object(
|
|
vdc, "_start_callback_server", return_value=(MagicMock(), MagicMock())
|
|
),
|
|
patch.object(vdc._CallbackHandler, "result", result),
|
|
patch.object(vdc, "_exchange_code", return_value=_ID_VALUE) as exchange,
|
|
patch.object(
|
|
vdc.secrets,
|
|
"token_urlsafe",
|
|
side_effect=lambda nbytes: "st" if nbytes == 16 else "v" * 86,
|
|
),
|
|
):
|
|
yield exchange
|
|
|
|
|
|
def test_fetch_id_token_sends_pkce_and_the_secret() -> None:
|
|
opener = MagicMock(return_value=True)
|
|
with _flow({"code": "abc", "state": "st"}) as exchange:
|
|
token = vdc.fetch_id_token(_CLIENT, "me@example.com", opener=opener)
|
|
assert token == _ID_VALUE
|
|
url = opener.call_args.args[0]
|
|
assert "redirect_uri=http%3A%2F%2F127.0.0.1%3A5555" in url
|
|
assert "code_challenge_method=S256" in url
|
|
code, client_id, held, redirect, verifier = exchange.call_args.args
|
|
assert (code, client_id, held) == ("abc", _CLIENT.client_id, _HELD)
|
|
assert redirect == "http://127.0.0.1:5555"
|
|
assert len(verifier) >= 43
|
|
|
|
|
|
def test_fetch_id_token_without_a_browser_still_waits() -> None:
|
|
opener = MagicMock(return_value=False)
|
|
with _flow({"code": "abc", "state": "st"}):
|
|
token = vdc.fetch_id_token(_CLIENT, "", open_browser=False, opener=opener)
|
|
assert token == _ID_VALUE
|
|
opener.assert_not_called()
|
|
|
|
|
|
def test_fetch_id_token_reports_a_refused_consent() -> None:
|
|
with (
|
|
_flow({"error": "access_denied"}),
|
|
pytest.raises(TokenError, match="access_denied"),
|
|
):
|
|
vdc.fetch_id_token(_CLIENT, "", opener=MagicMock())
|
|
|
|
|
|
def _firebase_reply(*, ok: bool, body: dict[str, object]) -> MagicMock:
|
|
response = MagicMock(ok=ok, status_code=200 if ok else 400)
|
|
response.json.return_value = body
|
|
return response
|
|
|
|
|
|
def test_sign_in_returns_the_email_and_writes_no_file() -> None:
|
|
reply = _firebase_reply(
|
|
ok=True,
|
|
body={
|
|
"localId": "uid-1",
|
|
"email": "me@example.com",
|
|
"idToken": "i",
|
|
"refreshToken": "r",
|
|
"expiresIn": "3600",
|
|
},
|
|
)
|
|
with patch("crdt_sync._firebase_auth.requests.post", return_value=reply) as post:
|
|
assert vdc.sign_in(_CONFIG, _ID_VALUE) == "me@example.com"
|
|
assert "signInWithIdp" in post.call_args.args[0]
|
|
|
|
|
|
def test_sign_in_surfaces_the_exact_firebase_error() -> None:
|
|
reply = _firebase_reply(
|
|
ok=False,
|
|
body={
|
|
"error": {"message": "INVALID_IDP_RESPONSE : id_token audience mismatch"}
|
|
},
|
|
)
|
|
with (
|
|
patch("crdt_sync._firebase_auth.requests.post", return_value=reply),
|
|
pytest.raises(FirebaseAuthError, match="audience mismatch"),
|
|
):
|
|
vdc.sign_in(_CONFIG, _ID_VALUE)
|
|
|
|
|
|
def test_exchange_sends_the_pkce_verifier() -> None:
|
|
reply = MagicMock(ok=True)
|
|
reply.json.return_value = {"id_token": _ID_VALUE}
|
|
with patch("tool._token_exchange.requests.post", return_value=reply) as post:
|
|
_exchange_code("c", "id", _HELD, "http://127.0.0.1:1", "ver")
|
|
assert post.call_args.kwargs["data"]["code_verifier"] == "ver"
|