From 9b8d90218cc3920a05fd8a3e36f98d6cc3534cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 31 Jul 2026 23:00:06 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=5Fbuild=5Fdefault=5Fjson=E2=86=92json.d?= =?UTF-8?q?umps=20+=20409=20repeat=20run=20+=20MOCK=5FAUTH=5FTOKEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/config/loader.py | 2 +- site/from_stands.py | 28 +++++++++++++++------------- site/routes/mock_routes.py | 20 +++++++++++++++++++- site/routes/run.py | 10 ++++++---- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/site/config/loader.py b/site/config/loader.py index 8d5fe94..bfc1b68 100644 --- a/site/config/loader.py +++ b/site/config/loader.py @@ -71,4 +71,4 @@ DELAY = float(os.getenv("MOCK_OP_DELAY", "0.1")) # Версия полигона — единый источник правды для app.py и routes/root.py. # Меняется при КАЖДОМ изменении кода. -VERSION = "0.2.4" +VERSION = "0.2.5" diff --git a/site/from_stands.py b/site/from_stands.py index 76d3fd1..1761e81 100644 --- a/site/from_stands.py +++ b/site/from_stands.py @@ -243,8 +243,12 @@ def convert_service_from_dict(raw): def _build_default_json(sub_params): - """Строит JSON-строку из defaults sub_params.""" - parts = [] + """Строит JSON-строку из defaults sub_params через json.dumps. + + Раньше использовался ручной string-concat с replace('"', '\\"') — + не экранировал \n, \r, \t, \\, что ломало JSON при спецсимволах в defaults. + Теперь строим dict и отдаём json.dumps() — все спецсимволы экранированы.""" + obj = {} for sp in sub_params: code = sp["code"] default = _ue(str(sp.get("default", ""))) @@ -252,23 +256,21 @@ def _build_default_json(sub_params): subsub = sp.get("sub_params") if subsub: - val = _build_default_json(subsub) + # Рекурсия: вложенные sub_params → вложенный объект + import json + obj[code] = json.loads(_build_default_json(subsub)) 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: try: - int(default) - val = default + obj[code] = int(default) except (ValueError, TypeError): - val = "0" + obj[code] = 0 else: - # string — quoted - escaped = default.replace('"', '\\"') - val = f'"{escaped}"' + obj[code] = default - parts.append(f'"{code}": {val}') - - return "{" + ", ".join(parts) + "}" + import json + return json.dumps(obj, ensure_ascii=False) # --------------------------------------------------------------------------- diff --git a/site/routes/mock_routes.py b/site/routes/mock_routes.py index acf9465..fb0e8f5 100644 --- a/site/routes/mock_routes.py +++ b/site/routes/mock_routes.py @@ -8,13 +8,31 @@ Blueprint "mock" с префиксом /api/v1/svc/_mock. POST /delay/ — изменить MOCK_OP_DELAY на лету (не перезапуская сервер) """ -from flask import Blueprint, jsonify +import os + +from flask import Blueprint, jsonify, request, abort import mock_state import config.loader as _cfg # для чтения/записи модульных переменных DELAY, SERVICES 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"]) def mock_reset(): diff --git a/site/routes/run.py b/site/routes/run.py index 5aaf142..efcf7fd 100644 --- a/site/routes/run.py +++ b/site/routes/run.py @@ -34,16 +34,18 @@ def run_operation(uid): После этого GET /instanceOperations/{uid} вернёт dtFinish, и poll_until_done() в app-autotest завершит поллинг. - ⛔ Нет защиты от повторного run — если вызвать дважды, dtStart - перезапишется и эффект применится повторно. Для тестовых целей - (MOCK_OP_DELAY ≤ 0.5с) вероятность минимальна. - Возвращает {"ok": True} — формат как в реальном Nubes API. + Повторный вызов → 409 (operation already completed). """ op = mock_state.state.get_operation(uid) if not op: 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()