Репозиторий, сервис и эндпоинты /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
83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
"""Политика видимости на 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"
|