|
| 1 | +#!/usr/bin/env bash |
| 2 | +# End-to-end test: Service (config resolution) → Engine (sync execution) |
| 3 | +# |
| 4 | +# Tests the same flow that Temporal activities execute: |
| 5 | +# 1. Create sync via service API |
| 6 | +# 2. GET /syncs/{id}?include_credentials=true → resolved config |
| 7 | +# 3. POST /setup, /sync, /teardown on engine with X-Sync-Params |
| 8 | +# 4. Verify data in Postgres, verify teardown |
| 9 | +# |
| 10 | +# Requires: |
| 11 | +# - STRIPE_API_KEY (or .env) |
| 12 | +# - Postgres on localhost:5432 (or POSTGRES_URL) |
| 13 | +# - pnpm build must have been run |
| 14 | +set -euo pipefail |
| 15 | + |
| 16 | +ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 17 | +cd "$ROOT" |
| 18 | + |
| 19 | +# Load .env if present |
| 20 | +[ -f .env ] && set -a && source .env && set +a |
| 21 | + |
| 22 | +: "${STRIPE_API_KEY:?Set STRIPE_API_KEY}" |
| 23 | +POSTGRES_URL="${POSTGRES_URL:-postgresql://postgres:postgres@localhost:5432/postgres}" |
| 24 | +SCHEMA="temporal_sh_$(date +%Y%m%d%H%M%S)_$$" |
| 25 | + |
| 26 | +SERVICE_PORT=0 |
| 27 | +ENGINE_PORT=0 |
| 28 | +SERVICE_PID="" |
| 29 | +ENGINE_PID="" |
| 30 | + |
| 31 | +cleanup() { |
| 32 | + echo "" |
| 33 | + echo "--- Cleanup ---" |
| 34 | + [ -n "$SERVICE_PID" ] && kill "$SERVICE_PID" 2>/dev/null && echo " Stopped service ($SERVICE_PID)" |
| 35 | + [ -n "$ENGINE_PID" ] && kill "$ENGINE_PID" 2>/dev/null && echo " Stopped engine ($ENGINE_PID)" |
| 36 | + if [ "${KEEP_TEST_DATA:-}" != "1" ]; then |
| 37 | + psql "$POSTGRES_URL" -c "DROP SCHEMA IF EXISTS \"$SCHEMA\" CASCADE" 2>/dev/null && echo " Dropped schema $SCHEMA" |
| 38 | + fi |
| 39 | + [ -n "${DATA_DIR:-}" ] && rm -rf "$DATA_DIR" && echo " Removed $DATA_DIR" |
| 40 | +} |
| 41 | +trap cleanup EXIT |
| 42 | + |
| 43 | +find_free_port() { |
| 44 | + python3 -c 'import socket; s=socket.socket(); s.bind(("",0)); print(s.getsockname()[1]); s.close()' |
| 45 | +} |
| 46 | + |
| 47 | +wait_for_port() { |
| 48 | + local port=$1 label=$2 timeout=${3:-30} |
| 49 | + for i in $(seq 1 "$timeout"); do |
| 50 | + if nc -z 127.0.0.1 "$port" 2>/dev/null; then |
| 51 | + echo " $label is up (port $port)" |
| 52 | + return 0 |
| 53 | + fi |
| 54 | + sleep 1 |
| 55 | + done |
| 56 | + echo " FAIL: $label not reachable on port $port after ${timeout}s" |
| 57 | + exit 1 |
| 58 | +} |
| 59 | + |
| 60 | +# --- Start engine (from its package dir so connector bins are findable) --- |
| 61 | +ENGINE_PORT=$(find_free_port) |
| 62 | +echo "Starting engine on port $ENGINE_PORT ..." |
| 63 | +(cd "$ROOT/apps/engine" && PORT=$ENGINE_PORT node dist/api/index.js) &>/dev/null & |
| 64 | +ENGINE_PID=$! |
| 65 | +wait_for_port "$ENGINE_PORT" "Engine" |
| 66 | + |
| 67 | +# --- Start service (no Temporal — just config CRUD) --- |
| 68 | +SERVICE_PORT=$(find_free_port) |
| 69 | +DATA_DIR=$(mktemp -d) |
| 70 | +echo "Starting service on port $SERVICE_PORT (data: $DATA_DIR) ..." |
| 71 | +node "$ROOT/apps/service/dist/bin/cli.js" serve \ |
| 72 | + --port "$SERVICE_PORT" \ |
| 73 | + --data-dir "$DATA_DIR" &>/dev/null & |
| 74 | +SERVICE_PID=$! |
| 75 | +wait_for_port "$SERVICE_PORT" "Service" |
| 76 | + |
| 77 | +SERVICE_URL="http://localhost:$SERVICE_PORT" |
| 78 | +ENGINE_URL="http://localhost:$ENGINE_PORT" |
| 79 | + |
| 80 | +echo "" |
| 81 | +echo "=== Service → Engine E2E Test ===" |
| 82 | +echo " Service: $SERVICE_URL" |
| 83 | +echo " Engine: $ENGINE_URL" |
| 84 | +echo " Postgres: $POSTGRES_URL" |
| 85 | +echo " Schema: $SCHEMA" |
| 86 | +echo "" |
| 87 | + |
| 88 | +# --- Create sync --- |
| 89 | +echo "--- 1. Create sync ---" |
| 90 | +SYNC_RESP=$(curl -sf -X POST "$SERVICE_URL/syncs" \ |
| 91 | + -H 'Content-Type: application/json' \ |
| 92 | + -d "{ |
| 93 | + \"source\": { \"type\": \"stripe\", \"api_key\": \"$STRIPE_API_KEY\", \"backfill_limit\": 5 }, |
| 94 | + \"destination\": { \"type\": \"postgres\", \"connection_string\": \"$POSTGRES_URL\", \"schema\": \"$SCHEMA\" }, |
| 95 | + \"streams\": [{ \"name\": \"products\" }] |
| 96 | + }") |
| 97 | +SYNC_ID=$(echo "$SYNC_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") |
| 98 | +echo " Sync: $SYNC_ID" |
| 99 | + |
| 100 | +# --- Resolve config --- |
| 101 | +echo "" |
| 102 | +echo "--- 2. Resolve config (include_credentials=true) ---" |
| 103 | +RESOLVED=$(curl -sf "$SERVICE_URL/syncs/$SYNC_ID?include_credentials=true") |
| 104 | +SRC_TYPE=$(echo "$RESOLVED" | python3 -c "import sys,json; print(json.load(sys.stdin)['source']['type'])") |
| 105 | +HAS_KEY=$(echo "$RESOLVED" | python3 -c "import sys,json; print('api_key' in json.load(sys.stdin)['source'])") |
| 106 | +echo " source.type: $SRC_TYPE" |
| 107 | +echo " has api_key: $HAS_KEY" |
| 108 | +[ "$HAS_KEY" = "True" ] || { echo "FAIL: expected api_key in resolved config"; exit 1; } |
| 109 | + |
| 110 | +# Build X-Sync-Params (same as what activities do) |
| 111 | +PARAMS=$(echo "$RESOLVED" | python3 -c " |
| 112 | +import sys, json |
| 113 | +c = json.load(sys.stdin) |
| 114 | +src = {k:v for k,v in c['source'].items() if k != 'type'} |
| 115 | +dst = {k:v for k,v in c['destination'].items() if k != 'type'} |
| 116 | +print(json.dumps({ |
| 117 | + 'source_name': c['source']['type'], |
| 118 | + 'source_config': src, |
| 119 | + 'destination_name': c['destination']['type'], |
| 120 | + 'destination_config': dst, |
| 121 | + 'streams': c.get('streams', []) |
| 122 | +})) |
| 123 | +") |
| 124 | +echo " X-Sync-Params built ($(echo "$PARAMS" | wc -c | tr -d ' ') bytes)" |
| 125 | + |
| 126 | +# --- Setup --- |
| 127 | +echo "" |
| 128 | +echo "--- 3. Engine: setup ---" |
| 129 | +SETUP_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "$ENGINE_URL/setup" \ |
| 130 | + -H "X-Sync-Params: $PARAMS") |
| 131 | +echo " HTTP $SETUP_STATUS" |
| 132 | +[ "$SETUP_STATUS" = "204" ] || { echo "FAIL: expected 204, got $SETUP_STATUS"; exit 1; } |
| 133 | + |
| 134 | +# --- Sync --- |
| 135 | +echo "" |
| 136 | +echo "--- 4. Engine: sync ---" |
| 137 | +SYNC_OUTPUT=$(curl -sf -X POST "$ENGINE_URL/sync" -H "X-Sync-Params: $PARAMS") |
| 138 | +LINE_COUNT=$(echo "$SYNC_OUTPUT" | wc -l | tr -d ' ') |
| 139 | +echo " NDJSON lines: $LINE_COUNT" |
| 140 | + |
| 141 | +ERROR_COUNT=$(echo "$SYNC_OUTPUT" | python3 -c " |
| 142 | +import sys, json |
| 143 | +errors = 0 |
| 144 | +for line in sys.stdin: |
| 145 | + line = line.strip() |
| 146 | + if not line: continue |
| 147 | + msg = json.loads(line) |
| 148 | + if msg.get('type') == 'error': |
| 149 | + errors += 1 |
| 150 | + print(f' ERROR: {msg.get(\"message\", \"unknown\")}', file=sys.stderr) |
| 151 | +print(errors) |
| 152 | +") |
| 153 | +echo " Errors: $ERROR_COUNT" |
| 154 | + |
| 155 | +# --- Verify Postgres --- |
| 156 | +echo "" |
| 157 | +echo "--- 5. Verify Postgres ---" |
| 158 | +ROW_COUNT=$(psql "$POSTGRES_URL" -t -c "SELECT count(*) FROM \"$SCHEMA\".\"products\"" | tr -d ' ') |
| 159 | +echo " products: $ROW_COUNT rows" |
| 160 | +[ "$ROW_COUNT" -gt 0 ] || { echo "FAIL: expected > 0 rows"; exit 1; } |
| 161 | + |
| 162 | +SAMPLE=$(psql "$POSTGRES_URL" -t -c "SELECT id FROM \"$SCHEMA\".\"products\" LIMIT 1" | tr -d ' ') |
| 163 | +echo " sample: $SAMPLE" |
| 164 | +[[ "$SAMPLE" == prod_* ]] || { echo "FAIL: expected prod_ prefix, got $SAMPLE"; exit 1; } |
| 165 | + |
| 166 | +# --- Teardown --- |
| 167 | +echo "" |
| 168 | +echo "--- 6. Engine: teardown ---" |
| 169 | +TEARDOWN_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST "$ENGINE_URL/teardown" \ |
| 170 | + -H "X-Sync-Params: $PARAMS") |
| 171 | +echo " HTTP $TEARDOWN_STATUS" |
| 172 | +[ "$TEARDOWN_STATUS" = "204" ] || { echo "FAIL: expected 204, got $TEARDOWN_STATUS"; exit 1; } |
| 173 | + |
| 174 | +TABLE_COUNT=$(psql "$POSTGRES_URL" -t -c \ |
| 175 | + "SELECT count(*) FROM information_schema.tables WHERE table_schema = '$SCHEMA'" | tr -d ' ') |
| 176 | +echo " Tables remaining: $TABLE_COUNT" |
| 177 | +[ "$TABLE_COUNT" -eq 0 ] || { echo "FAIL: expected 0 tables after teardown"; exit 1; } |
| 178 | + |
| 179 | +echo "" |
| 180 | +echo "=== All checks passed ===" |
0 commit comments