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
This commit is contained in:
319
backend/scripts/smoke_admin_ops.sh
Normal file
319
backend/scripts/smoke_admin_ops.sh
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/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}"
|
||||
166
backend/scripts/smoke_authz_admin_ops.sh
Normal file
166
backend/scripts/smoke_authz_admin_ops.sh
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/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"
|
||||
|
||||
ADMIN_API_KEY="${ADMIN_API_KEY:-admin-local-dev-key}"
|
||||
OPERATOR_API_KEY="${OPERATOR_API_KEY:-operator-local-dev-key}"
|
||||
VIEWER_API_KEY="${VIEWER_API_KEY:-viewer-local-dev-key}"
|
||||
|
||||
wait_for_health
|
||||
|
||||
create_fresh_scheme_from_upload "smoke-authz-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 authz admin ops smoke")
|
||||
print(seat["seat_id"])
|
||||
PY
|
||||
)"
|
||||
echo "TARGET_SEAT_ID=${TARGET_SEAT_ID}"
|
||||
|
||||
STAMP="$(date +%s)-$$"
|
||||
CLEANUP_PREFIX="AUTHZ_ADMINOPS_${STAMP}_"
|
||||
DELETE_CATEGORY_NAME="authz-adminops-delete-${STAMP}"
|
||||
DELETE_CATEGORY_CODE="${CLEANUP_PREFIX}DELETE"
|
||||
KEEP_CATEGORY_NAME="authz-adminops-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\":\"666.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_with_api_key "${ADMIN_API_KEY}" "admin_publish_preview_audit" "GET" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/audit" "200"
|
||||
request_with_api_key "${OPERATOR_API_KEY}" "operator_publish_preview_audit" "GET" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/audit" "403"
|
||||
request_with_api_key "${VIEWER_API_KEY}" "viewer_publish_preview_audit" "GET" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/audit" "403"
|
||||
assert_file_contains "${TMP_DIR}/operator_publish_preview_audit.body" "Admin role required"
|
||||
assert_file_contains "${TMP_DIR}/viewer_publish_preview_audit.body" "Admin role required"
|
||||
|
||||
request_with_api_key "${ADMIN_API_KEY}" "admin_publish_preview_cleanup_dry_run" "POST" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "200"
|
||||
request_with_api_key "${OPERATOR_API_KEY}" "operator_publish_preview_cleanup_dry_run" "POST" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "403"
|
||||
request_with_api_key "${VIEWER_API_KEY}" "viewer_publish_preview_cleanup_dry_run" "POST" \
|
||||
"${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "403"
|
||||
assert_file_contains "${TMP_DIR}/operator_publish_preview_cleanup_dry_run.body" "Admin role required"
|
||||
assert_file_contains "${TMP_DIR}/viewer_publish_preview_cleanup_dry_run.body" "Admin role required"
|
||||
|
||||
request_with_api_key "${ADMIN_API_KEY}" "admin_pricing_cleanup_preview" "GET" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=${CLEANUP_PREFIX}" "200"
|
||||
request_with_api_key "${OPERATOR_API_KEY}" "operator_pricing_cleanup_preview" "GET" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=${CLEANUP_PREFIX}" "403"
|
||||
request_with_api_key "${VIEWER_API_KEY}" "viewer_pricing_cleanup_preview" "GET" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=${CLEANUP_PREFIX}" "403"
|
||||
assert_file_contains "${TMP_DIR}/operator_pricing_cleanup_preview.body" "Admin role required"
|
||||
assert_file_contains "${TMP_DIR}/viewer_pricing_cleanup_preview.body" "Admin role required"
|
||||
|
||||
request_with_api_key "${ADMIN_API_KEY}" "admin_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}"
|
||||
request_with_api_key "${OPERATOR_API_KEY}" "operator_pricing_cleanup_dry_run" "POST" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "403" \
|
||||
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":true}"
|
||||
request_with_api_key "${VIEWER_API_KEY}" "viewer_pricing_cleanup_dry_run" "POST" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "403" \
|
||||
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":true}"
|
||||
assert_file_contains "${TMP_DIR}/operator_pricing_cleanup_dry_run.body" "Admin role required"
|
||||
assert_file_contains "${TMP_DIR}/viewer_pricing_cleanup_dry_run.body" "Admin role required"
|
||||
|
||||
request_with_api_key "${OPERATOR_API_KEY}" "operator_pricing_cleanup_execute" "POST" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "403" \
|
||||
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":false}"
|
||||
request_with_api_key "${VIEWER_API_KEY}" "viewer_pricing_cleanup_execute" "POST" \
|
||||
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "403" \
|
||||
"{\"code_prefixes\":[\"${CLEANUP_PREFIX}\"],\"name_prefixes\":[],\"pricing_category_ids\":[],\"delete_only_without_rules\":true,\"dry_run\":false}"
|
||||
assert_file_contains "${TMP_DIR}/operator_pricing_cleanup_execute.body" "Admin role required"
|
||||
assert_file_contains "${TMP_DIR}/viewer_pricing_cleanup_execute.body" "Admin role required"
|
||||
|
||||
request_with_api_key "${ADMIN_API_KEY}" "admin_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_int_eq "${TMP_DIR}/admin_pricing_cleanup_execute.body" "deleted_count" "1"
|
||||
assert_json_int_eq "${TMP_DIR}/admin_pricing_cleanup_execute.body" "skipped_count" "1"
|
||||
|
||||
request "pricing_bundle_after_admin_cleanup_execute" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" "200"
|
||||
assert_json_len_eq "${TMP_DIR}/pricing_bundle_after_admin_cleanup_execute.body" "categories" "1"
|
||||
assert_json_len_eq "${TMP_DIR}/pricing_bundle_after_admin_cleanup_execute.body" "rules" "1"
|
||||
|
||||
python3 - "${TMP_DIR}/pricing_bundle_after_admin_cleanup_execute.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("Authz cleanup execute left deletable category behind")
|
||||
if keep_category_id not in category_ids:
|
||||
raise SystemExit("Authz cleanup execute removed protected category")
|
||||
if keep_rule_id not in rule_ids:
|
||||
raise SystemExit("Authz cleanup execute removed protected rule")
|
||||
PY
|
||||
echo "[OK] admin cleanup execute remained destructive only for safe fixture category"
|
||||
|
||||
echo
|
||||
echo "===== done ====="
|
||||
echo "[OK] smoke authz admin ops completed successfully"
|
||||
echo "FRESH_SCHEME_ID=${SCHEME_ID}"
|
||||
@@ -101,6 +101,58 @@ PY
|
||||
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"
|
||||
@@ -141,6 +193,51 @@ PY
|
||||
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"
|
||||
|
||||
@@ -10,6 +10,22 @@ echo
|
||||
echo "===== smoke pricing/publish ====="
|
||||
bash "${SCRIPT_DIR}/smoke_pricing_publish.sh"
|
||||
|
||||
echo
|
||||
echo "===== smoke version lifecycle ====="
|
||||
bash "${SCRIPT_DIR}/smoke_version_lifecycle.sh"
|
||||
|
||||
echo
|
||||
echo "===== smoke admin ops ====="
|
||||
bash "${SCRIPT_DIR}/smoke_admin_ops.sh"
|
||||
|
||||
echo
|
||||
echo "===== smoke authz admin ops ====="
|
||||
bash "${SCRIPT_DIR}/smoke_authz_admin_ops.sh"
|
||||
|
||||
echo
|
||||
echo "===== smoke upload negative ====="
|
||||
bash "${SCRIPT_DIR}/smoke_upload_negative.sh"
|
||||
|
||||
echo
|
||||
echo "===== done ====="
|
||||
echo "[OK] smoke regression orchestration completed successfully"
|
||||
|
||||
53
backend/scripts/smoke_upload_negative.sh
Normal file
53
backend/scripts/smoke_upload_negative.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/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
|
||||
require_fixture_svg
|
||||
|
||||
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
|
||||
MAX_FILE_SIZE_BYTES="$(json_get "${TMP_DIR}/manifest.body" "svg_limits.max_file_size_bytes")"
|
||||
echo "MAX_FILE_SIZE_BYTES=${MAX_FILE_SIZE_BYTES}"
|
||||
|
||||
EMPTY_SVG_PATH="${TMP_DIR}/empty.svg"
|
||||
NON_SVG_PATH="${TMP_DIR}/not-svg.txt"
|
||||
SVG_BODY_WRONG_EXTENSION_PATH="${TMP_DIR}/svg-body.txt"
|
||||
OVERSIZE_SVG_PATH="${TMP_DIR}/oversize.svg"
|
||||
|
||||
: > "${EMPTY_SVG_PATH}"
|
||||
printf 'plain text payload\n' > "${NON_SVG_PATH}"
|
||||
cp "${FIXTURE_SVG_PATH}" "${SVG_BODY_WRONG_EXTENSION_PATH}"
|
||||
|
||||
python3 - "${OVERSIZE_SVG_PATH}" "${MAX_FILE_SIZE_BYTES}" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
output_path = Path(sys.argv[1])
|
||||
max_file_size_bytes = int(sys.argv[2])
|
||||
payload = "<svg xmlns='http://www.w3.org/2000/svg'>" + (" " * max_file_size_bytes) + "</svg>"
|
||||
output_path.write_text(payload, encoding="utf-8")
|
||||
if output_path.stat().st_size <= max_file_size_bytes:
|
||||
raise SystemExit("Generated oversize SVG is not larger than configured limit")
|
||||
PY
|
||||
|
||||
upload_file_expect_status "upload_empty_file" "${EMPTY_SVG_PATH}" "empty.svg" "image/svg+xml" "400"
|
||||
assert_file_contains "${TMP_DIR}/upload_empty_file.body" "Uploaded file is empty"
|
||||
|
||||
upload_file_expect_status "upload_non_svg_text_plain" "${NON_SVG_PATH}" "not-svg.txt" "text/plain" "400"
|
||||
assert_file_contains "${TMP_DIR}/upload_non_svg_text_plain.body" "Only SVG files are allowed"
|
||||
|
||||
upload_file_expect_status "upload_svg_body_wrong_extension" "${SVG_BODY_WRONG_EXTENSION_PATH}" "valid-svg-body.txt" "text/plain" "400"
|
||||
assert_file_contains "${TMP_DIR}/upload_svg_body_wrong_extension.body" "Only SVG files are allowed"
|
||||
|
||||
upload_file_expect_status "upload_oversize_svg" "${OVERSIZE_SVG_PATH}" "oversize.svg" "image/svg+xml" "413"
|
||||
assert_file_contains "${TMP_DIR}/upload_oversize_svg.body" "SVG file exceeds configured size limit"
|
||||
|
||||
echo
|
||||
echo "===== done ====="
|
||||
echo "[OK] smoke upload negative completed successfully"
|
||||
236
backend/scripts/smoke_version_lifecycle.sh
Normal file
236
backend/scripts/smoke_version_lifecycle.sh
Normal file
@@ -0,0 +1,236 @@
|
||||
#!/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-version-lifecycle"
|
||||
|
||||
request "scheme_detail_initial" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_detail_initial.body" "status" "draft"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_detail_initial.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_current_initial" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
VERSION1_ID="$(json_get "${TMP_DIR}/scheme_current_initial.body" "scheme_version_id")"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_current_initial.body" "version_number" "1"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_initial.body" "status" "draft"
|
||||
echo "VERSION1_ID=${VERSION1_ID}"
|
||||
|
||||
request "ensure_draft_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure?expected_current_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
assert_json_eq "${TMP_DIR}/ensure_draft_v1.body" "scheme_version_id" "${VERSION1_ID}"
|
||||
assert_json_eq "${TMP_DIR}/ensure_draft_v1.body" "created" "false"
|
||||
|
||||
request "draft_structure_v1" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
|
||||
read -r VERSION1_SEAT_RECORD_ID VERSION1_SEAT_ID ORIGINAL_ROW_LABEL ORIGINAL_SEAT_NUMBER <<EOF
|
||||
$(python3 - "${TMP_DIR}/draft_structure_v1.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 in version 1 draft structure")
|
||||
print(
|
||||
seat["seat_record_id"],
|
||||
seat["seat_id"],
|
||||
seat.get("row_label") or "__EMPTY__",
|
||||
seat.get("seat_number") or "__EMPTY__",
|
||||
)
|
||||
PY
|
||||
)
|
||||
EOF
|
||||
|
||||
echo "VERSION1_SEAT_RECORD_ID=${VERSION1_SEAT_RECORD_ID}"
|
||||
echo "VERSION1_SEAT_ID=${VERSION1_SEAT_ID}"
|
||||
echo "ORIGINAL_ROW_LABEL=${ORIGINAL_ROW_LABEL}"
|
||||
echo "ORIGINAL_SEAT_NUMBER=${ORIGINAL_SEAT_NUMBER}"
|
||||
|
||||
STAMP="$(date +%s)-$$"
|
||||
PRICING_CATEGORY_NAME="lifecycle-publish-${STAMP}"
|
||||
PRICING_CATEGORY_CODE="LIFECYCLE_${STAMP}"
|
||||
UPDATED_ROW_LABEL="LC-${STAMP}"
|
||||
|
||||
request "create_pricing_category_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/categories?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200" \
|
||||
"{\"name\":\"${PRICING_CATEGORY_NAME}\",\"code\":\"${PRICING_CATEGORY_CODE}\"}"
|
||||
PRICING_CATEGORY_ID="$(json_get "${TMP_DIR}/create_pricing_category_v1.body" "pricing_category_id")"
|
||||
echo "PRICING_CATEGORY_ID=${PRICING_CATEGORY_ID}"
|
||||
|
||||
request "create_price_rule_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200" \
|
||||
"{\"pricing_category_id\":\"${PRICING_CATEGORY_ID}\",\"target_type\":\"seat\",\"target_ref\":\"${VERSION1_SEAT_ID}\",\"amount\":\"777.00\",\"currency\":\"RUB\"}"
|
||||
PRICE_RULE_ID="$(json_get "${TMP_DIR}/create_price_rule_v1.body" "price_rule_id")"
|
||||
echo "PRICE_RULE_ID=${PRICE_RULE_ID}"
|
||||
|
||||
request "draft_pricing_snapshot_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
|
||||
request "publish_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/publish?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
assert_json_eq "${TMP_DIR}/publish_v1.body" "status" "published"
|
||||
assert_json_int_eq "${TMP_DIR}/publish_v1.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_detail_after_publish_v1" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_detail_after_publish_v1.body" "status" "published"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_detail_after_publish_v1.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_current_after_publish_v1" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_publish_v1.body" "scheme_version_id" "${VERSION1_ID}"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_current_after_publish_v1.body" "version_number" "1"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_publish_v1.body" "status" "published"
|
||||
|
||||
request "ensure_draft_v2" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure?expected_current_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
VERSION2_ID="$(json_get "${TMP_DIR}/ensure_draft_v2.body" "scheme_version_id")"
|
||||
echo "VERSION2_ID=${VERSION2_ID}"
|
||||
assert_json_eq "${TMP_DIR}/ensure_draft_v2.body" "created" "true"
|
||||
assert_json_eq "${TMP_DIR}/ensure_draft_v2.body" "source_scheme_version_id" "${VERSION1_ID}"
|
||||
assert_json_int_eq "${TMP_DIR}/ensure_draft_v2.body" "version_number" "2"
|
||||
|
||||
request "draft_structure_v2_before_mutation" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200"
|
||||
|
||||
VERSION2_SEAT_RECORD_ID="$(python3 - "${TMP_DIR}/draft_structure_v2_before_mutation.body" "${VERSION1_SEAT_ID}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
seat_id = sys.argv[2]
|
||||
seat = next((item for item in payload.get("seats", []) if item.get("seat_id") == seat_id), None)
|
||||
if seat is None:
|
||||
raise SystemExit("Target seat_id not found in version 2 draft structure")
|
||||
print(seat["seat_record_id"])
|
||||
PY
|
||||
)"
|
||||
echo "VERSION2_SEAT_RECORD_ID=${VERSION2_SEAT_RECORD_ID}"
|
||||
|
||||
request "patch_seat_v2" "PATCH" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${VERSION2_SEAT_RECORD_ID}?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200" \
|
||||
"{\"row_label\":\"${UPDATED_ROW_LABEL}\"}"
|
||||
assert_json_eq "${TMP_DIR}/patch_seat_v2.body" "row_label" "${UPDATED_ROW_LABEL}"
|
||||
|
||||
request "draft_structure_v2_after_mutation" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200"
|
||||
assert_file_contains "${TMP_DIR}/draft_structure_v2_after_mutation.body" "\"row_label\":\"${UPDATED_ROW_LABEL}\""
|
||||
|
||||
request "draft_compare_preview_v2" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200"
|
||||
assert_file_contains "${TMP_DIR}/draft_compare_preview_v2.body" "\"status\":\"changed\""
|
||||
|
||||
request "draft_pricing_snapshot_v2" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200"
|
||||
|
||||
request "publish_v2" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/publish?expected_scheme_version_id=${VERSION2_ID}" \
|
||||
"200"
|
||||
assert_json_eq "${TMP_DIR}/publish_v2.body" "status" "published"
|
||||
assert_json_int_eq "${TMP_DIR}/publish_v2.body" "current_version_number" "2"
|
||||
|
||||
request "scheme_detail_after_publish_v2" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_detail_after_publish_v2.body" "status" "published"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_detail_after_publish_v2.body" "current_version_number" "2"
|
||||
|
||||
request "scheme_current_after_publish_v2" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_publish_v2.body" "scheme_version_id" "${VERSION2_ID}"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_current_after_publish_v2.body" "version_number" "2"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_publish_v2.body" "status" "published"
|
||||
|
||||
request "scheme_versions_after_publish_v2" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/versions?limit=20&offset=0" "200"
|
||||
assert_json_len_eq "${TMP_DIR}/scheme_versions_after_publish_v2.body" "items" "2"
|
||||
|
||||
request "rollback_to_v1" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/rollback" \
|
||||
"200" \
|
||||
"{\"target_version_number\":1}"
|
||||
assert_json_eq "${TMP_DIR}/rollback_to_v1.body" "status" "draft"
|
||||
assert_json_int_eq "${TMP_DIR}/rollback_to_v1.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_detail_after_rollback" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_detail_after_rollback.body" "status" "draft"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_detail_after_rollback.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_current_after_rollback" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_rollback.body" "scheme_version_id" "${VERSION1_ID}"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_current_after_rollback.body" "version_number" "1"
|
||||
assert_json_eq "${TMP_DIR}/scheme_current_after_rollback.body" "status" "draft"
|
||||
|
||||
request "editor_context_after_rollback" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
|
||||
assert_json_eq "${TMP_DIR}/editor_context_after_rollback.body" "current_scheme_version_id" "${VERSION1_ID}"
|
||||
assert_json_eq "${TMP_DIR}/editor_context_after_rollback.body" "current_is_draft" "true"
|
||||
|
||||
request "draft_structure_after_rollback" "GET" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${VERSION1_ID}" \
|
||||
"200"
|
||||
|
||||
python3 - "${TMP_DIR}/draft_structure_after_rollback.body" "${VERSION1_SEAT_ID}" "${ORIGINAL_ROW_LABEL}" "${ORIGINAL_SEAT_NUMBER}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
seat_id = sys.argv[2]
|
||||
expected_row_label = sys.argv[3]
|
||||
expected_seat_number = sys.argv[4]
|
||||
|
||||
seat = next((item for item in payload.get("seats", []) if item.get("seat_id") == seat_id), None)
|
||||
if seat is None:
|
||||
raise SystemExit(f"Seat {seat_id} not found after rollback")
|
||||
|
||||
actual_row_label = seat.get("row_label") or "__EMPTY__"
|
||||
actual_seat_number = seat.get("seat_number") or "__EMPTY__"
|
||||
if actual_row_label != expected_row_label:
|
||||
raise SystemExit(
|
||||
f"Rollback row_label mismatch: expected {expected_row_label}, got {actual_row_label}"
|
||||
)
|
||||
if actual_seat_number != expected_seat_number:
|
||||
raise SystemExit(
|
||||
f"Rollback seat_number mismatch: expected {expected_seat_number}, got {actual_seat_number}"
|
||||
)
|
||||
PY
|
||||
echo "[OK] rollback restored version 1 seat semantics"
|
||||
|
||||
request "unpublish_after_rollback" "POST" \
|
||||
"${API_URL}/api/v1/schemes/${SCHEME_ID}/unpublish" \
|
||||
"200"
|
||||
assert_json_eq "${TMP_DIR}/unpublish_after_rollback.body" "status" "draft"
|
||||
assert_json_int_eq "${TMP_DIR}/unpublish_after_rollback.body" "current_version_number" "1"
|
||||
|
||||
request "scheme_detail_after_unpublish" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
|
||||
assert_json_eq "${TMP_DIR}/scheme_detail_after_unpublish.body" "status" "draft"
|
||||
assert_json_int_eq "${TMP_DIR}/scheme_detail_after_unpublish.body" "current_version_number" "1"
|
||||
|
||||
request "audit_trail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
|
||||
assert_file_contains "${TMP_DIR}/audit_trail.body" "\"event_type\":\"scheme.published\""
|
||||
assert_file_contains "${TMP_DIR}/audit_trail.body" "\"event_type\":\"scheme.version.created\""
|
||||
assert_file_contains "${TMP_DIR}/audit_trail.body" "\"event_type\":\"scheme.rolled_back\""
|
||||
assert_file_contains "${TMP_DIR}/audit_trail.body" "\"event_type\":\"scheme.unpublished\""
|
||||
|
||||
echo
|
||||
echo "===== done ====="
|
||||
echo "[OK] smoke version lifecycle completed successfully"
|
||||
echo "FRESH_SCHEME_ID=${SCHEME_ID}"
|
||||
echo "VERSION1_ID=${VERSION1_ID}"
|
||||
echo "VERSION2_ID=${VERSION2_ID}"
|
||||
Reference in New Issue
Block a user