mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:07:19 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""ASGI startup: the app must serve before it can talk to Signal, not after."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import httpx
|
|
from fastapi.testclient import TestClient
|
|
|
|
from signal_bot import app as app_module
|
|
from signal_bot.app import lifespan
|
|
from signal_bot.versioning import __version__
|
|
from tests.conftest import WATCHED_GROUP, mock_client, signal_client
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
|
|
BASE_ENV = {
|
|
"PHONE_NUMBER": "+15550000000",
|
|
"GROUP_IDS": "",
|
|
"API_BASE_URL": "http://localhost:9922",
|
|
"DATABASE_PATH": ":memory:",
|
|
}
|
|
|
|
DEAD = httpx.ConnectError("Server disconnected without sending a response.")
|
|
|
|
|
|
def _offline(request: httpx.Request) -> httpx.Response:
|
|
raise DEAD
|
|
|
|
|
|
def _serving(monkeypatch: pytest.MonkeyPatch, client: httpx.AsyncClient) -> Iterator[TestClient]:
|
|
for key, value in BASE_ENV.items():
|
|
monkeypatch.setenv(key, value)
|
|
monkeypatch.setattr(app_module.httpx, "AsyncClient", lambda **kwargs: client)
|
|
return TestClient(app_module.app)
|
|
|
|
|
|
def test_healthz_serves_while_signal_cli_is_unreachable(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The 2026-08-30 reboot: startup used to fail outright and stay failed."""
|
|
with _serving(monkeypatch, mock_client(_offline)) as client:
|
|
response = client.get("/healthz")
|
|
assert response.status_code == httpx.codes.SERVICE_UNAVAILABLE
|
|
body = response.json()
|
|
# Not "deaf": a bot that has never connected is a different state
|
|
# from one that could hear and then stopped.
|
|
assert body["status"] == "connecting"
|
|
assert "attempt" in body["reason"]
|
|
assert body["version"] == __version__
|
|
|
|
|
|
def test_healthz_turns_green_once_the_container_answers(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""The old app never finished startup, so no route was ever reachable."""
|
|
with _serving(monkeypatch, signal_client(WATCHED_GROUP)) as client:
|
|
body: dict[str, str] = {}
|
|
# Each request hands the event loop back to the connecting task.
|
|
for _ in range(50):
|
|
body = client.get("/healthz").json()
|
|
if body["status"] == "ok":
|
|
break
|
|
assert body["status"] == "ok"
|
|
assert body["version"] == __version__
|
|
|
|
|
|
async def test_lifespan_is_an_async_context_manager(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
for key, value in BASE_ENV.items():
|
|
monkeypatch.setenv(key, value)
|
|
monkeypatch.setattr(
|
|
app_module.httpx,
|
|
"AsyncClient",
|
|
lambda **kwargs: signal_client(WATCHED_GROUP),
|
|
)
|
|
async with lifespan(app_module.app):
|
|
# Present immediately, before anything has reached Signal: /healthz
|
|
# reads it, and an absent attribute would be a 500 rather than a 503.
|
|
assert app_module.app.state.starter is not None
|