test(backend): split smoke regression into core and pricing publish flows

separate smoke coverage into core backend checks and pricing publish flow checks

make regression runs more focused and easier to maintain
improve troubleshooting when a smoke stage fails
This commit is contained in:
greebo
2026-03-20 13:25:32 +03:00
parent 239b32a246
commit 210981c953
5 changed files with 746 additions and 249 deletions

View File

@@ -0,0 +1,354 @@
#!/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
}
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
}
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}"
}

View File

@@ -0,0 +1,133 @@
#!/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
request "ping" "GET" "${API_URL}/api/v1/ping" "200"
request "db_ping" "GET" "${API_URL}/api/v1/db/ping" "200"
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
create_fresh_scheme_from_upload "smoke-core"
request "scheme_detail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
assert_json_eq "${TMP_DIR}/scheme_detail.body" "scheme_id" "${SCHEME_ID}"
assert_json_eq "${TMP_DIR}/scheme_detail.body" "name" "${FRESH_SCHEME_NAME}"
assert_json_eq "${TMP_DIR}/scheme_detail.body" "status" "draft"
request "scheme_versions" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/versions?limit=20&offset=0" "200"
assert_json_len_eq "${TMP_DIR}/scheme_versions.body" "items" "1"
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")"
CURRENT_STATUS="$(json_get "${TMP_DIR}/scheme_current.body" "status")"
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
echo "CURRENT_STATUS=${CURRENT_STATUS}"
assert_json_eq "${TMP_DIR}/scheme_current.body" "status" "draft"
request "editor_context" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
assert_json_eq "${TMP_DIR}/editor_context.body" "current_scheme_version_id" "${CURRENT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/editor_context.body" "current_is_draft" "true"
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "200"
DRAFT_VERSION_ID="$(json_get "${TMP_DIR}/ensure_draft.body" "scheme_version_id")"
DRAFT_CREATED="$(json_get "${TMP_DIR}/ensure_draft.body" "created")"
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
echo "DRAFT_CREATED=${DRAFT_CREATED}"
assert_json_eq "${TMP_DIR}/ensure_draft.body" "scheme_version_id" "${CURRENT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/ensure_draft.body" "created" "false"
request "draft_summary" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_structure" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_validation" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/validation?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_compare_preview" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_structure.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_validation.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_compare_preview.body" "draft_scheme_version_id" "${DRAFT_VERSION_ID}"
TOTAL_SEATS="$(json_get "${TMP_DIR}/draft_summary.body" "total_seats")"
echo "TOTAL_SEATS=${TOTAL_SEATS}"
read -r SEAT_RECORD_ID SECTOR_RECORD_ID GROUP_RECORD_ID EXPLAIN_SEAT_ID <<EOF
$(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"))
seats = payload.get("seats", [])
sectors = payload.get("sectors", [])
groups = payload.get("groups", [])
seat_with_id = next((seat for seat in seats if seat.get("seat_id")), None)
if seat_with_id is None:
raise SystemExit("No seat with seat_id found in fresh draft structure")
print(
seat_with_id["seat_record_id"],
sectors[0]["sector_record_id"],
groups[0]["group_record_id"],
seat_with_id["seat_id"],
)
PY
)
EOF
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
echo "SECTOR_RECORD_ID=${SECTOR_RECORD_ID}"
echo "GROUP_RECORD_ID=${GROUP_RECORD_ID}"
echo "EXPLAIN_SEAT_ID=${EXPLAIN_SEAT_ID}"
request "stale_draft_conflict" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
assert_json_eq "${TMP_DIR}/stale_draft_conflict.body" "detail.code" "stale_draft_version"
request "draft_seat_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_sector_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors/records/${SECTOR_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_group_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups/records/${GROUP_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_unknown_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/deadbeefdeadbeefdeadbeefdeadbeef" "404"
request "current_sectors" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/sectors" "200"
request "current_groups" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/groups" "200"
request "current_seats" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats" "200"
request "display_meta" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/svg/display/meta" "200"
request "pricing_bundle" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" "200"
assert_json_len_eq "${TMP_DIR}/pricing_bundle.body" "categories" "0"
assert_json_len_eq "${TMP_DIR}/pricing_bundle.body" "rules" "0"
request "pricing_coverage" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/coverage" "200"
assert_json_int_eq "${TMP_DIR}/pricing_coverage.body" "priced_seats" "0"
assert_json_int_eq "${TMP_DIR}/pricing_coverage.body" "unpriced_seats" "${TOTAL_SEATS}"
request "pricing_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/unpriced-seats" "200"
assert_json_int_eq "${TMP_DIR}/pricing_unpriced.body" "total" "${TOTAL_SEATS}"
request "pricing_explain_empty" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${EXPLAIN_SEAT_ID}" "200"
assert_json_eq "${TMP_DIR}/pricing_explain_empty.body" "has_price" "false"
assert_json_eq "${TMP_DIR}/pricing_explain_empty.body" "reason_code" "no_price_rule"
request "pricing_rule_diagnostics" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules/diagnostics" "200"
assert_json_int_eq "${TMP_DIR}/pricing_rule_diagnostics.body" "summary.total_rules" "0"
assert_json_int_eq "${TMP_DIR}/pricing_rule_diagnostics.body" "summary.active_rules_count" "0"
assert_json_int_eq "${TMP_DIR}/pricing_rule_diagnostics.body" "summary.matched_seats_total" "0"
assert_json_len_eq "${TMP_DIR}/pricing_rule_diagnostics.body" "items" "0"
request "audit_trail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
assert_json_int_ge "${TMP_DIR}/audit_trail.body" "total" "0"
log "editor mutation regression"
API_URL="${API_URL}" API_KEY="${API_KEY}" SCHEME_ID="${SCHEME_ID}" \
bash "${SCRIPT_DIR}/editor_mutation_regression.sh"
echo
echo "===== done ====="
echo "[OK] smoke core completed successfully"
echo "FRESH_SCHEME_ID=${SCHEME_ID}"

