# План: мок-полигон для интеграционных тестов Дата: 2026-07-31. Архитектор: Опус. Утверждено: все 10 решений. --- ## 1. Концепция Отдельный Flask-процесс на порту 5001, притворяющийся Nubes API. Data-driven: сервисы описаны в `polygon/services/*.yaml`, эмулятор достраивает недостающие поля по типу. Состояние инстансов/операций — в памяти, `dtFinish` вычисляется лениво. Приложение ходит в мок реальным HTTP через существующий `http_client`. Никакой БД, никакого облака, никакого Kubernetes. Только HTTP-ответы. --- ## 2. Принятые решения (10/10) | Q | Решение | Обоснование | |---|---------|-------------| | Q1 | Отдельный процесс :5001 (A) | `http_client` делает реальные GET/POST/Location — blueprint не проверит | | Q2 | Папка `polygon/services/*.yaml` | Каждый сервис в своём файле, легко добавлять | | Q3 | Мин. поля (id+код+тип), остальное достраивается | 20+ параметров вручную — ад. `default_for(dataType)` | | Q4 | Ленивый dtFinish (A) | Без потоков, детерминированно, `MOCK_OP_DELAY` (по умолчанию 0.1с) | | Q5 | Единая стейт-машина | create→running→suspended→deleted, без кастомизаций | | Q6 | Реальный мерж params | Иначе тест modify→проверить state.params бессмысленен | | Q7 | Статический stateOut из YAML | Для MVP, генерация из параметров — потом | | Q8 | `/_mock/reset` | Без сброса тесты влияют друг на друга | | Q9 | Тесты через `app_client` | Проверяет реальную связку app-autotest ↔ эмулятор | | Q10 | refSvcId игнорируем в MVP | validate-cfs всегда OK, ссылки не проверяются | --- ## 3. Критические точки интеграции ### 3.1 Location обязателен `HttpClient.post` достаёт UUID из заголовка `Location` (последний сегмент, `len >= 32`). Мок ОБЯЗАН отдавать `Location: ./` на: - `POST /instances` → `Location: ./{instanceUid}` - `POST /instanceOperations` → `Location: ./{instanceOperationUid}` Без Location executor не получит instanceUid/opUid. ### 3.2 Поллинг спит 5с `poll_until_done` делает GET, затем `time.sleep(5)` в цикле. При `MOCK_OP_DELAY=0` dtFinish появится на первом же GET — вторая итерация со сном не случится. ### 3.3 Short-circuit localhost `detect_endpoint()` хардкодит dev/test стенды и не читает `NUBES_API_ENDPOINT`. Даже при `NUBES_API_ENDPOINT=http://localhost:5001` он будет долбиться в реальные стенды. Решение: проверка `localhost`/`127.0.0.1` в `get_client()` и `get_stand()` (auth.py): ``` если endpoint.startswith("http://localhost") или "http://127.0.0.1": пропустить detect_endpoint() вернуть HttpClient(endpoint, token) stand → "mock" иначе: прежняя логика ``` Ноль новых env-переменных. `NUBES_API_ENDPOINT` уже есть в конфиге. ### 3.4 validate-cfs = пустое тело `send_params_terraform` считает успехом пустой/не-JSON ответ. Мок отдаёт `200` с пустым телом. ### 3.5 state.params по коду, cfsParams по числовому id `get_params_with_current_values` мержит `state.params[код]` с шаблоном из `GET /instanceOperations/default/{opId}`. YAML должен связывать числовой `svcOperationCfsParamId` ↔ код параметра. --- ## 4. Структура файлов ``` app-autotest/ ├── site/ │ ├── api/ │ │ └── auth.py # ИЗМЕНИТЬ: +short-circuit localhost │ └── ... ├── polygon/ # НОВАЯ папка │ ├── server.py # Flask-приложение эмулятора │ ├── state.py # MockState (в памяти) │ ├── config_loader.py # загрузка YAML + достройка defaults │ ├── defaults.py # default_for(dataType) │ └── services/ │ ├── dummy.yaml # Болванка (реальные ID из HAR) │ └── postgresql.yaml # PostgreSQL (stateOut: users, databases) └── tests/ ├── conftest.py # ИЗМЕНИТЬ: +фикстура поднятия мока └── test_mock_integration.py # НОВЫЙ: интеграционные тесты ``` --- ## 5. План реализации (3 фазы, 12 шагов) ### Фаза 1 — MVP (create + поллинг) **Шаг 1. `polygon/defaults.py`** Функция `default_for(dataType)`: `int→"0"`, `bool→"false"`, `array→"[]"`, `map/json→"{}"`, иначе `""`. Зеркалит `normalize_value` из terraform.py. **Шаг 2. `polygon/config_loader.py`** - Грузит все `polygon/services/*.yaml` - Для каждого cfsParam достраивает недостающие поля (`defaultValue`, `valueList`, `isRequired`, `refSvcId`, `dataDescriptor`) через `default_for()` - Возвращает dict: `{service_id: {svc, operations, cfsParams, stateParams, stateOut}}` **Шаг 3. `polygon/state.py` — MockState** ``` class MockState: instances: dict[uid] → {serviceId, displayName, status, params, ...} operations: dict[opUid] → {instanceUid, svcOperationId, operation, dtRunStart, params, ...} create_instance(service_id, display_name) → instanceUid create_operation(instanceUid, svcOperationId, operation) → opUid set_param(opUid, paramId, value) run(opUid) — записывает dtRunStart get_operation(opUid) → {dtFinish, isSuccessful, ...} (ленивый dtFinish) apply_effect(opUid) — modify→мерж params, delete→удаление инстанса, suspend/resume→статус reset() ``` Ленивый dtFinish: при GET `/instanceOperations/{uid}` сравнивает `now - dtRunStart >= MOCK_OP_DELAY`. Если да — выставляет `dtFinish=now`, `isSuccessful=True` и вызывает `apply_effect`. **Шаг 4. `polygon/server.py`** Flask-приложение, префикс `/api/v1/svc`. На этом шаге — минимальный набор: - `POST /instances` — создаёт инстанс, возвращает 201 + `Location: ./{uid}` - `POST /instanceOperations` — создаёт операцию, `Location: ./{opUid}` - `POST /instanceOperationCfsParams` — устанавливает параметр - `POST /instanceOperations/{uid}/run` — запускает операцию - `GET /instanceOperations/{uid}?fields=...` — статус операции (ленивый dtFinish) - `GET /instanceOperations/{uid}/validate-cfs` — 200 OK, пустое тело **Шаг 5. `polygon/services/dummy.yaml`** Реальные ID из HAR (распарсены Опусом): ```yaml 1: # serviceId svc: "Болванка" svcShort: "dummy" svcExtendedName: "Болванка" operations: - {svcOperationId: 18, operation: create, isCreate: true} - {svcOperationId: 92, operation: modify} - {svcOperationId: 71, operation: delete} - {svcOperationId: 93, operation: suspend} - {svcOperationId: 94, operation: resume} - {svcOperationId: 240, operation: redeploy} cfsParams: - {svcOperationCfsParamId: 242, svcOperationCfsParam: "resourceRealm", dataType: "string", valueList: ["dummy"], isRequired: true, defaultValue: "dummy"} - {svcOperationCfsParamId: 198, svcOperationCfsParam: "durationMs", dataType: "integer >= 0", defaultValue: "0"} - {svcOperationCfsParamId: 199, svcOperationCfsParam: "param199", dataType: "boolean", defaultValue: "false"} - {svcOperationCfsParamId: 200, svcOperationCfsParam: "param200", dataType: "boolean", defaultValue: "false"} - {svcOperationCfsParamId: 201, svcOperationCfsParam: "param201", dataType: "integer", defaultValue: "1"} - {svcOperationCfsParamId: 286, svcOperationCfsParam: "param286", dataType: "string"} - {svcOperationCfsParamId: 321, svcOperationCfsParam: "param321", dataType: "map", dataDescriptor: {subparam1: {dataType: "string"}, secret: {dataType: "string"}, subparam2: {dataType: "string"}}} - {svcOperationCfsParamId: 322, svcOperationCfsParam: "param322", dataType: "string"} - {svcOperationCfsParamId: 396, svcOperationCfsParam: "param396", dataType: "string"} - {svcOperationCfsParamId: 647, svcOperationCfsParam: "param647", dataType: "map", dataDescriptor: {bol1: {dataType: "boolean"}, minStr1: {dataType: "string"}, param1: {dataType: "string"}, param2: {dataType: "string"}, param3: {dataType: "string"}}} - {svcOperationCfsParamId: 654, svcOperationCfsParam: "param654", dataType: "array", dataDescriptor: {bol1: {dataType: "boolean"}, minStr1: {dataType: "string"}, param1: {dataType: "string"}, param2: {dataType: "string"}, param3: {dataType: "string"}}} - {svcOperationCfsParamId: 863, svcOperationCfsParam: "param863", dataType: "array"} stateParams: resourceRealm: "dummy" durationMs: "0" param199: "false" param200: "false" param201: "1" stateOut: {} ``` **Шаг 6. Интеграция в `auth.py`** Добавить short-circuit в `get_client()` и `get_stand()`: ```python def _is_localhost(endpoint): return (endpoint or "").startswith(("http://localhost", "http://127.0.0.1")) def get_client(): token = get_token() endpoint = current_app.config["NUBES_API_ENDPOINT"] if not _is_localhost(endpoint): endpoint = detect_endpoint(token) or endpoint return HttpClient(endpoint, token) def get_stand(): token = get_token() endpoint = current_app.config["NUBES_API_ENDPOINT"] if _is_localhost(endpoint): return "mock" endpoint = detect_endpoint(token) or endpoint return stand_name(endpoint) ``` ### Фаза 2 — полный CRUD + сервисы **Шаг 7. GET-эндпоинты** - `GET /instances?pageSize=N&page=P` — список с пагинацией (pageSize≤200, стоп по `len(batch): svc: "имя_сервиса" # обязательное svcShort: "короткое_имя" # опциональное svcExtendedName: "полное" # опциональное operations: # обязательное - svcOperationId: # обязательное operation: # create|modify|delete|suspend|resume|redeploy isCreate: # опциональное (true для create) cfsParams: # опциональное (мин. поля обязательны) - svcOperationCfsParamId: # обязательное svcOperationCfsParam: # обязательное (код параметра) dataType: # обязательное defaultValue: # опц. (достраивается по типу) isRequired: # опц. (достраивается) valueList: [, ...] # опц. refSvcId: # опц. dataDescriptor: {: {...}} # опц. (для map-параметров) stateParams: # опциональное (значения после create) <код>: <значение> stateOut: # опциональное (доп. данные) users: {: {}} databases: {: {}} ``` Правила достройки (`default_for`): - `defaultValue` отсутствует → `"0"` для integer, `"false"` для boolean, `"[]"` для array, `"{}"` для map/json, `""` для string - `isRequired` отсутствует → `false` - `valueList` отсутствует → `null` (не select, а input) --- ## 7. Машина состояний ``` create → running suspend → suspended resume → running modify → running (после мержа params) delete → УДАЛЁН (исчезает из GET /instances) redeploy→ running ``` Статусы: `creating` (пока dtFinish не появился), `running`, `suspended`, `deleted`. --- ## 8. Зависимости и окружение - **Зависимости**: PyYAML (для config_loader), Flask (уже есть) - **Env-переменные**: - `MOCK_OP_DELAY` — задержка операции в секундах (по умолчанию 0.1) - `NUBES_API_ENDPOINT` — `http://localhost:5001/api/v1/svc` (уже есть) - **Порт**: 5001 (не конфликтует с основным приложением на 5000/8000) - **Память**: всё в `MockState`, без БД, без файлов --- ## 9. Проверка (Verification) ```bash # 1. Запуск полигона cd app-autotest && NUBES_API_ENDPOINT=http://localhost:5001/api/v1/svc \ python polygon/server.py # 2. Дымовой тест curl http://localhost:5001/api/v1/svc/instances?pageSize=1 # → {"results": []} # 3. Интеграционные тесты pytest tests/test_mock_integration.py -v # Все 5 зелёные # 4. Полный прогон (старые тесты не сломаны) pytest tests/ -v ``` --- ## 10. Что НЕ делаем - ❌ Реальное выполнение операций (не Terraform, не Ansible) - ❌ Валидация параметров (всегда validate-cfs = OK) - ❌ База данных - ❌ refSvcId-резолв в MVP - ❌ Многопоточность - ❌ Деплой полигона (только локально/CI)