Separate staff roles and protect message delivery with operator guides
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-09-13 21:00:53 +09:00
parent d5a9c13b68
commit 3c1a6a02a5
25 changed files with 916 additions and 548 deletions

View File

@@ -0,0 +1,121 @@
import asyncio
from aiogram import Bot
import pytest
from sqlalchemy import select
from src.core.access import get_role
from src.core.database import async_session_maker
from src.core.models import User
from src.core.services import UserService
from src.core.staff_service import StaffError, change_role, get_member
from test_dispatcher import dispatch, event
async def create_user(telegram_id, **flags):
async with async_session_maker() as session:
user = await UserService.get_or_create_user(session, telegram_id, first_name="<Employee>")
for key, value in flags.items():
setattr(user, key, value)
await session.commit()
return user
async def test_roles_are_exclusive_and_removal_does_not_restore_old_cashier_privilege():
await create_user(810, is_cashier=True)
await change_role(900001, 810, "admin", (False, True))
member = await get_member(900001, 810)
assert member.role == "admin" and member.flags == (True, False)
await change_role(900001, 810, "cashier", member.flags)
assert await get_role(810) == "cashier"
await change_role(900001, 810, "user", (False, True))
assert await get_role(810) == "user"
@pytest.mark.parametrize("actor", [811, 900002])
async def test_non_system_staff_cannot_assign_roles_even_by_service(actor):
await create_user(actor, is_admin=actor == 811)
await create_user(812)
with pytest.raises(StaffError, match="системный администратор"):
await change_role(actor, 812, "admin", (False, False))
assert await get_role(812) == "user"
@pytest.mark.parametrize("target", [900001, 900002])
async def test_server_configured_staff_cannot_be_changed(target):
await create_user(target)
with pytest.raises(StaffError):
await change_role(900001, target, "user", (False, False))
async def test_two_system_admins_cannot_overwrite_a_changed_role(monkeypatch):
from src.core import staff_service
monkeypatch.setattr(staff_service, "ADMIN_IDS", [900001, 900003])
await create_user(813)
results = await asyncio.gather(
change_role(900001, 813, "admin", (False, False)),
change_role(900003, 813, "cashier", (False, False)), return_exceptions=True)
assert sum(isinstance(result, StaffError) for result in results) == 1
assert sum(result is None for result in results) == 1
@pytest.mark.parametrize("flags", [{"is_admin": True}, {"is_cashier": True}])
async def test_cleanup_cannot_revoke_staff_access(flags):
user = await create_user(814, **flags)
async with async_session_maker() as session:
assert not await UserService.delete_user(session, user.id)
assert await session.scalar(select(User.id).where(User.id == user.id)) == user.id
def confirmation(calls):
return next(button.callback_data for call in calls if getattr(call, "reply_markup", None)
for row in call.reply_markup.inline_keyboard for button in row
if button.callback_data and button.callback_data.startswith("staff:confirm:"))
async def test_actual_assignment_requires_confirmation_and_old_button_cannot_change_new_target():
await create_user(815)
await create_user(816)
first = await dispatch(event(900001, text="/add_admin 815"))
old_button = confirmation(first)
assert await get_role(815) == "user"
assert any("&lt;Employee&gt;" in (getattr(call, "text", "") or "") for call in first)
second = await dispatch(event(900001, text="/add_cashier 816"))
new_button = confirmation(second)
await dispatch(event(900001, callback_data=old_button))
assert await get_role(815) == "user" and await get_role(816) == "user"
await dispatch(event(900001, callback_data=new_button))
assert await get_role(816) == "cashier"
await change_role(900001, 816, "user", (False, True))
await dispatch(event(900001, callback_data=new_button))
assert await get_role(816) == "user"
@pytest.mark.parametrize("actor", [817, 900002])
async def test_actual_ordinary_admin_and_cashier_cannot_forge_staff_callback(actor):
await create_user(actor, is_admin=actor == 817)
await create_user(818)
for update in (event(actor, text="/add_admin 818"),
event(actor, callback_data="staff:role:admin"),
event(actor, callback_data="admin_remove_select:900001")):
calls = await dispatch(update)
assert any("системный администратор" in (getattr(call, "text", "") or "") for call in calls)
assert await get_role(818) == "user"
async def test_revoked_cashier_cannot_continue_open_participant_dialog():
await create_user(819, is_cashier=True)
await dispatch(event(819, callback_data="cash_add:1"))
await change_role(900001, 819, "user", (False, True))
calls = await dispatch(event(819, text="11-22-33-44-55-66-77"))
assert any("Недостаточно прав" in (getattr(call, "text", "") or "") for call in calls)
async def test_guides_are_delivered_only_to_the_appropriate_roles():
from src.handlers.operator_help import DOCS, GUIDES
assert all((DOCS / spec[0]).is_file() for spec in GUIDES.values())
calls = await dispatch(event(900002, text="/cashier_help"))
assert any(call.__api_method__ == "sendDocument" for call in calls)
for update in (event(900002, text="/admin_help"), event(820, callback_data="guide:system")):
calls = await dispatch(update)
assert not any(call.__api_method__ == "sendDocument" for call in calls)