Files
robotex/tests/unit/test_actors.py
T
protokeyandClaude Opus 5.5 1a4bd8d672 feature: API-ключи клиентов и журнал использования в Postgres (#4, #17)
- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests.
- Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без
  выпущенных ключей доступа нет. В базе — только sha256 ключа.
- Задача и окно сессии привязаны к клиенту: чужой ключ их не видит.
- Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу
  (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу.
- python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats.
- smoke и loadtest с ключом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:48:48 +04:00

66 lines
2.6 KiB
Python

"""Настоящий воркер dramatiq на брокере в памяти: actor'ы, event loop AsyncIO, пул окон и heartbeat."""
import asyncio
import dramatiq
import fakeredis
import pytest
from api import actions
from tasks import store as task_store
from tasks.broker import COMMON_QUEUE, personal_queue, task_message
from worker import windows
from .conftest import FakeSession, fake_execute
@pytest.fixture
def worker(monkeypatch, tmp_path):
server = fakeredis.FakeServer()
monkeypatch.setattr(task_store, "connect", lambda url=None: fakeredis.aioredis.FakeRedis(server=server, decode_responses=True))
monkeypatch.setattr(windows, "AsyncStealthySession", FakeSession)
monkeypatch.setattr(windows, "USER_DATA_ROOT", tmp_path)
monkeypatch.setattr(actions, "execute", fake_execute())
FakeSession.instances, FakeSession.error = [], None
from db.engine import connect as connect_db
from worker import actors
monkeypatch.setattr(actors, "connect_db", lambda: connect_db(f"sqlite+aiosqlite:///{tmp_path / 'robotex.db'}"))
w = dramatiq.Worker(actors.broker, worker_timeout=100, worker_threads=2)
w.start()
try:
yield actors, server
finally:
w.stop()
# при остановке воркер закрывает окна и снимает heartbeat
assert all(s.closed for s in FakeSession.instances)
assert not fakeredis.FakeRedis(server=server).keys("robotex:worker:*")
SID = "11111111-1111-1111-1111-111111111111"
def submit(actors, server, queue, idle=0):
"""То же, что делает api: задача в Redis, сообщение в очередь; ждём, пока воркер её выполнит."""
store = task_store.TaskStore(fakeredis.aioredis.FakeRedis(server=server, decode_responses=True))
asyncio.run(store.create("t1", SID, "https://example.com"))
actors.broker.enqueue(task_message("t1", SID, {"url": "https://example.com", "actions": []}, idle, queue))
actors.broker.join(queue)
return fakeredis.FakeRedis(server=server, decode_responses=True)
@pytest.mark.parametrize("queue", ["common", "personal"])
def test_task_executed_from_queue(worker, queue):
actors, server = worker
redis = submit(actors, server, COMMON_QUEUE if queue == "common" else personal_queue(actors.WORKER_ID))
task = redis.hgetall("robotex:task:t1")
assert task["status"] == "done", task
assert '"status": "ok"' in task["result"]
assert redis.exists(f"robotex:worker:{actors.WORKER_ID}")
def test_window_held_until_shutdown(worker):
actors, server = worker
redis = submit(actors, server, COMMON_QUEUE, idle=5)
assert redis.hgetall(f"robotex:session:{SID}")["worker_id"] == actors.WORKER_ID
assert not FakeSession.instances[0].closed