"""The Test button: it exists to tell a working channel from a broken one.""" from typing import Any import pytest from fastapi.testclient import TestClient from fluksio.core.config import settings from fluksio.flow.alerts import AlertsConfig, Channel PREFIX = f"{settings.API_V1_STR}/alerts" def _one_channel(client: TestClient) -> Any: alerts = client.app.state.flow_controller.alerts # type: ignore[attr-defined] alerts.config = AlertsConfig( channels=[Channel(name="phone", kind="ntfy", config={"topic": "t"})] ) return alerts def test_a_channel_that_cannot_deliver_is_not_a_success( client: TestClient, superuser_token_headers: dict[str, str], monkeypatch: pytest.MonkeyPatch, ) -> None: alerts = _one_channel(client) async def refuse(_config: dict[str, Any], _alert: Any) -> None: raise RuntimeError("nobody home") monkeypatch.setattr(alerts, "_send_ntfy", refuse) response = client.post(f"{PREFIX}/test/phone", headers=superuser_token_headers) assert response.status_code == 502 assert "nobody home" in response.json()["detail"] def test_a_channel_that_delivers_says_so( client: TestClient, superuser_token_headers: dict[str, str], monkeypatch: pytest.MonkeyPatch, ) -> None: alerts = _one_channel(client) sent = [] async def accept(_config: dict[str, Any], alert: Any) -> None: sent.append(alert) monkeypatch.setattr(alerts, "_send_ntfy", accept) response = client.post(f"{PREFIX}/test/phone", headers=superuser_token_headers) assert response.status_code == 200 assert len(sent) == 1 def test_an_unknown_channel_is_a_404( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: _one_channel(client) response = client.post(f"{PREFIX}/test/nowhere", headers=superuser_token_headers) assert response.status_code == 404 def test_an_ordinary_account_cannot_read_or_change_the_alert_setup( client: TestClient, normal_user_token_headers: dict[str, str] ) -> None: """Channels hold credential references, topics and urls: a superuser's.""" config = {"channels": [], "rules": []} assert ( client.get(f"{PREFIX}/config", headers=normal_user_token_headers).status_code == 403 ) assert ( client.put( f"{PREFIX}/config", headers=normal_user_token_headers, json=config ).status_code == 403 ) assert ( client.post( f"{PREFIX}/test/phone", headers=normal_user_token_headers ).status_code == 403 ) def test_a_stored_secret_cannot_be_pointed_at_a_host_an_ordinary_account_chose( client: TestClient, superuser_token_headers: dict[str, str], normal_user_token_headers: dict[str, str], monkeypatch: pytest.MonkeyPatch, ) -> None: """An ntfy `server` plus a `$secret` token used to send the secret there.""" import httpx client.put( f"{settings.API_V1_STR}/secrets/vault-token", headers=superuser_token_headers, json={"value": "S3CRET"}, ) reached: list[str] = [] async def post(_self: Any, url: Any, **_kwargs: Any) -> httpx.Response: reached.append(str(url)) return httpx.Response(200, request=httpx.Request("POST", url)) monkeypatch.setattr(httpx.AsyncClient, "post", post) config = { "channels": [ { "name": "x", "kind": "ntfy", "config": { "server": "http://attacker.invalid", "topic": "t", "token": {"$secret": "vault-token"}, }, } ], "rules": [], } client.put(f"{PREFIX}/config", headers=normal_user_token_headers, json=config) client.post(f"{PREFIX}/test/x", headers=normal_user_token_headers) assert reached == [] def test_any_account_can_subscribe_its_own_browser( client: TestClient, normal_user_token_headers: dict[str, str] ) -> None: """Where this device can be reached is not a superuser's to hold.""" response = client.get(f"{PREFIX}/webpush/key", headers=normal_user_token_headers) assert response.status_code == 200 assert response.json()["key"] def _telegram_config(token: Any) -> dict[str, Any]: return { "channels": [ { "name": "garage", "kind": "telegram", "config": {"token": token} if token else {}, } ], "rules": [], } def test_a_bot_token_has_to_be_a_stored_secret( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: plain = client.put( f"{PREFIX}/config", headers=superuser_token_headers, json=_telegram_config("123456:literal-token"), ) stored = client.put( f"{PREFIX}/config", headers=superuser_token_headers, json=_telegram_config({"$secret": "telegram-bot"}), ) assert plain.status_code == 422 assert "stored secret" in plain.json()["detail"] assert stored.status_code == 200 def test_a_chat_connects_through_the_link_and_the_token_is_never_returned( client: TestClient, superuser_token_headers: dict[str, str], normal_user_token_headers: dict[str, str], monkeypatch: pytest.MonkeyPatch, ) -> None: import json import time import httpx from fluksio.flow import telegram token = "123456:SECRET-token" client.put( f"{settings.API_V1_STR}/secrets/telegram-bot", headers=superuser_token_headers, json={"value": token}, ) client.put( f"{PREFIX}/config", headers=superuser_token_headers, json=_telegram_config({"$secret": "telegram-bot"}), ) def bot(request: httpx.Request) -> httpx.Response: method = request.url.path.rsplit("/", 1)[1] assert request.url.path.startswith(f"/bot{token}/") result: Any = True if method == "getMe": result = {"username": "fluksio_bot"} elif method == "getUpdates": time.sleep(0.05) codes = [c for c, p in telegram.linker._pending.items() if p.token == token] result = ( [ { "update_id": 1, "message": { "text": f"/start {codes[0]}", "chat": {"id": 42, "first_name": "Anna"}, }, } ] if codes else [] ) return httpx.Response(200, content=json.dumps({"ok": True, "result": result})) transport = httpx.MockTransport(bot) monkeypatch.setattr( telegram, "_client", lambda timeout: httpx.AsyncClient(transport=transport, timeout=timeout), ) link_url = f"{PREFIX}/telegram/garage/link" assert client.post(link_url, headers=normal_user_token_headers).status_code == 403 response = client.post(link_url, headers=superuser_token_headers) assert response.status_code == 200 body = response.json() assert body["url"].startswith("https://t.me/fluksio_bot?start=") assert body["group_url"].startswith("https://t.me/fluksio_bot?startgroup=") assert token not in response.text channel: dict[str, Any] = {} for _ in range(100): config = client.get(f"{PREFIX}/config", headers=superuser_token_headers) channel = config.json()["channels"][0] if channel["config"].get("chat_id"): break time.sleep(0.05) assert channel["config"]["chat_id"] == "42" assert channel["config"]["label"] == "Anna" assert channel["config"]["token"] == {"$secret": "telegram-bot"} assert token not in config.text status = client.get(link_url, headers=superuser_token_headers).json() assert status["waiting"] is False def test_a_link_needs_a_stored_token( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: client.put( f"{PREFIX}/config", headers=superuser_token_headers, json=_telegram_config(None), ) response = client.post( f"{PREFIX}/telegram/garage/link", headers=superuser_token_headers ) unknown = client.post( f"{PREFIX}/telegram/nowhere/link", headers=superuser_token_headers ) assert response.status_code == 422 assert unknown.status_code == 404