"""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, documents-api (Срез 2c) "/api/v1/legal-entities/{legal_entity_id}/files": {"get", "post"}, "/api/v1/files/{file_id}": {"get", "delete"}, "/api/v1/documents/{document_id}": {"get"}, "/api/v1/documents/{document_id}/entries": {"get"}, "/api/v1/documents/{document_id}/confirm": {"post"}, } # Префиксы, под которыми маршрутов сверх EXPECTED_ROUTES быть не должно CHECKED_PREFIXES = (D, A, "/api/v1/legal-entities", "/api/v1/files", "/api/v1/documents") # Модели, которые обязаны быть в схеме ровно один раз под коротким именем SINGLE_MODELS = [ "RegionalOfficeOut", "CompanyGroupOut", "LegalEntityOut", "UserOut", "RoleOut", "ImportReportOut", "FileOut", "DocumentOut", "EntryOut", ] @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(CHECKED_PREFIXES)} expected = {path for path in EXPECTED_ROUTES if path.startswith(CHECKED_PREFIXES)} 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