- app/db/models.py: 13 таблиц (ARCHITECTURE §2.10); перечисления — VARCHAR + CHECK, JSON → JSONB на Postgres, BIGINT/INTEGER-ключи для SQLite - миграция 0001 (автогенерация + ручная чистка: импорт sa.Text, дубли CHECK) - миграции применяются при старте бота программно (app.db.migrate), script_location из пакета - SqlStorage: FSM aiogram поверх fsm_state — состояние переживает рестарт (перенесено из #4) - cleanup_loop: истёкшие черновики, снимки контейнеров и попытки входа старше суток - тесты: upgrade/downgrade, дрейф моделей и миграций, CHECK, CASCADE, FSM, чистка Refs #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
import pytest
|
|
from aiogram.methods import AnswerCallbackQuery, EditMessageText, SendMessage
|
|
from aiogram.types import InlineKeyboardMarkup, ReplyKeyboardMarkup
|
|
|
|
from app.bot.menu import Nav, NavAction
|
|
from app.config import Settings
|
|
from app.db.session import SessionMaker
|
|
from app.main import build_dispatcher
|
|
from app.texts import Texts
|
|
from tests.conftest import ADMIN, OUTSIDER
|
|
from tests.fakes import TgClient
|
|
|
|
|
|
@pytest.fixture
|
|
def tg(settings: Settings, texts: Texts, sessions: SessionMaker) -> TgClient:
|
|
return TgClient(build_dispatcher(settings, texts, sessions))
|
|
|
|
|
|
def buttons(markup: ReplyKeyboardMarkup) -> list[str]:
|
|
return [b.text for row in markup.keyboard for b in row]
|
|
|
|
|
|
async def test_admin_start_shows_welcome_and_full_menu(tg: TgClient, texts: Texts) -> None:
|
|
await tg.send(ADMIN, "/start")
|
|
|
|
[reply] = tg.session.sent(SendMessage)
|
|
assert reply.text == texts.start.welcome
|
|
assert isinstance(reply.reply_markup, ReplyKeyboardMarkup)
|
|
m = texts.menu
|
|
assert buttons(reply.reply_markup) == [m.commit, m.status, m.logs, m.alerts, m.members]
|
|
|
|
|
|
async def test_outsider_gets_one_refusal_then_silence(tg: TgClient, texts: Texts) -> None:
|
|
await tg.send(OUTSIDER, "/start")
|
|
await tg.send(OUTSIDER, "hello")
|
|
await tg.send(OUTSIDER, texts.menu.commit)
|
|
|
|
assert [r.text for r in tg.session.sent(SendMessage)] == [texts.start.no_access]
|
|
|
|
|
|
async def test_outsider_stale_button_is_swallowed(tg: TgClient) -> None:
|
|
await tg.press(OUTSIDER, Nav(action=NavAction.BACK).pack())
|
|
|
|
assert tg.session.sent(EditMessageText) == []
|
|
assert len(tg.session.sent(AnswerCallbackQuery)) == 1
|
|
|
|
|
|
@pytest.mark.parametrize("section", ["commit", "status", "logs", "alerts", "members"])
|
|
async def test_menu_button_opens_section_with_nav(tg: TgClient, texts: Texts, section: str) -> None:
|
|
await tg.send(ADMIN, getattr(texts.menu, section))
|
|
|
|
[reply] = tg.session.sent(SendMessage)
|
|
assert reply.text == getattr(texts.section, section)
|
|
assert isinstance(reply.reply_markup, InlineKeyboardMarkup)
|
|
labels = [b.text for row in reply.reply_markup.inline_keyboard for b in row]
|
|
assert labels == [texts.menu.back, texts.menu.cancel]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "expected"),
|
|
[(NavAction.BACK, "placeholder"), (NavAction.CANCEL, "cancelled")],
|
|
)
|
|
async def test_nav_edits_section_message_in_place(tg: TgClient, texts: Texts, action: NavAction, expected: str) -> None:
|
|
await tg.press(ADMIN, Nav(action=action).pack())
|
|
|
|
[edit] = tg.session.sent(EditMessageText)
|
|
want = texts.menu.placeholder if expected == "placeholder" else texts.section.cancelled
|
|
assert edit.text == want
|
|
assert tg.session.sent(SendMessage) == []
|
|
|
|
|
|
async def test_menu_and_help_commands(tg: TgClient, texts: Texts) -> None:
|
|
await tg.send(ADMIN, "/menu")
|
|
await tg.send(ADMIN, "/help")
|
|
|
|
assert [r.text for r in tg.session.sent(SendMessage)] == [texts.menu.placeholder, texts.help.text]
|