Files
svg-backend/backend/scripts/smoke_common.sh
greebo 5aa35b1d04 feat(backend): enforce admin-only ops endpoints and cover destructive cleanup smoke
restrict ops endpoints to admin-only access

block operator and viewer keys from admin maintenance routes
cover destructive pricing cleanup in smoke execution, not only preview

extend orchestration without regressing existing smoke stages
2026-03-20 16:02:38 +03:00

452 lines
11 KiB
Bash

#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
API_URL="${API_URL:-http://127.0.0.1:9020}"
API_KEY="${API_KEY:-admin-local-dev-key}"
FIXTURE_SVG_PATH="${FIXTURE_SVG_PATH:-${REPO_ROOT}/sample-contract.svg}"
HEALTH_MAX_ATTEMPTS="${HEALTH_MAX_ATTEMPTS:-20}"
HEALTH_RETRY_DELAY_SECONDS="${HEALTH_RETRY_DELAY_SECONDS:-1}"
log() {
echo
echo "===== $* ====="
}
fail() {
echo
echo "[FAIL] $*" >&2
exit 1
}
require_fixture_svg() {
if [[ ! -f "${FIXTURE_SVG_PATH}" ]]; then
fail "Fixture SVG not found: ${FIXTURE_SVG_PATH}"
fi
}
wait_for_health() {
log "health"
echo "waiting for API to be ready..."
local health_ready="false"
local health_status=""
for ((i = 1; i <= HEALTH_MAX_ATTEMPTS; i++)); do
health_status="$(curl -sS -o /dev/null -w "%{http_code}" "${API_URL}/healthz" || true)"
if [[ "${health_status}" == "200" ]]; then
health_ready="true"
echo "API is ready"
break
fi
echo "waiting... (${i}/${HEALTH_MAX_ATTEMPTS}) healthz=${health_status}"
sleep "${HEALTH_RETRY_DELAY_SECONDS}"
done
if [[ "${health_ready}" != "true" ]]; then
fail "API did not become ready on ${API_URL}/healthz after ${HEALTH_MAX_ATTEMPTS} attempts"
fi
curl -sS -i "${API_URL}/healthz"
}
request() {
local name="$1"
local method="$2"
local url="$3"
local expected_status="$4"
local body="${5:-}"
local out_file="${TMP_DIR}/${name}.body"
local status_file="${TMP_DIR}/${name}.status"
echo
echo "===== ${name} ====="
if [[ -n "${body}" ]]; then
curl -sS \
-X "${method}" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-o "${out_file}" \
-w "%{http_code}" \
"${url}" \
--data "${body}" > "${status_file}"
else
curl -sS \
-X "${method}" \
-H "X-API-Key: ${API_KEY}" \
-o "${out_file}" \
-w "%{http_code}" \
"${url}" > "${status_file}"
fi
local actual_status
actual_status="$(python3 - "$status_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8").strip())
PY
)"
echo "[${method}] ${url} -> ${actual_status}"
python3 - "$out_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8"))
PY
echo
if [[ "${actual_status}" != "${expected_status}" ]]; then
fail "Unexpected HTTP status for ${name}: expected ${expected_status}, got ${actual_status}"
fi
}
request_with_api_key() {
local api_key="$1"
local name="$2"
local method="$3"
local url="$4"
local expected_status="$5"
local body="${6:-}"
local out_file="${TMP_DIR}/${name}.body"
local status_file="${TMP_DIR}/${name}.status"
echo
echo "===== ${name} ====="
if [[ -n "${body}" ]]; then
curl -sS \
-X "${method}" \
-H "X-API-Key: ${api_key}" \
-H "Content-Type: application/json" \
-o "${out_file}" \
-w "%{http_code}" \
"${url}" \
--data "${body}" > "${status_file}"
else
curl -sS \
-X "${method}" \
-H "X-API-Key: ${api_key}" \
-o "${out_file}" \
-w "%{http_code}" \
"${url}" > "${status_file}"
fi
local actual_status
actual_status="$(python3 - "$status_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8").strip())
PY
)"
echo "[${method}] ${url} -> ${actual_status}"
python3 - "$out_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8"))
PY
echo
if [[ "${actual_status}" != "${expected_status}" ]]; then
fail "Unexpected HTTP status for ${name}: expected ${expected_status}, got ${actual_status}"
fi
}
upload_svg() {
local name="$1"
local upload_filename="$2"
local out_file="${TMP_DIR}/${name}.body"
local status_file="${TMP_DIR}/${name}.status"
require_fixture_svg
echo
echo "===== ${name} ====="
curl -sS \
-X POST \
-H "X-API-Key: ${API_KEY}" \
-o "${out_file}" \
-w "%{http_code}" \
-F "file=@${FIXTURE_SVG_PATH};filename=${upload_filename};type=image/svg+xml" \
"${API_URL}/api/v1/schemes/upload" > "${status_file}"
local actual_status
actual_status="$(python3 - "$status_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8").strip())
PY
)"
echo "[POST] ${API_URL}/api/v1/schemes/upload -> ${actual_status}"
python3 - "$out_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8"))
PY
echo
if [[ "${actual_status}" != "200" ]]; then
fail "Upload failed for ${upload_filename}: expected 200, got ${actual_status}"
fi
}
upload_file_expect_status() {
local name="$1"
local file_path="$2"
local upload_filename="$3"
local content_type="$4"
local expected_status="$5"
local out_file="${TMP_DIR}/${name}.body"
local status_file="${TMP_DIR}/${name}.status"
if [[ ! -f "${file_path}" ]]; then
fail "Upload fixture file not found: ${file_path}"
fi
echo
echo "===== ${name} ====="
curl -sS \
-X POST \
-H "X-API-Key: ${API_KEY}" \
-o "${out_file}" \
-w "%{http_code}" \
-F "file=@${file_path};filename=${upload_filename};type=${content_type}" \
"${API_URL}/api/v1/schemes/upload" > "${status_file}"
local actual_status
actual_status="$(python3 - "$status_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8").strip())
PY
)"
echo "[POST] ${API_URL}/api/v1/schemes/upload -> ${actual_status}"
python3 - "$out_file" <<'PY'
from pathlib import Path
import sys
print(Path(sys.argv[1]).read_text(encoding="utf-8"))
PY
echo
if [[ "${actual_status}" != "${expected_status}" ]]; then
fail "Upload failed for ${upload_filename}: expected ${expected_status}, got ${actual_status}"
fi
}
json_get() {
local file="$1"
local expr="$2"
python3 - "$file" "$expr" <<'PY'
import json
import re
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
expr = sys.argv[2]
def apply_selector(value, selector):
if value is None:
return None
if selector == "LAST":
return value[-1] if value else None
if selector.isdigit():
idx = int(selector)
return value[idx] if len(value) > idx else None
match = re.fullmatch(r"([^!=]+?)(==|!=)(.+)", selector)
if not match:
return value[0] if value else None
key, op, raw_expected = match.groups()
key = key.strip()
raw_expected = raw_expected.strip()
if raw_expected == "null":
expected = None
else:
expected = raw_expected
for item in value:
if not isinstance(item, dict):
continue
item_value = item.get(key)
matched = item_value == expected if op == "==" else item_value != expected
if matched:
return item
return None
value = data
for part in expr.split("."):
if not part:
continue
if part.startswith("[") and part.endswith("]"):
value = apply_selector(value, part[1:-1])
elif part.isdigit():
idx = int(part)
value = value[idx] if value is not None and len(value) > idx else None
elif isinstance(value, dict):
value = value.get(part)
else:
value = None
if isinstance(value, bool):
print("true" if value else "false")
elif value is None:
print("")
elif isinstance(value, (dict, list)):
print(json.dumps(value, ensure_ascii=False))
else:
print(value)
PY
}
json_len() {
local file="$1"
local expr="$2"
python3 - "$file" "$expr" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
expr = sys.argv[2]
value = data
for part in expr.split("."):
if not part:
continue
if part.isdigit():
value = value[int(part)]
else:
value = value.get(part) if isinstance(value, dict) else None
if value is None:
print(0)
elif isinstance(value, (list, dict, str)):
print(len(value))
else:
print(0)
PY
}
assert_json_eq() {
local file="$1"
local expr="$2"
local expected="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if [[ "${actual}" != "${expected}" ]]; then
fail "${expr}: expected '${expected}', got '${actual}'"
fi
echo "[OK] ${expr}=${actual}"
}
assert_json_int_eq() {
local file="$1"
local expr="$2"
local expected="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if ! [[ "${actual}" =~ ^[0-9]+$ ]]; then
fail "${expr}: expected integer, got '${actual}'"
fi
if (( actual != expected )); then
fail "${expr}: expected ${expected}, got ${actual}"
fi
echo "[OK] ${expr}=${actual}"
}
assert_json_int_gt() {
local file="$1"
local expr="$2"
local threshold="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if ! [[ "${actual}" =~ ^[0-9]+$ ]]; then
fail "${expr}: expected integer, got '${actual}'"
fi
if (( actual <= threshold )); then
fail "${expr}: expected > ${threshold}, got ${actual}"
fi
echo "[OK] ${expr}=${actual} (> ${threshold})"
}
assert_json_int_ge() {
local file="$1"
local expr="$2"
local threshold="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if ! [[ "${actual}" =~ ^[0-9]+$ ]]; then
fail "${expr}: expected integer, got '${actual}'"
fi
if (( actual < threshold )); then
fail "${expr}: expected >= ${threshold}, got ${actual}"
fi
echo "[OK] ${expr}=${actual} (>= ${threshold})"
}
assert_json_len_eq() {
local file="$1"
local expr="$2"
local expected="$3"
local actual
actual="$(json_len "${file}" "${expr}")"
if (( actual != expected )); then
fail "len(${expr}): expected ${expected}, got ${actual}"
fi
echo "[OK] len(${expr})=${actual}"
}
assert_file_contains() {
local file="$1"
local needle="$2"
if ! python3 - "$file" "$needle" <<'PY'
from pathlib import Path
import sys
haystack = Path(sys.argv[1]).read_text(encoding="utf-8")
needle = sys.argv[2]
if needle not in haystack:
raise SystemExit(1)
PY
then
fail "Expected '${needle}' in ${file}"
fi
echo "[OK] found '${needle}'"
}
create_fresh_scheme_from_upload() {
local scenario_prefix="$1"
local stamp
stamp="$(date +%s)-$$"
FRESH_SCHEME_NAME="${scenario_prefix}-${stamp}"
local upload_filename="${FRESH_SCHEME_NAME}.svg"
upload_svg "upload_${scenario_prefix}" "${upload_filename}"
request "schemes_after_upload_${scenario_prefix}" "GET" "${API_URL}/api/v1/schemes?limit=200&offset=0" "200"
if ! SCHEME_ID="$(python3 - "${TMP_DIR}/schemes_after_upload_${scenario_prefix}.body" "${FRESH_SCHEME_NAME}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
target_name = sys.argv[2]
for item in payload.get("items", []):
if item.get("name") == target_name:
print(item["scheme_id"])
raise SystemExit(0)
raise SystemExit(1)
PY
)"; then
fail "Unable to resolve uploaded scheme_id for ${FRESH_SCHEME_NAME}"
fi
echo "FRESH_SCHEME_NAME=${FRESH_SCHEME_NAME}"
echo "FRESH_SCHEME_ID=${SCHEME_ID}"
}