test(admin): ветка kept_roles, различающий тест экранирования, код LAST_ADMIN (T12.1, T12.3, T12.4)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-05 01:29:32 +04:00
parent fda907ba2e
commit d241c07f54

View File

@ -183,14 +183,15 @@ async def test_list_users_search_filter_and_page(auth_client: AuthClientFactory,
office = await _office(seeded, ROSTOV)
await make_user(seeded, email="Zoya_Search@example.com", office=office, roles=["KA_RF", "EXPERT"], full_name="Зоя")
await make_user(seeded, email="off@example.com", office=office, full_name="Зоя Отключённая", is_active=False)
await make_user(seeded, email="zoyaXs@example.com", office=office) # совпал бы с `zoya_s`, если `_` — шаблон LIKE
admin = await auth_client(ADMIN)
everyone = (await admin.get(USERS)).json()
assert set(everyone) == {"items", "total", "limit", "offset"}
assert everyone["total"] == len(everyone["items"]) >= 9 # noqa: PLR2004 - 7 dev-пользователей + 2 новых
assert everyone["total"] == len(everyone["items"]) >= 10 # noqa: PLR2004 - 7 dev-пользователей + 3 новых
by_email = (await admin.get(USERS, params={"q": "zoya_s"})).json() # `_` ищется буквально, без регистра
assert [item["email"] for item in by_email["items"]] == ["Zoya_Search@example.com"]
assert [item["email"] for item in by_email["items"]] == ["Zoya_Search@example.com"] # `zoyaXs@…` не найден
assert by_email["items"][0]["roles"] == ["EXPERT", "KA_RF"]
assert by_email["items"][0]["regional_office"]["name"] == ROSTOV
@ -364,6 +365,57 @@ async def test_second_admin_allows_dropping_own_admin_role(
assert (await admin.get("/admin/roles")).status_code == HTTP_FORBIDDEN # права пересчитаны на следующем запросе
async def _grant_users_admin(session: AsyncSession, role_code: str, *, is_active: bool = True) -> None:
"""Даёт роли право `USERS_ADMIN` напрямую в БД (и задаёт `is_active` роли) — как после правки матрицы прав."""
role = (await session.execute(select(Role).where(Role.code == role_code))).scalar_one()
permission_id = (await session.execute(select(Permission.id).where(Permission.code == "USERS_ADMIN"))).scalar_one()
session.add(RolePermission(role_id=role.id, permission_id=permission_id))
role.is_active = is_active
await session.flush()
@pytest.mark.integration
async def test_sole_admin_moved_to_active_role_with_users_admin_stays_admin(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
"""Ветка `kept_roles`: оставшаяся активная роль даёт `USERS_ADMIN` — другого администратора не требуется (T12.1)."""
await _grant_users_admin(seeded, "EXPERT")
admin_id = await _user_id(seeded, ADMIN)
admin = await auth_client(ADMIN)
response = await admin.put(f"{USERS}/{admin_id}/roles", json={"roles": ["EXPERT"]})
assert response.status_code == HTTP_OK, response.text
assert await _role_codes(seeded, admin_id) == ["EXPERT"]
assert (await admin.get(USERS)).status_code == HTTP_OK # право USERS_ADMIN сохранено через EXPERT
assert len(await _events(seeded, AuditAction.USER_ROLE_CHANGE)) == 1
@pytest.mark.integration
async def test_sole_admin_cannot_be_moved_to_inactive_role_with_users_admin(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
"""Неактивная роль с `USERS_ADMIN` не даёт права и не назначается: набор ролей и администратор не меняются (T12.1).
Сервис отсекает её раньше проверки `LAST_ADMIN` — 422 `UNKNOWN_ROLE`; сам фильтр `is_active` в `kept_roles`-проверке
(`roles_grant_permission`) защитный и до неё через API недостижим, поэтому проверяется отдельно.
"""
await _grant_users_admin(seeded, "EXPERT", is_active=False)
admin_id = await _user_id(seeded, ADMIN)
admin = await auth_client(ADMIN)
response = await admin.put(f"{USERS}/{admin_id}/roles", json={"roles": ["EXPERT"]})
assert response.status_code == HTTP_UNPROCESSABLE, response.text
assert _detail_code(response) == "UNKNOWN_ROLE"
assert await _role_codes(seeded, admin_id) == ["ADMIN"]
assert await _events(seeded, AuditAction.USER_ROLE_CHANGE) == []
assert (await admin.get(USERS)).status_code == HTTP_OK
roles = {
role.code: role.id
for role in (await seeded.execute(select(Role).where(Role.code.in_(["ADMIN", "EXPERT"])))).scalars()
}
assert await users_repo.roles_grant_permission(seeded, {roles["ADMIN"]}, "USERS_ADMIN") is True
assert await users_repo.roles_grant_permission(seeded, {roles["EXPERT"]}, "USERS_ADMIN") is False # неактивная роль
@pytest.mark.integration
async def test_last_admin_cannot_deactivate_self(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
"""Review Focus 5: последний активный с `USERS_ADMIN` не может деактивировать себя; при втором — может."""
@ -401,6 +453,7 @@ async def test_deactivating_other_admin_respects_last_admin(
admin_id = await _user_id(seeded, ADMIN)
response = await admin.patch(f"{USERS}/{admin_id}", json={"is_active": False})
assert response.status_code == HTTP_CONFLICT
assert _detail_code(response) == "LAST_ADMIN"
assert (await second_client.get(USERS)).status_code == HTTP_FORBIDDEN