feat(backend): stabilize draft editor flow and complete smoke regression baseline
- add editor entry flow with editor context and ensure-draft bootstrap - add draft summary read model and single-record draft read endpoints - add typed draft, edit and publish conflicts with validation errors - add pricing diagnostics and publish readiness endpoints - fix Decimal serialization in seat price and test preview flows - harden draft lifecycle guards for published vs draft current version - update API map and smoke regression checklist - add backend README and smoke regression script
This commit is contained in:
201
backend/scripts/smoke_regression.sh
Executable file
201
backend/scripts/smoke_regression.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
API_URL="${API_URL:-http://127.0.0.1:9020}"
|
||||
API_KEY="${API_KEY:-admin-local-dev-key}"
|
||||
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
|
||||
|
||||
WORKDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
HDR_AUTH=(-H "X-API-Key: ${API_KEY}")
|
||||
HDR_JSON=(-H "Content-Type: application/json")
|
||||
|
||||
step() {
|
||||
echo
|
||||
echo "===== $1 ====="
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo
|
||||
echo "[FAIL] $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
request() {
|
||||
local name="$1"
|
||||
local method="$2"
|
||||
local url="$3"
|
||||
local expected="$4"
|
||||
local body="${5:-}"
|
||||
local outfile="${WORKDIR}/${name}.body"
|
||||
local codefile="${WORKDIR}/${name}.code"
|
||||
|
||||
if [[ -n "$body" ]]; then
|
||||
curl -sS \
|
||||
-X "$method" \
|
||||
"${HDR_AUTH[@]}" \
|
||||
"${HDR_JSON[@]}" \
|
||||
-o "$outfile" \
|
||||
-w "%{http_code}" \
|
||||
"$url" \
|
||||
--data "$body" > "$codefile"
|
||||
else
|
||||
curl -sS \
|
||||
-X "$method" \
|
||||
"${HDR_AUTH[@]}" \
|
||||
-o "$outfile" \
|
||||
-w "%{http_code}" \
|
||||
"$url" > "$codefile"
|
||||
fi
|
||||
|
||||
local code
|
||||
code="$(cat "$codefile")"
|
||||
|
||||
echo "[$method] $url -> $code"
|
||||
cat "$outfile"
|
||||
echo
|
||||
|
||||
if [[ "$code" != "$expected" ]]; then
|
||||
fail "Unexpected HTTP status for ${name}: expected ${expected}, got ${code}"
|
||||
fi
|
||||
}
|
||||
|
||||
json_get() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
python3 - <<PY
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path("$file").read_text())
|
||||
value = payload
|
||||
for part in "$expr".split("."):
|
||||
if part.isdigit():
|
||||
value = value[int(part)]
|
||||
else:
|
||||
value = value[part]
|
||||
print(value if value is not None else "")
|
||||
PY
|
||||
}
|
||||
|
||||
assert_json_value() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
local expected="$3"
|
||||
local actual
|
||||
actual="$(json_get "$file" "$expr")"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
fail "JSON assertion failed for ${expr}: expected '${expected}', got '${actual}'"
|
||||
fi
|
||||
echo "[OK] ${expr}=${actual}"
|
||||
}
|
||||
|
||||
step "health"
|
||||
curl -sS -i "${API_URL}/healthz" || fail "Health endpoint is unavailable"
|
||||
|
||||
step "system"
|
||||
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"
|
||||
|
||||
step "scheme current"
|
||||
request "current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
CURRENT_VERSION_ID="$(json_get "${WORKDIR}/current.body" "scheme_version_id")"
|
||||
CURRENT_STATUS="$(json_get "${WORKDIR}/current.body" "status")"
|
||||
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
|
||||
echo "CURRENT_STATUS=${CURRENT_STATUS}"
|
||||
|
||||
step "editor context"
|
||||
request "editor_context" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
|
||||
|
||||
step "ensure draft"
|
||||
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "200"
|
||||
DRAFT_VERSION_ID="$(json_get "${WORKDIR}/ensure_draft.body" "scheme_version_id")"
|
||||
DRAFT_CREATED="$(json_get "${WORKDIR}/ensure_draft.body" "created")"
|
||||
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
|
||||
echo "DRAFT_CREATED=${DRAFT_CREATED}"
|
||||
|
||||
step "draft summary"
|
||||
request "draft_summary" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
|
||||
assert_json_value "${WORKDIR}/draft_summary.body" "status" "draft"
|
||||
|
||||
step "draft structure"
|
||||
request "draft_structure" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_structure.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
|
||||
SEAT_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.0.seat_record_id")"
|
||||
SECTOR_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "sectors.0.sector_record_id")"
|
||||
GROUP_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "groups.0.group_record_id")"
|
||||
PRICED_SEAT_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.0.seat_id")"
|
||||
UNPRICED_SEAT_ID="$(json_get "${WORKDIR}/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}"
|
||||
|
||||
step "draft validation"
|
||||
request "draft_validation" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/validation?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_validation.body" "status" "draft"
|
||||
|
||||
step "draft compare preview"
|
||||
request "draft_compare" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "stale draft conflict"
|
||||
request "draft_summary_stale" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
|
||||
assert_json_value "${WORKDIR}/draft_summary_stale.body" "detail.code" "stale_draft_version"
|
||||
|
||||
step "draft record reads"
|
||||
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"
|
||||
|
||||
step "structure read model"
|
||||
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"
|
||||
|
||||
step "svg display pipeline"
|
||||
request "current_svg_meta" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/svg/display/meta" "200"
|
||||
|
||||
step "pricing read model"
|
||||
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_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"
|
||||
|
||||
step "typed validation errors"
|
||||
request "invalid_amount" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"pricing_category_id":"deadbeefdeadbeefdeadbeefdeadbeef","target_type":"seat","target_ref":"seat-x","amount":"bad","currency":"RUB"}'
|
||||
assert_json_value "${WORKDIR}/invalid_amount.body" "detail.code" "invalid_amount"
|
||||
|
||||
request "remap_no_filters" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/remap/preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"seat_record_ids":null,"from_sector_id":null,"to_sector_id":"vip","from_group_id":null,"to_group_id":null}'
|
||||
assert_json_value "${WORKDIR}/remap_no_filters.body" "detail.code" "remap_filter_required"
|
||||
|
||||
step "draft pricing snapshot"
|
||||
request "draft_snapshot" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "publish readiness"
|
||||
request "publish_readiness" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-readiness?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "publish preview"
|
||||
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"
|
||||
|
||||
step "admin ops"
|
||||
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" "POST" "${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "200"
|
||||
|
||||
step "audit trail"
|
||||
request "audit" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
|
||||
|
||||
step "done"
|
||||
echo "[OK] smoke regression completed successfully"
|
||||
Reference in New Issue
Block a user