Files

99 lines
4.4 KiB
Python
Raw Permalink Normal View History

"""B-78 (DAT-69, AUD-11, AUD-12, API-32): why the lottery is paused.
`paused_reason` is `operator` for POST /admin/pause and an automatic reason when the
system pauses itself (`resurrected_tx` so far). An operator pause never overwrites an
automatic reason, and POST /admin/resume — the only way out — clears whichever it is and
logs what it lifted.
"""
import json
from sqlalchemy import select
from app.db.models import AuditLog
from app.rounds.pause import pause_automatically
from tests.unit.test_admin import client # noqa: F401 (fixture)
_HEADERS = {"X-Admin-Token": "test-admin-token"}
async def _audit(event_type: str) -> list[dict]:
from app.db import base as db_base
async with db_base.AsyncSessionLocal() as session:
rows = (await session.scalars(select(AuditLog).where(AuditLog.event_type == event_type).order_by(AuditLog.id))).all()
return [json.loads(r.payload_json) for r in rows]
async def test_an_operator_pause_has_the_reason_operator_and_may_carry_a_note(client):
assert (await client.get("/admin/config", headers=_HEADERS)).json()["paused_reason"] is None
response = await client.post("/admin/pause", headers=_HEADERS, json={"note": "migrating the server"})
assert response.status_code == 200
assert response.json()["paused"] is True and response.json()["paused_reason"] == "operator"
assert await _audit("lottery_paused") == [{"reason": "operator", "note": "migrating the server"}]
async def test_pause_still_works_without_a_body_and_rejects_a_note_over_256_characters(client):
assert (await client.post("/admin/pause", headers=_HEADERS)).status_code == 200
assert (await client.post("/admin/pause", headers=_HEADERS, json={"note": "x" * 257})).status_code == 422
assert await _audit("lottery_paused") == [{"reason": "operator"}]
async def test_an_automatic_reason_survives_an_operator_pause_and_resume_clears_it(client):
from app.db import base as db_base
async with db_base.AsyncSessionLocal() as session:
await pause_automatically(session, "resurrected_tx")
await session.commit()
assert (await client.get("/admin/config", headers=_HEADERS)).json()["paused_reason"] == "resurrected_tx"
again = await client.post("/admin/pause", headers=_HEADERS)
assert again.json()["paused_reason"] == "resurrected_tx" # the cause that needs a look is not hidden
assert (await _audit("lottery_paused"))[-1] == {"reason": "operator"} # but the row says who asked
resumed = await client.post("/admin/resume", headers=_HEADERS)
assert resumed.json()["paused"] is False and resumed.json()["paused_reason"] is None
assert (await _audit("lottery_resumed"))[-1] == {"reason": "resurrected_tx"} # what was lifted
async def test_resuming_a_lottery_that_was_not_paused_logs_a_null_reason(client):
await client.post("/admin/resume", headers=_HEADERS)
assert await _audit("lottery_resumed") == [{"reason": None}]
async def test_an_automatic_pause_overwrites_an_operator_reason(client):
"""DAT-69: the automatic reason is always written, even over an active pause."""
from app.db import base as db_base
await client.post("/admin/pause", headers=_HEADERS)
async with db_base.AsyncSessionLocal() as session:
await pause_automatically(session, "resurrected_tx")
await session.commit()
assert (await client.get("/admin/config", headers=_HEADERS)).json()["paused_reason"] == "resurrected_tx"
async def test_only_automatic_reasons_can_be_written_by_the_system(client):
import pytest
from app.db import base as db_base
async with db_base.AsyncSessionLocal() as session:
with pytest.raises(ValueError):
await pause_automatically(session, "operator")
async def test_the_public_round_endpoint_reports_the_reason(client):
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app.api.routes.rounds import router as rounds_router
from app.rounds.events import broadcaster # noqa: F401
app = FastAPI()
app.include_router(rounds_router)
app.state.electrum_listener = type("L", (), {"tip_height": 0, "client": None})()
await client.post("/admin/pause", headers=_HEADERS)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as public:
body = (await public.get("/rounds/current")).json()
assert body["lottery_paused"] is True and body["paused_reason"] == "operator"