arm-core/api/tests/test_api_consolidation_files.py
Andrey Epifancev 4abfa03539 fix-волна ревью: delta статусов, блокировка версий консолидации, тесты (#96)
I-2: delta MODIFIED consolidations-api «Статусы Среза 4» (загрузка в консолидацию).
T1.1: шапки моделей — DBML v5.18.
T3.1: lock_versions_of_consolidation — двухшаговая блокировка (FOR UPDATE без членства, затем выборка членов);
тесты гонки и выборки.
M-1: тест миграции 0006 — порядок кварталов внутри года.
M-2: тест 404 для ЮЛ чужого филиала на столе своей консолидации.
T4.1: docstring supersede_previous — перенос строк.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 02:23:09 +04:00

198 lines
10 KiB
Python
Raw 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.

"""Рабочий стол консолидации для ЮЛ и `consolidation_id` в карточке файла (spec documents-api «Рабочий стол консолидации
для ЮЛ», «Файлы организации и карточка файла»; design D-6, D-7). Поверх фикстуры `app`: данные — фабрики в транзакции
теста."""
import uuid
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.models.access import AppUser
from src.models.consolidation import ConsolidationDocument
from src.models.directories import LegalEntity, RegionalOffice
from src.models.enums import DocumentStatus, FileStatus, PeriodQuarter, RecognitionStatus
from tests.conftest import AuthClientFactory
from tests.factories import make_company_group, make_consolidation, make_document, make_legal_entity, make_stored_file
HTTP_OK = 200
HTTP_NOT_FOUND = 404
HTTP_FORBIDDEN = 403
KA_RF = "ka_rf@armka.local" # Ростовский филиал, DOCUMENT_VIEW + DOCUMENT_EDIT
CLIENT_MANAGER = "client_manager@armka.local" # Краснодарский филиал
CURATOR_GO = "curator_go@armka.local" # головной офис, права DOCUMENT_VIEW нет
ROSTOV = "Ростовский региональный филиал"
KRASNODAR = "Краснодарский региональный филиал"
pytestmark = pytest.mark.integration
async def _group(session: AsyncSession, office_name: str = ROSTOV, name: str = "ГК Стол"):
office = (await session.execute(select(RegionalOffice).where(RegionalOffice.name == office_name))).scalar_one()
group = await make_company_group(session, name=name, office=office)
entity = await make_legal_entity(session, name=f"ООО {name}", group=group)
initiator = (await session.execute(select(AppUser).where(AppUser.email == KA_RF))).scalar_one()
return group, entity, initiator
async def _consolidation( # noqa: PLR0913, PLR0917 - тестовый помощник
session: AsyncSession, group, entity, initiator, year: int, quarter: PeriodQuarter
):
consolidation, version = await make_consolidation(
session, group=group, year=year, quarter=quarter, members=[entity], initiator=initiator
)
return consolidation, version
async def _file(
session: AsyncSession, entity: LegalEntity, name: str, *, consolidation_id=None, status=FileStatus.PROCESSED
):
file, job = await make_stored_file(
session, entity=entity, status=status, job_status=RecognitionStatus.done, original_name=name
)
file.consolidation_id = consolidation_id
await session.flush()
return file, job
def _desk_url(consolidation_id: uuid.UUID, entity_id: uuid.UUID, **query: str) -> str:
base = f"/consolidations/{consolidation_id}/legal-entities/{entity_id}/files"
return base + ("?" + "&".join(f"{k}={v}" for k, v in query.items()) if query else "")
async def test_desk_shows_only_files_of_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
"""Сценарий «Стол показывает только файлы консоли»: три файла ЮЛ — 2025 Q1, 2025 Q2, без контекста."""
group, entity, initiator = await _group(seeded)
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
c2, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
f1, _ = await _file(seeded, entity, "q1.xls", consolidation_id=c1.id)
f2, _ = await _file(seeded, entity, "q2.xls", consolidation_id=c2.id)
f0, _ = await _file(seeded, entity, "none.xls")
client = await auth_client(KA_RF)
desk1 = await client.get(_desk_url(c1.id, entity.id))
desk2 = await client.get(_desk_url(c2.id, entity.id))
all_files = await client.get(f"/legal-entities/{entity.id}/files")
assert desk1.status_code == HTTP_OK, desk1.text
assert [item["id"] for item in desk1.json()["items"]] == [str(f1.id)]
assert desk1.json()["total"] == 1
assert desk1.json()["items"][0]["consolidation_id"] == str(c1.id)
assert [item["id"] for item in desk2.json()["items"]] == [str(f2.id)]
assert {item["id"] for item in all_files.json()["items"]} == {str(f1.id), str(f2.id), str(f0.id)}
by_id = {item["id"]: item["consolidation_id"] for item in all_files.json()["items"]}
assert by_id[str(f0.id)] is None
async def test_desk_includes_file_bound_to_version(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
"""Сценарий «Подхваченный файл виден через привязку»: файл без контекста, документ привязан к версии консоли."""
group, entity, initiator = await _group(seeded)
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
file, job = await _file(seeded, entity, "q1-confirmed.xls")
document = await make_document(
seeded,
file=file,
job=job,
account="60",
year=2025,
quarter=PeriodQuarter.Q1,
status=DocumentStatus.CONFIRMED,
is_cumulative=False,
)
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
await seeded.flush()
client = await auth_client(KA_RF)
response = await client.get(_desk_url(c2.id, entity.id))
assert response.status_code == HTTP_OK, response.text
assert [item["id"] for item in response.json()["items"]] == [str(file.id)]
assert response.json()["items"][0]["consolidation_id"] is None
async def test_desk_excludes_other_consolidation(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
"""Review Focus 2: документ привязан к версии ДРУГОЙ консолидации — файл в этом столе не показывается."""
group, entity, initiator = await _group(seeded)
c1, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
c2, v2 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q2)
file, job = await _file(seeded, entity, "bound-elsewhere.xls", consolidation_id=c2.id)
document = await make_document(seeded, file=file, job=job, status=DocumentStatus.CONFIRMED)
seeded.add(ConsolidationDocument(consolidation_version_id=v2.id, document_id=document.id))
await seeded.flush()
client = await auth_client(KA_RF)
response = await client.get(_desk_url(c1.id, entity.id))
assert response.status_code == HTTP_OK, response.text
assert response.json()["items"] == []
assert response.json()["total"] == 0
async def test_desk_superseded_hidden_by_default(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
"""Сценарий «Заменённый файл скрыт по умолчанию» для DESK и BOUND; `include_superseded=true` — оба."""
group, entity, initiator = await _group(seeded)
c1, v1 = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
new, _ = await _file(seeded, entity, "new.xls", consolidation_id=c1.id)
old_desk, _ = await _file(seeded, entity, "old-desk.xls", consolidation_id=c1.id, status=FileStatus.SUPERSEDED)
old_desk.superseded_by_file_id = new.id
old_bound, job = await _file(seeded, entity, "old-bound.xls", status=FileStatus.SUPERSEDED)
old_bound.superseded_by_file_id = new.id
document = await make_document(seeded, file=old_bound, job=job, status=DocumentStatus.CONFIRMED)
seeded.add(ConsolidationDocument(consolidation_version_id=v1.id, document_id=document.id))
await seeded.flush()
client = await auth_client(KA_RF)
default = await client.get(_desk_url(c1.id, entity.id))
with_superseded = await client.get(_desk_url(c1.id, entity.id, include_superseded="true"))
assert [item["id"] for item in default.json()["items"]] == [str(new.id)]
assert {item["id"] for item in with_superseded.json()["items"]} == {
str(new.id),
str(old_desk.id),
str(old_bound.id),
}
assert with_superseded.json()["total"] == 3 # noqa: PLR2004
async def test_desk_foreign_and_missing_consolidation_are_same_404(
seeded: AsyncSession, auth_client: AuthClientFactory
) -> None:
"""Сценарий «Чужая консолидация»: ГК другого филиала и несуществующий id — одинаковый 404; чужое ЮЛ — тоже 404."""
group, entity, initiator = await _group(seeded, office_name=KRASNODAR, name="ГК Чужая")
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
own_group, own_entity, own_initiator = await _group(seeded, name="ГК Своя")
own_c, _ = await _consolidation(seeded, own_group, own_entity, own_initiator, 2025, PeriodQuarter.Q1)
client = await auth_client(KA_RF)
foreign = await client.get(_desk_url(c.id, own_entity.id))
missing = await client.get(_desk_url(uuid.uuid4(), own_entity.id))
foreign_entity = await client.get(_desk_url(own_c.id, entity.id)) # своя консолидация, ЮЛ чужого филиала
assert foreign.status_code == missing.status_code == foreign_entity.status_code == HTTP_NOT_FOUND
assert foreign.json() == missing.json()
assert foreign.json()["detail"]["code"] == "NOT_FOUND"
assert foreign_entity.json()["detail"]["code"] == "NOT_FOUND"
async def test_desk_requires_document_view(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
group, entity, initiator = await _group(seeded)
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
client = await auth_client(CURATOR_GO)
response = await client.get(_desk_url(c.id, entity.id))
assert response.status_code == HTTP_FORBIDDEN
async def test_file_card_has_consolidation_id(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
"""Сценарий «Консолидация в карточке»: `GET /files/{id}` отдаёт `consolidation_id`."""
group, entity, initiator = await _group(seeded)
c, _ = await _consolidation(seeded, group, entity, initiator, 2025, PeriodQuarter.Q1)
file, _ = await _file(seeded, entity, "card.xls", consolidation_id=c.id)
client = await auth_client(KA_RF)
response = await client.get(f"/files/{file.id}")
assert response.status_code == HTTP_OK, response.text
assert response.json()["consolidation_id"] == str(c.id)