fix: _build_default_json→json.dumps + 409 repeat run + MOCK_AUTH_TOKEN

This commit is contained in:
2026-07-31 23:00:06 +04:00
parent 1fa2d9a7bc
commit 9b8d90218c
4 changed files with 41 additions and 19 deletions
+1 -1
View File
@@ -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
View File
@@ -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) + "}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+19 -1
View File
@@ -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
View File
@@ -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()