fix: _build_default_json→json.dumps + 409 repeat run + MOCK_AUTH_TOKEN
This commit is contained in:
@@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1"))
|
|||||||
|
|
||||||
# Версия полигона — единый источник правды для app.py и routes/root.py.
|
# Версия полигона — единый источник правды для app.py и routes/root.py.
|
||||||
# Меняется при КАЖДОМ изменении кода.
|
# Меняется при КАЖДОМ изменении кода.
|
||||||
VERSION = "0.2.4"
|
VERSION = "0.2.5"
|
||||||
|
|||||||
+15
-13
@@ -243,8 +243,12 @@ def convert_service_from_dict(raw):
|
|||||||
|
|
||||||
|
|
||||||
def _build_default_json(sub_params):
|
def _build_default_json(sub_params):
|
||||||
"""Строит JSON-строку из defaults sub_params."""
|
"""Строит JSON-строку из defaults sub_params через json.dumps.
|
||||||
parts = []
|
|
||||||
|
Раньше использовался ручной string-concat с replace('"', '\\"') —
|
||||||
|
не экранировал \n, \r, \t, \\, что ломало JSON при спецсимволах в defaults.
|
||||||
|
Теперь строим dict и отдаём json.dumps() — все спецсимволы экранированы."""
|
||||||
|
obj = {}
|
||||||
for sp in sub_params:
|
for sp in sub_params:
|
||||||
code = sp["code"]
|
code = sp["code"]
|
||||||
default = _ue(str(sp.get("default", "")))
|
default = _ue(str(sp.get("default", "")))
|
||||||
@@ -252,23 +256,21 @@ def _build_default_json(sub_params):
|
|||||||
|
|
||||||
subsub = sp.get("sub_params")
|
subsub = sp.get("sub_params")
|
||||||
if subsub:
|
if subsub:
|
||||||
val = _build_default_json(subsub)
|
# Рекурсия: вложенные sub_params → вложенный объект
|
||||||
|
import json
|
||||||
|
obj[code] = json.loads(_build_default_json(subsub))
|
||||||
elif "bool" in dt:
|
elif "bool" in dt:
|
||||||
val = default.lower() if default.lower() in ("true", "false") else "false"
|
obj[code] = default.lower() == "true"
|
||||||
elif "int" in dt or "integer" in dt:
|
elif "int" in dt or "integer" in dt:
|
||||||
try:
|
try:
|
||||||
int(default)
|
obj[code] = int(default)
|
||||||
val = default
|
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
val = "0"
|
obj[code] = 0
|
||||||
else:
|
else:
|
||||||
# string — quoted
|
obj[code] = default
|
||||||
escaped = default.replace('"', '\\"')
|
|
||||||
val = f'"{escaped}"'
|
|
||||||
|
|
||||||
parts.append(f'"{code}": {val}')
|
import json
|
||||||
|
return json.dumps(obj, ensure_ascii=False)
|
||||||
return "{" + ", ".join(parts) + "}"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -8,13 +8,31 @@ Blueprint "mock" с префиксом /api/v1/svc/_mock.
|
|||||||
POST /delay/<s> — изменить MOCK_OP_DELAY на лету (не перезапуская сервер)
|
POST /delay/<s> — изменить MOCK_OP_DELAY на лету (не перезапуская сервер)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Blueprint, jsonify
|
import os
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request, abort
|
||||||
|
|
||||||
import mock_state
|
import mock_state
|
||||||
import config.loader as _cfg # для чтения/записи модульных переменных DELAY, SERVICES
|
import config.loader as _cfg # для чтения/записи модульных переменных DELAY, SERVICES
|
||||||
|
|
||||||
bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock")
|
bp = Blueprint("mock", __name__, url_prefix="/api/v1/svc/_mock")
|
||||||
|
|
||||||
|
# Токен для защиты _mock/* эндпоинтов (из env, опционально).
|
||||||
|
# Если не задан — эндпоинты открыты (dev/локально).
|
||||||
|
# Если задан — требуется заголовок X-Mock-Auth: <токен>.
|
||||||
|
_MOCK_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_request
|
||||||
|
def _check_mock_auth():
|
||||||
|
"""Проверка X-Mock-Auth заголовка если MOCK_AUTH_TOKEN задан.
|
||||||
|
|
||||||
|
Вызывается Flask перед КАЖДЫМ обработчиком в этом blueprint'е.
|
||||||
|
Если токен не задан в env — пропускаем всех (dev-режим).
|
||||||
|
Если задан — требуем совпадения с заголовком."""
|
||||||
|
if _MOCK_TOKEN and request.headers.get("X-Mock-Auth") != _MOCK_TOKEN:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/reset", methods=["POST"])
|
@bp.route("/reset", methods=["POST"])
|
||||||
def mock_reset():
|
def mock_reset():
|
||||||
|
|||||||
+6
-4
@@ -34,16 +34,18 @@ def run_operation(uid):
|
|||||||
После этого GET /instanceOperations/{uid} вернёт dtFinish,
|
После этого GET /instanceOperations/{uid} вернёт dtFinish,
|
||||||
и poll_until_done() в app-autotest завершит поллинг.
|
и poll_until_done() в app-autotest завершит поллинг.
|
||||||
|
|
||||||
⛔ Нет защиты от повторного run — если вызвать дважды, dtStart
|
|
||||||
перезапишется и эффект применится повторно. Для тестовых целей
|
|
||||||
(MOCK_OP_DELAY ≤ 0.5с) вероятность минимальна.
|
|
||||||
|
|
||||||
Возвращает {"ok": True} — формат как в реальном Nubes API.
|
Возвращает {"ok": True} — формат как в реальном Nubes API.
|
||||||
|
Повторный вызов → 409 (operation already completed).
|
||||||
"""
|
"""
|
||||||
op = mock_state.state.get_operation(uid)
|
op = mock_state.state.get_operation(uid)
|
||||||
if not op:
|
if not op:
|
||||||
return jsonify({"error": "operation not found"}), 404
|
return jsonify({"error": "operation not found"}), 404
|
||||||
|
|
||||||
|
# Защита от повторного запуска: если dtFinish уже проставлен —
|
||||||
|
# операция уже выполнена, повторный run может испортить данные.
|
||||||
|
if op.get("dtFinish") is not None:
|
||||||
|
return jsonify({"error": "operation already completed"}), 409
|
||||||
|
|
||||||
# Фиксируем время старта
|
# Фиксируем время старта
|
||||||
op["dtStart"] = _now()
|
op["dtStart"] = _now()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user