Compare commits
19
Commits
9fb21a40a8
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f7617d7f | ||
|
|
4e899b8d36 | ||
|
|
8d111a05e1 | ||
|
|
547b0f9e21 | ||
|
|
b78bae571e | ||
|
|
366a6754fe | ||
|
|
9ca9f8ba54 | ||
|
|
f67facae20 | ||
|
|
cd06040d62 | ||
|
|
af2cede717 | ||
|
|
c64812b039 | ||
|
|
18d08ccccd | ||
|
|
cfc341db44 | ||
|
|
333a3d5e63 | ||
|
|
4ee0433e28 | ||
|
|
85b02c70f8 | ||
|
|
c31cdb73ef | ||
|
|
ef13e52c80 | ||
|
|
ede5b68b74 |
+320
-160
@@ -1,27 +1,36 @@
|
||||
# Архитектура app-autotest — текущее состояние
|
||||
# Архитектура app-autotest — полное описание
|
||||
|
||||
v1.0.93, 28.07.2026
|
||||
v1.2.43, 04.08.2026
|
||||
|
||||
---
|
||||
|
||||
## 1. Обзор
|
||||
|
||||
Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций
|
||||
сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд
|
||||
(dev/test) по JWT-токену.
|
||||
Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического
|
||||
тестирования сервисов облачной платформы Nubes через REST API.
|
||||
|
||||
**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров).
|
||||
**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд)
|
||||
**URL**: `https://atest.pythonk8s.dev.nubes.ru`
|
||||
**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
|
||||
**Два режима работы:**
|
||||
- Эмуляция — всё в Polygon (мок-сервер, без реальной инфраструктуры)
|
||||
- Облако — реальный Nubes API (dev/test, автоопределение по JWT)
|
||||
|
||||
**Ручной режим:** сервис -> операция (create/modify/delete/suspend/resume/redeploy) ->
|
||||
параметры -> запуск -> поллинг.
|
||||
|
||||
**Сценарный режим:** последовательность шагов с передачей контекста, редактор через
|
||||
модальный UI, хранение в БД.
|
||||
|
||||
**Деплой:** Nubes pythonk8s managed service, gunicorn --workers 2.
|
||||
**URL:** https://atest.pythonk8s.dev.nubes.ru
|
||||
**Полигон:** https://polygon.pythonk8s.dev.nubes.ru
|
||||
**Репозиторий:** https://gitea.services.ngcloud.ru/forcloud/app-autotest.git (master)
|
||||
|
||||
### Ключевые принципы
|
||||
|
||||
1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются
|
||||
только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
|
||||
2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока.
|
||||
3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`.
|
||||
4. **Мелкие функции**. Каждое действие — отдельная функция.
|
||||
1. Единый HttpClient — auth.py:get_client(), одна точка для обоих режимов.
|
||||
2. Изоляция по (client_id, stand) — все таблицы БД, трекер, сценарии.
|
||||
3. Cloud-first + tracker-fallback — инстансы сначала из API, трекер для только что созданных.
|
||||
4. Multi-worker safety — /tmp файлы защищены fcntl.flock, _op_results под threading.Lock.
|
||||
5. Защита от XSS — _esc() на всех данных из API перед innerHTML.
|
||||
|
||||
---
|
||||
|
||||
@@ -29,27 +38,59 @@ Flask-приложение (один HTML-файл, без SPA-фреймвор
|
||||
|
||||
```
|
||||
app-autotest/
|
||||
├── site/ ← НЕ пакет (без __init__.py)
|
||||
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
|
||||
│ ├── runner.py ← [LEGACY] Раннер тестов
|
||||
│ ├── config.yaml ← [LEGACY] Конфиг раннера
|
||||
├── requirements.txt
|
||||
├── site/
|
||||
│ ├── app.py # Точка входа: VERSION, blueprints, /health
|
||||
│ ├── api/
|
||||
│ │ └── http_client.py ← HttpClient, автостенд
|
||||
│ │ ├── http_client.py # HttpClient, detect_endpoint, stand_name
|
||||
│ │ ├── auth.py # get_client(), get_mode(), get_stand(), токены, cookie
|
||||
│ │ └── utils.py # find_uid(), uid_from_location()
|
||||
│ ├── operations/
|
||||
│ │ ├── get_services.py ← get_services(), get_service_detail()
|
||||
│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization()
|
||||
│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон
|
||||
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock
|
||||
│ │ ├── executor.py # ЕДИНЫЙ запуск: create->params->run
|
||||
│ │ ├── poll.py # poll_until_done()
|
||||
│ │ ├── scenario.py # run_scenario() — шаги с резолвингом
|
||||
│ │ ├── terraform.py # send_params_terraform() — нормализация
|
||||
│ │ ├── tracker.py # JSON-файловый кеш /tmp/instances-*.json (flock)
|
||||
│ │ ├── get_instances.py # GET /instances с пагинацией
|
||||
│ │ ├── get_params.py # Параметры с ТЕКУЩИМИ значениями из state.params
|
||||
│ │ ├── get_services.py # GET /services
|
||||
│ │ └── service_list.py # services_{stand}.txt фильтр
|
||||
│ ├── db/
|
||||
│ │ ├── pool.py # ThreadedConnectionPool(1,5)
|
||||
│ │ ├── init_db.py # CREATE TABLE + миграции + idx_one_running
|
||||
│ │ ├── save_run.py # Ручной UPSERT (UPDATE -> INSERT)
|
||||
│ │ └── scenario_defs.py # CRUD + lock_check
|
||||
│ ├── routes/
|
||||
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
|
||||
│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config
|
||||
│ │ └── api_test.py ← /api/test, /api/params, /api/log
|
||||
│ ├── templates/
|
||||
│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS)
|
||||
│ └── static/
|
||||
│ ├── style.css
|
||||
│ ├── logo.svg
|
||||
│ └── favicon.svg
|
||||
│ │ ├── main.py # GET/POST /, set_mode, api_operations
|
||||
│ │ ├── api_test.py # /api/test, /api/params, /api/log, _finish_op
|
||||
│ │ ├── api_scenario_run.py # /api/scenario/run, /api/scenario/status
|
||||
│ │ ├── api_scenario_defs.py# CRUD /api/scenario/definitions
|
||||
│ │ ├── api_scenario.py # [ДУБЛИКАТ] Не зарегистрирован
|
||||
│ │ └── api.py # [LEGACY] /api/run
|
||||
│ ├── static/
|
||||
│ │ ├── app.js # Инициализация: startLogPoll(), selectService()
|
||||
│ │ ├── style.css # Дизайн-система Nubes
|
||||
│ │ └── js/
|
||||
│ │ ├── utils.js # _esc(), validateJson(), logPoll, relativeTime()
|
||||
│ │ ├── views.js # switchView()
|
||||
│ │ ├── instances.js # selectService(), toggleInstance()
|
||||
│ │ ├── operations.js # startCreate(), runOp(), showParams()
|
||||
│ │ ├── params-render.js# renderParamRow(), collectParams()
|
||||
│ │ ├── history.js # toggleHistory(), loadHistory()
|
||||
│ │ ├── scenario-list.js# toggleScenario(), runScenario(), поллинг
|
||||
│ │ ├── scenario-form.js# showScenarioEditor()
|
||||
│ │ ├── scenario-create.js, scenario-edit.js, scenario-delete.js
|
||||
│ │ ├── icons.js # SVG-иконки
|
||||
│ │ └── snackbar.js # showSnackbar()
|
||||
│ └── templates/
|
||||
│ └── index.html # Весь UI: сайдбар, виды, лог-панель
|
||||
├── tests/
|
||||
│ ├── conftest.py # app_client, polygon_server фикстуры
|
||||
│ ├── test_api_scenario_run.py# 3 теста: 503, 409, unique violation
|
||||
│ ├── test_db_scenario_defs.py# 2 теста: lock_check None
|
||||
│ ├── test_polygon_integration.py# 15 интеграционных тестов
|
||||
│ ├── test_static_regressions.py# 2 статических теста
|
||||
│ └── README.md
|
||||
└── secrets/
|
||||
├── dev.token
|
||||
└── test.token
|
||||
@@ -57,174 +98,293 @@ app-autotest/
|
||||
|
||||
---
|
||||
|
||||
## 3. Бэкенд: HTTP-клиент и автостенд
|
||||
## 3. Система режимов — auth.py
|
||||
|
||||
### `http_client.py`
|
||||
Центральный модуль. Все роуты получают HttpClient ТОЛЬКО через функции этого модуля.
|
||||
|
||||
- `HttpClient(endpoint, token)` — обёртка над `requests.Session`:
|
||||
- Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
|
||||
- `get(path)` → `raise_for_status()` → `.json()`
|
||||
- `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
|
||||
- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
|
||||
- `stand_name(endpoint)` — "dev"/"test" по URL
|
||||
- `create_client(token, fallback)` — HttpClient + endpoint
|
||||
### Переменные окружения
|
||||
|
||||
### `get_instances.py`
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|-----------|-------------|-----------|
|
||||
| NUBES_API_ENDPOINT | lk-api-gateway-test.../api/v1/svc | Реальный API |
|
||||
| NUBES_API_TOKEN | (пусто) | Сервисный JWT-токен |
|
||||
| POLYGON_ENDPOINT | (пусто) | URL полигона. Если задан -> селектор режима |
|
||||
|
||||
- `get_instances(client)` — `GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`.
|
||||
- `get_organization(client)` — первый инстанс с `serviceId == 19`
|
||||
### Cookie
|
||||
|
||||
### `get_services.py`
|
||||
| Cookie | По умолчанию | Допустимые значения |
|
||||
|--------|-------------|-------------------|
|
||||
| mode | "polygon" | "polygon", "cloud" |
|
||||
| polygon_stand | "test" | "dev", "test", "prod" |
|
||||
| token | (пусто) | JWT пользователя |
|
||||
|
||||
- `get_services(client)` → `GET /services` → список
|
||||
- `get_service_detail(client, svc_id)` → `GET /services/{id}` → детали + операции
|
||||
Все cookie: httponly=True, samesite=Strict, secure=True, max_age=1 год.
|
||||
|
||||
### `get_params.py` — КЛЮЧЕВОЙ модуль
|
||||
### Функции
|
||||
|
||||
`get_params_with_current_values(client, op_id, instance_uid)`:
|
||||
1. `GET /instances/{uid}` → `state.params` (текущие значения: `{"whereFail":"1",...}`)
|
||||
2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor)
|
||||
3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
|
||||
4. Приведение типов: `bool` → `"true"/"false"`, `dict` → `json.dumps()`
|
||||
get_mode() -> "polygon" | "cloud"
|
||||
Без POLYGON_ENDPOINT -> всегда "cloud"
|
||||
Из cookie, whitelist-валидация
|
||||
|
||||
### `tracker.py`
|
||||
get_polygon_stand() -> "dev" | "test" | "prod"
|
||||
Из cookie, whitelist-валидация
|
||||
|
||||
- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду)
|
||||
- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s
|
||||
- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)`
|
||||
- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
|
||||
get_token() -> JWT-строка
|
||||
Эмуляция: только NUBES_API_TOKEN (env)
|
||||
Облако: cookie "token" -> NUBES_API_TOKEN
|
||||
|
||||
_cached_detect(token) -> URL эндпоинта (кеш в flask.g)
|
||||
Вызывает detect_endpoint() только при первом обращении за запрос
|
||||
Устраняет двойной HTTP-запрос (get_client + get_stand)
|
||||
|
||||
get_client() -> HttpClient
|
||||
Эмуляция: polygon_url.replace("/api/v1/svc", "/{stand}/api/v1/svc")
|
||||
Облако: _cached_detect(token)
|
||||
|
||||
get_stand() -> "polygon_test" | "dev" | "test" | "mock"
|
||||
Эмуляция: "polygon_" + get_polygon_stand()
|
||||
Облако: stand_name(_cached_detect(token))
|
||||
|
||||
### URL полигона
|
||||
|
||||
POLYGON_ENDPOINT = https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc
|
||||
|
||||
Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc:
|
||||
DEV: https://polygon.../dev/api/v1/svc
|
||||
TEST: https://polygon.../test/api/v1/svc
|
||||
PROD: https://polygon.../prod/api/v1/svc
|
||||
|
||||
---
|
||||
|
||||
## 4. Бэкенд: Роуты
|
||||
## 4. HTTP-клиент — http_client.py
|
||||
|
||||
### `main.py`
|
||||
HttpClient — обёртка над requests.Session:
|
||||
- Заголовки: Authorization: Bearer, User-Agent: Mozilla/5.0 (DDoS-Guard)
|
||||
- get(path) -> raise_for_status() -> .json(), таймаут 10с
|
||||
- post(path, data) -> r.ok, JSON, Location -> UUID, таймаут 30с
|
||||
- raw_delete(url) -> DELETE без авторизации (CMDB)
|
||||
|
||||
- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы
|
||||
- `action=save` → сохранить токен в cookie
|
||||
- `action=clear` → удалить cookie
|
||||
- **`GET /api/operations/<svc_id>`** — операции + autotest-инстансы:
|
||||
- Cloud-first: фильтр по префиксу `autotest-`
|
||||
- Tracker-fallback: статус "creating" для ещё невидимых
|
||||
|
||||
### `api_test.py` — основной модуль операций
|
||||
|
||||
- **`GET /api/params/<op_id>[?instanceUid=xxx]`**:
|
||||
- Без `instanceUid` → шаблонные `defaultValue`
|
||||
- С `instanceUid` → `get_params_with_current_values()`
|
||||
- **`POST /api/test`** — запуск операции:
|
||||
- **CREATE**: `POST /instances` → `instanceUid` → `POST /instanceOperations` → `opUid` → `tracker_add` → params → `/run`
|
||||
- **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
|
||||
- `displayName` = `_get_instance_display_name()` (из API, не UUID!)
|
||||
- Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
|
||||
- **`GET /api/test/status/<op_uid>`** — поллинг: in-memory `_op_results` → прямой API
|
||||
- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
|
||||
|
||||
### Логирование
|
||||
|
||||
`_log(msg)` в `api_test.py`:
|
||||
- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
|
||||
- Ротация при 512 КБ
|
||||
- UI: скрытая панель, кнопка `log` (правый нижний угол)
|
||||
STANDS = [dev, test URL]
|
||||
detect_endpoint(token) — пробует dev->test, returns URL where results != None
|
||||
stand_name(endpoint) — ТОЧНОЕ сравнение URL -> "dev"/"test"/"?"
|
||||
|
||||
---
|
||||
|
||||
## 5. Фронтенд
|
||||
## 5. Режимы работы и разделы БД
|
||||
|
||||
### Структура
|
||||
| Режим | Сервисы | Инстансы | Стенд в БД |
|
||||
|--------|---------|----------|-----------|
|
||||
| Эмуляция | Полигон | Полигон | polygon_{dev|test|prod} |
|
||||
| Облако | Реальный API | Реальный API | dev / test |
|
||||
|
||||
Один HTML-файл, три колонки:
|
||||
- Левая (280px): инфраструктура
|
||||
- Средняя (240px): сервисы
|
||||
- Правая (flex): инстансы + параметры + кнопка + этапы
|
||||
Облако (2 стенда): dev, test
|
||||
Эмуляция (3 стенда): polygon_dev, polygon_test, polygon_prod
|
||||
PROD в облаке недоступен — только через эмуляцию.
|
||||
|
||||
### Глобальное состояние (JS)
|
||||
### Сервис-листы
|
||||
|
||||
```
|
||||
svcInstances — кеш инстансов (из /api/operations)
|
||||
selectedInst — UID выбранного инстанса
|
||||
selectedOp — {opId, opName, svcId}
|
||||
pollTimer — таймер поллинга
|
||||
SVC_ID = 1 — фиксированный сервис (Болванка)
|
||||
```
|
||||
load_service_ids(stand) обрезает префикс polygon_:
|
||||
polygon_test -> test -> services_test.txt
|
||||
polygon_dev -> dev -> services_dev.txt
|
||||
polygon_prod -> prod -> services_prod.txt (не existe -> пустой set -> все сервисы)
|
||||
|
||||
### Потоки операций
|
||||
|
||||
**CREATE**: `startCreate()` → `showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
|
||||
|
||||
**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)` → `showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
|
||||
|
||||
**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
|
||||
|
||||
### Валидация JSON
|
||||
|
||||
- `_esc(s)` — HTML-экранирование (`"` → `"`, `&` → `&`, `<` → `<`)
|
||||
- `validateJson(el, quiet)` — `JSON.parse()` на `onblur` (красная рамка + текст)
|
||||
- Batch-проверка перед отправкой — ошибка → запрос не уходит
|
||||
В шаблоне: если список пуст — показываются все сервисы.
|
||||
|
||||
---
|
||||
|
||||
## 6. Nubes API — используемые endpoint'ы
|
||||
## 6. Роуты
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|-------|------|-----------|
|
||||
| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) |
|
||||
| GET | `/instances/{uid}` | Детали + state.params |
|
||||
| GET | `/services` | Список сервисов |
|
||||
| GET | `/services/{id}` | Детали + операции |
|
||||
| GET | `/instanceOperations/default/{opId}` | Шаблон параметров |
|
||||
| POST | `/instances` | Создать инстанс |
|
||||
| POST | `/instanceOperations` | Создать операцию |
|
||||
| POST | `/instanceOperationCfsParams` | Установить параметр |
|
||||
| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
|
||||
| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса |
|
||||
### main.py
|
||||
|
||||
GET/POST / — главная страница
|
||||
action=save — сохранить токен в cookie
|
||||
action=clear — удалить cookie
|
||||
action=set_mode — сохранить mode + polygon_stand в cookie
|
||||
polygon_stand: из формы, иначе из cookie (Облако->Эмуляция)
|
||||
Валидация: mode in {polygon,cloud}, stand in {dev,test,prod}
|
||||
Загрузка: mode=="polygon" or active_token
|
||||
Единый client = get_client()
|
||||
|
||||
GET /api/operations/<svc_id> — операции + autotest-инстансы
|
||||
Cloud-first + tracker-fallback, дедупликация
|
||||
|
||||
### api_test.py — ручной режим
|
||||
|
||||
GET /api/services — список сервисов
|
||||
GET /api/instances/list — все инстансы
|
||||
GET /api/params/<op_id>[?instanceUid=xxx] — параметры (шаблон или текущие)
|
||||
POST /api/test — запуск операции, возвращает opUid
|
||||
Валидация: serviceId(int>0), operation, svcOperationId(int>0), UUID(36), params(dict)
|
||||
CREATE: _unique_display_name() + execute_operation()
|
||||
non-CREATE: execute_operation()
|
||||
delete: CMDB -> fallback API delete
|
||||
Фон: _finish_op() — поллинг + save_run + tracker_remove
|
||||
GET /api/test/status/<op_uid> — статус (_op_results -> API)
|
||||
GET /api/log — последние 200 строк из /tmp/app-autotest.log
|
||||
GET /api/history — последние 50 записей из runs (client_id + stand)
|
||||
|
||||
### api_scenario_run.py — сценарии
|
||||
|
||||
GET /api/scenarios — список определений
|
||||
POST /api/scenario/run — запуск
|
||||
lock_check -> None(503), False(409)
|
||||
INSERT scenario_runs status=RUNNING (partial unique index)
|
||||
UniqueViolation(pgcode 23505) -> 409
|
||||
Фон: run_scenario(client, steps, cid, stand, ...)
|
||||
GET /api/scenario/run/<run_id> — статус
|
||||
GET /api/scenario/status — последние 10 запусков
|
||||
|
||||
### api_scenario_defs.py — CRUD
|
||||
|
||||
GET/POST /api/scenario/definitions — список/создать
|
||||
GET/PUT/DELETE /api/scenario/definitions/<id> — один/обновить/удалить
|
||||
PUT: оптимистичная блокировка (version -> 409)
|
||||
DELETE: мягкое (is_active=FALSE)
|
||||
|
||||
---
|
||||
|
||||
## 7. Ограничения платформы
|
||||
## 7. Операции
|
||||
|
||||
- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.`
|
||||
- `app.run(host="0.0.0.0", port=5000)`
|
||||
- Gunicorn multi-worker → общие данные через файлы + flock
|
||||
- `/tmp/` теряется при редеплое
|
||||
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
||||
### executor.py
|
||||
|
||||
execute_operation() — единый запуск:
|
||||
1. CREATE: POST /instances -> instanceUid -> tracker_add -> POST /instanceOperations -> opUid
|
||||
2. non-CREATE: POST /instanceOperations -> opUid
|
||||
3. send_params_terraform() — нормализация, refSvcId, validate-cfs
|
||||
4. POST /run -> запуск
|
||||
|
||||
### scenario.py
|
||||
|
||||
run_scenario() — для каждого шага:
|
||||
1. get_service_detail() -> svcOperationId
|
||||
2. GET /instanceOperations/default/{id} -> резолв параметров
|
||||
3. Резолвинг instance_uid: явный uid > instance_ref > service_id
|
||||
4. execute_operation()
|
||||
5. save_run(RUNNING)
|
||||
6. poll_until_done()
|
||||
7. save_run(final) — UPDATE существующей строки
|
||||
8. tracker_remove() после успешного delete
|
||||
9. _save_scenario_run() — прогресс
|
||||
|
||||
### tracker.py
|
||||
|
||||
JSON-файловый кеш: /tmp/instances-{clientId}-{stand}.json
|
||||
fcntl.flock(LOCK_EX|LOCK_NB) + retry 2s
|
||||
add/remove/list_all — атомарные операции
|
||||
|
||||
---
|
||||
|
||||
## 8. Безопасность и конкуренция (аудит GPT-5.3-Codex, 2026-07-31)
|
||||
## 8. База данных
|
||||
|
||||
Полный аудит 29 файлов (~6000 строк Python + vanilla JS). Исправлено в v1.2.19-v1.2.20.
|
||||
### pool.py
|
||||
ThreadedConnectionPool(1,5), ленивый init в каждом воркере
|
||||
_ensure_schema() -> init_db() при первом обращении
|
||||
|
||||
### Защита от XSS (Frontend)
|
||||
### Таблицы
|
||||
|
||||
- **`_esc(s)` в utils.js** — HTML-escape: `&` → `&`, `"` → `"`, `<` → `<`
|
||||
Применяется ко ВСЕМ данным из API перед `innerHTML`.
|
||||
- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования).
|
||||
- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед
|
||||
вставкой в JS-строку внутри HTML-атрибута.
|
||||
runs — история операций:
|
||||
id, created_at, client_id, stand, user_email, svc_id, svc_name,
|
||||
op_name, svc_op_id, op_uid, instance_uid, display_name, status,
|
||||
duration_sec, error_log, params(JSONB), stages(JSONB), app_version,
|
||||
scenario_run_id, step_number, instance_meta(JSONB)
|
||||
Индексы: client_stand, instance, created, op_uid(UNIQUE WHERE NOT NULL)
|
||||
|
||||
### Защита от гонок (Backend)
|
||||
scenario_runs — запуски сценариев:
|
||||
id, client_id, stand, scenario_name, status, current_step, total_steps,
|
||||
instance_bindings(JSONB), duration_sec, error_log
|
||||
Partial UNIQUE INDEX: (client_id, stand) WHERE status='RUNNING'
|
||||
|
||||
- **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup.
|
||||
`pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе.
|
||||
- **Partial unique index для сценариев** — `CREATE UNIQUE INDEX idx_one_running
|
||||
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'`.
|
||||
Делает `INSERT INTO scenario_runs ... status='RUNNING'` атомарной проверкой:
|
||||
вторая параллельная вставка получает unique violation → 409.
|
||||
Это заменило сломанную реализацию на `pg_try_advisory_lock` (v1.2.20),
|
||||
где lock брался на одном соединении, а unlock — на другом (из пула).
|
||||
- **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`.
|
||||
Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn.
|
||||
scenario_definitions — определения:
|
||||
id, client_id, stand, name, steps(JSONB), version, is_active
|
||||
UNIQUE: (client_id, stand, LOWER(name))
|
||||
Seed: client_id="" AND stand="" — видны всем
|
||||
|
||||
### Защита от зависания (Frontend polling)
|
||||
### save_run.py
|
||||
|
||||
- **Счётчик ошибок** в `scenarioPollTimer` — после 5 последовательных ошибок:
|
||||
`stopScenarioPoll()` + `busy=false` + сообщение об ошибке.
|
||||
- **Generation token** в `scenario-form.js` — `_renderGen` предотвращает перезапись
|
||||
нового DOM старыми данными от async `loadStepParams()`.
|
||||
Ручной UPSERT:
|
||||
1. UPDATE runs SET ... WHERE op_uid = %s
|
||||
2. Если rowcount == 0 -> INSERT
|
||||
|
||||
### Известные ограничения
|
||||
---
|
||||
|
||||
- **`_op_results` in-memory на воркер** — не shared между gunicorn-воркерами.
|
||||
При отсутствии stickiness статус может читаться из API fallback вместо кеша.
|
||||
Решение (отложено): Redis или общая таблица в БД для статусов операций.
|
||||
## 9. Фронтенд
|
||||
|
||||
Сайдбар (192px, flex column):
|
||||
sidebar-nav (flex:0) — кнопки видов
|
||||
Селектор режима (радиокнопки, только при POLYGON_ENDPOINT)
|
||||
sidebar-bottom — версия, email, токен (disabled в эмуляции)
|
||||
|
||||
Виды: workbench, overview, history-view, scenarios-view, logs-view
|
||||
|
||||
JS модули (12 файлов):
|
||||
utils.js — _esc(), startLogPoll(), relativeTime()
|
||||
views.js — switchView()
|
||||
instances.js — selectService(), toggleInstance()
|
||||
operations.js — startCreate(), runOp(), executeOp()
|
||||
history.js — toggleHistory(), loadHistory()
|
||||
scenario-list.js — runScenario(), поллинг (5 ошибок -> стоп)
|
||||
scenario-form.js — редактор сценариев
|
||||
|
||||
Защита от XSS:
|
||||
_esc(s) -> &, ", <
|
||||
o.operation: JS-escape + HTML-escape
|
||||
escName: \\, \', "
|
||||
|
||||
---
|
||||
|
||||
## 10. Nubes API — эндпоинты
|
||||
|
||||
GET /services — список
|
||||
GET /services/{id} — детали + операции
|
||||
GET /instances — все (пагинация)
|
||||
GET /instances/{uid} — детали + state.params
|
||||
GET /instanceOperations/default/{id} — шаблон cfsParams
|
||||
POST /instances — создать -> 201 + Location
|
||||
POST /instanceOperations — создать операцию -> 201 + Location
|
||||
POST /instanceOperationCfsParams — установить параметр
|
||||
GET /.../validate-cfs — валидация
|
||||
POST /.../run — запустить
|
||||
GET /.../{uid}?fields=... — поллинг
|
||||
|
||||
---
|
||||
|
||||
## 11. Безопасность
|
||||
|
||||
Гонки:
|
||||
_op_results — threading.Lock(), pop(k,None)
|
||||
idx_one_running — partial unique index (атомарный lock)
|
||||
Tracker — fcntl.flock, атомарное read->mutate->write
|
||||
save_run — ручной UPSERT без индекса
|
||||
|
||||
Cookie: httponly, samesite=Strict, secure, 1 год
|
||||
|
||||
Валидация:
|
||||
serviceId: int>0, instanceUid: UUID(36), svcOperationId: int>0
|
||||
params: dict, mode: {polygon,cloud}, stand: {dev,test,prod}
|
||||
|
||||
---
|
||||
|
||||
## 12. Тесты
|
||||
|
||||
22 теста: python3 -m pytest tests/ -v
|
||||
|
||||
test_api_scenario_run.py — 3 теста (503, 409, unique violation)
|
||||
test_db_scenario_defs.py — 2 теста (lock_check None)
|
||||
test_polygon_integration.py — 15 тестов (полный цикл с полигоном)
|
||||
test_static_regressions.py — 2 теста (DDL, escName)
|
||||
|
||||
---
|
||||
|
||||
## 13. История версий (v1.2.28 -> v1.2.43)
|
||||
|
||||
1.2.28 — исходная (03.08)
|
||||
1.2.29 — фикс сервисов при POLYGON_ENDPOINT
|
||||
1.2.30 — селектор режима/стенда
|
||||
1.2.31 — фикс URL полигона
|
||||
1.2.35 — sidebar-nav flex:0 + форма по центру
|
||||
1.2.36 — tracker_remove в сценариях + логи
|
||||
1.2.39 — ручной UPSERT
|
||||
1.2.40 — код-ревью #1: _esc() для o.operation
|
||||
1.2.41 — код-ревью #2: кеш detect_endpoint
|
||||
1.2.42 — код-ревью #3: whitelist mode/stand
|
||||
1.2.43 — код-ревью #5,#6,#8,#9,#10: finally, stand_name, индекс, flush
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Сергей Мищук, [22.07.2026 17:20]
|
||||
у меня просьба подумать, как сделать автотесты на выбранные операции выбранных сервисов. Может быть, конфигурить через файл. Может быть, сделать веб интерфейс со списком сервисов-операций и статусами тестирования... Может сделать там же конфигурирование тестов. Запускать наверно захочется прямо на платформе
|
||||
|
||||
Владимир Крупский, [22.07.2026 17:36]
|
||||
операции - create modify redeploy и тд ?
|
||||
|
||||
Владимир Крупский, [22.07.2026 17:37]
|
||||
можно ... по всем сервисам есть yaml с полным перечнем всего что в сервисах есть.
|
||||
|
||||
Владимир Крупский, [22.07.2026 17:38]
|
||||
сделаю во фласке или нодеjs
|
||||
|
||||
Владимир Крупский, [22.07.2026 17:39]
|
||||
пока со своим токеном ... токенами по соотв. стендам
|
||||
|
||||
Сергей Мищук, [22.07.2026 18:07]
|
||||
только не по всем можно все, поэтому нужна настройка. Это будет гоняться на проде. Например, организацию создавать нельзя, объекты надо делать и удалять в специальной тестовой организации. Кроме того, для многих сервисов действует удаление с передержкой. Поэтому для них можно 1 раз сделать создание и не делать удаление
|
||||
|
||||
Сергей Мищук, [22.07.2026 18:07]
|
||||
это все надо конфигурить, а не кодировать
|
||||
+9
-95
@@ -1,103 +1,17 @@
|
||||
# DOCS — Архив документации
|
||||
|
||||
## Архитектура
|
||||
## Актуальное
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `ARCHITECTURE.md` | Актуальная архитектура: эндпоинты, схема БД, безопасность (§8 с фиксами аудита) |
|
||||
| `ARCHITECTURE-FULL.md` | Развёрнутая архитектура: полный список файлов, все роуты, зависимости |
|
||||
| `ARCHITECTURE.legacy.md` | Архитектура v1.0.x (до рефакторинга) |
|
||||
| `architecture-final.md` | Финальная архитектура после всех рефакторингов и аудитов |
|
||||
| `architecture-next.md` | Черновик архитектуры vNext — идеи на будущее |
|
||||
| `architecture-questions-for-sonnet.md` | Вопросы к Sonnet по архитектуре (перед ревью) |
|
||||
| `architecture-review-sonnet.md` | Ревью архитектуры от Sonnet |
|
||||
| `architecture-round2.md` | Второй раунд архитектурных решений |
|
||||
| `ARCHITECTURE.md` | **Главный документ.** Полная архитектура v1.2.43: режимы, БД, роуты, безопасность, тесты |
|
||||
| `howto-flask-nubes.md` | Как писать Flask-приложение под Nubes: структура `site/`, порт, `app.run()` |
|
||||
| `polygon-plan.md` | План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
|
||||
| `sonnet-app-autotest-review-v1.2.39.md` | Код-ревью Соннета 4.6 (2026-08-03): 10 находок |
|
||||
|
||||
## Как делать (How-to)
|
||||
## LEGACY/ — исторические
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `howto-flask-nubes.md` | **ВАЖНО.** Как писать Flask-приложение под Nubes: структура `site/`, порт 5000, `app.run()`, запреты |
|
||||
53 файла в папке `LEGACY/` — история разработки: старые архитектуры, ревью
|
||||
v1.0.x/v1.1.x, промпты для Sonnet/Opus/Sol, аудиты, планы, обсуждения.
|
||||
|
||||
## API Nubes
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `api-access.md` | Как получить доступ к Nubes API: токены, clientId, стенды |
|
||||
| `api-create-flow.md` | Полный flow создания инстанса: instances → instanceOperations → params → run → poll |
|
||||
| `api-operation-stages.md` | Стадии выполнения операций (plan, apply, etc.) |
|
||||
| `terraform-operations-full-logic.md` | Логика Terraform-операций: validate-cfs, refSvc, нормализация параметров |
|
||||
|
||||
## Планы
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `polygon-plan.md` | **ВАЖНО.** План мок-полигона: 3 фазы, 12 шагов, YAML-спецификация |
|
||||
| `opus-plan-2026-07-31.md` | План Опуса: унификация executor, гибкие ссылки, 4 фазы 13 шагов |
|
||||
| `test-results-history-plan.md` | План истории результатов тестов |
|
||||
| `gpt56-sol-plan.md` | План Sol по GPT-5.6 |
|
||||
|
||||
## Промпты для агентов
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `agent-analysis-request.md` | Запрос на анализ проекта (архитектура, риски, рекомендации) |
|
||||
| `opus-architecture-prompt.md` | Промпт Опусу: полное описание проекта + вопросы по архитектуре |
|
||||
| `opus-frontend-prompt.md` | Промпт Опусу: вопросы по фронтенду (instance dropdown) |
|
||||
| `gpt56-sol-prompt.md` | Промпт GPT-5.6: описание проекта для Sol |
|
||||
| `sonnet-review-prompt.md` | Промпт Sonnet для code review |
|
||||
|
||||
## Ревью и аудиты — Sonnet
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `sonnet-full-code-review.md` | Полный code review всех файлов |
|
||||
| `sonnet-full-response.md` | Полный ответ Sonnet на review |
|
||||
| `sonnet-full-review.md` | Сводка review |
|
||||
| `sonnet-architecture-review-v1.0.89.md` | Архитектурное ревью v1.0.89 |
|
||||
| `sonnet-architecture-review-v1.0.89-r2.md` | Второй раунд архитектурного ревью |
|
||||
| `sonnet-review-v1.1.11.md` | Ревью v1.1.11 |
|
||||
| `sonnet-review-answers.md` | Ответы на замечания Sonnet |
|
||||
| `sonnet-review-round2.md` | Второй раунд ревью |
|
||||
| `sonnet-review-round3.md` | Третий раунд ревью |
|
||||
| `sonnet-final-audit.md` | Финальный аудит |
|
||||
| `sonnet-final-review.md` | Финальное ревью |
|
||||
| `sonnet-missed-bugs.md` | Пропущенные баги (что Sonnet не нашёл) |
|
||||
| `sonnet-params-and-audit.md` | Параметры + аудит |
|
||||
| `sonnet-response-params-audit.md` | Ответ: параметры и аудит |
|
||||
| `sonnet-response-final.md` | Финальный ответ |
|
||||
| `sonnet-response-round2.md` | Ответ второго раунда |
|
||||
| `sonnet-response-v1.1.11.md` | Ответ на ревью v1.1.11 |
|
||||
| `sonnet-response-serviceInstanceUid.md` | Ответ про serviceInstanceUid |
|
||||
| `sonnet-response-state-out.md` | Ответ про state.out |
|
||||
| `sonnet-state-out-valuelist.md` | stateOut + valueList |
|
||||
| `sonnet-terraform-diff.md` | Разбор Terraform diff |
|
||||
| `sonnet-new-chat.md` | Новый чат с Sonnet |
|
||||
| `sonnet-question-tracker.md` | Трекер вопросов к Sonnet |
|
||||
|
||||
## Ревью — Опус
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `opus-questions-2026-07-31.md` | 19 уточняющих вопросов Опуса по архитектуре |
|
||||
| `opus-instance-dropdown-questions.md` | Вопросы Опуса про instance dropdown |
|
||||
| `opus-review-v1.2.0.md` | Ревью v1.2.0 от Опуса |
|
||||
|
||||
## Вопросы-ответы
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `questions-to-sol.md` | Вопросы к Sol |
|
||||
| `sol-answers.md` | Ответы Sol |
|
||||
| `sol-scenario-editor.md` | Sol про редактор сценариев |
|
||||
|
||||
## Прочее
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `HISTORY.md` | Краткая хронология версий (v1.0.x → v1.2.x) |
|
||||
| `dialogues.md` | Диалоги/дискуссии в процессе разработки |
|
||||
| `service-categories.md` | Категории сервисов Nubes (37 сервисов, их типы) |
|
||||
| `review-comparison-sonnet-opus.md` | Сравнение ревью Sonnet vs Opus |
|
||||
| `step-by-step-audit-v1.0.50.md` | Пошаговый аудит v1.0.50 |
|
||||
| `vm-213.md` | Заметки о VM-213 (тестовый стенд) |
|
||||
Актуальная архитектура — только в `ARCHITECTURE.md` (v1.2.43).
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# Code Review: app-autotest v1.2.39 — Соннет
|
||||
|
||||
Дата: 2026-08-03
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Критические
|
||||
|
||||
**1. XSS в instances.js — `o.operation` в innerHTML без `_esc()`**
|
||||
|
||||
instances.js (функция `toggleInstance`):
|
||||
```js
|
||||
opsEl.innerHTML = ops.map(o =>
|
||||
`<button ... onclick="runOp('${o.operation}',${o.svcOperationId})">${o.operation}</button>`
|
||||
).join('');
|
||||
```
|
||||
`o.operation` вставляется **три раза без `_esc()`**: в onclick-атрибут (`'${...}'`), в текст кнопки (`>${...}<`). Если API вернёт `operation = "'; alert(1)//"` — onclick-атрибут ломается. На практике операции — это "modify"/"delete" из Nubes API, но принцип нарушен. `_esc()` определён специально для этого.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Важные
|
||||
|
||||
**2. Двойной вызов `detect_endpoint()` при каждом HTTP-запросе**
|
||||
|
||||
auth.py — `get_client()` вызывает `detect_endpoint(token)`, и `get_stand()` вызывает `detect_endpoint(token)` независимо. В api_test.py при `POST /api/test`:
|
||||
```python
|
||||
client = get_client() # detect_endpoint() #1
|
||||
...
|
||||
get_client_id(), get_stand() # detect_endpoint() #2
|
||||
```
|
||||
Итого **2 лишних HTTP-запроса к Nubes API** (dev + test) при каждом запросе в облачном режиме. Оба блока `if endpoint in STANDS` срабатывают при стандартной конфигурации.
|
||||
|
||||
**3. `mode` и `polygon_stand` не валидируются при `set_mode`**
|
||||
|
||||
main.py:
|
||||
```python
|
||||
new_mode = request.form.get("mode", "polygon")
|
||||
new_stand = request.form.get("polygon_stand") or request.cookies.get("polygon_stand", "test")
|
||||
```
|
||||
Любое значение пишется в cookie. Если `polygon_stand = "evil/../../../etc"`, то в `get_client()` формируется URL:
|
||||
```
|
||||
polygon_url.replace("/api/v1/svc", "/evil/../../../etc/api/v1/svc")
|
||||
```
|
||||
Путь нормализуется HTTP-клиентом/сервером. Допустимые значения известны: `mode` ∈ {"polygon","cloud"}, `polygon_stand` ∈ {"dev","test","prod"} — надо добавить whitelist.
|
||||
|
||||
**4. В polygon-режиме у всех пользователей одинаковый `client_id`**
|
||||
|
||||
auth.py:
|
||||
```python
|
||||
def get_token():
|
||||
if get_mode() == "polygon":
|
||||
return current_app.config["NUBES_API_TOKEN"] # env-токен
|
||||
```
|
||||
`get_client_id()` тоже парсит env-токен → у всех пользователей в эмуляции одинаковый ClientID. Вся изоляция данных в БД (`runs`, `scenario_runs`, `scenario_definitions`) — по `(client_id, stand)`. Если два пользователя переключатся в polygon-режим — они видят историю и сценарии **друг друга**. Если приложение использует только один человек — не проблема. Если несколько — критично.
|
||||
|
||||
**5. `api_history` — соединение не в `finally`**
|
||||
|
||||
api_test.py:
|
||||
```python
|
||||
try:
|
||||
conn = get_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(...)
|
||||
...
|
||||
cur.close()
|
||||
put_conn(conn) # явный return соединения
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
try: cur.close() # NameError если исключение до cur = ...
|
||||
except: pass
|
||||
try: put_conn(conn) # NameError если исключение до conn = ...
|
||||
except: pass
|
||||
return jsonify({"error": str(e)}), 500
|
||||
```
|
||||
Все остальные функции (`api_scenario_run_status`, `api_scenario_status`) используют `finally: put_conn(conn)` — корректный паттерн. Здесь — нет. При ошибке до `cur.close()` соединение возвращается, но `cur` не закрывается. Не утечка (psycopg2 закроет при возврате conn в пул), но несоответствие стилю.
|
||||
|
||||
**6. `stand_name()` — подстрочный поиск**
|
||||
|
||||
http_client.py:
|
||||
```python
|
||||
for name in ("dev", "test"):
|
||||
if name in (endpoint or ""):
|
||||
return name
|
||||
```
|
||||
Если URL содержит "dev" как часть другого слова (например, "development", "devnull"), вернёт неверное значение. URL-то сейчас конкретные, но хрупко.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Рекомендации
|
||||
|
||||
**7. Двойной отступ в scenario.py**
|
||||
|
||||
scenario.py:
|
||||
```python
|
||||
for i, step in enumerate(steps):
|
||||
step_num = i + 1 # 8 пробелов вместо 4
|
||||
```
|
||||
Весь цикл имеет нестандартный отступ (8 пробелов). Работает корректно (Python), но выглядит как след удалённого `try:` или `with:` блока, который был внутри for.
|
||||
|
||||
**8. `get_real_client()` — мёртвый алиас**
|
||||
|
||||
auth.py: функция идентична `get_client()`, есть только для обратной совместимости. В main.py и других файлах есть вызовы `get_real_client()`. Стоит постепенно заменить на `get_client()` и убрать алиас.
|
||||
|
||||
**9. Нет `UNIQUE` на `runs.op_uid`**
|
||||
|
||||
init_db.py: ручной UPSERT в save_run.py (UPDATE → INSERT) предполагает уникальность `op_uid`. Индекса нет. При маловероятной гонке двух параллельных `save_run` с одним `op_uid` — оба сделают UPDATE (rowcount=0) → оба сделают INSERT → дубль. Добавить `CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_op_uid ON runs (op_uid) WHERE op_uid IS NOT NULL`.
|
||||
|
||||
**10. Ротация лога: `flock(LOCK_UN)` перед flush буфера**
|
||||
|
||||
api_test.py:
|
||||
```python
|
||||
f.write(rest)
|
||||
fcntl.flock(f, fcntl.LOCK_UN) # лок снят, но буфер ещё не сброшен
|
||||
```
|
||||
Python-буфер сбрасывается при закрытии файла (`with`-блок), но лок уже снят. Другой воркер может прочитать неполные данные. Добавить `f.flush()` перед `LOCK_UN`.
|
||||
|
||||
---
|
||||
|
||||
## Итого по приоритетам
|
||||
|
||||
| # | Файл | Проблема | Серьёзность |
|
||||
|---|------|----------|-------------|
|
||||
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
|
||||
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
|
||||
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
|
||||
| 4 | `api/auth.py` | Shared `client_id` в polygon-режиме | 🟡 |
|
||||
| 5 | `routes/api_test.py` | `api_history` без `finally` | 🟡 |
|
||||
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
|
||||
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
|
||||
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
|
||||
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
|
||||
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации | 🟢 |
|
||||
@@ -0,0 +1,124 @@
|
||||
# 2026-08-03-session — Полигон: интеграция, баг с сервисами
|
||||
|
||||
## Контекст
|
||||
|
||||
Сессия началась с чтения двух файлов AI-агентом:
|
||||
- `AGENT_BRIEFING.md` — контекст app-autotest (v1.2.28)
|
||||
- `polygon-docs/POLYGON-FULL.md` — полное описание полигона (v0.5.9)
|
||||
|
||||
jsonEnv автотеста на проде:
|
||||
```json
|
||||
{
|
||||
"POLYGON_ENDPOINT": "https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc",
|
||||
"NUBES_API_ENDPOINT": "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
|
||||
}
|
||||
```
|
||||
|
||||
## Архитектурное решение: разделение сервисы/инстансы
|
||||
|
||||
**Зафиксировано (2026-08-03):**
|
||||
|
||||
| Режим | Сервисы (метаданные) | Инстансы |
|
||||
|--------|---------------------|-----------|
|
||||
| Без полигона | Реальный API | Реальное облако |
|
||||
| С полигоном | Реальный API | Виртуальные (полигон) |
|
||||
|
||||
`POLYGON_ENDPOINT` подменяет ТОЛЬКО инстансы/операции. Сервисы — это «справочник», всегда из реального API.
|
||||
|
||||
### Где это в коде
|
||||
|
||||
`auth.py:_make_client(use_polygon)`:
|
||||
- `use_polygon=True` → `get_client()`: если `POLYGON_ENDPOINT` задан → полигон, иначе → `NUBES_API_ENDPOINT`
|
||||
- `use_polygon=False` → `get_real_client()`: **всегда** `NUBES_API_ENDPOINT`
|
||||
|
||||
## Баг: «нет сервисов» при POLYGON_ENDPOINT
|
||||
|
||||
### Причина
|
||||
|
||||
Цепочка:
|
||||
1. `get_stand()` → `"polygon"` (auth.py:141, POLYGON_ENDPOINT задан)
|
||||
2. `load_service_ids("polygon")` → `services_polygon.txt` не существует → пустой `set`
|
||||
3. `sorted(пустой_set)` → `[]`
|
||||
4. Шаблон `index.html:81`: `{% if svc.svcId in config.service_ids %}` → `svc.svcId in []` → **всегда False** → ни один сервис не показывается
|
||||
|
||||
Хотя `get_real_client()` корректно получает сервисы из реального API, фильтр в шаблоне убивает весь список.
|
||||
|
||||
### Исправление (v1.2.29)
|
||||
|
||||
Шаблон `index.html:81`: добавить `or not config.service_ids`:
|
||||
|
||||
```jinja2
|
||||
{# Было #}
|
||||
{% if svc.svcId in config.service_ids %}
|
||||
|
||||
{# Стало #}
|
||||
{% if not config.service_ids or svc.svcId in config.service_ids %}
|
||||
```
|
||||
|
||||
Это соответствует изначальному замыслу `load_service_ids`: «пустой set → показываем все».
|
||||
|
||||
---
|
||||
|
||||
## Фаза 2: селектор режима и стенда (v1.2.30)
|
||||
|
||||
### Требования
|
||||
|
||||
1. **Селектор режима** над версией: 🎭 Эмуляция / ☁️ Облако (только если POLYGON_ENDPOINT задан)
|
||||
2. **Селектор стенда** (DEV/TEST/PROD) — только в режиме эмуляции
|
||||
3. **По умолчанию:** эмуляция + TEST
|
||||
4. **В эмуляции:** токен дезактивирован
|
||||
5. **В облаке:** стенд автоопределяется по токену (как раньше)
|
||||
6. При смене режима/стенда → сервисы обновляются
|
||||
|
||||
### Новая архитектура
|
||||
|
||||
| Режим | Сервисы | Инстансы | Стенд |
|
||||
|--------|---------|----------|-------|
|
||||
| Эмуляция | Полигон (/{stand}/api/v1/svc) | Полигон | Выбор (dev/test/prod) |
|
||||
| Облако | Реальный API | Реальный API | Авто (токен→dev/test) |
|
||||
|
||||
В эмуляции **всё** идёт в полигон — и сервисы, и инстансы. Полигон имеет полный набор эндпоинтов (/services, /instances, /instanceOperations).
|
||||
|
||||
`get_real_client()` упраздняется — становится алиасом `get_client()`.
|
||||
|
||||
### Cookie
|
||||
|
||||
- `mode` — `"polygon"` (по умолчанию) или `"cloud"`
|
||||
- `polygon_stand` — `"test"` (по умолчанию), `"dev"`, `"prod"`
|
||||
|
||||
### Изменённые файлы
|
||||
|
||||
1. **`auth.py`** — новые функции `get_mode()`, `get_polygon_stand()`; `get_token()` в режиме полигона только env-токен; `get_client()` строит URL с префиксом стенда; `get_real_client()` → алиас
|
||||
2. **`main.py`** — `action=set_mode` (cookie), single `get_client()` вместо real_client/inst_client, переменные `mode`/`polygon_stand`/`polygon_enabled`
|
||||
3. **`index.html`** — селекторы над версией, disable токена в эмуляции
|
||||
4. **`service_list.py`** — обрезать `polygon_` префикс
|
||||
5. **`api_test.py`** — `get_real_client()` → `get_client()`
|
||||
|
||||
---
|
||||
|
||||
## Фаза 3: Код-ревью Соннета 4.6 (v1.2.39)
|
||||
|
||||
### Промпт
|
||||
|
||||
```
|
||||
Ты — senior backend-разработчик, делаешь код-ревью веб-приложения app-autotest.
|
||||
Flask 3.1 + vanilla JS + PostgreSQL. Два режима: эмуляция (Polygon) / облако.
|
||||
Проверь внимательно: безопасность, URL, гонки, целостность данных, ошибки, архитектуру, JS.
|
||||
Для каждой находки: 🔴 критическое / 🟡 важное / 🟢 рекомендация.
|
||||
Если неясно — спроси, устроим диалог.
|
||||
```
|
||||
|
||||
### Результаты
|
||||
|
||||
| # | Файл | Проблема | Серьёзность |
|
||||
|---|------|----------|-------------|
|
||||
| 1 | `static/js/instances.js` | `o.operation` в innerHTML без `_esc()` | 🔴 |
|
||||
| 2 | `api/auth.py` | Двойной `detect_endpoint()` на запрос | 🟡 |
|
||||
| 3 | `routes/main.py` | `mode`/`polygon_stand` без whitelist-валидации | 🟡 |
|
||||
| 4 | `api/auth.py` | Общий `client_id` в polygon-режиме (env-токен) | 🟡 |
|
||||
| 5 | `routes/api_test.py` | `api_history` без `finally` для conn | 🟡 |
|
||||
| 6 | `api/http_client.py` | `stand_name()` substring match | 🟡 |
|
||||
| 7 | `operations/scenario.py` | Двойной отступ в for-цикле | 🟢 |
|
||||
| 8 | `api/auth.py` | `get_real_client()` мёртвый алиас | 🟢 |
|
||||
| 9 | `db/init_db.py` | Нет UNIQUE индекса на `runs.op_uid` | 🟢 |
|
||||
| 10 | `routes/api_test.py` | `flock(LOCK_UN)` перед flush в ротации лога | 🟢 |
|
||||
@@ -0,0 +1,458 @@
|
||||
# Polygon — эмулятор Nubes API
|
||||
|
||||
> Версия: v0.5.9 | Деплой: `polygon.pythonk8s.dev.nubes.ru` | Формат: Nubes managed Flask
|
||||
|
||||
---
|
||||
|
||||
## 1. Что такое Polygon
|
||||
|
||||
Polygon — **эмулятор REST API облачной платформы Nubes** для интеграционных тестов.
|
||||
|
||||
Он полностью повторяет контракты реального API (сервисы, инстансы, операции, параметры, валидацию),
|
||||
но работает **без реальной инфраструктуры** — в памяти, с мгновенным откликом.
|
||||
|
||||
### Зачем нужен
|
||||
|
||||
- Тестировать создание/изменение/удаление инстансов без реальных облачных ресурсов
|
||||
- Отлаживать UI автодеплоя (`app-autotest`) на мок-данных
|
||||
- Писать интеграционные тесты с детерминированным состоянием
|
||||
- Проверять краевые случаи (ошибки валидации, сбои операций)
|
||||
- Демонстрировать API заказчикам через Swagger UI
|
||||
|
||||
### Что НЕ делает
|
||||
|
||||
- Не управляет реальной инфраструктурой
|
||||
- Не хранит данные между перезапусками (всё в памяти)
|
||||
- Не авторизует пользователей (кроме `_mock/*` служебных эндпоинтов)
|
||||
- Не повторяет ВСЕ эндпоинты реального API — только те что нужны для тестов
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектура
|
||||
|
||||
### Технологический стек
|
||||
|
||||
| Компонент | Технология | Зачем |
|
||||
|-----------|-----------|-------|
|
||||
| Веб-фреймворк | Flask 3.0 | Маршрутизация, шаблоны, JSON-ответы |
|
||||
| WSGI-сервер | gunicorn | Запуск на проде (встроен в Nubes managed Flask) |
|
||||
| Конфигурация | YAML (PyYAML) | Описание сервисов, операций, параметров |
|
||||
| Состояние | In-memory dict | Инстансы, операции, параметры |
|
||||
| Шаблоны | Jinja2 | HTML-страницы (главная, Swagger) |
|
||||
| OpenAPI | OpenAPI 3.1.0 | Спека API (генерится динамически) |
|
||||
| Swagger UI | Swagger UI 5 (CDN) | Интерактивная документация |
|
||||
|
||||
### Ключевое ограничение: 1 воркер
|
||||
|
||||
```python
|
||||
# app.py
|
||||
os.environ.setdefault("WEB_CONCURRENCY", "1")
|
||||
```
|
||||
|
||||
**Почему:** состояние (инстансы, операции) хранится в памяти Python-процесса.
|
||||
Два воркера = два набора инстансов = хаос. Поэтому жёстко 1 gunicorn-воркер.
|
||||
|
||||
**Следствия:**
|
||||
- Только 1 запрос обрабатывается одновременно
|
||||
- Большие ответы (>20KB) могут обрываться из-за таймаута сети
|
||||
- Swagger-спека встроена прямо в HTML чтобы избежать второго запроса
|
||||
- Нельзя горизонтально масштабировать
|
||||
|
||||
### Модель данных в памяти
|
||||
|
||||
```
|
||||
MockState (синглтон на стенд)
|
||||
├── instances: {instanceUid → {instanceUid, serviceId, displayName, status, state, ...}}
|
||||
├── operations: {opUid → {instanceOperationUid, instanceUid, operation, dtStart, dtFinish, ...}}
|
||||
├── op_params: {opUid → {paramId(int) → paramValue(str)}}
|
||||
└── fail_next: bool (one-shot флаг для симуляции ошибок)
|
||||
```
|
||||
|
||||
### Жизненный цикл запроса
|
||||
|
||||
```
|
||||
HTTP-запрос → Nubes ingress → gunicorn (1 воркер) → Flask
|
||||
→ StandMiddleware (извлекает stand_id из URL, перезаписывает PATH_INFO)
|
||||
→ before_request (копирует stand_id в flask.g)
|
||||
→ Blueprint-роут (через LocalProxy резолвит state/SERVICES под текущий стенд)
|
||||
→ JSON-ответ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Стенды (dev / test / prod)
|
||||
|
||||
Polygon эмулирует **три изолированных стенда** облака:
|
||||
|
||||
| Стенд | URL-префикс | Сервисов | YAML из |
|
||||
|-------|------------|----------|---------|
|
||||
| dev | `/dev/api/v1/svc/...` | 37 | `~/tf_provider/generated/dev/` |
|
||||
| test | `/test/api/v1/svc/...` | 37 | `~/tf_provider/generated/test/` |
|
||||
| prod | `/prod/api/v1/svc/...` | 35 | `~/tf_provider/generated/prod/` |
|
||||
|
||||
**Изоляция:** у каждого стенда **свои** инстансы, операции, параметры.
|
||||
`fail-next` на dev не влияет на test. Состояние каждого стенда независимо.
|
||||
|
||||
**Переключение в Swagger:** выпадающий список серверов (Server selector).
|
||||
|
||||
**Переключение в URL:** просто добавь префикс — `/dev/`, `/test/`, `/prod/`.
|
||||
Без префикса — стенд по умолчанию (dev).
|
||||
|
||||
**Настройка:** переменная окружения `POLYGON_STANDS`:
|
||||
```
|
||||
POLYGON_STANDS=dev:services/dev,test:services/test,prod:services/prod
|
||||
```
|
||||
Формат: `имя_стенда:путь_к_YAML,имя_стенда:путь_к_YAML,...`
|
||||
|
||||
---
|
||||
|
||||
## 4. YAML-пайплайн (откуда берутся сервисы)
|
||||
|
||||
Polygon не знает о сервисах сам — он читает их из YAML-конфигов,
|
||||
которые генерируются из терраформ-репы.
|
||||
|
||||
### Цепочка
|
||||
|
||||
```
|
||||
~/tf_provider/generated/{dev,test,prod}/resources_yaml/*.yaml ← источник (терраформ)
|
||||
│
|
||||
│ from_stands.py — конвертация формата (ВРУЧНУЮ)
|
||||
│ • дедупликация параметров (один param в create/modify/delete → одна запись)
|
||||
│ • построение cfsParamsByOp (какие параметры к какой операции)
|
||||
│ • сбор stateParams из create-операции
|
||||
│ • генерация stateOut из subresource-операций
|
||||
│ • раскодирование HTML-entities (" → ")
|
||||
│
|
||||
▼
|
||||
polygon/site/services/{dev,test,prod}/*.yaml ← скоммичены в git
|
||||
│
|
||||
│ config/loader.py — загрузка ВСЕХ YAML в память при старте
|
||||
│ • _load_one() для каждой папки стенда
|
||||
│ • STANDS = {dev: {SERVICES, OPS_INDEX}, test: {...}, prod: {...}}
|
||||
│ • ~0.7 MB на 3 стенда
|
||||
│
|
||||
▼
|
||||
Память Flask-процесса → API отдаёт данные из памяти (без диска)
|
||||
```
|
||||
|
||||
### Когда обновлять YAML
|
||||
|
||||
Когда терраформ-репа обновилась (добавили сервис, изменили параметры):
|
||||
|
||||
```bash
|
||||
cd polygon/site
|
||||
python from_stands.py ~/tf_provider/generated/dev/resources_yaml services/dev
|
||||
python from_stands.py ~/tf_provider/generated/test/resources_yaml services/test
|
||||
python from_stands.py ~/tf_provider/generated/prod/resources_yaml services/prod
|
||||
cd .. && git add services/ && git commit -m "regenerate YAML" && git push
|
||||
# → редеплоить polygon через Nubes UI
|
||||
```
|
||||
|
||||
**Это ручная операция.** Нет автоматического триггера.
|
||||
|
||||
---
|
||||
|
||||
## 5. Структура кода
|
||||
|
||||
```
|
||||
polygon/
|
||||
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||||
├── Procfile # web: gunicorn site.app:app --bind 0.0.0.0:8000
|
||||
├── tests/
|
||||
│ ├── test_api.py # 39 smoke-тестов против деплоя
|
||||
│ ├── fuzz_test.py # 147 фаззинг-тестов (злонамеренные входные данные)
|
||||
│ ├── compare_test.py # Сравнение полигона с реальным Nubes API
|
||||
│ ├── test_converter.py # Юнит-тесты from_stands.py
|
||||
│ └── test_state_machine.py # Юнит-тесты apply_effect()
|
||||
└── site/
|
||||
├── app.py # Flask-приложение: StandMiddleware, blueprint'ы
|
||||
├── mock_state.py # MockState — хранилище инстансов/операций
|
||||
├── state_machine.py # apply_effect() — мутация состояния инстанса
|
||||
├── from_stands.py # Конвертер terraform YAML → polygon YAML
|
||||
├── config/
|
||||
│ └── loader.py # Загрузка YAML → SERVICES, OPS_INDEX, STANDS
|
||||
├── routes/
|
||||
│ ├── root.py # /health, /, /swagger
|
||||
│ ├── services_routes.py # /api/v1/svc/services
|
||||
│ ├── instances_routes.py # /api/v1/svc/instances
|
||||
│ ├── operations_routes.py # /api/v1/svc/instanceOperations/*
|
||||
│ ├── run.py # /api/v1/svc/instanceOperations/<uid>/run
|
||||
│ ├── mock_routes.py # /api/v1/svc/_mock/*
|
||||
│ └── openapi.py # /api/v1/svc/openapi.json (OpenAPI 3.1.0 спека)
|
||||
├── services/
|
||||
│ ├── dev/ # 37 YAML-конфигов (стенд dev)
|
||||
│ ├── test/ # 37 YAML-конфигов (стенд test)
|
||||
│ └── prod/ # 35 YAML-конфигов (стенд prod)
|
||||
├── static/
|
||||
│ ├── style.css # Дизайн-система Nubes
|
||||
│ ├── logo.svg # Логотип
|
||||
│ └── favicon.svg # Иконка
|
||||
├── templates/
|
||||
│ ├── index.html # Главная страница (инфо + ссылки на стенды)
|
||||
│ └── swagger.html # Swagger UI (спека встроена в HTML)
|
||||
└── utils/
|
||||
├── now.py # now() — UTC ISO с 'Z'
|
||||
└── pluralize.py # pluralize() — плюрализация
|
||||
```
|
||||
|
||||
### Ключевые архитектурные решения
|
||||
|
||||
**LocalProxy** — все роуты используют `werkzeug.local.LocalProxy` для доступа к `state` и `SERVICES`:
|
||||
|
||||
```python
|
||||
# В каждом роуте:
|
||||
from flask import g
|
||||
from werkzeug.local import LocalProxy
|
||||
import mock_state, config.loader as _cfg
|
||||
|
||||
state = LocalProxy(lambda: mock_state.get_state(g.stand_id))
|
||||
SERVICES = LocalProxy(lambda: _cfg.get_services(g.stand_id))
|
||||
```
|
||||
|
||||
Это позволяет одному и тому же коду роута работать с разными стендами — `g.stand_id` определяет какой стенд используется.
|
||||
|
||||
**StandMiddleware** (app.py) — WSGI-middleware который извлекает `stand_id` из URL ДО Flask-роутинга:
|
||||
|
||||
```python
|
||||
class StandMiddleware:
|
||||
def __call__(self, environ, start_response):
|
||||
path = environ["PATH_INFO"]
|
||||
parts = path.split("/")
|
||||
if parts[1] in STANDS:
|
||||
environ["polygon.stand_id"] = parts[1]
|
||||
environ["PATH_INFO"] = "/" + "/".join(parts[2:]) # /dev/api/... → /api/...
|
||||
else:
|
||||
environ["polygon.stand_id"] = DEFAULT_STAND
|
||||
return self.wsgi_app(environ, start_response)
|
||||
```
|
||||
|
||||
**before_request** (app.py) — копирует stand_id в `flask.g`:
|
||||
```python
|
||||
@app.before_request
|
||||
def _set_stand():
|
||||
g.stand_id = request.environ.get("polygon.stand_id", DEFAULT_STAND)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API — 17 эндпоинтов
|
||||
|
||||
| Метод | Путь | Назначение | Ответ |
|
||||
|-------|------|------------|-------|
|
||||
| GET | `/health` | Healthcheck для Nubes | `OK` (text) |
|
||||
| GET | `/` | HTML с инфо + ссылки на стенды | HTML |
|
||||
| GET | `/swagger` | Swagger UI (спека в HTML) | HTML |
|
||||
| GET | `/api/v1/svc/openapi.json` | OpenAPI 3.1.0 спека | JSON |
|
||||
| GET | `/api/v1/svc/services` | Список сервисов | `{results: [...]}` |
|
||||
| GET | `/api/v1/svc/services/{id}` | Детали сервиса (операции) | `{svc: {operations: [...]}}` |
|
||||
| GET | `/api/v1/svc/instances` | Список инстансов (пагинация) | `{results, pageSize, page, total}` |
|
||||
| GET | `/api/v1/svc/instances/{uid}` | Полные данные инстанса | `{instance: {...}}` |
|
||||
| POST | `/api/v1/svc/instances` | Создать инстанс | `201 + Location + {instanceUid}` |
|
||||
| GET | `/api/v1/svc/instanceOperations/default/{id}` | Шаблон операции (cfsParams) | `{svcOperation: {cfsParams: [...]}}` |
|
||||
| POST | `/api/v1/svc/instanceOperations` | Создать операцию | `201 + Location + {instanceOperationUid}` |
|
||||
| GET | `/api/v1/svc/instanceOperations/{uid}` | Статус операции | `{instanceOperation: {...}}` |
|
||||
| POST | `/api/v1/svc/instanceOperationCfsParams` | Установить параметр | `{}` |
|
||||
| GET | `/api/v1/svc/instanceOperations/{uid}/validate-cfs` | Валидация параметров | `""` (пустое тело) |
|
||||
| POST | `/api/v1/svc/instanceOperations/{uid}/run` | Выполнить операцию | `{ok: true/false, error?}` |
|
||||
| POST | `/api/v1/svc/_mock/reset` | Сброс состояния | `{reset: "ok"}` |
|
||||
| GET | `/api/v1/svc/_mock/state` | Дамп состояния (отладка) | `{instances, operations}` |
|
||||
| GET | `/api/v1/svc/_mock/services` | Список загруженных сервисов (отладка) | `{count, services}` |
|
||||
| POST | `/api/v1/svc/_mock/delay/{s}` | Задать задержку операций (max 5s) | `{delay: N}` |
|
||||
| POST | `/api/v1/svc/_mock/fail-next` | Следующая операция упадёт (one-shot) | `{fail_next: true}` |
|
||||
|
||||
### Типовой сценарий: создать инстанс и запустить операцию
|
||||
|
||||
Это основной флоу, который повторяет логику реального облачного API.
|
||||
Все примеры — с jq для наглядности, но работают и с `python3 -m json.tool`.
|
||||
|
||||
**Шаг 1. Узнать ID сервиса по имени:**
|
||||
|
||||
```bash
|
||||
curl -s "$URL/services" | jq '.results[] | select(.svc == "НазваниеСервиса") | .svcId'
|
||||
```
|
||||
|
||||
**Шаг 2. Узнать ID операции у этого сервиса:**
|
||||
|
||||
```bash
|
||||
curl -s "$URL/services/$SVC_ID" | jq '.svc.operations[] | select(.operation == "create") | .svcOperationId'
|
||||
```
|
||||
|
||||
**Шаг 3. Получить список входных параметров (cfsParams):**
|
||||
|
||||
```bash
|
||||
curl -s "$URL/instanceOperations/default/$OP_ID" | jq '.svcOperation.cfsParams[] | {svcOperationCfsParamId, svcOperationCfsParam, dataType, valueList, isRequired}'
|
||||
```
|
||||
|
||||
Ответ показывает для каждого параметра: числовой ID, код, тип данных, список допустимых значений (если есть), обязательность.
|
||||
|
||||
**Шаг 4. Создать инстанс:**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$URL/instances" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"serviceId": '$SVC_ID', "displayName": "мой тестовый инстанс"}' \
|
||||
| jq '.instanceUid'
|
||||
```
|
||||
|
||||
**Шаг 5. Создать операцию:**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$URL/instanceOperations" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instanceUid": "'$INST_UID'", "operation": "create", "svcOperationId": '$OP_ID'}' \
|
||||
| jq '.instanceOperationUid'
|
||||
```
|
||||
|
||||
**Шаг 6. Установить параметры (повторить для каждого):**
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$URL/instanceOperationCfsParams" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instanceOperationUid": "'$OP_UID'", "svcOperationCfsParamId": '$PARAM_ID', "paramValue": "значение"}'
|
||||
```
|
||||
|
||||
**Шаг 7. Проверить валидацию и запустить:**
|
||||
|
||||
```bash
|
||||
# Проверить что все параметры валидны (200 = OK)
|
||||
curl -s -o /dev/null -w "%{http_code}" "$URL/instanceOperations/$OP_UID/validate-cfs"
|
||||
|
||||
# Запустить операцию (будет ждать DELAY секунд)
|
||||
curl -s -X POST "$URL/instanceOperations/$OP_UID/run" | jq '{ok, error}'
|
||||
```
|
||||
|
||||
**Шаг 8. Проверить результат:**
|
||||
|
||||
```bash
|
||||
curl -s "$URL/instanceOperations/$OP_UID?fields=dtFinish,isSuccessful" | jq '.instanceOperation | {dtFinish, isSuccessful}'
|
||||
```
|
||||
|
||||
Полигон полностью повторяет эту последовательность: те же эндпоинты, те же поля, те же статус-коды (201 при создании, 409 при повторном run, 404 при несуществующем ресурсе).
|
||||
|
||||
### Валидация входных данных
|
||||
|
||||
- `serviceId` — только integer > 0 (строка/float/отрицательное → 400)
|
||||
- Тело запроса — должно быть JSON-объектом (массив → 400)
|
||||
- `svcOperationCfsParamId` — только integer (строка → 400)
|
||||
- Повторный `run` — 409 (already completed)
|
||||
|
||||
### Аутентификация
|
||||
|
||||
- Основные эндпоинты — **без авторизации**
|
||||
- `_mock/*` — заголовок `X-Mock-Auth` (если `MOCK_AUTH_TOKEN` задан в env)
|
||||
- По умолчанию `MOCK_AUTH_TOKEN` не задан → `_mock/*` открыты
|
||||
|
||||
---
|
||||
|
||||
## 7. Тесты
|
||||
|
||||
### test_api.py — 39 smoke-тестов
|
||||
|
||||
Запуск: `python3 tests/test_api.py`
|
||||
|
||||
Проверяет деплоенный полигон по всем 17 эндпоинтам:
|
||||
- Health, services, instances (CRUD + pagination + edge cases)
|
||||
- Operations (create → params → validate → run → double-run 409)
|
||||
- Fail-next (one-shot error simulation)
|
||||
- Auth (без токена, неверный токен, верный токен)
|
||||
- Mock state (reset, state dump, delay bounds)
|
||||
- Multi-stand (изоляция dev/test/prod, fail-next изоляция)
|
||||
|
||||
### fuzz_test.py — 147 фаззинг-тестов
|
||||
|
||||
Запуск: `cd site && PYTHONPATH=. python3 ../tests/fuzz_test.py` (локально, Flask test client)
|
||||
|
||||
Злонамеренные и экстремальные сценарии:
|
||||
- SQL injection, XSS, Unicode-emoji, null-байты, 10000-символьные строки
|
||||
- Неверные типы (строка вместо int, float, отрицательные)
|
||||
- Отсутствующие поля, null-поля
|
||||
- Path traversal, двойные слеши
|
||||
- Быстрые повторы (10 инстансов подряд)
|
||||
- Невалидные UUID
|
||||
- Неправильные HTTP-методы (PUT на GET, DELETE на POST)
|
||||
|
||||
### compare_test.py — сравнение с реальным API
|
||||
|
||||
Запуск: `python3 tests/compare_test.py`
|
||||
|
||||
Сравнивает read-only эндпоинты полигона с реальным Nubes API (dev/test/prod):
|
||||
- `GET /services` — одинаковый ли список сервисов
|
||||
- `GET /services/{id}` — одинаковые ли операции (svcOperationId, operation, kind, action)
|
||||
- `GET /instanceOperations/default/{id}` — одинаковые ли cfsParams
|
||||
|
||||
Классификация расхождений:
|
||||
- 🔴 BUG — полигон неправ (лишний/неверный параметр)
|
||||
- 🟡 LAG — реальный API обогнал (новый параметр, YAML устарел)
|
||||
- 🟢 BETTER — полигон правильнее реального API (null→"string", HTML entities)
|
||||
|
||||
### Юнит-тесты
|
||||
|
||||
```bash
|
||||
pytest tests/test_converter.py -v # 10 тестов from_stands.py
|
||||
pytest tests/test_state_machine.py -v # 9 тестов apply_effect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Переменные окружения
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|-----------|-------------|----------|
|
||||
| `POLYGON_STANDS` | (пусто) | Стенды: `dev:services/dev,test:services/test,prod:services/prod` |
|
||||
| `MOCK_AUTH_TOKEN` | (пусто) | Токен для `_mock/*`. Если пусто — auth отключена |
|
||||
| `MOCK_OP_DELAY` | `0.1` | Задержка операции в секундах |
|
||||
| `POLYGON_ENDPOINT` | `https://polygon.pythonk8s.dev.nubes.ru` | URL для OpenAPI-спеке |
|
||||
| `WEB_CONCURRENCY` | `1` | ⛔ НЕ менять — сломает изоляцию состояния |
|
||||
|
||||
---
|
||||
|
||||
## 9. Ограничения и известные проблемы
|
||||
|
||||
| Ограничение | Причина | Обход |
|
||||
|------------|---------|-------|
|
||||
| 1 gunicorn-воркер | Состояние в памяти | Не менять WEB_CONCURRENCY |
|
||||
| Большие ответы (>20KB) обрываются | 1 воркер + сетевой таймаут | Swagger-спека встроена в HTML |
|
||||
| Нет персистентности | Всё в памяти | Использовать `_mock/reset` в тестах |
|
||||
| Auth не включена на проде | `MOCK_AUTH_TOKEN` не задан | Добавить в env деплоя |
|
||||
| YAML обновляется вручную | Нет автотриггера | Запускать `from_stands.py` при изменении терраформа |
|
||||
| Нет HTTPS-валидации сертификатов | Dev-инструмент | — |
|
||||
| Swagger загружается с CDN (jsdelivr) | Нет офлайн-версии | — |
|
||||
|
||||
---
|
||||
|
||||
## 10. Деплой
|
||||
|
||||
Polygon — managed Flask на Nubes pythonk8s.
|
||||
|
||||
**Procfile:**
|
||||
```
|
||||
web: gunicorn site.app:app --bind 0.0.0.0:8000
|
||||
```
|
||||
|
||||
**Процесс редеплоя:**
|
||||
1. Закоммитить и запушить изменения в `master`
|
||||
2. В Nubes UI нажать «Redeploy»
|
||||
3. Nubes: стягивает репу, билдит, запускает gunicorn на порту 8000
|
||||
4. Healthcheck: `GET /health` → должен вернуть `OK`
|
||||
|
||||
**Важно:** YAML-конфиги (`services/`) скоммичены в git.
|
||||
При редеплое Nubes клонирует репу → YAML уже на месте.
|
||||
Не нужно запускать `from_stands.py` при деплое.
|
||||
|
||||
---
|
||||
|
||||
## 11. Чеклист для нового AI-агента
|
||||
|
||||
При старте нового чата прочитай:
|
||||
1. Этот документ
|
||||
2. `polygon/site/app.py` — точка входа
|
||||
3. `polygon/site/config/loader.py` — загрузка конфигов
|
||||
4. `polygon/site/routes/openapi.py` — OpenAPI-спека
|
||||
5. `polygon/tests/test_api.py` — основной тест-сьют
|
||||
|
||||
Ключевые инварианты (НЕ нарушать):
|
||||
- WEB_CONCURRENCY = 1 всегда
|
||||
- `state` и `SERVICES` — через LocalProxy, не напрямую
|
||||
- `_mock/*` требует X-Mock-Auth (если задан MOCK_AUTH_TOKEN)
|
||||
- `serviceId` валидируется как int > 0
|
||||
- YAML обновляется через `from_stands.py`, коммитится, редеплоится
|
||||
@@ -0,0 +1,184 @@
|
||||
# Соннет: анализ сравнительного тестирования Polygon ↔ реальный Nubes API
|
||||
|
||||
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||
> ⛔ Режим: **диалог**. Задавай встречные вопросы если нужно уточнение.
|
||||
> ⛔ НЕ редактировать файлы. Только анализ и советы в чат.
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
**Polygon** (v0.5.4) — эмулятор REST API облачной платформы Nubes.
|
||||
3 стенда: dev (37 сервисов), test (37), prod (35). YAML-конфиги генерируются
|
||||
из терраформ-репы (`~/tf_provider/generated/{dev,test,prod}/resources_yaml/`).
|
||||
|
||||
**Реальное API**:
|
||||
- `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc`
|
||||
- `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc`
|
||||
- `https://lk-api-gateway.ngcloud.ru/api/v1/svc`
|
||||
|
||||
Токены в `secrets/{dev,test,prod}.token`.
|
||||
|
||||
## Что сделано
|
||||
|
||||
Написан скрипт `compare_test.py` который сравнивает read-only эндпоинты
|
||||
полигона и реального API:
|
||||
- `GET /services` — список сервисов
|
||||
- `GET /services/{id}` — операции (svcOperationId, operation, kind, action)
|
||||
- `GET /instanceOperations/default/{id}` — cfsParams (id, код, dataType, isRequired)
|
||||
|
||||
Первый прогон показал:
|
||||
- **dev**: операции совпадают, но у 2 параметров `dataType: None` вместо `"string"`
|
||||
- **test**: аналогично
|
||||
- **prod**: чисто, расхождений нет
|
||||
|
||||
Также обнаружено что реальный API возвращает HTML-entities в dataType
|
||||
(`integer >= 0`), а полигон — чистый текст (`integer >= 0`).
|
||||
Полигон здесь правильнее реального API.
|
||||
|
||||
## Ключевой нюанс: идеология стендов
|
||||
|
||||
Стенды НЕ идентичны. **Dev опережает test, test опережает prod**.
|
||||
Новые сервисы и параметры появляются сначала в dev, потом через какое-то
|
||||
время попадают в test, и только затем в prod. Поэтому:
|
||||
|
||||
- Если в dev-полигоне и dev-реальном API есть расхождения — это может быть
|
||||
нормально (реальный API уже обновился, а YAML в полигоне — ещё нет)
|
||||
- Если в prod есть расхождения — скорее всего баг в генерации YAML
|
||||
- Нужно различать «допустимое отставание» и «реальный баг»
|
||||
|
||||
## Что нужно от тебя
|
||||
|
||||
### 1. Стратегия сравнительного тестирования
|
||||
|
||||
Как правильно сравнивать полигон с реальным API учитывая что:
|
||||
- Стенды могут и должны отличаться
|
||||
- YAML генерируется не в реальном времени, а батчами из терраформа
|
||||
- Некоторые сервисы есть в реальном API но НЕ в терраформе (их не тестируем)
|
||||
|
||||
Что должно считаться PASS, а что FAIL? Какие допуски?
|
||||
|
||||
### 2. Какие ещё эндпоинты сравнивать?
|
||||
|
||||
Сейчас сравниваются 3 read-only эндпоинта. Какие ещё можно безопасно
|
||||
сравнять? Что ещё есть в реальном API такого что полигон должен
|
||||
повторять один-в-один?
|
||||
|
||||
### 3. Периодичность и автоматизация
|
||||
|
||||
Как часто запускать сравнение? При каких событиях (изменение терраформа,
|
||||
деплой полигона)? Должно ли это быть частью CI?
|
||||
|
||||
### 4. dataType: None
|
||||
|
||||
В `from_stands.py` для некоторых параметров dataType падает в None
|
||||
(хотя дефолт "string"). Где конкретно искать причину?
|
||||
|
||||
### 5. Общие советы
|
||||
|
||||
Что ещё мы упускаем в тестировании полигона? Какие сценарии, краевые
|
||||
случаи, проверки контрактов?
|
||||
|
||||
---
|
||||
|
||||
## Формат диалога
|
||||
|
||||
Ты можешь:
|
||||
- Сразу дать развёрнутый ответ по всем пунктам
|
||||
- Или задать уточняющие вопросы — и тогда я отвечу, а ты продолжишь
|
||||
|
||||
Я хочу чтобы в итоге получился **конкретный план действий**:
|
||||
что тестировать, как часто, что считать ошибкой, что — допустимым
|
||||
расхождением.
|
||||
|
||||
---
|
||||
|
||||
## Ответ Соннета (2026-08-02)
|
||||
|
||||
### 4. dataType: None — первопричина найдена
|
||||
|
||||
`dict.get(key, default)` возвращает `default` **только если ключ отсутствует**.
|
||||
Если в YAML написано `data_type: null` — ключ *есть*, значение — `None`,
|
||||
дефолт `"string"` не срабатывает.
|
||||
|
||||
### Мои ответы
|
||||
|
||||
**Q4.1 — data_type: null в YAML?** Проверил — в терраформ-YAML нет
|
||||
`data_type: null`. Реальный API возвращает `dataType: null` для параметра
|
||||
`nestedRefExample` (param 396). Полигон возвращает `"string"` — он ПРАВИЛЬНО
|
||||
применяет дефолт там, где реальный API отдаёт null. Это не баг полигона,
|
||||
а улучшение.
|
||||
|
||||
**Q4.2 — _convert_sub_params?** Та же уязвимость потенциально есть, но не
|
||||
проявляется — sub_params всегда имеют data_type.
|
||||
|
||||
**Q1.1 — частота регенерации YAML?** ВРУЧНУЮ. `from_stands.py` запускается
|
||||
человеком когда он вспомнит. Никакого cron/webhook.
|
||||
|
||||
**Q1.2 — лаг от реального API до YAML?** Непредсказуемо. От часов до недель.
|
||||
Зависит от того когда кто-то запустит `from_stands.py`.
|
||||
|
||||
**Q1.3 — потребитель результатов?** Разработчик. Ему нужно знать «полигон
|
||||
устарел, перегенери YAML», а не «полигон сломан».
|
||||
|
||||
**Q2.1 — дополнительные эндпоинты в реальном API?** Не проверял. Надо
|
||||
сравнить полный список эндпоинтов.
|
||||
|
||||
**Q2.2 — lifecycle поля?** Не сравниваются в текущем compare_test.py. Надо
|
||||
добавить.
|
||||
|
||||
---
|
||||
|
||||
## Ответ Соннета — раунд 2
|
||||
|
||||
### Три категории расхождений — 👍 принимаю
|
||||
|
||||
| Категория | Значение | Реакция |
|
||||
|---|---|---|
|
||||
| 🔴 REAL BUG | полигон ≠ реальный API, полигон неправ | FAIL |
|
||||
| 🟡 LAG | новый параметр в реальном API, нет в полигоне | WARN |
|
||||
| 🟢 POLYGON BETTER | реальный API отдаёт null/entities, полигон — правильно | INFO |
|
||||
|
||||
Для prod 🟡 LAG тоже должен быть заметен.
|
||||
|
||||
### Мои ответы — раунд 2
|
||||
|
||||
**Q5.1 — сервис есть в полигоне, пропал из реального API?**
|
||||
Теоретически да — если сервис удалили из реального API, а terraform ещё
|
||||
не обновили. Это 🔴 REAL BUG и должно быть FAIL. Полигон не должен
|
||||
эмулировать несуществующие сервисы.
|
||||
|
||||
**Q5.2 — HTML-entities?**
|
||||
Нормализовать при сравнении: `html.unescape()` для real API перед сравнением.
|
||||
Считать 🟢 POLYGON BETTER, не ошибка.
|
||||
|
||||
**Q5.3 — lifecycle поля?**
|
||||
Проверил — ни реальный API, ни полигон НЕ возвращают `lifecycle` в
|
||||
`GET /services/{id}`. Сравнивать нечего, вопрос снят.
|
||||
|
||||
---
|
||||
|
||||
## Ответ Соннета — раунд 3 (финальный)
|
||||
|
||||
### Q6.1 — defaultValue, valueList и др.
|
||||
|
||||
Реальный API возвращает **29 полей** на каждый cfsParam: `defaultValue`,
|
||||
`valueList`, `isModifiable`, `isRequired`, `isHidden`, `descr`, `man`,
|
||||
`regex`, `maxlength`, `minvalue` и т.д. Полигон возвращает подмножество
|
||||
из ~6-8 полей.
|
||||
|
||||
Сравнивать нужно только те поля, которые `from_stands.py` реально генерирует:
|
||||
`defaultValue`, `valueList`, `isModifiable`, `isRequired`. Остальные либо
|
||||
отсутствуют в терраформ-YAML, либо не имеют смысла для мока.
|
||||
|
||||
### Q6.2 — cfsParamsByOp
|
||||
|
||||
Это **внутренний индекс** полигона, не API-эндпоинт. Связь «какие параметры
|
||||
к какой операции» уже проверяется через `GET /instanceOperations/default/{id}`
|
||||
— если в ответе правильный набор параметров, значит cfsParamsByOp правильный.
|
||||
Отдельно сравнивать не нужно.
|
||||
|
||||
### Q6.3 — формат вывода
|
||||
|
||||
stdout + exit code — достаточно. Разработчик запускает вручную, смотрит
|
||||
глазами. Файл отчёта переусложнит. Если понадобится история — можно потом.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Соннет: полный аудит Polygon v0.5.5 + Swagger + тесты
|
||||
|
||||
> Адресат: Claude Sonnet 4.6 (новый чат)
|
||||
> ⛔ Только анализ и советы в чат. Не редактировать файлы.
|
||||
|
||||
---
|
||||
|
||||
## Что такое Polygon
|
||||
|
||||
Эмулятор REST API облачной платформы Nubes для интеграционных тестов.
|
||||
Задеплоен на `polygon.pythonk8s.dev.nubes.ru`. Flask 3.0 + gunicorn, 1 воркер.
|
||||
|
||||
**3 изолированных стенда:** dev (37 сервисов), test (37), prod (35).
|
||||
Состояние в памяти, URL: `/dev/api/v1/svc/...`, `/test/...`, `/prod/...`.
|
||||
В Swagger — выпадайка выбора стенда.
|
||||
|
||||
YAML-конфиги сервисов генерируются из терраформ-репы
|
||||
(`~/tf_provider/generated/{dev,test,prod}/resources_yaml/`) через `from_stands.py`.
|
||||
|
||||
**Файлы для анализа:**
|
||||
- `polygon/site/routes/openapi.py` — OpenAPI 3.1.0 спека (~500 строк)
|
||||
- `polygon/site/templates/swagger.html` — Swagger UI 5
|
||||
- `polygon/site/templates/index.html` — главная страница
|
||||
- `polygon/site/static/style.css` — дизайн-система
|
||||
- `polygon/site/routes/` — все роуты (7 blueprint'ов)
|
||||
- `polygon/tests/test_api.py` — 35 smoke-тестов
|
||||
- `polygon/tests/fuzz_test.py` — 147 фаззинг-тестов
|
||||
- `polygon/tests/compare_test.py` — сравнение с реальным API
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
- 17 эндпоинтов, полный CRUD инстансов и операций
|
||||
- Аутентификация `X-Mock-Auth` для `_mock/*`
|
||||
- Валидация serviceId (int > 0, не массив, не null)
|
||||
- 35 smoke + 147 fuzz тестов — 0 реальных багов
|
||||
- Сравнение с реальным API: YAML ↔ API — 0 расхождений
|
||||
|
||||
## Что нужно от тебя
|
||||
|
||||
### 1. Swagger/OpenAPI
|
||||
|
||||
Открой `polygon/site/routes/openapi.py` и `polygon/site/templates/swagger.html`.
|
||||
Проанализируй:
|
||||
|
||||
- Полнота схем — все ли поля ответов описаны?
|
||||
- Правильные ли status codes (200/201/400/404/409)?
|
||||
- Удобство Try it out — example'ы, enum'ы, default'ы
|
||||
- Группировка тегов — логично ли?
|
||||
- Авторизация в Swagger UI — правильно ли работает?
|
||||
- Нет ли лишнего или недостающего?
|
||||
- Русские описания — понятны ли, не слишком ли длинные?
|
||||
|
||||
### 2. Сравнительное тестирование
|
||||
|
||||
У нас есть 3 read-only эндпоинта для сравнения:
|
||||
- `GET /services`
|
||||
- `GET /services/{id}`
|
||||
- `GET /instanceOperations/default/{id}`
|
||||
|
||||
Какие ещё эндпоинты можно безопасно сравнивать с реальным API?
|
||||
Что ещё можно проверить не делая мутирующих запросов?
|
||||
|
||||
### 3. Дополнительные тесты
|
||||
|
||||
Что мы упустили? Какие сценарии, краевые случаи, негативные тесты
|
||||
стоит добавить? В том числе:
|
||||
- Тесты через Swagger UI (браузерные)
|
||||
- Нагрузочные/параллельные
|
||||
- Специфичные для отдельных сервисов
|
||||
- Тесты на совместимость с app-autotest
|
||||
|
||||
### 4. Замечания по коду/архитектуре
|
||||
|
||||
Что можно улучшить не переписывая всё? Любые баги, уязвимости,
|
||||
потенциальные проблемы которые ты видишь.
|
||||
|
||||
---
|
||||
|
||||
Формат: свободный. Главное — **конкретные советы** с указанием что и где
|
||||
менять, а не общие рассуждения.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Результаты тестирования Polygon v0.4.3
|
||||
|
||||
Дата: 2026-08-01
|
||||
|
||||
---
|
||||
|
||||
## Python API-тесты: 35/35 PASS ✅
|
||||
|
||||
Файл: `polygon/tests/test_api.py`
|
||||
Цель: `https://polygon.pythonk8s.dev.nubes.ru`
|
||||
|
||||
### 1. Health (1/1)
|
||||
- ✅ `GET /health → 200 OK`
|
||||
|
||||
### 2. Services (4/4)
|
||||
- ✅ `GET /services → 200, >=37 сервисов`
|
||||
- ✅ `GET /services → каждый имеет svcId, svc, svcShort`
|
||||
- ✅ `GET /services/1 → 200 + operations`
|
||||
- ✅ `GET /services/99999 → 404`
|
||||
|
||||
### 3. Instances (10/10)
|
||||
- ✅ `GET /instances (после reset) → total=0`
|
||||
- ✅ `POST /instances (без serviceId) → 400`
|
||||
- ✅ `POST /instances (serviceId=99999) → 404`
|
||||
- ✅ `POST /instances → 201 + Location + instanceUid`
|
||||
- ✅ `GET /instances → total=1`
|
||||
- ✅ `GET /instances?pageSize=500 → pageSize=200 (clamped)`
|
||||
- ✅ `GET /instances?page=-1 → не падает`
|
||||
- ✅ `GET /instances/{uid} → status=creating, имя верное`
|
||||
- ✅ `GET /instances/{uid}?fields=... → 200`
|
||||
- ✅ `GET /instances/nonexistent → 404`
|
||||
|
||||
### 4. Operations (8/8)
|
||||
- ✅ `GET /instanceOperations/default/18 → 200 + cfsParams`
|
||||
- ✅ `POST /instanceOperations → 201 + Location`
|
||||
- ✅ `GET /op/{uid} → dtStart=null, dtFinish=null, isSuccessful=null`
|
||||
- ✅ `POST /instanceOperationCfsParams → 200`
|
||||
- ✅ `GET /validate-cfs → 200`
|
||||
- ✅ `POST /run → ok=true`
|
||||
- ✅ `POST /run (повторно) → 409`
|
||||
- ✅ `GET /op/{uid} (после run) → dtFinish!=null, isSuccessful=true`
|
||||
|
||||
### 5. Fail-next (4/4)
|
||||
- ✅ `POST /_mock/fail-next → 200, fail_next=true`
|
||||
- ✅ `POST /instances (fail-next) → 201`
|
||||
- ✅ `POST /instanceOperations (fail-next) → 201`
|
||||
- ✅ `POST /run (fail-next) → ok=false, error='mock failure'`
|
||||
|
||||
### 6. Auth (3/3 + 2 skipped)
|
||||
- ⚠️ Auth отключена на проде (MOCK_AUTH_TOKEN не задан)
|
||||
- ✅ `POST /_mock/reset (верный токен) → 200`
|
||||
- ✅ `GET /services (без токена) → 200`
|
||||
- ✅ `POST /instances (без токена) → 201`
|
||||
|
||||
### 7. Mock state (5/5)
|
||||
- ✅ `GET /_mock/state → instances + operations`
|
||||
- ✅ `GET /_mock/services → count >= 37`
|
||||
- ✅ `POST /_mock/delay/0.1 → delay=0.1`
|
||||
- ✅ `POST /_mock/delay/100 → 400`
|
||||
- ✅ `POST /_mock/delay/-1 → 400`
|
||||
|
||||
---
|
||||
|
||||
## Swagger UI (browser)
|
||||
|
||||
| Проверка | Результат |
|
||||
|----------|-----------|
|
||||
| Страница загружается | ✅ |
|
||||
| Логотип Nubes + версия | ✅ |
|
||||
| Ссылка «← На главную» | ✅ |
|
||||
| Все 6 тегов | ✅ services, instances, operations, mock, health |
|
||||
| Все 17 эндпоинтов | ✅ |
|
||||
| Все 20 схем | ✅ |
|
||||
| Спека — валидный JSON | ✅ |
|
||||
| `security: []` (глобальный) | ✅ |
|
||||
| `mockAuth` (apiKey, X-Mock-Auth) | ✅ |
|
||||
| `_mock/*` имеют `security: [mockAuth]` | ✅ 5/5 |
|
||||
| `dtStart/dtFinish` → `["string","null"]` | ✅ OAS 3.1.0 |
|
||||
| `isSuccessful` → `["boolean","null"]` | ✅ |
|
||||
| `RunResponse` → `ok + error` | ✅ |
|
||||
| Кнопка Authorize | ⚠️ недоступна в browser-окружении |
|
||||
|
||||
---
|
||||
|
||||
## Найденные расхождения (prod vs код)
|
||||
|
||||
| Проблема | Статус |
|
||||
|----------|--------|
|
||||
| Описание всё ещё говорит «Bearer-токен» | 🔧 Исправлено в коде, не задеплоено |
|
||||
| Auth (MOCK_AUTH_TOKEN) не включена | ⚙️ Конфигурация деплоя |
|
||||
| `pageSize` обрезается молча | ✅ Задокументировано в спеке |
|
||||
|
||||
---
|
||||
|
||||
## Итого
|
||||
|
||||
- **API-тесты**: 35/35 PASS
|
||||
- **Swagger UI**: страница работает, спека валидна, все эндпоинты и схемы на месте
|
||||
- **Баги v0.4.2**: все 5 исправлены, проверены локально
|
||||
- **К деплою**: закоммитить исправление описания auth, повысить версию, redeploy
|
||||
Reference in New Issue
Block a user