Files
svg-backend/backend/scripts/smoke_admin_ops.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

320 lines
16 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
# shellcheck source=backend/scripts/smoke_common.sh
source "${SCRIPT_DIR}/smoke_common.sh"
wait_for_health
create_fresh_scheme_from_upload "smoke-admin-ops"
request "scheme_current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
CURRENT_VERSION_ID="$(json_get "${TMP_DIR}/scheme_current.body" "scheme_version_id")"
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
request "ensure_draft" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure?expected_current_scheme_version_id=${CURRENT_VERSION_ID}" \
"200"
DRAFT_VERSION_ID="$(json_get "${TMP_DIR}/ensure_draft.body" "scheme_version_id")"
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
request "draft_structure" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200"
TARGET_SEAT_ID="$(python3 - "${TMP_DIR}/draft_structure.body" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
seat = next((item for item in payload.get("seats", []) if item.get("seat_id")), None)
if seat is None:
raise SystemExit("No seat with seat_id found for admin ops smoke")
print(seat["seat_id"])
PY
)"
echo "TARGET_SEAT_ID=${TARGET_SEAT_ID}"
STAMP="$(date +%s)-$$"
CLEANUP_PREFIX="ADMINOPS_CLEAN_${STAMP}_"
DELETE_CATEGORY_NAME="adminops-clean-delete-${STAMP}"
DELETE_CATEGORY_CODE="${CLEANUP_PREFIX}DELETE"
KEEP_CATEGORY_NAME="adminops-clean-keep-${STAMP}"
KEEP_CATEGORY_CODE="${CLEANUP_PREFIX}KEEP"
request "create_delete_category" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/categories?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200" \
"{\"name\":\"${DELETE_CATEGORY_NAME}\",\"code\":\"${DELETE_CATEGORY_CODE}\"}"
DELETE_CATEGORY_ID="$(json_get "${TMP_DIR}/create_delete_category.body" "pricing_category_id")"
echo "DELETE_CATEGORY_ID=${DELETE_CATEGORY_ID}"
request "create_keep_category" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/categories?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200" \
"{\"name\":\"${KEEP_CATEGORY_NAME}\",\"code\":\"${KEEP_CATEGORY_CODE}\"}"
KEEP_CATEGORY_ID="$(json_get "${TMP_DIR}/create_keep_category.body" "pricing_category_id")"
echo "KEEP_CATEGORY_ID=${KEEP_CATEGORY_ID}"
request "create_keep_category_rule" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200" \
"{\"pricing_category_id\":\"${KEEP_CATEGORY_ID}\",\"target_type\":\"seat\",\"target_ref\":\"${TARGET_SEAT_ID}\",\"amount\":\"555.00\",\"currency\":\"RUB\"}"
KEEP_RULE_ID="$(json_get "${TMP_DIR}/create_keep_category_rule.body" "price_rule_id")"
echo "KEEP_RULE_ID=${KEEP_RULE_ID}"
request "draft_pricing_snapshot" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200"
request "publish_preview_refresh" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-preview?refresh=true&expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200"
request "publish_preview_cached" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200"
request "admin_current_artifacts" "GET" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/artifacts" \
"200"
assert_json_eq "${TMP_DIR}/admin_current_artifacts.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_int_ge "${TMP_DIR}/admin_current_artifacts.body" "total" "4"
assert_file_contains "${TMP_DIR}/admin_current_artifacts.body" "\"artifact_type\":\"publish_preview\""
request "admin_current_validation" "GET" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/validation" \
"200"
assert_json_eq "${TMP_DIR}/admin_current_validation.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_file_contains "${TMP_DIR}/admin_current_validation.body" "\"report\":"
request "publish_preview_audit" "GET" \
"${API_URL}/api/v1/admin/artifacts/publish-preview/audit" \
"200"
assert_json_eq "${TMP_DIR}/publish_preview_audit.body" "artifact_type" "publish_preview"
assert_json_int_ge "${TMP_DIR}/publish_preview_audit.body" "db_rows_count" "1"
assert_json_int_ge "${TMP_DIR}/publish_preview_audit.body" "disk_files_count" "1"
PRE_CLEANUP_DB_ROWS_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit.body" "db_rows_count")"
PRE_CLEANUP_DISK_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit.body" "disk_files_count")"
PRE_CLEANUP_ORPHAN_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit.body" "orphan_files_count")"
PRE_CLEANUP_MISSING_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit.body" "missing_files_for_db_rows_count")"
echo "PRE_CLEANUP_DB_ROWS_COUNT=${PRE_CLEANUP_DB_ROWS_COUNT}"
echo "PRE_CLEANUP_DISK_FILES_COUNT=${PRE_CLEANUP_DISK_FILES_COUNT}"
echo "PRE_CLEANUP_ORPHAN_FILES_COUNT=${PRE_CLEANUP_ORPHAN_FILES_COUNT}"
echo "PRE_CLEANUP_MISSING_FILES_COUNT=${PRE_CLEANUP_MISSING_FILES_COUNT}"
request "publish_preview_cleanup_dry_run" "POST" \
"${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" \
"200"
assert_json_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "artifact_type" "publish_preview"
assert_json_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "dry_run" "true"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "deleted_files_count" "0"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "deleted_db_rows_count" "0"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "orphan_files_count" "${PRE_CLEANUP_ORPHAN_FILES_COUNT}"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_dry_run.body" "missing_files_for_db_rows_count" "${PRE_CLEANUP_MISSING_FILES_COUNT}"
request "publish_preview_cleanup_execute" "POST" \
"${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=false" \
"200"
assert_json_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "artifact_type" "publish_preview"
assert_json_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "dry_run" "false"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "orphan_files_count" "${PRE_CLEANUP_ORPHAN_FILES_COUNT}"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "missing_files_for_db_rows_count" "${PRE_CLEANUP_MISSING_FILES_COUNT}"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "deleted_files_count" "${PRE_CLEANUP_ORPHAN_FILES_COUNT}"
assert_json_int_eq "${TMP_DIR}/publish_preview_cleanup_execute.body" "deleted_db_rows_count" "${PRE_CLEANUP_MISSING_FILES_COUNT}"
request "publish_preview_audit_after_cleanup" "GET" \
"${API_URL}/api/v1/admin/artifacts/publish-preview/audit" \
"200"
assert_json_eq "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "artifact_type" "publish_preview"
assert_json_int_eq "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "orphan_files_count" "0"
assert_json_int_eq "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "missing_files_for_db_rows_count" "0"
POST_CLEANUP_DB_ROWS_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "db_rows_count")"
POST_CLEANUP_DISK_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "disk_files_count")"
POST_CLEANUP_ORPHAN_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "orphan_files_count")"
POST_CLEANUP_MISSING_FILES_COUNT="$(json_get "${TMP_DIR}/publish_preview_audit_after_cleanup.body" "missing_files_for_db_rows_count")"
echo "POST_CLEANUP_DB_ROWS_COUNT=${POST_CLEANUP_DB_ROWS_COUNT}"
echo "POST_CLEANUP_DISK_FILES_COUNT=${POST_CLEANUP_DISK_FILES_COUNT}"
echo "POST_CLEANUP_ORPHAN_FILES_COUNT=${POST_CLEANUP_ORPHAN_FILES_COUNT}"
echo "POST_CLEANUP_MISSING_FILES_COUNT=${POST_CLEANUP_MISSING_FILES_COUNT}"
if [[ "${POST_CLEANUP_DB_ROWS_COUNT}" != "${POST_CLEANUP_DISK_FILES_COUNT}" ]]; then
fail "publish-preview audit mismatch after cleanup: db_rows_count=${POST_CLEANUP_DB_ROWS_COUNT}, disk_files_count=${POST_CLEANUP_DISK_FILES_COUNT}"
fi
echo "[OK] publish-preview audit is fully consistent after cleanup"
request "pricing_cleanup_preview" "GET" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=${CLEANUP_PREFIX}" \
"200"
assert_json_eq "${TMP_DIR}/pricing_cleanup_preview.body" "scheme_id" "${SCHEME_ID}"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_preview.body" "total_candidates" "2"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_preview.body" "safe_to_delete_count" "1"
python3 - "${TMP_DIR}/pricing_cleanup_preview.body" "${DELETE_CATEGORY_ID}" "${KEEP_CATEGORY_ID}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
delete_category_id = sys.argv[2]
keep_category_id = sys.argv[3]
items = {item["pricing_category_id"]: item for item in payload.get("items", [])}
if delete_category_id not in items:
raise SystemExit(f"Delete candidate {delete_category_id} missing from cleanup preview")
if keep_category_id not in items:
raise SystemExit(f"Protected category {keep_category_id} missing from cleanup preview")
if not items[delete_category_id]["deletable"]:
raise SystemExit("Delete candidate is expected to be deletable")
if items[keep_category_id]["deletable"]:
raise SystemExit("Protected category is expected to be skipped because it has rules")
PY
echo "[OK] pricing cleanup preview matched expected deletable/skipped categories"
request "pricing_cleanup_dry_run" "POST" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" \
"200" \
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":true}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "scheme_id" "${SCHEME_ID}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "dry_run" "true"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "matched_total" "2"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "would_delete_count" "1"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "deleted_count" "0"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run.body" "skipped_count" "1"
python3 - "${TMP_DIR}/pricing_cleanup_dry_run.body" "${DELETE_CATEGORY_ID}" "${KEEP_CATEGORY_ID}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
delete_category_id = sys.argv[2]
keep_category_id = sys.argv[3]
would_delete_ids = set(payload.get("would_delete_category_ids", []))
if would_delete_ids != {delete_category_id}:
raise SystemExit(
f"Dry run expected would_delete_category_ids={[delete_category_id]}, got={sorted(would_delete_ids)}"
)
skipped_ids = {item["pricing_category_id"] for item in payload.get("skipped", [])}
if skipped_ids != {keep_category_id}:
raise SystemExit(
f"Dry run expected skipped={[keep_category_id]}, got={sorted(skipped_ids)}"
)
PY
echo "[OK] pricing cleanup dry-run kept protected category and selected only empty fixture category"
request "pricing_cleanup_execute" "POST" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" \
"200" \
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":false}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_execute.body" "scheme_id" "${SCHEME_ID}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_execute.body" "dry_run" "false"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_execute.body" "matched_total" "2"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_execute.body" "would_delete_count" "1"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_execute.body" "deleted_count" "1"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_execute.body" "skipped_count" "1"
python3 - "${TMP_DIR}/pricing_cleanup_execute.body" "${DELETE_CATEGORY_ID}" "${KEEP_CATEGORY_ID}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
delete_category_id = sys.argv[2]
keep_category_id = sys.argv[3]
deleted_ids = set(payload.get("deleted_category_ids", []))
if deleted_ids != {delete_category_id}:
raise SystemExit(
f"Cleanup execute expected deleted_category_ids={[delete_category_id]}, got={sorted(deleted_ids)}"
)
skipped_ids = {item["pricing_category_id"] for item in payload.get("skipped", [])}
if skipped_ids != {keep_category_id}:
raise SystemExit(
f"Cleanup execute expected skipped={[keep_category_id]}, got={sorted(skipped_ids)}"
)
PY
echo "[OK] pricing cleanup execute deleted only safe fixture category"
request "pricing_bundle_after_cleanup" "GET" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" \
"200"
assert_json_len_eq "${TMP_DIR}/pricing_bundle_after_cleanup.body" "categories" "1"
assert_json_len_eq "${TMP_DIR}/pricing_bundle_after_cleanup.body" "rules" "1"
python3 - "${TMP_DIR}/pricing_bundle_after_cleanup.body" "${DELETE_CATEGORY_ID}" "${KEEP_CATEGORY_ID}" "${KEEP_RULE_ID}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
delete_category_id = sys.argv[2]
keep_category_id = sys.argv[3]
keep_rule_id = sys.argv[4]
category_ids = {item["pricing_category_id"] for item in payload.get("categories", [])}
rule_ids = {item["price_rule_id"] for item in payload.get("rules", [])}
if delete_category_id in category_ids:
raise SystemExit("Deleted cleanup category still present in pricing bundle")
if keep_category_id not in category_ids:
raise SystemExit("Protected cleanup category missing after execute cleanup")
if keep_rule_id not in rule_ids:
raise SystemExit("Protected pricing rule missing after execute cleanup")
PY
echo "[OK] pricing bundle reflects destructive cleanup result"
request "pricing_cleanup_preview_after_cleanup" "GET" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=${CLEANUP_PREFIX}" \
"200"
assert_json_eq "${TMP_DIR}/pricing_cleanup_preview_after_cleanup.body" "scheme_id" "${SCHEME_ID}"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_preview_after_cleanup.body" "total_candidates" "1"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_preview_after_cleanup.body" "safe_to_delete_count" "0"
request "pricing_cleanup_dry_run_after_cleanup" "POST" \
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" \
"200" \
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":true}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "scheme_id" "${SCHEME_ID}"
assert_json_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "dry_run" "true"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "matched_total" "1"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "would_delete_count" "0"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "deleted_count" "0"
assert_json_int_eq "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "skipped_count" "1"
python3 - "${TMP_DIR}/pricing_cleanup_dry_run_after_cleanup.body" "${KEEP_CATEGORY_ID}" <<'PY'
import json
import sys
from pathlib import Path
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
keep_category_id = sys.argv[2]
would_delete_ids = payload.get("would_delete_category_ids", [])
if would_delete_ids:
raise SystemExit(f"Expected no deletable categories after cleanup, got={would_delete_ids}")
skipped_ids = {item["pricing_category_id"] for item in payload.get("skipped", [])}
if skipped_ids != {keep_category_id}:
raise SystemExit(
f"Post-cleanup dry run expected skipped={[keep_category_id]}, got={sorted(skipped_ids)}"
)
PY
echo "[OK] repeated cleanup state is stable after destructive cleanup"
echo
echo "===== done ====="
echo "[OK] smoke admin ops completed successfully"
echo "FRESH_SCHEME_ID=${SCHEME_ID}"
echo "DELETE_CATEGORY_ID=${DELETE_CATEGORY_ID}"
echo "KEEP_CATEGORY_ID=${KEEP_CATEGORY_ID}"
echo "KEEP_RULE_ID=${KEEP_RULE_ID}"