From b6fe08fc5bdab7a956e749834d35cc84f48e0a5c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 05:30:10 +0000 Subject: [PATCH] feat(gateway): add E2E integration smoke test for all services - Add tests/gateway-e2e-smoke.sh that validates: - Gateway health (GET /healthz -> 200) - Route reachability for all 5 services through YARP proxy - Auth enforcement (401 without token, skipped for scaffolds) - Identity register/login flow (skipped for scaffolds) - Authenticated CRUD through gateway (skipped for scaffolds) - Authorization header forwarding verification - Fix broken Shared project reference paths in all service .csproj files (../../Shared -> ../../../Shared) so docker builds succeed The test is idempotent, CI-ready, and gracefully handles scaffold services by marking auth-dependent checks as SKIP rather than FAIL. --- .../Customer/Customer.API/Customer.API.csproj | 4 +- .../Identity/Identity.API/Identity.API.csproj | 4 +- .../Notification.API/Notification.API.csproj | 4 +- src/Services/Order/Order.API/Order.API.csproj | 4 +- .../Product/Product.API/Product.API.csproj | 4 +- tests/gateway-e2e-smoke.sh | 325 ++++++++++++++++++ 6 files changed, 335 insertions(+), 10 deletions(-) create mode 100755 tests/gateway-e2e-smoke.sh diff --git a/src/Services/Customer/Customer.API/Customer.API.csproj b/src/Services/Customer/Customer.API/Customer.API.csproj index 8c284d4..4a0b5b7 100644 --- a/src/Services/Customer/Customer.API/Customer.API.csproj +++ b/src/Services/Customer/Customer.API/Customer.API.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index 9b3e932..c4e24d9 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Services/Notification/Notification.API/Notification.API.csproj b/src/Services/Notification/Notification.API/Notification.API.csproj index 25b5fb0..1518ca2 100644 --- a/src/Services/Notification/Notification.API/Notification.API.csproj +++ b/src/Services/Notification/Notification.API/Notification.API.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Services/Order/Order.API/Order.API.csproj b/src/Services/Order/Order.API/Order.API.csproj index 54aad4b..12339d9 100644 --- a/src/Services/Order/Order.API/Order.API.csproj +++ b/src/Services/Order/Order.API/Order.API.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Services/Product/Product.API/Product.API.csproj b/src/Services/Product/Product.API/Product.API.csproj index 68be876..5ff75f3 100644 --- a/src/Services/Product/Product.API/Product.API.csproj +++ b/src/Services/Product/Product.API/Product.API.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/tests/gateway-e2e-smoke.sh b/tests/gateway-e2e-smoke.sh new file mode 100755 index 0000000..dfae7bb --- /dev/null +++ b/tests/gateway-e2e-smoke.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################################################### +# Gateway E2E Integration Smoke Test +# +# Validates that ALL services are reachable through the YARP reverse proxy, +# health checks respond, and (when implemented) JWT auth is enforced. +# +# Usage: +# ./tests/gateway-e2e-smoke.sh # from repo root +# SKIP_COMPOSE=1 ./tests/gateway-e2e-smoke.sh # skip docker compose up/down +############################################################################### + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +COMPOSE_FILE="$REPO_ROOT/src/docker-compose.yml" +GATEWAY_URL="http://localhost:5000" + +PASS=0 +FAIL=0 +SKIP=0 + +# ---------- helpers ---------------------------------------------------------- + +red() { printf '\033[1;31m%s\033[0m\n' "$*"; } +green() { printf '\033[1;32m%s\033[0m\n' "$*"; } +yellow(){ printf '\033[1;33m%s\033[0m\n' "$*"; } +bold() { printf '\033[1m%s\033[0m\n' "$*"; } + +assert_status() { + local description="$1" url="$2" expected="$3" + shift 3 + local extra_args=("$@") + + local status + status=$(curl -s -o /dev/null -w '%{http_code}' "${extra_args[@]}" "$url") || status="000" + + if [[ "$status" == "$expected" ]]; then + green " PASS $description (got $status)" + PASS=$((PASS + 1)) + else + red " FAIL $description (expected $expected, got $status)" + FAIL=$((FAIL + 1)) + fi +} + +assert_status_any_of() { + local description="$1" url="$2" + shift 2 + local expected_list=() + while [[ "$1" != "--" ]]; do + expected_list+=("$1") + shift + done + shift # consume the "--" + local extra_args=("$@") + + local status + status=$(curl -s -o /dev/null -w '%{http_code}' "${extra_args[@]}" "$url") || status="000" + + local matched=false + for exp in "${expected_list[@]}"; do + if [[ "$status" == "$exp" ]]; then + matched=true + break + fi + done + + if $matched; then + green " PASS $description (got $status)" + PASS=$((PASS + 1)) + else + red " FAIL $description (expected one of [${expected_list[*]}], got $status)" + FAIL=$((FAIL + 1)) + fi +} + +skip_test() { + yellow " SKIP $1" + SKIP=$((SKIP + 1)) +} + +# ---------- docker compose --------------------------------------------------- + +cleanup() { + if [[ "${SKIP_COMPOSE:-}" != "1" ]]; then + bold "Tearing down containers..." + docker compose -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null || true + fi +} +trap cleanup EXIT + +if [[ "${SKIP_COMPOSE:-}" != "1" ]]; then + bold "Validating docker compose config..." + docker compose -f "$COMPOSE_FILE" config --quiet + green " docker compose config: OK" + + bold "Bringing up all services..." + docker compose -f "$COMPOSE_FILE" up --build -d + + bold "Waiting for services to be healthy (polling /healthz)..." + + services=( + "api-gateway:5000" + "identity-service:5001" + "customer-service:5002" + "order-service:5003" + "product-service:5004" + "notification-service:5005" + ) + + MAX_WAIT=120 + POLL_INTERVAL=3 + + for svc_port in "${services[@]}"; do + svc="${svc_port%%:*}" + port="${svc_port##*:}" + elapsed=0 + printf " Waiting for %-25s " "$svc..." + while true; do + if curl -sf "http://localhost:$port/healthz" > /dev/null 2>&1; then + green "ready (${elapsed}s)" + break + fi + if (( elapsed >= MAX_WAIT )); then + red "TIMEOUT after ${MAX_WAIT}s" + bold "Container logs for $svc:" + docker compose -f "$COMPOSE_FILE" logs --tail=40 "$svc" 2>/dev/null || true + break + fi + sleep "$POLL_INTERVAL" + elapsed=$((elapsed + POLL_INTERVAL)) + done + done +fi + +echo "" +bold "============================================================" +bold " GATEWAY E2E SMOKE TESTS" +bold "============================================================" + +# ---------- 1. Gateway health ------------------------------------------------ + +bold "1. Gateway health check" +assert_status "GET /healthz -> 200" "$GATEWAY_URL/healthz" "200" + +# ---------- 2. Route reachability (scaffold or full) ------------------------- + +bold "2. Route reachability through gateway" + +# The gateway strips the prefix (e.g. /api/customers) and forwards the rest. +# Service controllers are at api/{controller} (singular). +# So: gateway /api/customers/api/customer -> service /api/customer + +declare -A ROUTE_MAP=( + ["customer"]="/api/customers/api/customer" + ["order"]="/api/orders/api/order" + ["product"]="/api/products/api/product" + ["notification"]="/api/notifications/api/notification" + ["identity"]="/api/identity/api/identity" +) + +for svc in customer order product notification identity; do + path="${ROUTE_MAP[$svc]}" + # Expect 200 (scaffold returns OK) or 401 (if auth is added) + assert_status_any_of \ + "GET $path -> 200 or 401 (route reachable)" \ + "$GATEWAY_URL$path" \ + "200" "401" \ + -- +done + +# ---------- 3. Auth enforcement (401 without token) -------------------------- + +bold "3. Auth enforcement (no Bearer token -> 401)" + +AUTH_TESTED=false +for svc in customer order product notification; do + path="${ROUTE_MAP[$svc]}" + status=$(curl -s -o /dev/null -w '%{http_code}' "$GATEWAY_URL$path") || status="000" + if [[ "$status" == "401" ]]; then + green " PASS GET $path -> 401 (auth enforced)" + PASS=$((PASS + 1)) + AUTH_TESTED=true + elif [[ "$status" == "200" ]]; then + skip_test "GET $path -> 401 (service in scaffold mode, no auth yet — got 200)" + else + red " FAIL GET $path -> 401 (got $status)" + FAIL=$((FAIL + 1)) + fi +done + +# ---------- 4. Identity flow (register + login) ------------------------------ + +bold "4. Identity flow (register + login)" + +TOKEN="" + +# Try to register +REG_BODY='{"userName":"smoketest","email":"smoke@test.local","password":"Smoke123!"}' +REG_STATUS=$(curl -s -o /tmp/reg_response.json -w '%{http_code}' \ + -X POST "$GATEWAY_URL/api/identity/api/identity/register" \ + -H "Content-Type: application/json" \ + -d "$REG_BODY") || REG_STATUS="000" + +if [[ "$REG_STATUS" == "200" || "$REG_STATUS" == "201" ]]; then + green " PASS POST /api/identity/api/identity/register -> $REG_STATUS" + PASS=$((PASS + 1)) + + # Try to login + LOGIN_BODY='{"userName":"smoketest","password":"Smoke123!"}' + LOGIN_STATUS=$(curl -s -o /tmp/login_response.json -w '%{http_code}' \ + -X POST "$GATEWAY_URL/api/identity/api/identity/login" \ + -H "Content-Type: application/json" \ + -d "$LOGIN_BODY") || LOGIN_STATUS="000" + + if [[ "$LOGIN_STATUS" == "200" ]]; then + green " PASS POST /api/identity/api/identity/login -> 200" + PASS=$((PASS + 1)) + # Extract JWT token from response + TOKEN=$(cat /tmp/login_response.json | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + # Try common token field names + for key in ('token', 'accessToken', 'access_token', 'jwt', 'Token'): + if key in data: + print(data[key]) + sys.exit(0) + # If the response is a string, use it directly + if isinstance(data, str): + print(data) + sys.exit(0) +except: + pass +" 2>/dev/null) || TOKEN="" + if [[ -n "$TOKEN" ]]; then + green " PASS JWT token extracted from login response" + PASS=$((PASS + 1)) + else + skip_test "Could not extract JWT token from login response" + fi + else + red " FAIL POST /api/identity/api/identity/login -> 200 (got $LOGIN_STATUS)" + FAIL=$((FAIL + 1)) + fi +elif [[ "$REG_STATUS" == "404" || "$REG_STATUS" == "405" ]]; then + skip_test "POST /api/identity/api/identity/register (endpoint not implemented — scaffold)" + skip_test "POST /api/identity/api/identity/login (skipped — no register endpoint)" +else + red " FAIL POST /api/identity/api/identity/register -> 200|201 (got $REG_STATUS)" + FAIL=$((FAIL + 1)) + skip_test "POST /api/identity/api/identity/login (skipped — register failed)" +fi + +# ---------- 5. Authenticated CRUD (with JWT) --------------------------------- + +bold "5. Authenticated CRUD (with Bearer token)" + +if [[ -n "$TOKEN" ]]; then + for svc in customer product order notification; do + path="${ROUTE_MAP[$svc]}" + assert_status \ + "GET $path (with token) -> 200" \ + "$GATEWAY_URL$path" \ + "200" \ + -H "Authorization: Bearer $TOKEN" + done +else + for svc in customer product order notification; do + path="${ROUTE_MAP[$svc]}" + if $AUTH_TESTED; then + skip_test "GET $path (with token) -> 200 (no JWT token available)" + else + # No auth on scaffolds — requests already return 200 without token + skip_test "GET $path (with token) -> 200 (scaffold — no auth, already returning 200)" + fi + done +fi + +# ---------- 6. Header forwarding verification -------------------------------- + +bold "6. Authorization header forwarding" + +# Verify the gateway doesn't strip Authorization headers. +# We send a request with a bogus Authorization header and confirm the +# downstream service receives it (YARP forwards all headers by default). +# If the service has auth, a bad token returns 401 (header was forwarded). +# If scaffold (no auth), it returns 200 (header forwarding is a no-op). + +BOGUS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.bogus" +for svc in customer; do + path="${ROUTE_MAP[$svc]}" + status=$(curl -s -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $BOGUS_TOKEN" \ + "$GATEWAY_URL$path") || status="000" + if [[ "$status" == "401" ]]; then + green " PASS Auth header forwarded (bad token -> 401 from downstream)" + PASS=$((PASS + 1)) + elif [[ "$status" == "200" ]]; then + skip_test "Auth header forwarding (scaffold — no auth on downstream, can't verify)" + else + red " FAIL Unexpected status $status for header forwarding check" + FAIL=$((FAIL + 1)) + fi +done + +# ---------- Summary ---------------------------------------------------------- + +echo "" +bold "============================================================" +bold " RESULTS" +bold "============================================================" +green " Passed: $PASS" +[[ $FAIL -gt 0 ]] && red " Failed: $FAIL" || echo " Failed: $FAIL" +[[ $SKIP -gt 0 ]] && yellow " Skipped: $SKIP" || echo " Skipped: $SKIP" +bold "============================================================" + +if [[ $FAIL -gt 0 ]]; then + red "SMOKE TEST FAILED" + exit 1 +else + green "SMOKE TEST PASSED" + exit 0 +fi