- AccessService: одноразовые приглашения (128 бит, в БД только sha256, TTL 24 ч), атомарный захват приглашения (условный UPDATE — из двух сканов одного QR проходит один), антиперебор (> 5 неудач в час — молчание), отзыв (черновик и FSM удаляются), повторное приглашение - ADMIN_IDS заводятся в БД при старте; дальше — только по приглашению - AccessMiddleware: участники из БД (+ last_seen раз в 5 мин), новичкам — только /start <токен> - «👥 Участники» для админов: список, QR (segno) + ссылка, отзыв с подтверждением - один ответ на неверный/истёкший/использованный токен — без подсказок Refs #25 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
1.4 KiB
Python
51 lines
1.4 KiB
Python
from collections.abc import AsyncIterator
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
|
|
from app.access.service import AccessService
|
|
from app.config import AccessSettings, RuntimeSettings, Settings, TelegramSettings
|
|
from app.db.migrate import upgrade
|
|
from app.db.session import SessionMaker, make_engine, make_sessionmaker
|
|
from app.texts import Texts, load_texts
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
ADMIN = 1001
|
|
OUTSIDER = 2002
|
|
|
|
|
|
@pytest.fixture
|
|
def texts() -> Texts:
|
|
return load_texts(ROOT / "config")
|
|
|
|
|
|
@pytest.fixture
|
|
def settings(tmp_path: Path) -> Settings:
|
|
return Settings(
|
|
telegram=TelegramSettings(bot_token="42:TEST-TOKEN"),
|
|
access=AccessSettings(admin_ids=frozenset({ADMIN})),
|
|
runtime=RuntimeSettings(config_dir=ROOT / "config", heartbeat_file=tmp_path / "hb"),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def engine() -> AsyncIterator[AsyncEngine]:
|
|
"""Fresh in-memory SQLite with all migrations applied — the same path the bot takes at startup."""
|
|
eng = make_engine("sqlite+aiosqlite://")
|
|
await upgrade(eng)
|
|
yield eng
|
|
await eng.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def sessions(engine: AsyncEngine) -> SessionMaker:
|
|
return make_sessionmaker(engine)
|
|
|
|
|
|
@pytest.fixture
|
|
async def access(sessions: SessionMaker, settings: Settings) -> AccessService:
|
|
service = AccessService(sessions, settings.access)
|
|
await service.bootstrap_admins()
|
|
return service
|