- UploadsRepo: локальный клон, fetch не чаще fetch_interval, ls-tree -z (пробелы/кириллица), cat-file, последний коммит по пути; git без shell, ssh с -F /dev/null (без конфига хоста) - «📝 Коммит»: папки/файлы с размерами, ⬆️ Вверх, страницы по 20, карточка файла с последним коммитом, «👁 Показать» (<pre> или документом); в кнопках только индексы - ключ для контейнера: GIT_SSH_KEY_B64 → временный файл 0600 (смонтированный файл хоста контейнерному uid не прочитать); volume /data для клона - тесты на настоящем bare-репозитории во временной папке Refs #8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
import pytest
|
|
from aiogram.methods import AnswerCallbackQuery, EditMessageText, SendMessage
|
|
from aiogram.types import InlineKeyboardMarkup, ReplyKeyboardMarkup
|
|
|
|
from app.access.service import AccessService
|
|
from app.bot.menu import Nav, NavAction
|
|
from app.db.session import SessionMaker
|
|
from app.main import build_dispatcher
|
|
from app.repo.git import UploadsRepo
|
|
from app.texts import Texts
|
|
from tests.conftest import ADMIN, OUTSIDER
|
|
from tests.fakes import TgClient
|
|
|
|
|
|
@pytest.fixture
|
|
def tg(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo) -> TgClient:
|
|
return TgClient(build_dispatcher(texts, sessions, access, repo))
|
|
|
|
|
|
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", ["status", "logs", "alerts"])
|
|
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]
|