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

237 lines
11 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-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}"