← HTTP API

Проверки контракта

http api · conformance
Принятые решения
Сквозные инварианты владеет слой, не модуль Форма ошибки, UTC-сериализация, пагинация, лимит частоты — одинаковы на всех роутерах. Один параметризованный набор идёт против ASGI-приложения и обходит зарегистрированные роуты автоматически: новый эндпойнт наследует проверки, не заводя своей копии. Модули тестируют семантику своих ресурсов (кто владелец, какой код на какое нарушение) — эти общие грани там не дублируются.
Правила — на карте HTTP API Здесь — их исполнение тестом. Правило задаёт «как должно быть», кейс ниже проверяет, что так и есть, на каждом роутере обоих ярусов (/api/v1, /public/v1).
Стек и охват
Инструмент httpx · ASGI-transport — реальное приложение без сети
Обход роутов интроспекция app.routes — каждый зарегистрированный роут в параметрах
Схема Schemathesis по OpenAPI — ловит 500 и расхождение со схемой; envelope и семантику покрывают кейсы ниже
Маркеры @pytest.mark.api — контрактный слой поверх приложения
API · P1 Сквозные инварианты — единый контракт всех роутеров httpx · ASGI · параметризация по app.routes
test_api_conformance.py
Ошибка · коды · время · версия · личность · лимит
Кейсы
единая форма ошибкилюбой отказ → RFC 9457 application/problem+json: {type, title, status, detail, code, request_id} (+ errors[] на 422); клиент читает машинный code, не парсит текст; форма одна на всех роутерах
бизнес-нарушение → 4xx, не 500нарушение правила отдаёт свой код (401 · 403 · 404 · 409 · 422 · 429), не проваливается в 500; 500 — только незапланированный сбой
словарь кодов размечен401 нет личности · 403 нет права · 404 нет ресурса (приватное маскируем под 404, не 403) · 409 конфликт/замок · 422 валидация тела · 429 лимит
время — UTC ISO-8601каждое datetime в ответе сериализуется как …Z (UTC); ни naive, ни локальная зона; пояс применяет фронт
версия в пути/api/v1 · /public/v1; без версии → 404; ломающее изменение → новая версия, старая жива
личность до обработчикаJWT или ключ ach_… резолвится в user_id → роль → ACL прежде входа в хендлер; нет личности → 401 единообразно на защищённых роутах
лимит частоты → 429 + Retry-Afterпревышение окна → 429 с заголовком Retry-After; тело — тот же envelope (rate-limit)
нет голых роутовпараметризация по app.routes: каждый защищённый роут отвечает 401 без личности — новый эндпойнт не проскочит мимо проверки
test_api_pagination.py
Единый list-контракт списочных GET
Кейсы
единая обёртка страницысписочный GET отдаёт одинаковую форму (элементы + курсор/итог), одну на всех роутерах, а не свою схему у каждого
потолок размера страницызапрос сверх серверного потолка усечён до предела, не безлимитная выдача; дефолтный размер при отсутствии параметра
стабильный порядоксортировка детерминирована при равных ключах (tie-break по id) — страницы не пересекаются и не теряют строк
курсор переживает вставкукурсорная пагинация не сдвигает окно при вставке новых строк между запросами страниц
кривой параметр → 422отрицательный/нечисловой limit, битый курсор → 422 (envelope), не 500 и не тихий дефолт