test(ws): кэш соединения - unit и интеграционный через websocket_connect
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1a3d25afce
commit
4dae72da2b
@ -0,0 +1,49 @@
|
||||
"""AURORA-1845: WebSocket листа формы читает user/форму один раз на соединение."""
|
||||
from src.services.budget_form_service import BudgetFormService
|
||||
from src.services.user_service import UserService
|
||||
|
||||
WS_URL = "/api/v1/ws/form/1/sheet/AHR?direction=Support"
|
||||
CELL = {"line_id": 1, "column": "plan.q1"}
|
||||
|
||||
|
||||
def _roundtrip(ws, event: str, data: dict) -> dict:
|
||||
ws.send_json({"event": event, "data": data})
|
||||
return ws.receive_json()
|
||||
|
||||
|
||||
def test_ws_reads_user_and_form_once_per_connection(client, admin_tokens, monkeypatch):
|
||||
calls = {"user": 0, "form": 0}
|
||||
orig_user_get = UserService.get
|
||||
orig_form_get = BudgetFormService.get
|
||||
|
||||
async def user_get(self, *a, **kw):
|
||||
calls["user"] += 1
|
||||
return await orig_user_get(self, *a, **kw)
|
||||
|
||||
async def form_get(self, *a, **kw):
|
||||
calls["form"] += 1
|
||||
return await orig_form_get(self, *a, **kw)
|
||||
|
||||
monkeypatch.setattr(UserService, "get", user_get)
|
||||
monkeypatch.setattr(BudgetFormService, "get", form_get)
|
||||
|
||||
n = 3
|
||||
with client.websocket_connect(WS_URL) as ws:
|
||||
ws.send_json({"event": "user_login", "data": {"token": admin_tokens["access_token"]}})
|
||||
try:
|
||||
for i in range(n):
|
||||
started = _roundtrip(ws, "cell_edit_start", CELL)
|
||||
assert not started.get("error")
|
||||
updated = _roundtrip(ws, "cell_updated", {**CELL, "value": 100 + i})
|
||||
assert not updated.get("error"), updated
|
||||
_roundtrip(ws, "cell_edit_end", CELL)
|
||||
# Только счётчики до отката: откат ниже идёт тем же соединением.
|
||||
user_calls, form_calls = calls["user"], calls["form"]
|
||||
finally:
|
||||
# WS коммитит: возвращаем исходное значение (в фикстуре plan.q1 = NULL).
|
||||
_roundtrip(ws, "cell_edit_start", CELL)
|
||||
_roundtrip(ws, "cell_updated", {**CELL, "value": None})
|
||||
_roundtrip(ws, "cell_edit_end", CELL)
|
||||
|
||||
assert user_calls == 1
|
||||
assert form_calls == 1
|
||||
74
api/tests/unit/test_websocket_connection_cache.py
Normal file
74
api/tests/unit/test_websocket_connection_cache.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""AURORA-1845: user/форма читаются из БД один раз на соединение, а не на каждое сообщение."""
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.api.v1.websocket import ConnectionCache, FormEventProcess
|
||||
|
||||
|
||||
def _processor(user=None, form=object()):
|
||||
proc = FormEventProcess(MagicMock())
|
||||
proc.user_service = MagicMock(get=AsyncMock(return_value=user or SimpleNamespace(id=7, role_id=2)))
|
||||
proc.bf_service = MagicMock(get=AsyncMock(return_value=form))
|
||||
proc.sheet_service = MagicMock(update_cell=AsyncMock(return_value=[]))
|
||||
return proc
|
||||
|
||||
|
||||
EVENT = {"event": "cell_updated", "data": {"line_id": 1, "column": "c", "value": 1}}
|
||||
|
||||
|
||||
async def _send(proc, cache, n):
|
||||
for _ in range(n):
|
||||
await proc.process(event_data=EVENT, form_id=5, user_id=7, sheet="S", cache=cache)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_reads_user_and_form_once_per_connection():
|
||||
proc = _processor()
|
||||
cache = ConnectionCache()
|
||||
await _send(proc, cache, 5)
|
||||
assert proc.user_service.get.await_count == 1
|
||||
assert proc.bf_service.get.await_count == 1
|
||||
assert proc.sheet_service.update_cell.await_count == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_without_cache_reads_every_message():
|
||||
proc = _processor()
|
||||
await _send(proc, None, 3)
|
||||
assert proc.user_service.get.await_count == 3
|
||||
assert proc.bf_service.get.await_count == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_expires_after_ttl():
|
||||
now = [0.0]
|
||||
cache = ConnectionCache(ttl=30, clock=lambda: now[0])
|
||||
proc = _processor()
|
||||
await _send(proc, cache, 2)
|
||||
now[0] = 31
|
||||
await _send(proc, cache, 2)
|
||||
assert proc.user_service.get.await_count == 2
|
||||
assert proc.bf_service.get.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_form_is_not_cached():
|
||||
proc = _processor(form=None)
|
||||
cache = ConnectionCache()
|
||||
for _ in range(3):
|
||||
res = await proc.process(event_data=EVENT, form_id=5, user_id=7, sheet="S", cache=cache)
|
||||
assert res is None
|
||||
assert proc.bf_service.get.await_count == 3
|
||||
proc.sheet_service.update_cell.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_user_is_not_cached():
|
||||
proc = _processor()
|
||||
proc.user_service.get = AsyncMock(return_value=None)
|
||||
cache = ConnectionCache()
|
||||
assert cache.get_user(7) is None
|
||||
cache.set_user(None)
|
||||
assert cache.get_user(7) is None
|
||||
Loading…
x
Reference in New Issue
Block a user