View File

@@ -0,0 +1,117 @@
#!/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-pricing-publish"
request "scheme_current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "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"
read -r PRICED_SEAT_ID UNPRICED_SEAT_ID <<EOF
$(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_ids = [seat["seat_id"] for seat in payload.get("seats", []) if seat.get("seat_id")]
if len(seat_ids) < 2:
raise SystemExit("Fixture must contain at least two seats with seat_id for pricing smoke")
print(seat_ids[0], seat_ids[1])
PY
)
EOF
echo "PRICED_SEAT_ID=${PRICED_SEAT_ID}"
echo "UNPRICED_SEAT_ID=${UNPRICED_SEAT_ID}"
STAMP="$(date +%s)-$$"
PRICING_CATEGORY_NAME="smoke-pricing-${STAMP}"
PRICING_CATEGORY_CODE="SMOKE_${STAMP}"
request "create_pricing_category" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/categories?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200" \
"{\"name\":\"${PRICING_CATEGORY_NAME}\",\"code\":\"${PRICING_CATEGORY_CODE}\"}"
PRICING_CATEGORY_ID="$(json_get "${TMP_DIR}/create_pricing_category.body" "pricing_category_id")"
echo "PRICING_CATEGORY_ID=${PRICING_CATEGORY_ID}"
request "create_price_rule" "POST" \
"${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" \
"200" \
"{\"pricing_category_id\":\"${PRICING_CATEGORY_ID}\",\"target_type\":\"seat\",\"target_ref\":\"${PRICED_SEAT_ID}\",\"amount\":\"1234.56\",\"currency\":\"RUB\"}"
PRICE_RULE_ID="$(json_get "${TMP_DIR}/create_price_rule.body" "price_rule_id")"
echo "PRICE_RULE_ID=${PRICE_RULE_ID}"
request "pricing_bundle" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" "200"
assert_json_len_eq "${TMP_DIR}/pricing_bundle.body" "categories" "1"
assert_json_len_eq "${TMP_DIR}/pricing_bundle.body" "rules" "1"
request "pricing_coverage" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/coverage" "200"
assert_json_int_gt "${TMP_DIR}/pricing_coverage.body" "priced_seats" "0"
assert_json_int_gt "${TMP_DIR}/pricing_coverage.body" "unpriced_seats" "0"
request "pricing_explain_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${PRICED_SEAT_ID}" "200"
assert_json_eq "${TMP_DIR}/pricing_explain_priced.body" "has_price" "true"
assert_json_eq "${TMP_DIR}/pricing_explain_priced.body" "reason_code" "ok"
assert_json_eq "${TMP_DIR}/pricing_explain_priced.body" "matched_rule.matched_rule_level" "seat"
assert_json_eq "${TMP_DIR}/pricing_explain_priced.body" "matched_rule.matched_target_ref" "${PRICED_SEAT_ID}"
request "pricing_explain_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${UNPRICED_SEAT_ID}" "200"
assert_json_eq "${TMP_DIR}/pricing_explain_unpriced.body" "has_price" "false"
assert_json_eq "${TMP_DIR}/pricing_explain_unpriced.body" "reason_code" "no_price_rule"
request "pricing_rule_diagnostics" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules/diagnostics" "200"
assert_json_int_eq "${TMP_DIR}/pricing_rule_diagnostics.body" "summary.total_rules" "1"
assert_json_int_gt "${TMP_DIR}/pricing_rule_diagnostics.body" "summary.matched_seats_total" "0"
request "seat_price" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats/${PRICED_SEAT_ID}/price" "200"
assert_json_eq "${TMP_DIR}/seat_price.body" "matched_rule_level" "seat"
assert_json_eq "${TMP_DIR}/seat_price.body" "matched_target_ref" "${PRICED_SEAT_ID}"
assert_json_eq "${TMP_DIR}/seat_price.body" "amount" "1234.56"
request "test_mode_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${PRICED_SEAT_ID}" "200"
assert_json_eq "${TMP_DIR}/test_mode_priced.body" "has_price" "true"
assert_json_eq "${TMP_DIR}/test_mode_priced.body" "selectable" "true"
request "test_mode_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${UNPRICED_SEAT_ID}" "200"
assert_json_eq "${TMP_DIR}/test_mode_unpriced.body" "has_price" "false"
assert_json_eq "${TMP_DIR}/test_mode_unpriced.body" "reason_code" "no_price_rule"
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_readiness" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-readiness?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/publish_readiness.body" "snapshot.available" "true"
assert_json_eq "${TMP_DIR}/publish_readiness.body" "readiness.is_ready_to_publish" "true"
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 "publish_scheme" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/publish?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/publish_scheme.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/publish_scheme.body" "status" "published"
request "scheme_detail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}" "200"
assert_json_eq "${TMP_DIR}/scheme_detail.body" "status" "published"
request "scheme_current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
assert_json_eq "${TMP_DIR}/scheme_current.body" "status" "published"
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\""
echo
echo "===== done ====="
echo "[OK] smoke pricing/publish completed successfully"
echo "FRESH_SCHEME_ID=${SCHEME_ID}"

View File

@@ -1,233 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
API_URL="${API_URL:-http://127.0.0.1:9020}"
API_KEY="${API_KEY:-admin-local-dev-key}"
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
HEALTH_MAX_ATTEMPTS="${HEALTH_MAX_ATTEMPTS:-20}"
HEALTH_RETRY_DELAY_SECONDS="${HEALTH_RETRY_DELAY_SECONDS:-1}"
echo "===== smoke core ====="
bash "${SCRIPT_DIR}/smoke_core.sh"
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="$(cat "${status_file}")"
echo "[${method}] ${url} -> ${actual_status}"
cat "${out_file}"
echo
if [[ "${actual_status}" != "${expected_status}" ]]; then
echo "[FAIL] Unexpected HTTP status for ${name}: expected ${expected_status}, got ${actual_status}" >&2
exit 1
fi
}
json_get() {
local file="$1"
local expr="$2"
python3 - "$file" "$expr" <<'PY'
import json
import sys
path = sys.argv[2].split(".")
value = json.load(open(sys.argv[1], "r", encoding="utf-8"))
for part in path:
if not part:
continue
if part.isdigit():
value = value[int(part)]
else:
value = value[part]
if isinstance(value, bool):
print("true" if value else "false")
elif value is None:
print("null")
else:
print(value)
PY
}
assert_json_eq() {
local file="$1"
local expr="$2"
local expected="$3"
local actual
actual="$(json_get "${file}" "${expr}")"
if [[ "${actual}" != "${expected}" ]]; then
echo "[FAIL] ${expr}: expected '${expected}', got '${actual}'" >&2
exit 1
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
echo "[FAIL] ${expr}: expected integer, got '${actual}'" >&2
exit 1
fi
if (( actual <= threshold )); then
echo "[FAIL] ${expr}: expected > ${threshold}, got ${actual}" >&2
exit 1
fi
echo "[OK] ${expr}=${actual} (> ${threshold})"
}
echo "===== health ====="
echo "waiting for API to be ready..."
health_ready="false"
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
echo "[FAIL] API did not become ready on ${API_URL}/healthz after ${HEALTH_MAX_ATTEMPTS} attempts" >&2
exit 1
fi
curl -sS -i "${API_URL}/healthz"
request "ping" "GET" "${API_URL}/api/v1/ping" "200"
request "db_ping" "GET" "${API_URL}/api/v1/db/ping" "200"
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
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")"
CURRENT_STATUS="$(json_get "${TMP_DIR}/scheme_current.body" "status")"
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
echo "CURRENT_STATUS=${CURRENT_STATUS}"
request "editor_context" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "200"
DRAFT_VERSION_ID="$(json_get "${TMP_DIR}/ensure_draft.body" "scheme_version_id")"
DRAFT_CREATED="$(json_get "${TMP_DIR}/ensure_draft.body" "created")"
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
echo "DRAFT_CREATED=${DRAFT_CREATED}"
request "draft_summary" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
assert_json_eq "${TMP_DIR}/draft_summary.body" "status" "draft"
request "draft_structure" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/draft_structure.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
SEAT_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.0.seat_record_id")"
SECTOR_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "sectors.0.sector_record_id")"
GROUP_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "groups.0.group_record_id")"
PRICED_SEAT_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.0.seat_id")"
UNPRICED_SEAT_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.2.seat_id")"
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
echo "SECTOR_RECORD_ID=${SECTOR_RECORD_ID}"
echo "GROUP_RECORD_ID=${GROUP_RECORD_ID}"
echo "PRICED_SEAT_ID=${PRICED_SEAT_ID}"
echo "UNPRICED_SEAT_ID=${UNPRICED_SEAT_ID}"
request "draft_validation" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/validation?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
assert_json_eq "${TMP_DIR}/draft_validation.body" "status" "draft"
request "draft_compare_preview" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "stale_draft_conflict" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
assert_json_eq "${TMP_DIR}/stale_draft_conflict.body" "detail.code" "stale_draft_version"
request "draft_seat_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_sector_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors/records/${SECTOR_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_group_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups/records/${GROUP_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
request "draft_unknown_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/deadbeefdeadbeefdeadbeefdeadbeef" "404"
request "current_sectors" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/sectors" "200"
request "current_groups" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/groups" "200"
request "current_seats" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats" "200"
request "display_meta" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/svg/display/meta" "200"
request "pricing_bundle" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" "200"
request "pricing_coverage" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/coverage" "200"
request "pricing_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/unpriced-seats" "200"
request "pricing_explain_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${PRICED_SEAT_ID}" "200"
request "pricing_explain_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${UNPRICED_SEAT_ID}" "200"
request "pricing_rule_diagnostics" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules/diagnostics" "200"
request "seat_price" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats/${PRICED_SEAT_ID}/price" "200"
request "test_mode_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${PRICED_SEAT_ID}" "200"
request "test_mode_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${UNPRICED_SEAT_ID}" "200"
request "typed_invalid_amount" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"pricing_category_id":"4ef9e2b78fe0447f9f5db02714c7cad5","target_type":"sector","target_ref":"vip","amount":"bad","currency":"RUB"}'
assert_json_eq "${TMP_DIR}/typed_invalid_amount.body" "detail.code" "invalid_amount"
request "typed_remap_filter_required" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/remap/preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{}'
assert_json_eq "${TMP_DIR}/typed_remap_filter_required.body" "detail.code" "remap_filter_required"
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_readiness" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-readiness?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_artifacts" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/artifacts" "200"
request "admin_validation" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/validation" "200"
request "admin_preview_audit" "GET" "${API_URL}/api/v1/admin/artifacts/publish-preview/audit" "200"
request "admin_preview_cleanup_dry_run" "POST" "${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "200"
request "admin_cleanup_preview" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=FAIL_&code_prefix=DIAG_&code_prefix=AUTO_&code_prefix=TYPED_&name_prefix=should-fail-&name_prefix=diag-&name_prefix=auto%20&name_prefix=typed-response-" "200"
request "admin_cleanup_dry_run" "POST" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "200" '{"code_prefixes":["FAIL_","DIAG_","AUTO_","TYPED_"],"name_prefixes":["should-fail-","diag-","auto ","typed-response-"],"delete_only_without_rules":true,"dry_run":true}'
assert_json_eq "${TMP_DIR}/admin_cleanup_dry_run.body" "dry_run" "true"
assert_json_eq "${TMP_DIR}/admin_cleanup_dry_run.body" "deleted_count" "0"
MATCHED_TOTAL="$(json_get "${TMP_DIR}/admin_cleanup_dry_run.body" "matched_total")"
WOULD_DELETE="$(json_get "${TMP_DIR}/admin_cleanup_dry_run.body" "would_delete_count")"
if [[ "${MATCHED_TOTAL}" == "0" ]]; then
if [[ "${WOULD_DELETE}" != "0" ]]; then
echo "[FAIL] would_delete_count expected 0 when matched_total is 0, got ${WOULD_DELETE}" >&2
exit 1
fi
echo "[OK] matched_total=0, would_delete_count=0 (clean state)"
else
assert_json_int_gt "${TMP_DIR}/admin_cleanup_dry_run.body" "would_delete_count" "0"
fi
request "audit_trail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
echo
echo "===== smoke pricing/publish ====="
bash "${SCRIPT_DIR}/smoke_pricing_publish.sh"
echo
echo "===== done ====="
echo "[OK] smoke regression completed successfully"
echo "[OK] smoke regression orchestration completed successfully"