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]