/
systemsstrategyy
/
Treker
Обзор
Документация
Войти
/
systemsstrategyy
/
Treker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
tests/modules/checklists/test_openapi_problem_checklists.py
131 строка
7 KB
SystemsStrategy
Initial import
02 июл 2026, 15:15
02 июл 2026, 15:15
9624ed1
Код
Авторство
О чём код?
"""Static OpenAPI conformance тесты для checklist endpoints. Цель — поймать «Undocumented Content-Type / HTTP status code» violations, которые schemathesis обнаружил бы при contract fuzzing'е. Запускается быстро без testcontainer'а — просто инспектирует `app.openapi()`. Покрытые endpoints: - Templates (4 ops): создание/список (под space), get-by-uid (flat), archive-delete (flat) - Runs (~9 ops): apply-to-card, standalone-create, list+get-by-uid, PUT item, complete, history, history/stats, history/export.xlsx - Schedules (3 ops): create/list (под template), hard-delete (flat) Паттерн зеркалит `tests/modules/billing/test_openapi_problem_billing.py` (issue #140 от Foundation). RFC 9457: все 4xx должны быть `application/problem+json`, не дефолтный FastAPI `application/json`. """ from __future__ import annotations from typing import Any import pytest from api.main import app # (path, method, set_of_expected_problem_json_status_codes) # 200/201/204 в этот set НЕ кладём — у них application/json или вообще # нет content. Только 4xx-коды, которые должны быть problem+json. # # 422 включаем везде где есть body / query params — FastAPI генерит # RequestValidationError, обёрнутый в наш handler через # api.shared.schemas.errors.AUTHORIZED_RESPONSES (если используется). CHECKLIST_ENDPOINTS: list[tuple[str, str, set[int]]] = [ # ── Templates (nested под space + flat) ────────────────────────────── ("/api/v1/spaces/{space_uid}/checklist-templates", "post", {401, 403, 404, 422}), ("/api/v1/spaces/{space_uid}/checklist-templates", "get", {401, 403, 404, 422}), ("/api/v1/checklist-templates/{uid}", "get", {401, 403, 404, 422}), ("/api/v1/checklist-templates/{uid}", "delete", {401, 403, 404, 409, 422}), # ── Schedules ──────────────────────────────────────────────────────── ("/api/v1/checklist-templates/{template_uid}/schedules", "post", {401, 403, 404, 422}), ("/api/v1/checklist-templates/{template_uid}/schedules", "get", {401, 403, 404, 422}), ("/api/v1/checklist-schedules/{uid}", "delete", {401, 403, 404, 422}), # ── Runs (POST + GET + state machine + history) ────────────────────── ("/api/v1/cards/{card_uid}/checklist-runs", "post", {401, 403, 404, 422}), ("/api/v1/checklist-runs", "post", {401, 403, 404, 422}), ("/api/v1/checklist-runs", "get", {401, 403, 422}), ("/api/v1/checklist-runs/{uid}", "get", {401, 403, 404, 422}), ("/api/v1/checklist-run-items/{uid}", "put", {401, 403, 404, 409, 422}), ("/api/v1/checklist-runs/{uid}/complete", "post", {401, 403, 404, 409, 422}), # ── History (admin dashboard) ──────────────────────────────────────── ("/api/v1/checklist-runs/history", "get", {401, 403, 422}), ("/api/v1/checklist-runs/history/stats", "get", {401, 403, 422}), ("/api/v1/checklist-runs/history/export.xlsx", "get", {401, 403, 422}), ] @pytest.fixture(scope="module") def openapi_schema() -> dict[str, Any]: """OpenAPI-схема app'а — computed один раз на тест-модуль.""" return app.openapi() def _operation(schema: dict[str, Any], path: str, method: str) -> dict[str, Any]: """Возвращает FastAPI operation-object для (path, method) или assertion fails.""" paths = schema.get("paths", {}) assert path in paths, f"Path {path} not found in OpenAPI schema" methods = paths[path] assert method in methods, ( f"Method {method.upper()} not registered for {path} (available: {list(methods)})" ) return dict(methods[method]) @pytest.mark.parametrize(("path", "method", "statuses"), CHECKLIST_ENDPOINTS) def test_checklist_endpoint_declares_problem_json_for_all_4xx( openapi_schema: dict[str, Any], path: str, method: str, statuses: set[int], ) -> None: """Каждый ожидаемый 4xx-status должен быть в `responses` и иметь `application/problem+json` content-type (RFC 9457). Schemathesis ловит violations через: Undocumented HTTP status code (если status не в responses) + Undocumented Content-Type (если content-type не problem+json для 4xx). """ op = _operation(openapi_schema, path, method) responses = op.get("responses", {}) for status in sorted(statuses): status_key = str(status) assert status_key in responses, ( f"{method.upper()} {path} не декларирует {status} " f"(schemathesis: Undocumented HTTP status code)" ) content = responses[status_key].get("content", {}) assert "application/problem+json" in content, ( f"{method.upper()} {path} {status}-response не объявляет " f"application/problem+json (текущие: {list(content)}). " f"FastAPI shorthand `model=ProblemDetails` генерит application/json — " f"используй inline content={{'application/problem+json': ...}} или " f"shared AUTHORIZED_RESPONSES / NOT_FOUND_RESPONSES." ) def test_export_xlsx_endpoint_declares_xlsx_content_type_for_200( openapi_schema: dict[str, Any], ) -> None: """GET /checklist-runs/history/export.xlsx 200 должен быть `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, не `application/json` (default FastAPI). Этот endpoint возвращает Response с binary XLSX content. Без явной декларации в OpenAPI consumers (schemathesis, openapi-generator, swagger-ui) будут пытаться парсить как JSON и видеть Undocumented Content-Type violation. """ op = _operation(openapi_schema, "/api/v1/checklist-runs/history/export.xlsx", "get") response_200 = op.get("responses", {}).get("200", {}) content = response_200.get("content", {}) xlsx_mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" # Допустимо: declare xlsx mime explicitly, ИЛИ FastAPI default behavior # где Response(media_type=...) generates `*/*`. Главное — НЕ application/json. assert "application/json" not in content, ( f"GET /checklist-runs/history/export.xlsx 200 объявляет application/json " f"(но endpoint возвращает binary XLSX). Schemathesis Undocumented Content-Type. " f"Текущие content: {list(content)}. " f"Ожидаем либо {xlsx_mime!r}, либо */* (FastAPI Response default)." )