mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
Some checks failed
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
crdt-sync pre-commit / pre-commit (push) Has been cancelled
crdt-sync tests / test (push) Has been cancelled
The sufler PC TTS relay reads ~/.config/sufler/firebase_auth.json via
credential_store_for("sufler"), like tutor, so it needs a seeded session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jw52sNWquCp34dy3rj19GD
248 lines
8.9 KiB
Python
248 lines
8.9 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Tests for ``main()`` in ``tool/seed_session``.
|
|
|
|
Split from :mod:`test_tool_seed_session` to stay under the 250-line cap.
|
|
|
|
The behaviour worth pinning here is what happens when a run dies partway. It
|
|
leaves some apps working and some not, and saying exactly which is the
|
|
difference between rerunning the ones that need it and a hunt through
|
|
``~/.config`` for what actually happened -- so the rerun line is asserted to
|
|
list the remaining apps and only those.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from crdt_sync._config import ConfigError
|
|
from crdt_sync._firebase_auth import FirebaseAuthError
|
|
from crdt_sync.tests.test_tool_seed_session import _CONFIG, _ID_VALUE
|
|
from tool import google_id_token as git
|
|
from tool import seed_session as ss
|
|
from tool.google_id_token import TokenError
|
|
|
|
_CLIENT_ID = "the-web-client-id"
|
|
_CLIENT_CREDENTIAL = "the-web-client-secret"
|
|
_ARGV = ["--client-id", _CLIENT_ID, "--client-secret", _CLIENT_CREDENTIAL]
|
|
|
|
|
|
def test_main_seeds_every_default_app(caplog: pytest.LogCaptureFixture) -> None:
|
|
"""With no --app, all of DEFAULT_APPS are seeded."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", return_value=list(ss.DEFAULT_APPS)) as seeded,
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
code = ss.main(_ARGV)
|
|
|
|
assert code == 0
|
|
assert seeded.call_args.args[2] == ss.DEFAULT_APPS
|
|
assert f"{len(ss.DEFAULT_APPS)} desktop session(s)" in caplog.text
|
|
|
|
|
|
def test_todo_is_among_the_default_apps() -> None:
|
|
"""It grew a desktop wrapper with its own Firebase REST client.
|
|
|
|
Its credential cache needs seeding exactly like the Python daemons, which
|
|
is why it is no longer grouped with the phone-only Flutter apps.
|
|
"""
|
|
assert "todo" in ss.DEFAULT_APPS
|
|
|
|
|
|
def test_home_guard_is_among_the_default_apps() -> None:
|
|
"""The fifth gatelock sibling; ``home_guard._sync_client`` calls
|
|
``firebase_client_for("home_guard")`` from the systemd-run gate."""
|
|
assert "home_guard" in ss.DEFAULT_APPS
|
|
|
|
|
|
def test_sufler_is_among_the_default_apps() -> None:
|
|
"""Its PC TTS relay (``sufler_tts.relay.setup``) reads
|
|
``~/.config/sufler/firebase_auth.json`` via ``credential_store_for``."""
|
|
assert "sufler" in ss.DEFAULT_APPS
|
|
|
|
|
|
def test_the_app_flag_accepts_sufler() -> None:
|
|
"""``--app sufler`` must parse (argparse choices come from DEFAULT_APPS)."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", return_value=["sufler"]) as seeded,
|
|
):
|
|
code = ss.main([*_ARGV, "--app", "sufler"])
|
|
|
|
assert code == 0
|
|
assert seeded.call_args.args[2] == ("sufler",)
|
|
|
|
|
|
def test_a_single_app_can_be_selected() -> None:
|
|
"""--app narrows the run, which is what a rerun after a failure uses."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", return_value=["diet_guard"]) as seeded,
|
|
):
|
|
code = ss.main([*_ARGV, "--app", "diet_guard"])
|
|
|
|
assert code == 0
|
|
assert seeded.call_args.args[2] == ("diet_guard",)
|
|
|
|
|
|
def test_the_app_flag_is_repeatable() -> None:
|
|
"""Rerunning two failed apps should not need two invocations."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", return_value=["diet_guard", "todo"]) as seeded,
|
|
):
|
|
ss.main([*_ARGV, "--app", "diet_guard", "--app", "todo"])
|
|
|
|
assert seeded.call_args.args[2] == ("diet_guard", "todo")
|
|
|
|
|
|
def test_an_unknown_app_is_refused_by_argparse() -> None:
|
|
"""The constrained choices exist because of a real failure.
|
|
|
|
A typo like "diet-guard" would otherwise create ~/.config/diet-guard/,
|
|
verify against it happily, and report PASS while the real diet_guard
|
|
stayed dead.
|
|
"""
|
|
with pytest.raises(SystemExit):
|
|
ss.main([*_ARGV, "--app", "diet-guard"])
|
|
|
|
|
|
def test_the_consent_flow_receives_the_client_and_port() -> None:
|
|
"""A Web client needs the registered port verbatim, not a free one."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE) as fetch,
|
|
patch.object(ss, "seed_apps", return_value=[]),
|
|
):
|
|
ss.main([*_ARGV, "--port", "9999", "--no-browser"])
|
|
|
|
assert fetch.call_args.args == (_CLIENT_ID, _CLIENT_CREDENTIAL)
|
|
assert fetch.call_args.kwargs == {"open_browser": False, "port": 9999}
|
|
|
|
|
|
def test_the_default_port_is_the_registered_one() -> None:
|
|
"""8765 is what is registered on the Web client as http://localhost:8765."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE) as fetch,
|
|
patch.object(ss, "seed_apps", return_value=[]),
|
|
):
|
|
ss.main(_ARGV)
|
|
|
|
assert fetch.call_args.kwargs["port"] == 8765
|
|
|
|
|
|
def test_the_expected_uid_is_announced_before_the_flow(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""So the wrong Google account can be spotted at the picker, not after."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", return_value=[]),
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
ss.main(_ARGV)
|
|
|
|
assert _CONFIG.uid in caplog.text
|
|
assert _CONFIG.email in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"failure",
|
|
[
|
|
ConfigError("no config"),
|
|
FirebaseAuthError("wrong uid"),
|
|
TokenError("consent refused"),
|
|
],
|
|
)
|
|
def test_the_named_failures_return_1(
|
|
failure: Exception,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Each is reported, not raised as a traceback."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", side_effect=failure),
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
code = ss.main(_ARGV)
|
|
|
|
assert code == 1
|
|
assert "FAIL" in caplog.text
|
|
|
|
|
|
def test_an_unexpected_error_is_not_swallowed() -> None:
|
|
"""The reason the handler names its four types instead of Exception."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", side_effect=RuntimeError("a real bug")),
|
|
pytest.raises(RuntimeError, match="a real bug"),
|
|
):
|
|
ss.main(_ARGV)
|
|
|
|
|
|
def test_a_total_failure_suggests_rerunning_every_app(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Nothing verified, so nothing may be reported as already working."""
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", side_effect=ss.SeedError("all dead")),
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
code = ss.main([*_ARGV, "--app", "diet_guard", "--app", "todo"])
|
|
|
|
assert code == 1
|
|
assert "already working" not in caplog.text
|
|
assert "--app diet_guard --app todo" in caplog.text
|
|
|
|
|
|
def test_a_partial_failure_reports_what_already_works(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""The apps that verified must not be reseeded needlessly."""
|
|
partial = ss.SeedError("todo is dead", done=("diet_guard",))
|
|
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", side_effect=partial),
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
code = ss.main([*_ARGV, "--app", "diet_guard", "--app", "todo"])
|
|
|
|
assert code == 1
|
|
assert "already working: diet_guard" in caplog.text
|
|
|
|
|
|
def test_the_rerun_line_excludes_the_apps_that_already_work(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""The whole value of the partial report is a copy-pasteable rerun."""
|
|
partial = ss.SeedError("todo is dead", done=("diet_guard",))
|
|
|
|
with (
|
|
patch.object(ss.FirebaseConfig, "load", return_value=_CONFIG),
|
|
patch.object(git, "fetch_id_token", return_value=_ID_VALUE),
|
|
patch.object(ss, "seed_apps", side_effect=partial),
|
|
caplog.at_level(logging.INFO, logger="seed_session"),
|
|
):
|
|
ss.main([*_ARGV, "--app", "diet_guard", "--app", "todo"])
|
|
|
|
assert "rerun with: --app todo" in caplog.text
|
|
|
|
|
|
def test_both_credentials_are_required() -> None:
|
|
"""Neither half of the OAuth client is optional."""
|
|
with pytest.raises(SystemExit):
|
|
ss.main(["--client-id", _CLIENT_ID])
|