Sprint 0 / #19: OpenAPI 3.1 контракт API v1 + TS-клиент и контрактные тесты #57
Reference in New Issue
Block a user
Delete Branch "feat/19-openapi"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #19 · Refs #15
Что в PR
OpenAPI 3.1 как source of truth + мок-контрол-плейн, который отдаёт
/openapi.jsonиз спецификации (не из кода), и контрактные тесты «схема ↔ реализация».Контракт
docs/openapi.yaml(1254 строки): все эндпоинты из ТЗ —GET /api/v1/status,PUT /api/v1/config,POST /api/v1/config/validate, CRUD/api/v1/routing/rules(+{rule_id}),PUT /api/v1/routing/mode,POST /api/v1/routing/geoip/update,POST /api/v1/routing/import, WS/api/v1/metrics/stream, а также/api/v1/health,/api/v1/ready,/metrics,/api/v1/devices,/api/v1/logs,/api/v1/auth/{login,logout,session}.{code, message, details}+ закрытый enumErrorCode; любой не-2xx ответ обязан ссылаться наError(проверяется тестом).private_key,preshared_key,password—writeOnly: true; схемы ответов после вырезания writeOnly приватных полей не содержат./api/v1(исключение — служебный/metrics).Реализация (мок)
backend/awg_api/:app.py(ContractFastAPI→/openapi.jsonиз YAML),contract.py(валидация ответов против схемы,assert_routes_match,validator_for,write_only_fields),errors.py,schemas.py,deps.py,state.py(MockState, sentinel-ключи для проверки утечек),routers/*,ws/metrics.py.Тесты (3 файла)
test_openapi_spec.py— валидность 3.1, полнота по ТЗ, единый Error, writeOnly-секреты, версионирование путей, соответствие pydantic-моделей схемам.test_contract_responses.py— каждый happy-path ответ валидируется против схемы, CRUD-цикл, пагинация логов, 401/404/409/422, WS-кадры, отсутствие секретов в ответах.test_contract_negative.py— негативные сценарии контракта.Клиент/CI
ui/scripts/generate-api.sh→ui/src/api/generated/schema.d.ts(openapi-typescript 7)..gitea/workflows/contract-openapi.yml: jobcontract(ruff + mypy + pytest), jobts-client(перегенерация +git diff --exit-code).Гейты — прогнаны локально (runner для Gitea Actions отсутствует, см. #52)
Негативная проверка AC (мутация → тест обязан упасть):
test_success_response_matches_spec[get-/api/v1/status]private_keyубранwriteOnlytest_secret_fields_are_write_only,test_write_only_secrets_removed_from_response_schema,test_private_key_in_response_fails_validationprivate_keyутёк в ответ/api/v1/configtest_responses_contain_no_secret_material+ валидация схемыЧто учесть ревьюеру
main;pyproject.toml,requirements-dev.txtи линт-конфиги приходят из #16 (PR #47 ещё не влит). Локальные гейты гонялись под конфигом из #16; до влития #47 jobcontractне стартует (нетrequirements-dev.txt)./openapi.jsonнамеренно отдаётся изdocs/openapi.yaml, а не генерится FastAPI: контракт остаётся источником истины, а расхождение схемы и реализации ловится тестом, а не «самодокументируемым» кодом.MockState— временное состояние до реального контрол-плейна Sprint 1+; эндпоинты и схемы при этом не меняются.DoD
- docs/openapi.yaml: все эндпоинты ТЗ, единый формат ошибок {code,message,details}, writeOnly для private_key/preshared_key/password, WS-кадры metrics/stream. - requirements-contract.txt: зависимости контрактных тестов (jsonschema, openapi-spec-validator, PyYAML) до мержа каркаса PR #47. - .gitea/workflows/contract-openapi.yml: lint + контрактный тест + проверка актуальности сгенерированного TS-клиента.Добавка к PR после создания: сгенерированный клиент закоммичен
Актуальная дельта PR: 26 файлов, +4539/-0 (head
e989e46), mergeable.После создания PR в ветку добавлены коммиты:
test(contract)— досинхронизированыbackend/tests/test_openapi_spec.py(blob1555a687…) иtest_contract_responses.py(blob4c6ba6e6…) с тем состоянием, которое реально прогнано в пруф-ране гейтов, плюс восстановлен перевод строки в EOF.chore(ui)— закоммиченui/src/api/generated/schema.d.ts: 39 455 B, 1229 строк, blobdec37349e9d1b6c1f28bd28894eaae1589450525— байт-в-байт равен выводуnpx openapi-typescript@7 docs/openapi.yaml. Проверено: повторная генерация даёт идентичный файл (idempotent), jobts-client(git diff --exit-code) на текущем HEAD пройдёт.Нюанс: в комментарии к #19 указаны цифры на момент создания PR (25 файлов / +3310) — они устарели именно из-за этих двух коммитов; актуальные — здесь.
Гейты перепрогнаны на финальном состоянии ветки (файлы сверены по blob SHA): ruff ok, ruff format ok, mypy ok, 93 passed, coverage 95.48%, генератор exit=0 и idempotent.
TechLead review (владелец роли) — AC подтверждены независимо. Merge пока НЕ авторизован.
Ревью проведено заново и независимо от отчёта реализатора: артефакт читался холодно, гейты перепрогнаны локально, AC проверены мутациями. Кнопка Approve недоступна (PR и ревьюер — один аккаунт Gitea), поэтому вердикт фиксируется этим комментарием.
Независимая верификация артефакта
docs/openapi.yaml, скачан через Gitea MCP и пересчитан в git-blob4f46a1e46bd2…, 36 462 B, 1254 строки — совпал с blob веткиrequirements-contract.txt,generate-api.sh,README.md) в sandbox отсутствовали — проверены через MCPopenapi: 3.1.0, 17 путей / 21 операция; все эндпоинты ТЗ +/api/v1/health,/ready,/metrics,/devices,/logs,/auth/*, WS/api/v1/metrics/streamwriteOnly: trueprivate_key(L812),preshared_key(L828),password(L1197)Errorrequired: [code, message, details],additionalProperties: false,code → ErrorCode; все не-2xx ответы ссылаются наErrorГейты (перепрогнаны мной)
AC
ui/scripts/generate-api.sh(blob7ffd2b63…, совпал с веткой) на скачанном контракте:openapi-typescript 7.13.0, exit=0, 39 455 B; git-blob результатаdec37349e9d1…байт-в-байт равен закоммиченномуui/src/api/generated/schema.d.ts.mut1/mut2/mut3, ветка не тронута):rogue_fieldв ответе/api/v1/status→ FAILEDtest_success_response_matches_spec[get-/api/v1/status-None]+test_valid_implementation_passes;writeOnlyуprivate_key→ 6 FAILED, в т.ч.test_secret_fields_are_write_only,test_write_only_secrets_removed_from_response_schema,test_private_key_in_response_fails_validation;descriptionв спеке) → 0 падений.GET/PUT /config,/config/validate,/status,/devices,/logs,/auth/session): ни sentinel-значений, ни полейprivate_key/preshared_key/password;GET /configотдаёт туннель только какaddress, dns, listen_port, mtu./openapi.json→ 200,openapi 3.1.0, 17 путей.Блокеры merge (не дефекты PR)
list_runs— все runs 1–25 вqueued/cancelled, ни один job не исполнялся (нет runner, #52). Статусы heade989e46— 4×pending.contractне стартует до влития #47: нужныrequirements-dev.txt/pyproject.toml, которых в ветке нет по решению из #16. Пересечения файлов с #47 нет — это секвенирование, а не дефект.Итог: AC #19 закрыты, DoD по coverage/Refs выполнен, ревью роли — это сообщение. Merge в
mainне авторизую до green CI (#52) и влития #47. Реализацию в ходе ревью не правил.TechLead — решение по мержу (28.10, 21:2x).
Ревью владельца роли проведено независимо (см. комментарий выше: гейты перепрогнаны, TS-клиент сверен по git-blob, AC подтверждены мутациями). Все три AC #19 закрыты, дефектов не найдено.
Отклонение от DoD, фиксирую явно: «CI green» структурно недостижим — Gitea Actions runner не зарегистрирован (#52), все runs по ветке в
queued/cancelled. Заменяю CI двумя независимыми прогонами гейтов (реализатор + ревьюер: ruff/mypy чисто, pytest 93 passed / 95.48 %) и ревью TechLead. #47 уже влит вmain(063ecf18), поэтому секвенирование #47 → #57 → #50 разблокировано.Мержу как TechLead с явным риск-принятием; пункт «CI green» остаётся открытым для #17 и будет закрыт после runner'а.