""" routes/mock_routes.py — служебные эндпоинты /_mock/*. Blueprint "mock" с префиксом /api/v1/svc/_mock. POST /reset — полный сброс состояния (для тестов) GET /state — отладочный дамп instances + operations GET /services — отладочный список загруженных сервисов POST /delay/ — изменить MOCK_OP_DELAY на лету (не перезапуская сервер) """ 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, опционально). # Дублирует глобальную проверку в app.py, но оставлен для обратной совместимости # и ясности — _mock/* исторически имели отдельную защиту. _MOCK_TOKEN = os.getenv("MOCK_AUTH_TOKEN", "") @bp.before_request def _check_mock_auth(): """Проверка X-Mock-Auth заголовка если MOCK_AUTH_TOKEN задан. Вызывается Flask перед КАЖДЫМ обработчиком в этом blueprint'е. Если токен не задан в env — пропускаем всех (dev-режим). Если задан — требуем совпадения с заголовком. Примечание: app.before_request уже проверяет Authorization: Bearer; этот хук — дополнительная защита для _mock/* эндпоинтов.""" if _MOCK_TOKEN and request.headers.get("X-Mock-Auth") != _MOCK_TOKEN: abort(403) @bp.route("/reset", methods=["POST"]) def mock_reset(): """POST /api/v1/svc/_mock/reset — полный сброс состояния. Удаляет ВСЕ инстансы, операции и параметры из MockState. Используется в тестах (autouse fixture) для изоляции тестов друг от друга. """ mock_state.state.reset() return jsonify({"reset": "ok"}) @bp.route("/state", methods=["GET"]) def mock_state_view(): """GET /api/v1/svc/_mock/state — отладочный дамп состояния. Возвращает {instances: {uid: {instanceUid, displayName, status, serviceId}}, operations: {uid: {instanceOperationUid, instanceUid, operation, dtFinish, isSuccessful}}}. Полезно для отладки тестов: «какие инстансы сейчас живы?», «завершилась ли операция?». НЕ для production-использования. """ st = mock_state.state return jsonify({ "instances": { uid: { "instanceUid": inst["instanceUid"], "displayName": inst["displayName"], "status": inst["status"], "serviceId": inst["serviceId"], } for uid, inst in st.instances.items() }, "operations": { uid: { "instanceOperationUid": op["instanceOperationUid"], "instanceUid": op["instanceUid"], "operation": op["operation"], "dtFinish": op["dtFinish"], "isSuccessful": op["isSuccessful"], } for uid, op in st.operations.items() }, }) @bp.route("/services", methods=["GET"]) def mock_services(): """GET /api/v1/svc/_mock/services — отладочный список сервисов. Возвращает {count: N, services: {svcId: {name, displayName, operations, params}}}. Полезно чтобы быстро проверить что все 37+ сервисов загрузились, не дёргая реальный /api/v1/svc/services. """ result = {} for svc_id, svc_def in _cfg.SERVICES.items(): result[str(svc_id)] = { "name": svc_def.get("name", ""), "displayName": svc_def.get("service_display_name", ""), "operations": len(svc_def.get("operations", [])), "params": len(svc_def.get("cfsParams", [])), } return jsonify({"count": len(result), "services": result}) # Максимальная задержка — защита от блокировки /health (1 воркер). # При DELAY > healthcheck timeout платформа рестартит под. _MAX_DELAY = 5.0 @bp.route("/delay/", methods=["POST"]) def mock_set_delay(seconds): """POST /api/v1/svc/_mock/delay/{s} — изменить задержку операции. Меняет config.loader.DELAY «на лету», без перезапуска сервера. Полезно в тестах: установить delay=0 для мгновенных операций, или delay=5 чтобы проверить таймауты поллинга. ⛔ Максимум {max_delay}с — защита от блокировки /health (1 воркер). ⛔ Минимум 0с — отрицательные значения запрещены. Принимает строку и конвертирует в float — Flask не матчит целые числа без десятичной точки (0, 2). """.format(max_delay=_MAX_DELAY) try: val = float(seconds) except (ValueError, TypeError): return jsonify({"error": f"invalid delay: {seconds}"}), 400 if val < 0: return jsonify({"error": "delay must be >= 0"}), 400 if val > _MAX_DELAY: return jsonify({"error": f"delay must be <= {_MAX_DELAY}s (Nubes healthcheck protection)"}), 400 _cfg.DELAY = val return jsonify({"delay": _cfg.DELAY}) @bp.route("/fail-next", methods=["POST"]) def mock_fail_next(): """POST /api/v1/svc/_mock/fail-next — следующая операция упадёт. Устанавливает флаг в MockState. При следующем POST /run: - isSuccessful = False - errorLog = "mock failure (requested by /_mock/fail-next)" - эффект НЕ применяется (состояние инстанса не меняется) После использования флаг сбрасывается (one-shot). Позволяет тестировать негативные сценарии. """ mock_state.state.fail_next = True return jsonify({"fail_next": True})