arm-core/api/tests/test_api_policy.py
Andrey Epifancev 80e8905e68 feat(directories): филиалы и ГК — API, история, аудит, видимость
Репозиторий, сервис и эндпоинты /api/v1/directories/{regional-offices,company-groups}:
код GK-NNNNNN из последовательности, снимки в company_group_history, no-op PATCH
без истории и аудита, поиск q по name/crm_id/code с экранированием LIKE, видимость
по филиалу (чужая ГК = 404 как несуществующая), аудит GROUP_CHANGE/ASSIGNMENT_CHANGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 21:28:06 +04:00

83 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Политика видимости на API ГК (spec directory-policy): филиал видит своё, чужое — 404, создание из филиала."""
import uuid
import httpx
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.models.directories import RegionalOffice
from tests.conftest import AuthClientFactory
from tests.factories import make_company_group
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_NOT_FOUND = 404
HTTP_UNPROCESSABLE = 422
ROSTOV = "Ростовский региональный филиал"
KRASNODAR = "Краснодарский региональный филиал"
GROUPS = "/directories/company-groups"
async def _office(session: AsyncSession, name: str) -> RegionalOffice:
return (await session.execute(select(RegionalOffice).where(RegionalOffice.name == name))).scalar_one()
@pytest.mark.integration
async def test_branch_user_sees_only_own_groups(auth_client: AuthClientFactory, seeded: AsyncSession) -> None:
rostov, krasnodar = await _office(seeded, ROSTOV), await _office(seeded, KRASNODAR)
group_a = await make_company_group(seeded, name="ГК А", office=rostov)
group_b = await make_company_group(seeded, name="ГК Б", office=krasnodar)
branch = await auth_client("ka_rf@armka.local") # Ростовский филиал
page = (await branch.get(GROUPS)).json()
assert page["total"] == 1
assert [item["id"] for item in page["items"]] == [str(group_a.id)]
head = await auth_client("ka_go@armka.local")
ids = {item["id"] for item in (await head.get(GROUPS)).json()["items"]}
assert {str(group_a.id), str(group_b.id)} <= ids
@pytest.mark.integration
async def test_foreign_group_is_indistinguishable_from_missing(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
krasnodar = await _office(seeded, KRASNODAR)
group_b = await make_company_group(seeded, name="ГК Б", office=krasnodar)
branch = await auth_client("ka_rf@armka.local")
foreign = await branch.get(f"{GROUPS}/{group_b.id}")
missing = await branch.get(f"{GROUPS}/{uuid.uuid4()}")
assert foreign.status_code == missing.status_code == HTTP_NOT_FOUND
assert foreign.json() == missing.json()
# то же для остальных операций над чужой ГК
for call in (
branch.patch(f"{GROUPS}/{group_b.id}", json={"name": "Взлом"}),
branch.post(f"{GROUPS}/{group_b.id}/archive"),
branch.post(f"{GROUPS}/{group_b.id}/unarchive"),
branch.get(f"{GROUPS}/{group_b.id}/history"),
):
response = await call
assert response.status_code == HTTP_NOT_FOUND
assert response.json() == missing.json()
@pytest.mark.integration
async def test_branch_user_creates_group_in_own_office_only(
auth_client: AuthClientFactory, seeded: AsyncSession
) -> None:
rostov, krasnodar = await _office(seeded, ROSTOV), await _office(seeded, KRASNODAR)
branch: httpx.AsyncClient = await auth_client("ka_rf@armka.local")
created = await branch.post(GROUPS, json={"name": "ГК Из филиала"})
assert created.status_code == HTTP_CREATED
assert created.json()["base_regional_office_id"] == str(rostov.id)
assert (await branch.get(f"{GROUPS}/{created.json()['id']}")).status_code == HTTP_OK # видна создателю
foreign = await branch.post(GROUPS, json={"name": "ГК Чужая", "base_regional_office_id": str(krasnodar.id)})
assert foreign.status_code == HTTP_UNPROCESSABLE
assert foreign.json()["detail"]["code"] == "OFFICE_NOT_ALLOWED"