Files
go-cart-actor/api-tests/test_ucp.sh
T
mats e1eb342edf
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled
more ucp
2026-06-24 12:46:52 +02:00

292 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────────
# UCP API test suite
# Tests the Universal Commerce Protocol REST endpoints (cart, order) via curl.
#
# Prerequisites:
# make up-infra (infra compose up — order :8092, cart :8090)
# OR full stack (docker compose --profile "*" up — nginx on :8080)
#
# Usage:
# # Direct to Docker services (infra stack):
# bash api-tests/test_ucp.sh --infra
#
# # Via nginx (full compose stack):
# bash api-tests/test_ucp.sh --edge
#
# # Single service:
# bash api-tests/test_ucp.sh --order
# bash api-tests/test_ucp.sh --cart
#
# # Verify signatures (requires signing key mounted):
# bash api-tests/test_ucp.sh --verify-sig
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
MODE="${1:---infra}"
JQ="${JQ:-$(command -v jq || true)}"
# ── Base URLs ────────────────────────────────────────────────────────────────
if [ "$MODE" = "--edge" ]; then
BASE="http://localhost:8080"
CART_URL="$BASE"
ORDER_URL="$BASE"
CHECKOUT_URL="$BASE"
elif [ "$MODE" = "--order" ]; then
ORDER_URL="http://localhost:8092"
elif [ "$MODE" = "--cart" ]; then
CART_URL="http://localhost:8090"
elif [ "$MODE" = "--verify-sig" ]; then
# If signing is enabled (key mounted), you need the full stack or direct with
# env var — try both common ports.
ORDER_URL="${ORDER_URL:-http://localhost:8092}"
CART_URL="${CART_URL:-http://localhost:8090}"
else
# Default: infra stack, direct to Docker ports
CART_URL="${CART_URL:-http://localhost:8090}"
ORDER_URL="${ORDER_URL:-http://localhost:8092}"
CHECKOUT_URL="${CHECKOUT_URL:-}"
fi
echo "═══════════════════════════════════════════════════════════════"
echo " UCP API Test Suite — mode: $MODE"
echo " CART: ${CART_URL:-"(unreachable)"}"
echo " ORDER: ${ORDER_URL:-"(unreachable)"}"
echo " CHECK: ${CHECKOUT_URL:-"(unreachable)"}"
echo "═══════════════════════════════════════════════════════════════"
echo ""
# ── Helpers ──────────────────────────────────────────────────────────────────
pass() { echo " ✅ $1"; }
fail() { echo " ❌ $1"; }
header() { echo ""; echo "─── $1 ───"; }
check_jq() {
if [ -z "$JQ" ]; then
echo " ⚠️ jq not installed — showing raw JSON instead"
fi
}
maybe_jq() {
if [ -n "$JQ" ]; then
echo "$1" | jq "$2" 2>/dev/null || echo "$1" | head -c 200
else
echo "$1" | head -c 200
fi
}
assert_status() {
local label="$1" expected="$2" actual="$3" body="$4"
if [ "$actual" -eq "$expected" ]; then
pass "$label (HTTP $actual)"
else
fail "$label — expected HTTP $expected, got $actual"
echo " body: $(echo "$body" | head -c 300)"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 1: Order service tests
# ══════════════════════════════════════════════════════════════════════════════
if [ -n "${ORDER_URL:-}" ]; then
header "1. Order Service → $ORDER_URL"
# ── Health ──────────────────────────────────────────────────────────────────
echo "--- 1.1 Health check ---"
resp=$(curl -sf "$ORDER_URL/healthz" 2>&1) && pass "/healthz" || fail "/healthz: $resp"
# ── Create order via checkout ───────────────────────────────────────────────
echo "--- 1.2 Create order via POST /checkout ---"
create_resp=$(curl -s -w '\n%{http_code}' -X POST "$ORDER_URL/checkout" \
-H 'Content-Type: application/json' \
-d '{
"orderReference": "ucp-test-1",
"currency": "SEK",
"lines": [
{
"reference": "l1",
"sku": "TEST-SKU-1",
"name": "Test Product",
"quantity": 2,
"unitPrice": 19900,
"taxRate": 2500
}
]
}')
create_code=$(echo "$create_resp" | tail -1)
create_body=$(echo "$create_resp" | sed '$d')
assert_status "Create order" 201 "$create_code" "$create_body"
ORDER_ID=$(echo "$create_body" | sed -n 's/.*"orderId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
if [ -n "$ORDER_ID" ]; then
pass "Order ID: $ORDER_ID"
else
ORDER_ID="1"
fail "Could not extract order ID, using '$ORDER_ID'"
echo " response: $(echo "$create_body" | head -c 400)"
fi
# ── Read order via UCP ──────────────────────────────────────────────────────
echo "--- 1.3 GET /ucp/v1/orders/{id} ---"
get_resp=$(curl -s -w '\n%{http_code}' "$ORDER_URL/ucp/v1/orders/$ORDER_ID")
get_code=$(echo "$get_resp" | tail -1)
get_body=$(echo "$get_resp" | sed '$d')
assert_status "GET UCP order" 200 "$get_code" "$get_body"
echo " order: $(maybe_jq "$get_body" '.data.orderId + " status=" + .data.status')"
# ── Cancel order ────────────────────────────────────────────────────────────
echo "--- 1.4 POST /ucp/v1/orders/{id}/cancel ---"
cancel_resp=$(curl -s -w '\n%{http_code}' -X POST "$ORDER_URL/ucp/v1/orders/$ORDER_ID/cancel" \
-H 'Content-Type: application/json' \
-d '{"reason": "test cancellation"}')
cancel_code=$(echo "$cancel_resp" | tail -1)
cancel_body=$(echo "$cancel_resp" | sed '$d')
# May be 200 (cancelled) or 422 (already cancelled by flow) — both ok
if [ "$cancel_code" = "200" ] || [ "$cancel_code" = "422" ]; then
pass "Cancel order (HTTP $cancel_code)"
echo " response: $(echo "$cancel_body" | head -c 200)"
if [ "$cancel_code" = "200" ]; then
ORDER_CANCELLED=true
fi
else
fail "Cancel order — expected 200 or 422, got $cancel_code"
echo " body: $(echo "$cancel_body" | head -c 300)"
fi
# ── 404 for non-existent order ──────────────────────────────────────────────
echo "--- 1.5 GET non-existent order (404) ---"
notfound_resp=$(curl -s -w '\n%{http_code}' "$ORDER_URL/ucp/v1/orders/nonexistent")
notfound_code=$(echo "$notfound_resp" | tail -1)
notfound_body=$(echo "$notfound_resp" | sed '$d')
assert_status "GET non-existent" 404 "$notfound_code" "$notfound_body"
# ── Signature headers (if enabled) ──────────────────────────────────────────
echo "--- 1.6 Check signature headers ---"
sig_resp=$(curl -s -i "$ORDER_URL/ucp/v1/orders/$ORDER_ID" 2>/dev/null | head -30)
if echo "$sig_resp" | grep -qi "signature-input"; then
pass "Signature-Input header present"
echo " $(echo "$sig_resp" | grep -i "signature-input" | head -1)"
else
echo " ️ No Signature-Input header (signing key not mounted — expected in infra stack)"
fi
if echo "$sig_resp" | grep -qi "^signature:"; then
pass "Signature header present"
else
echo " ️ No Signature header"
fi
echo ""
fi
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 2: Cart service tests
# ══════════════════════════════════════════════════════════════════════════════
if [ -n "${CART_URL:-}" ]; then
header "2. Cart Service → $CART_URL"
# ── Health ──────────────────────────────────────────────────────────────────
echo "--- 2.1 Health check ---"
resp=$(curl -sf "$CART_URL/healthz" 2>&1) && pass "/healthz" || fail "/healthz: $resp"
# ── Create cart via UCP ─────────────────────────────────────────────────────
echo "--- 2.2 POST /ucp/v1/carts/ (create cart) ---"
# Note: must use trailing slash — Go's ServeMux with StripPrefix only
# registers the subtree pattern (/ucp/v1/carts/) to avoid a redirect bug.
cart_resp=$(curl -s -w '\n%{http_code}' -X POST "$CART_URL/ucp/v1/carts/" \
-H 'Content-Type: application/json' \
-d '{
"currency": "SEK",
"country": "se",
"locale": "sv-SE"
}')
cart_code=$(echo "$cart_resp" | tail -1)
cart_body=$(echo "$cart_resp" | sed '$d')
assert_status "Create cart" 201 "$cart_code" "$cart_body"
CART_ID=$(echo "$cart_body" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
if [ -n "$CART_ID" ]; then
pass "Cart ID: $CART_ID"
else
CART_ID="1"
fail "Could not extract cart ID, using '$CART_ID'"
echo " response: $(echo "$cart_body" | head -c 400)"
fi
# ── Add item to cart ────────────────────────────────────────────────────────
echo "--- 2.3 POST /ucp/v1/carts/{id}/items ---"
add_resp=$(curl -s -w '\n%{http_code}' -X POST "$CART_URL/ucp/v1/carts/$CART_ID/items" \
-H 'Content-Type: application/json' \
-d '{
"sku": "TEST-SKU-1",
"name": "Test Product",
"quantity": 1,
"unitPrice": 19900,
"taxRate": 2500
}')
add_code=$(echo "$add_resp" | tail -1)
add_body=$(echo "$add_resp" | sed '$d')
assert_status "Add item" 201 "$add_code" "$add_body"
# ── Get cart ────────────────────────────────────────────────────────────────
echo "--- 2.4 GET /ucp/v1/carts/{id} ---"
get_cart_resp=$(curl -s -w '\n%{http_code}' "$CART_URL/ucp/v1/carts/$CART_ID")
get_cart_code=$(echo "$get_cart_resp" | tail -1)
get_cart_body=$(echo "$get_cart_resp" | sed '$d')
assert_status "Get cart" 200 "$get_cart_code" "$get_cart_body"
echo " total: $(echo "$get_cart_body" | sed -n 's/.*"total"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p')"
echo ""
fi
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 3: Full flow — cart → checkout → order (via edge/nginx only)
# ══════════════════════════════════════════════════════════════════════════════
if [ "$MODE" = "--edge" ] && [ -n "${CHECKOUT_URL:-}" ]; then
header "3. Full Flow (via nginx)"
# ── This section needs the full compose stack with nginx on :8080,
# and checkout must be reachable. Skip for infra/direct modes.
echo "--- 3.1 Create checkout session ---"
checkout_resp=$(curl -s -w '\n%{http_code}' -X POST "$CHECKOUT_URL/ucp/v1/checkout-sessions" \
-H 'Content-Type: application/json' \
-d '{
"cartId": "'"$CART_ID"'",
"currency": "SEK",
"country": "se"
}')
checkout_code=$(echo "$checkout_resp" | tail -1)
checkout_body=$(echo "$checkout_resp" | sed '$d')
assert_status "Create checkout session" 201 "$checkout_code" "$checkout_body"
fi
# ══════════════════════════════════════════════════════════════════════════════
# SECTION 4: Signature verification (standalone)
# ══════════════════════════════════════════════════════════════════════════════
if [ "$MODE" = "--verify-sig" ]; then
header "4. Signature Verification"
# Verify that the response includes RFC 9421 HTTP Message Signatures.
# Requires the service to have UCP_SIGNING_KEY_PATH set and the key mounted.
echo "--- 4.1 Check Signed Response ---"
sig_output=$(curl -s -D - "$ORDER_URL/ucp/v1/orders/$ORDER_ID" 2>/dev/null | head -40)
echo "$sig_output" | while IFS= read -r line; do
if echo "$line" | grep -qiE "(signature-input|signature:|x-ucp-timestamp|content-type)"; then
echo " $line"
fi
done
echo ""
echo " To manually verify:"
echo " 1. Extract the Signature-Input and Signature headers"
echo " 2. Reconstruct the signature base string per RFC 9421 §2.2"
echo " 3. Verify using the public key (x/y in docs/ucp-profile.json)"
echo ""
fi
# ══════════════════════════════════════════════════════════════════════════════
echo "═══════════════════════════════════════════════════════════════"
echo " Done."
echo ""
echo " Note: devProxies for /ucp/v1/* are now in islands.config.json."
echo " Run 'make dev' for same-origin proxied access."
echo "═══════════════════════════════════════════════════════════════"