POST /api/v1/legal-entities/{id}/files (DOCUMENT_EDIT, видимость ЮЛ → 404): тип по расширению
со сверкой сигнатуры (domain/file_types.py), лимит UPLOAD_MAX_BYTES → FILE_TOO_LARGE, дедуп по
sha256 в пределах ЮЛ (кроме SUPERSEDED) с рекомендательной блокировкой (ЮЛ, sha256) до конца
транзакции запроса, s3_object → put_object → stored_file(UPLOADED) → recognition_job(queued) →
current_recognition_job_id, аудит FILE_UPLOAD. Каждый файл — SAVEPOINT: частичный успех
(201/422), отказ S3 на файле — STORAGE_UNAVAILABLE только для него; без S3 — 503.
safe_filename/CONTENT_TYPES/read_limited вынесены в common/filenames.py (импорт справочников
их переиспользует). Схемы UploadResult/FileOut/JobBrief/DocumentBrief, PrimaryFilesService и
зависимость для Task 9–10. tasks.md 2.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
86 lines
4.3 KiB
Python
86 lines
4.3 KiB
Python
"""OpenAPI содержит все маршруты Среза 1 (specs directories-api, directory-import, users-admin) без дублей моделей.
|
|
|
|
Схема строится из объявленных маршрутов без БД и сети. Состав маршрутов — по спекам change `slice-1-directories`;
|
|
проверка нужна, чтобы маршрут не пропал при переносе роутеров и чтобы `RegionalOfficeOut` (R-13) был одной моделью.
|
|
"""
|
|
|
|
import importlib
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
app_module = importlib.import_module("src.app")
|
|
|
|
D = "/api/v1/directories"
|
|
A = "/api/v1/admin"
|
|
|
|
# путь → методы (нижний регистр, как в OpenAPI)
|
|
EXPECTED_ROUTES: dict[str, set[str]] = {
|
|
"/api/v1/me": {"get"},
|
|
# directories-api: филиалы, ГК, ГСК/ГСЗ, ЮЛ/ИП
|
|
f"{D}/regional-offices": {"get"},
|
|
f"{D}/company-groups": {"get", "post"},
|
|
f"{D}/company-groups/{{group_id}}": {"get", "patch"},
|
|
f"{D}/company-groups/{{group_id}}/archive": {"post"},
|
|
f"{D}/company-groups/{{group_id}}/unarchive": {"post"},
|
|
f"{D}/company-groups/{{group_id}}/history": {"get"},
|
|
f"{D}/company-groups/{{company_group_id}}/related-groups": {"get", "post"},
|
|
f"{D}/related-groups/{{group_id}}": {"get", "patch", "delete"},
|
|
f"{D}/related-groups/{{group_id}}/members": {"post"},
|
|
f"{D}/related-groups/{{group_id}}/members/{{legal_entity_id}}": {"delete"},
|
|
f"{D}/legal-entities": {"get", "post"},
|
|
f"{D}/legal-entities/{{entity_id}}": {"get", "patch"},
|
|
f"{D}/legal-entities/{{entity_id}}/move": {"post"},
|
|
f"{D}/legal-entities/{{entity_id}}/withdraw": {"post"},
|
|
f"{D}/legal-entities/{{entity_id}}/history": {"get"},
|
|
f"{D}/legal-entities/{{entity_id}}/aliases": {"post"},
|
|
f"{D}/legal-entities/{{entity_id}}/aliases/{{alias_id}}": {"delete"},
|
|
# directory-import
|
|
f"{D}/import": {"post"},
|
|
# users-admin
|
|
f"{A}/users": {"get", "post"},
|
|
f"{A}/users/{{user_id}}": {"patch"},
|
|
f"{A}/users/{{user_id}}/roles": {"put"},
|
|
f"{A}/roles": {"get"},
|
|
f"{A}/permissions": {"get"},
|
|
f"{A}/roles/{{code}}/permissions": {"put"},
|
|
# primary-files (Срез 2c)
|
|
"/api/v1/legal-entities/{legal_entity_id}/files": {"post"},
|
|
}
|
|
|
|
# Модели, которые обязаны быть в схеме ровно один раз под коротким именем
|
|
SINGLE_MODELS = ["RegionalOfficeOut", "CompanyGroupOut", "LegalEntityOut", "UserOut", "RoleOut", "ImportReportOut"]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def schema() -> dict[str, Any]:
|
|
spec: dict[str, Any] = app_module.app.openapi()
|
|
return spec
|
|
|
|
|
|
@pytest.mark.parametrize("path", sorted(EXPECTED_ROUTES))
|
|
def test_spec_route_is_in_openapi(schema: dict[str, Any], path: str) -> None:
|
|
assert path in schema["paths"], f"в OpenAPI нет маршрута {path}"
|
|
assert set(schema["paths"][path]) >= EXPECTED_ROUTES[path], (path, sorted(schema["paths"][path]))
|
|
|
|
|
|
def test_no_unexpected_routes_under_directories_and_admin(schema: dict[str, Any]) -> None:
|
|
"""Маршрутов сверх спек нет: новые маршруты сначала попадают в спеку и в этот список."""
|
|
actual = {path for path in schema["paths"] if path.startswith((D, A))}
|
|
expected = {path for path in EXPECTED_ROUTES if path.startswith((D, A))}
|
|
assert actual == expected
|
|
for path in actual:
|
|
assert set(schema["paths"][path]) == EXPECTED_ROUTES[path], path
|
|
|
|
|
|
def test_components_have_no_duplicated_models(schema: dict[str, Any]) -> None:
|
|
"""Одна модель — одно имя: нет автоимён вида `src__schemas__x__RegionalOfficeOut` и суффиксов `-Input`/`-Output`.
|
|
|
|
Имена — ключи словаря, поэтому дубликатов быть не может; реальный страж — проверка на `__`/`-` (FastAPI даёт такие
|
|
имена, когда две разные модели совпали по имени или одна модель разошлась на вход/выход).
|
|
"""
|
|
names = list(schema["components"]["schemas"])
|
|
for model in SINGLE_MODELS:
|
|
assert model in names, model
|
|
assert not [name for name in names if "__" in name or "-" in name], names
|