From a8d8b17a03d55f93e86a17c0bbd746912b660177 Mon Sep 17 00:00:00 2001 From: Admin Bot Date: Thu, 27 Aug 2026 11:17:51 -0700 Subject: [PATCH] test: add integration test suite + canary deployment script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration tests: - internal/serviceadapter/integration_test.go (8 test cases) - Tests real gateway: health, routing, 404s, auth flow - Configurable via GATEWAY_URL, TEST_JWT_TOKEN, SKIP_AUTH_TESTS Canary deployment script: - scripts/test-canary.sh: scale→1, test, scale→N on pass - Keeps 1 pod for debugging on test failure - Supports custom NAMESPACE, DEPLOYMENT, REPLICAS Usage: - Local: ./scripts/test-integration.sh - Production: GATEWAY_URL=https://api.riotpiao.com ./scripts/test-integration.sh - Canary: ./scripts/test-canary.sh Added INTEGRATION_TESTS.md with full documentation. --- INTEGRATION_TESTS.md | 122 +++++++++++ internal/serviceadapter/integration_test.go | 214 ++++++++++++++++++++ scripts/test-canary.sh | 54 +++++ scripts/test-integration.sh | 25 +++ 4 files changed, 415 insertions(+) create mode 100644 INTEGRATION_TESTS.md create mode 100644 internal/serviceadapter/integration_test.go create mode 100755 scripts/test-canary.sh create mode 100755 scripts/test-integration.sh diff --git a/INTEGRATION_TESTS.md b/INTEGRATION_TESTS.md new file mode 100644 index 0000000..743833c --- /dev/null +++ b/INTEGRATION_TESTS.md @@ -0,0 +1,122 @@ +# Integration Tests + +Integration tests call the real deployed gateway to verify X-Service routing works end-to-end. + +## Quick Start + +### Local Test (requires running gateway) + +```bash +# Terminal 1: Start the gateway +CONFIG_PATH=k8s/configmap.yaml go run ./cmd/gateway + +# Terminal 2: Run tests +./scripts/test-integration.sh +``` + +### Cluster Test (production gateway) + +```bash +GATEWAY_URL=https://api.riotpiao.com ./scripts/test-integration.sh +``` + +### Canary Deployment (scale to 1, test, scale back) + +```bash +./scripts/test-canary.sh + +# Or with custom settings: +NAMESPACE=api DEPLOYMENT=api-gateway REPLICAS=3 ./scripts/test-canary.sh +``` + +## Configuration + +Create `.dev.test.local` (gitignored) with: + +```bash +GATEWAY_URL=https://api.riotpiao.com +TEST_JWT_TOKEN=eyJ... # Real JWT from Authentik +SKIP_AUTH_TESTS=false +TEST_TIMEOUT=30 +``` + +Or set env vars directly: + +```bash +export GATEWAY_URL=https://api.riotpiao.com +export TEST_JWT_TOKEN=eyJ... +export SKIP_AUTH_TESTS=false +go test -tags integration -v ./internal/serviceadapter +``` + +## Test Matrix + +| Test | Type | Expected | Notes | +|------|------|----------|-------| +| Health check | GET /healthz | 200 OK | Always works | +| SQS list-queues | GET X-Service: sqs | 200 or 502 | 502 if service unreachable | +| S3 list-objects | GET X-Service: s3 | 200 or 502 | 502 if service unreachable | +| Memory query | POST X-Service: memory | 200 or 502 | 502 if service unreachable | +| Service not found | GET X-Service: nonexistent | 404 | Routing error | +| Resource not found | GET X-Service: sqs X-Resource: invalid | 404 | Resource error | +| IAM with JWT | GET X-Service: iam + Bearer token | 200 or 502 | Requires valid JWT | +| Missing X-Service | GET (no header) | 404 | Routed to default handler | + +## Canary Deployment Flow + +``` +Current: 3/3 replicas running + ↓ +scale → 1/3 replicas + ↓ +wait for pod ready + ↓ +run integration tests + ├─ PASS → scale → 3/3 replicas ✅ + └─ FAIL → keep 1/3 for debugging ❌ +``` + +## Running in CI + +Add to `.gitea/workflows/ci.yaml`: + +```yaml +- name: Integration Tests + run: | + GATEWAY_URL=https://api.riotpiao.com \ + SKIP_AUTH_TESTS=true \ + TEST_TIMEOUT=30 \ + go test -tags integration -v ./internal/serviceadapter +``` + +## Debugging Failed Tests + +If a test fails: + +1. **Check pod logs:** + ```bash + kubectl -n api logs -l app=api-gateway --tail=50 + ``` + +2. **Check service availability:** + ```bash + kubectl get svc -A | grep -E "sqs|minio|authentik|poimen" + ``` + +3. **Test service directly:** + ```bash + kubectl -n sqs port-forward svc/management-service 9090:9090 + curl http://localhost:9090/sqs/queues + ``` + +4. **Check ConfigMap:** + ```bash + kubectl get configmap api-gateway-config -n api -o yaml | grep -A50 "adapters:" + ``` + +## Notes + +- Auth tests are skipped by default (`SKIP_AUTH_TESTS=true`) +- To test with JWT, set `TEST_JWT_TOKEN` and `SKIP_AUTH_TESTS=false` +- Services in different namespaces may not be reachable from the gateway (NetworkPolicy) +- Canary tests expect `/healthz` endpoint to be available diff --git a/internal/serviceadapter/integration_test.go b/internal/serviceadapter/integration_test.go new file mode 100644 index 0000000..e3806e7 --- /dev/null +++ b/internal/serviceadapter/integration_test.go @@ -0,0 +1,214 @@ +// +build integration + +package serviceadapter + +import ( + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "testing" + "time" +) + +// TestIntegration tests the real gateway deployment. +// Run with: go test -tags integration -v ./internal/serviceadapter +func TestIntegration(t *testing.T) { + // Load config from .dev.test + gatewayURL := os.Getenv("GATEWAY_URL") + if gatewayURL == "" { + gatewayURL = "http://localhost:8080" + } + + testJWT := os.Getenv("TEST_JWT_TOKEN") + skipAuth := os.Getenv("SKIP_AUTH_TESTS") == "true" + + timeoutStr := os.Getenv("TEST_TIMEOUT") + timeout := 10 + if t, err := strconv.Atoi(timeoutStr); err == nil { + timeout = t + } + + client := &http.Client{Timeout: time.Duration(timeout) * time.Second} + + t.Run("health check", func(t *testing.T) { + resp, err := client.Get(gatewayURL + "/healthz") + if err != nil { + t.Fatalf("health check failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("health check returned %d: %s", resp.StatusCode, string(body)) + } + }) + + t.Run("public service - sqs", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "sqs") + req.Header.Set("X-Resource", "list-queues") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusBadGateway { + t.Logf("sqs service not reachable (expected in test env): %d", resp.StatusCode) + return // Service might not be reachable from outside cluster + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + t.Logf("sqs returned %d: %s", resp.StatusCode, string(body)) + } + }) + + t.Run("public service - s3", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "s3") + req.Header.Set("X-Resource", "list-objects") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusBadGateway { + t.Logf("s3 service not reachable (expected in test env): %d", resp.StatusCode) + return + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + t.Logf("s3 returned %d: %s", resp.StatusCode, string(body)) + } + }) + + t.Run("public service - memory", func(t *testing.T) { + req, err := http.NewRequest("POST", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "memory") + req.Header.Set("X-Resource", "query") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusBadGateway { + t.Logf("memory service not reachable (expected in test env): %d", resp.StatusCode) + return + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + t.Logf("memory returned %d: %s", resp.StatusCode, string(body)) + } + }) + + t.Run("service not found", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "nonexistent") + req.Header.Set("X-Resource", "foo") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected 404, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + if !strings.Contains(string(body), "not found") { + t.Errorf("expected 'not found' in response, got: %s", string(body)) + } + }) + + t.Run("resource not found", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "sqs") + req.Header.Set("X-Resource", "invalid-resource") + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected 404, got %d", resp.StatusCode) + } + }) + + if !skipAuth && testJWT != "" { + t.Run("iam service with valid JWT", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Service", "iam") + req.Header.Set("X-Resource", "list-roles") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", testJWT)) + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusBadGateway { + t.Logf("iam service not reachable: %d", resp.StatusCode) + return + } + + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + t.Logf("iam returned %d: %s", resp.StatusCode, string(body)) + } + }) + } else { + t.Log("Skipping auth tests (set TEST_JWT_TOKEN to enable)") + } + + t.Run("missing X-Service header", func(t *testing.T) { + req, err := http.NewRequest("GET", gatewayURL+"/", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + // No X-Service header + + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + // Should route through other paths, not via X-Service dispatcher + // This is expected - no error, just not dispatched + t.Logf("Request without X-Service returned %d", resp.StatusCode) + }) +} diff --git a/scripts/test-canary.sh b/scripts/test-canary.sh new file mode 100755 index 0000000..f7087f9 --- /dev/null +++ b/scripts/test-canary.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Canary deployment test: +# 1. Scale to 1 pod +# 2. Wait for pod to be ready +# 3. Run integration tests +# 4. If pass, scale to 3 pods +# 5. If fail, keep at 1 pod for debugging + +set -e + +NAMESPACE=${NAMESPACE:-api} +DEPLOYMENT=${DEPLOYMENT:-api-gateway} +REPLICAS=${REPLICAS:-3} +GATEWAY_URL=${GATEWAY_URL:-https://api.riotpiao.com} + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "CANARY TEST: $DEPLOYMENT" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# Step 1: Scale to 1 pod +echo "[1/4] Scaling to 1 canary pod..." +kubectl -n "$NAMESPACE" scale deploy "$DEPLOYMENT" --replicas=1 +kubectl -n "$NAMESPACE" rollout status deploy "$DEPLOYMENT" --timeout=2m + +echo "[2/4] Waiting for pod to be ready..." +sleep 5 + +# Step 2: Run integration tests +echo "[3/4] Running integration tests..." +export GATEWAY_URL="$GATEWAY_URL" +export SKIP_AUTH_TESTS=true +export TEST_TIMEOUT=30 + +if ! go test -tags integration -v ./internal/serviceadapter -run TestIntegration; then + echo "" + echo "❌ Integration tests FAILED" + echo "Keeping 1 canary pod for debugging." + echo "Pod logs:" + kubectl -n "$NAMESPACE" logs -l app="$DEPLOYMENT" --tail=50 + exit 1 +fi + +echo "" +echo "✅ Integration tests PASSED" + +# Step 3: Scale back to full replicas +echo "[4/4] Scaling back to $REPLICAS pods..." +kubectl -n "$NAMESPACE" scale deploy "$DEPLOYMENT" --replicas="$REPLICAS" +kubectl -n "$NAMESPACE" rollout status deploy "$DEPLOYMENT" --timeout=5m + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ Canary test complete. Rollout successful." +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh new file mode 100755 index 0000000..9238bb3 --- /dev/null +++ b/scripts/test-integration.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Run integration tests against real gateway +# Usage: +# ./scripts/test-integration.sh # Test local gateway +# GATEWAY_URL=https://api.riotpiao.com ./scripts/test-integration.sh # Test production + +set -e + +GATEWAY_URL=${GATEWAY_URL:-http://localhost:8080} +SKIP_AUTH_TESTS=${SKIP_AUTH_TESTS:-true} +TEST_TIMEOUT=${TEST_TIMEOUT:-10} + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Integration Tests" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Gateway URL: $GATEWAY_URL" +echo "Skip Auth Tests: $SKIP_AUTH_TESTS" +echo "Timeout: ${TEST_TIMEOUT}s" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +export GATEWAY_URL +export SKIP_AUTH_TESTS +export TEST_TIMEOUT + +go test -tags integration -v ./internal/serviceadapter -run TestIntegration