152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
|
|
"""Колонка сигналов реестра: очередь, повторные отказы, курсант не на связи."""
|
|||
|
|
|
|||
|
|
import time
|
|||
|
|
from datetime import UTC, datetime, timedelta
|
|||
|
|
from uuid import uuid4
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from fastapi.testclient import TestClient
|
|||
|
|
|
|||
|
|
from app.api.http import sessions as sessions_http
|
|||
|
|
from app.config import get_settings
|
|||
|
|
from app.main import app
|
|||
|
|
from app.session.hub import hub
|
|||
|
|
from app.session.store import MemorySessionStore
|
|||
|
|
|
|||
|
|
POOL = ["fire-apartment-l2", "t01-1-fire-container"]
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def client(monkeypatch):
|
|||
|
|
monkeypatch.setenv("DEV_AUTH_BYPASS", "true")
|
|||
|
|
get_settings.cache_clear()
|
|||
|
|
|
|||
|
|
async def audit_override(*_args, **_kwargs):
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
async def optional_session_override():
|
|||
|
|
yield None
|
|||
|
|
|
|||
|
|
monkeypatch.setattr(sessions_http, "audit_required", audit_override)
|
|||
|
|
monkeypatch.setitem(
|
|||
|
|
app.dependency_overrides, sessions_http.optional_session, optional_session_override
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
with TestClient(app) as test_client:
|
|||
|
|
test_client.post("/api/auth/dev-token")
|
|||
|
|
hub.store = MemorySessionStore()
|
|||
|
|
yield test_client
|
|||
|
|
finally:
|
|||
|
|
get_settings.cache_clear()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wait_for(predicate, timeout=3):
|
|||
|
|
end = time.monotonic() + timeout
|
|||
|
|
while time.monotonic() < end:
|
|||
|
|
value = predicate()
|
|||
|
|
if value:
|
|||
|
|
return value
|
|||
|
|
time.sleep(0.02)
|
|||
|
|
raise AssertionError("состояние не обновилось")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read_until(socket, wanted):
|
|||
|
|
received = []
|
|||
|
|
for _ in range(20):
|
|||
|
|
event = socket.receive_json()
|
|||
|
|
received.append(event["type"])
|
|||
|
|
if event["type"] == wanted:
|
|||
|
|
return event
|
|||
|
|
raise AssertionError(f"событие {wanted} не пришло; получены: {received}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def start_two_card_dds(client):
|
|||
|
|
session_id = uuid4()
|
|||
|
|
context = client.websocket_connect(f"/ws/control/{session_id}")
|
|||
|
|
control = context.__enter__()
|
|||
|
|
control.send_json({
|
|||
|
|
"type": "scenario.start",
|
|||
|
|
"scenario_id": POOL[0],
|
|||
|
|
"trainee": "Иванов",
|
|||
|
|
"mode": "training",
|
|||
|
|
"exercise": "dds",
|
|||
|
|
"random_scenario_ids": POOL,
|
|||
|
|
})
|
|||
|
|
wait_for(lambda: hub.get(session_id))
|
|||
|
|
return session_id, control
|
|||
|
|
|
|||
|
|
|
|||
|
|
def row_for(client, session_id):
|
|||
|
|
rows = client.get("/api/sessions/active").json()
|
|||
|
|
return next(item for item in rows if item["session_id"] == str(session_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def signal_kinds(row):
|
|||
|
|
return {signal["kind"] for signal in row["signals"]}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_signal_backlog_when_queue_reaches_threshold(client, monkeypatch):
|
|||
|
|
monkeypatch.setenv("SIGNAL_BACKLOG_THRESHOLD", "2")
|
|||
|
|
get_settings.cache_clear()
|
|||
|
|
session_id, control = start_two_card_dds(client)
|
|||
|
|
try:
|
|||
|
|
state = hub.get(session_id)
|
|||
|
|
assert len(state.desk.cards) == 2, "оба билета должны прийти сразу без интервала"
|
|||
|
|
row = row_for(client, session_id)
|
|||
|
|
assert row["dds_open_cards"] == 2
|
|||
|
|
assert "backlog" in signal_kinds(row)
|
|||
|
|
finally:
|
|||
|
|
hub.stop_ticker(session_id)
|
|||
|
|
control.__exit__(None, None, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_signal_refusals_after_two_consecutive_declines(client):
|
|||
|
|
session_id, control = start_two_card_dds(client)
|
|||
|
|
try:
|
|||
|
|
state = hub.get(session_id)
|
|||
|
|
first_card, second_card = state.desk.ordered()
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}?role=dds") as station:
|
|||
|
|
read_until(station, "station.state")
|
|||
|
|
first_service = state.card_services(first_card)[0]
|
|||
|
|
station.send_json({
|
|||
|
|
"type": "card.status", "service": first_service, "status": "declined",
|
|||
|
|
"comment": "не наш адрес, передано в УК",
|
|||
|
|
})
|
|||
|
|
read_until(station, "station.state")
|
|||
|
|
|
|||
|
|
row = row_for(client, session_id)
|
|||
|
|
assert "refusals" not in signal_kinds(row), "одного отказа недостаточно для сигнала"
|
|||
|
|
|
|||
|
|
station.send_json({"type": "card.open", "card_id": str(second_card.card_id)})
|
|||
|
|
read_until(station, "station.state")
|
|||
|
|
second_service = state.card_services(second_card)[0]
|
|||
|
|
station.send_json({
|
|||
|
|
"type": "card.status", "service": second_service, "status": "declined",
|
|||
|
|
"comment": "не наша территория, передано в ОМВД",
|
|||
|
|
})
|
|||
|
|
read_until(station, "station.state")
|
|||
|
|
|
|||
|
|
row = row_for(client, session_id)
|
|||
|
|
assert state.consecutive_refusals == 2
|
|||
|
|
assert "refusals" in signal_kinds(row)
|
|||
|
|
finally:
|
|||
|
|
hub.stop_ticker(session_id)
|
|||
|
|
control.__exit__(None, None, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_signal_offline_when_station_socket_is_closed_past_the_window(client):
|
|||
|
|
session_id, control = start_two_card_dds(client)
|
|||
|
|
try:
|
|||
|
|
state = hub.get(session_id)
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}?role=dds") as station:
|
|||
|
|
read_until(station, "station.state")
|
|||
|
|
row = row_for(client, session_id)
|
|||
|
|
assert "offline" not in signal_kinds(row), "сокет подключён — сигнала быть не должно"
|
|||
|
|
|
|||
|
|
window = get_settings().signal_offline_window_seconds
|
|||
|
|
state.socket_last_seen_at = datetime.now(UTC) - timedelta(seconds=window + 1)
|
|||
|
|
row = row_for(client, session_id)
|
|||
|
|
assert "offline" in signal_kinds(row)
|
|||
|
|
finally:
|
|||
|
|
hub.stop_ticker(session_id)
|
|||
|
|
control.__exit__(None, None, None)
|