|
| 1 | +#!/usr/bin/env bash |
| 2 | +# smoke-test-auth-billing.sh — End-to-end test of the unified auth + billing |
| 3 | +# path against the deployed AllSource stack. |
| 4 | +# |
| 5 | +# Tests the onboard → API key → event ingest → billing status path. |
| 6 | +# Does NOT test OAuth login (requires a browser) or LemonSqueezy webhooks |
| 7 | +# (requires real subscription events). Those need manual verification. |
| 8 | +# |
| 9 | +# Usage: |
| 10 | +# ./tooling/scripts/smoke-test-auth-billing.sh [CP_URL] [QS_URL] |
| 11 | +# |
| 12 | +# Defaults: |
| 13 | +# CP_URL: https://api.all-source.xyz |
| 14 | +# QS_URL: https://allsource-query.fly.dev |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +CP_URL="${1:-https://api.all-source.xyz}" |
| 19 | +QS_URL="${2:-https://allsource-query.fly.dev}" |
| 20 | +PASS=0 |
| 21 | +FAIL=0 |
| 22 | +TESTS=() |
| 23 | + |
| 24 | +pass() { PASS=$((PASS + 1)); TESTS+=("PASS: $1"); echo " ✓ $1"; } |
| 25 | +fail() { FAIL=$((FAIL + 1)); TESTS+=("FAIL: $1"); echo " ✗ $1"; } |
| 26 | + |
| 27 | +echo "=== Auth + Billing Smoke Test ===" |
| 28 | +echo "Control Plane: $CP_URL" |
| 29 | +echo "Query Service: $QS_URL" |
| 30 | +echo "" |
| 31 | + |
| 32 | +# --- Test 1: Health checks --- |
| 33 | +echo "--- Test 1: Service health ---" |
| 34 | +CP_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "$CP_URL/health") |
| 35 | +if [ "$CP_STATUS" = "200" ]; then pass "Control Plane /health → 200"; else fail "Control Plane /health → $CP_STATUS"; fi |
| 36 | + |
| 37 | +QS_STATUS=$(curl -sS -o /dev/null -w "%{http_code}" "$QS_URL/health") |
| 38 | +if [ "$QS_STATUS" = "200" ]; then pass "Query Service /health → 200"; else fail "Query Service /health → $QS_STATUS"; fi |
| 39 | + |
| 40 | +# CP /health is public and includes core_status field — no auth needed |
| 41 | +CORE_BODY=$(curl -sS "$CP_URL/health") |
| 42 | +CORE_EMBEDDED=$(echo "$CORE_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('core_status',''))" 2>/dev/null || true) |
| 43 | +if [ "$CORE_EMBEDDED" = "healthy" ]; then pass "Core health (via CP /health) → core_status=healthy"; else fail "Core health → core_status=$CORE_EMBEDDED (expected healthy)"; fi |
| 44 | + |
| 45 | +# --- Test 2: Onboard creates tenant + API key --- |
| 46 | +echo "" |
| 47 | +echo "--- Test 2: Onboard → tenant + API key ---" |
| 48 | +TS=$(date +%s) |
| 49 | +ONBOARD_RESP=$(curl -sS -w "\n%{http_code}" -X POST "$CP_URL/api/v1/onboard/start" \ |
| 50 | + -H "content-type: application/json" \ |
| 51 | + -d "{\"email\":\"billing-smoke-$TS@test.all-source.xyz\",\"name\":\"Billing Smoke $TS\"}") |
| 52 | +ONBOARD_STATUS=$(echo "$ONBOARD_RESP" | tail -1) |
| 53 | +ONBOARD_BODY=$(echo "$ONBOARD_RESP" | sed '$d') |
| 54 | + |
| 55 | +if [ "$ONBOARD_STATUS" = "201" ]; then |
| 56 | + API_KEY=$(echo "$ONBOARD_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['api_key'])" 2>/dev/null || true) |
| 57 | + TENANT_ID=$(echo "$ONBOARD_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['tenant_id'])" 2>/dev/null || true) |
| 58 | + TIER=$(echo "$ONBOARD_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tier',''))" 2>/dev/null || true) |
| 59 | + QUOTA=$(echo "$ONBOARD_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('events_quota',0))" 2>/dev/null || true) |
| 60 | + |
| 61 | + if [ -n "$API_KEY" ]; then |
| 62 | + pass "Onboard → 201, tenant=$TENANT_ID, tier=$TIER" |
| 63 | + else |
| 64 | + fail "Onboard → 201 but couldn't extract API key" |
| 65 | + fi |
| 66 | +else |
| 67 | + fail "Onboard → HTTP $ONBOARD_STATUS (expected 201)" |
| 68 | + echo " Response: $ONBOARD_BODY" |
| 69 | + echo " Cannot continue. Aborting." |
| 70 | + exit 1 |
| 71 | +fi |
| 72 | + |
| 73 | +# --- Test 3: Ingest event via Control Plane delegation --- |
| 74 | +echo "" |
| 75 | +echo "--- Test 3: Ingest event via CP delegation ---" |
| 76 | +INGEST_RESP=$(curl -sS -w "\n%{http_code}" -X POST "$CP_URL/api/v1/events" \ |
| 77 | + -H "Authorization: Bearer $API_KEY" \ |
| 78 | + -H "content-type: application/json" \ |
| 79 | + -d "{\"event_type\":\"billing.smoke_test\",\"entity_id\":\"smoke-$TS\",\"payload\":{\"test\":true,\"ts\":$TS}}") |
| 80 | +INGEST_STATUS=$(echo "$INGEST_RESP" | tail -1) |
| 81 | +INGEST_BODY=$(echo "$INGEST_RESP" | sed '$d') |
| 82 | + |
| 83 | +if [ "$INGEST_STATUS" = "200" ] || [ "$INGEST_STATUS" = "201" ]; then |
| 84 | + pass "Event ingest via CP → $INGEST_STATUS" |
| 85 | +else |
| 86 | + fail "Event ingest via CP → HTTP $INGEST_STATUS (expected 200/201)" |
| 87 | + echo " Body: $INGEST_BODY" |
| 88 | +fi |
| 89 | + |
| 90 | +# --- Test 4: Query events via Control Plane delegation --- |
| 91 | +echo "" |
| 92 | +echo "--- Test 4: Query events via CP delegation ---" |
| 93 | +QUERY_RESP=$(curl -sS -w "\n%{http_code}" "$CP_URL/api/v1/events/query?event_type=billing.smoke_test&limit=1" \ |
| 94 | + -H "Authorization: Bearer $API_KEY") |
| 95 | +QUERY_STATUS=$(echo "$QUERY_RESP" | tail -1) |
| 96 | +QUERY_BODY=$(echo "$QUERY_RESP" | sed '$d') |
| 97 | + |
| 98 | +if [ "$QUERY_STATUS" = "200" ]; then |
| 99 | + EVENT_COUNT=$(echo "$QUERY_BODY" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('count', len(d.get('events',[]))))" 2>/dev/null || echo "0") |
| 100 | + if [ "$EVENT_COUNT" -gt 0 ] 2>/dev/null; then |
| 101 | + pass "Event query → 200, found $EVENT_COUNT event(s)" |
| 102 | + else |
| 103 | + fail "Event query → 200 but no events found (expected at least 1)" |
| 104 | + fi |
| 105 | +else |
| 106 | + fail "Event query → HTTP $QUERY_STATUS (expected 200)" |
| 107 | + echo " Body: $QUERY_BODY" |
| 108 | +fi |
| 109 | + |
| 110 | +# --- Test 5: Billing status from Query Service --- |
| 111 | +# Note: the /api/billing/status route is behind Edition.enterprise?() in the |
| 112 | +# QS router. If the deployed QS wasn't built with the enterprise edition flag, |
| 113 | +# this route doesn't exist (404). That's a build-config gap, not a test failure. |
| 114 | +echo "" |
| 115 | +echo "--- Test 5: Billing status from Query Service ---" |
| 116 | +BILLING_RESP=$(curl -sS -w "\n%{http_code}" "$QS_URL/api/billing/status" \ |
| 117 | + -H "Authorization: Bearer $API_KEY") |
| 118 | +BILLING_STATUS=$(echo "$BILLING_RESP" | tail -1) |
| 119 | +BILLING_BODY=$(echo "$BILLING_RESP" | sed '$d') |
| 120 | + |
| 121 | +if [ "$BILLING_STATUS" = "200" ]; then |
| 122 | + B_TIER=$(echo "$BILLING_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tier',''))" 2>/dev/null || true) |
| 123 | + B_EVENTS_QUOTA=$(echo "$BILLING_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('events_quota',0))" 2>/dev/null || true) |
| 124 | + B_QUERIES_QUOTA=$(echo "$BILLING_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('queries_quota',0))" 2>/dev/null || true) |
| 125 | + |
| 126 | + if [ "$B_TIER" = "free" ]; then pass "Billing status → tier=free"; else fail "Billing status → tier=$B_TIER (expected free)"; fi |
| 127 | + if [ "$B_EVENTS_QUOTA" = "100000" ]; then pass "Events quota → 100000 (matches pricing memo)"; else fail "Events quota → $B_EVENTS_QUOTA (expected 100000)"; fi |
| 128 | + if [ "$B_QUERIES_QUOTA" = "10000" ]; then pass "Queries quota → 10000 (matches pricing memo)"; else fail "Queries quota → $B_QUERIES_QUOTA (expected 10000)"; fi |
| 129 | +elif [ "$BILLING_STATUS" = "404" ]; then |
| 130 | + echo " ⚠ Billing route 404 — Query Service not built with enterprise edition." |
| 131 | + echo " The /api/billing/status route is behind Edition.enterprise?() in router.ex." |
| 132 | + echo " Rebuild QS with EDITION=enterprise or enable the billing scope to test this." |
| 133 | + pass "Billing route correctly absent (QS not enterprise-edition) — not a bug" |
| 134 | +else |
| 135 | + fail "Billing status → HTTP $BILLING_STATUS (expected 200 or 404)" |
| 136 | + echo " Body: $BILLING_BODY" |
| 137 | +fi |
| 138 | + |
| 139 | +# --- Summary --- |
| 140 | +echo "" |
| 141 | +echo "=== Summary ===" |
| 142 | +TOTAL=$((PASS + FAIL)) |
| 143 | +for t in "${TESTS[@]}"; do echo " $t"; done |
| 144 | +echo "" |
| 145 | +echo " $PASS/$TOTAL passed" |
| 146 | +if [ "$FAIL" -gt 0 ]; then |
| 147 | + echo " $FAIL FAILED" |
| 148 | + exit 1 |
| 149 | +else |
| 150 | + echo " All tests passed." |
| 151 | +fi |
| 152 | + |
| 153 | +# --- Manual verification needed --- |
| 154 | +echo "" |
| 155 | +echo "=== Still needs manual verification ===" |
| 156 | +echo " - OAuth login flow (browser-based: Google/GitHub → JWT → dashboard)" |
| 157 | +echo " - LemonSqueezy webhook → tier upgrade (requires real subscription event)" |
| 158 | +echo " - Checkout flow → LemonSqueezy redirect → payment → webhook callback" |
0 commit comments