Реестр имя ограничения PostgreSQL -> (код, постоянный текст) в common/constraint_conflicts.py;
integrity_error_handler для unique_violation (23505) по известному ограничению отдаёт
{"detail": {"code", "message"}}, иначе прежняя общая строка. Имя берётся из полей драйвера
(asyncpg __cause__.constraint_name, psycopg diag), не из текста ошибки; resolved_code пишется в лог.
Тесты: unit обработчика/реестра (ключи есть в Base.metadata), интеграционные гонки (ГК/ЮЛ crm_id,
ИНН, алиас, email, членство, откат и следующий запрос, FK остаётся общим 409).
T15.3: README шаг 4 импорта; T15.4: test_components_have_no_duplicated_models.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
84 lines
4.2 KiB
Python
84 lines
4.2 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"},
|
|
}
|
|
|
|
# Модели, которые обязаны быть в схеме ровно один раз под коротким именем
|
|
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
|