Files
Juan Denis 55d8fb1e44 Read route bodies through json_object()
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.

Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
2026-10-10 20:53:43 -04:00

75 lines
2.4 KiB
Python

"""REST surface for the Test Sandbox (distro test matrix in Docker).
Mounted at /api/v1/test-sandbox (registered in app/__init__.py).
"""
from flask import Blueprint, jsonify, request
from ..middleware.rbac import admin_required, viewer_required
from ..services.test_sandbox_service import TestSandboxService
from app.api._request import json_object
test_sandbox_bp = Blueprint('test_sandbox', __name__)
@test_sandbox_bp.route('/distros', methods=['GET'])
@viewer_required
def list_distros():
"""Distro registry + which modes each supports, plus host Docker status."""
return jsonify({
'distros': TestSandboxService.list_distros(),
'docker_available': TestSandboxService.docker_available(),
})
@test_sandbox_bp.route('/runs', methods=['GET'])
@viewer_required
def list_runs():
limit = min(int(request.args.get('limit', 20)), 100)
return jsonify({'runs': TestSandboxService.list_runs(limit=limit)})
@test_sandbox_bp.route('/runs', methods=['POST'])
@admin_required
def start_run():
data = json_object(optional=True)
distros = data.get('distros') or []
mode = data.get('mode', 'quick')
try:
from flask_jwt_extended import get_jwt_identity
user_id = get_jwt_identity()
except Exception: # noqa: BLE001 — API-key callers have no JWT identity
user_id = None
try:
run = TestSandboxService.start_run(distros, mode, user_id=user_id)
except ValueError as exc:
return jsonify({'error': str(exc)}), 400
except RuntimeError as exc:
return jsonify({'error': str(exc)}), 409
return jsonify({'run': run.to_dict()}), 202
@test_sandbox_bp.route('/runs/<int:run_id>', methods=['GET'])
@viewer_required
def get_run(run_id):
run = TestSandboxService.get_run(run_id)
if not run:
return jsonify({'error': 'run not found'}), 404
return jsonify({'run': run.to_dict()})
@test_sandbox_bp.route('/runs/<int:run_id>/cancel', methods=['POST'])
@admin_required
def cancel_run(run_id):
if not TestSandboxService.cancel_run(run_id):
return jsonify({'error': 'run not found or not running'}), 404
return jsonify({'ok': True})
@test_sandbox_bp.route('/runs/<int:run_id>/logs/<distro>', methods=['GET'])
@viewer_required
def get_log(run_id, distro):
log = TestSandboxService.get_log(run_id, distro)
if log is None:
return jsonify({'error': 'run or distro not found'}), 404
return log, 200, {'Content-Type': 'text/plain; charset=utf-8'}