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
355 lines
8.6 KiB
Bash
355 lines
8.6 KiB
Bash
#!/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}"
|
|
}
|