diff --git a/api/tests/test_api_admin.py b/api/tests/test_api_admin.py index 7fb4c93..18356d3 100644 --- a/api/tests/test_api_admin.py +++ b/api/tests/test_api_admin.py @@ -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