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:
354
backend/scripts/smoke_common.sh
Normal file
354
backend/scripts/smoke_common.sh
Normal 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}"
|
||||
}
|
||||
Reference in New Issue
Block a user