From c639daa7a2add59544ee027fbbf53f3ed9a31e54 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Date: Wed, 3 Jun 2026 06:14:20 +0100 Subject: [PATCH] Add companion pairing route response tests (#920) --- tests/test_companion_pairing.py | 114 +++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 8 deletions(-) diff --git a/tests/test_companion_pairing.py b/tests/test_companion_pairing.py index d7ec04d..f604d01 100644 --- a/tests/test_companion_pairing.py +++ b/tests/test_companion_pairing.py @@ -62,7 +62,7 @@ for _name, _attrs in { from fastapi import HTTPException # noqa: E402 import companion.pairing as P # noqa: E402 -import companion.routes as companion_routes # noqa: E402 +import companion.routes as R # noqa: E402 from companion.routes import mint_pairing_token, setup_companion_routes # noqa: E402 from core.middleware import require_admin # noqa: E402 @@ -159,14 +159,32 @@ def _pair_methods(): return methods -def _pair_endpoint(method): - router = setup_companion_routes() - for r in router.routes: - if getattr(r, "path", "").endswith("/pair") and method in getattr(r, "methods", set()): - return r.endpoint +def _pair_route(method): + for route in setup_companion_routes().routes: + path = getattr(route, "path", "") + if path.endswith("/pair") and method in getattr(route, "methods", set()): + return route.endpoint raise AssertionError(f"{method} /api/companion/pair route not found") +def _fake_pair_request(format=None, port=7000): + query_params = {} + if format is not None: + query_params["format"] = format + return SimpleNamespace( + state=SimpleNamespace(current_user="alice", api_token=False), + headers={}, + app=SimpleNamespace( + state=SimpleNamespace( + auth_manager=_admin_mgr(True), + invalidate_token_cache=MagicMock(), + ) + ), + query_params=query_params, + url=SimpleNamespace(port=port), + ) + + def test_pair_is_minted_via_post_not_get(): methods = _pair_methods() assert "POST" in methods, "pairing must accept POST (the mint)" @@ -176,7 +194,87 @@ def test_pair_is_minted_via_post_not_get(): def test_pair_page_uses_imported_admin_gate(monkeypatch): - monkeypatch.setattr(companion_routes, "require_admin", lambda request: None) - response = _pair_endpoint("GET")(SimpleNamespace()) + monkeypatch.setattr(R, "require_admin", lambda request: None) + response = _pair_route("GET")(SimpleNamespace()) assert "Pair a device" in str(getattr(response, "body", response)) + + +def test_pair_get_renders_form_without_minting(monkeypatch): + mint = MagicMock(side_effect=AssertionError("GET must not mint a token")) + monkeypatch.setattr(R, "require_admin", lambda request: None, raising=False) + monkeypatch.setattr(R, "mint_pairing_token", mint) + + response = _pair_route("GET")(_fake_pair_request()) + body = response.body.decode() + + assert response.media_type == "text/html" + assert '
' in body + assert "Generate pairing code" in body + mint.assert_not_called() + + +def test_pair_post_json_returns_pairing_payload(monkeypatch): + mint = MagicMock(return_value=("tok123", "ody_raw")) + monkeypatch.setattr(R, "require_admin", lambda request: None, raising=False) + monkeypatch.setattr(R, "get_current_user", lambda request: "alice") + monkeypatch.setattr(R, "mint_pairing_token", mint) + monkeypatch.setattr(R._pairing, "lan_ip_candidates", lambda: ["192.168.1.50"]) + + request = _fake_pair_request(format="json", port=7000) + response = _pair_route("POST")(request) + + mint.assert_called_once_with("alice", request.app.state.invalidate_token_cache) + assert response["host"] == "192.168.1.50" + assert response["port"] == 7000 + assert response["token"] == "ody_raw" + assert response["token_id"] == "tok123" + assert response["payload"] == { + "v": 1, + "host": "192.168.1.50", + "port": 7000, + "token": "ody_raw", + } + for secret_key in ("token_hash", "token_prefix", "scopes", "is_active", "owner", "name"): + assert secret_key not in response + assert secret_key not in response["payload"] + + +def test_pair_post_json_qr_failure_returns_null_qr(monkeypatch): + monkeypatch.setattr(R, "require_admin", lambda request: None, raising=False) + monkeypatch.setattr(R, "get_current_user", lambda request: "alice") + monkeypatch.setattr(R, "mint_pairing_token", lambda owner, invalidate: ("tok123", "ody_raw")) + monkeypatch.setattr(R._pairing, "lan_ip_candidates", lambda: ["192.168.1.50"]) + monkeypatch.setattr(R._pairing, "pairing_qr_png_data_uri", lambda payload: None) + + response = _pair_route("POST")(_fake_pair_request(format="json", port=7000)) + + assert response["qr"] is None + assert response["host"] == "192.168.1.50" + assert response["port"] == 7000 + assert response["token"] == "ody_raw" + assert response["payload"] == { + "v": 1, + "host": "192.168.1.50", + "port": 7000, + "token": "ody_raw", + } + + +def test_pair_post_html_escapes_pairing_values(monkeypatch): + monkeypatch.setattr(R, "require_admin", lambda request: None, raising=False) + monkeypatch.setattr(R, "get_current_user", lambda request: "alice") + monkeypatch.setattr(R, "mint_pairing_token", lambda owner, invalidate: ("tok<123>", "ody_&")) + monkeypatch.setattr(R._pairing, "lan_ip_candidates", lambda: ["host&"]) + monkeypatch.setattr(R._pairing, "pairing_qr_png_data_uri", lambda payload: None) + + response = _pair_route("POST")(_fake_pair_request()) + body = response.body.decode() + + assert response.media_type == "text/html" + assert "host&" not in body + assert "ody_&" not in body + assert "tok<123>" not in body + assert "host<one>&" in body + assert "ody_<raw>&" in body + assert "tok<123>" in body