-
Notifications
You must be signed in to change notification settings - Fork 0
Gateway E2E verification: add full-stack smoke test + CI workflow #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
devin-ai-integration
wants to merge
3
commits into
devin/1782450422-integration
Choose a base branch
from
devin/1782450504-gateway-e2e-ci
base: devin/1782450422-integration
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, master] | ||
| pull_request: | ||
|
|
||
| concurrency: | ||
| group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| build: | ||
| name: Build (Release) | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup .NET SDK | ||
| uses: actions/setup-dotnet@v4 | ||
| with: | ||
| global-json-file: src/global.json | ||
|
|
||
| - name: Build gateway + all services (Release) | ||
| working-directory: src | ||
| run: | | ||
| set -euo pipefail | ||
| for proj in \ | ||
| ApiGateway/ApiGateway.csproj \ | ||
| Services/Identity/Identity.API/Identity.API.csproj \ | ||
| Services/Customer/Customer.API/Customer.API.csproj \ | ||
| Services/Order/Order.API/Order.API.csproj \ | ||
| Services/Product/Product.API/Product.API.csproj \ | ||
| Services/Notification/Notification.API/Notification.API.csproj; do | ||
| echo "::group::build $proj" | ||
| dotnet build "$proj" -c Release --nologo | ||
| echo "::endgroup::" | ||
| done | ||
|
|
||
| e2e: | ||
| name: Full-stack E2E smoke (gateway) | ||
| runs-on: ubuntu-latest | ||
| needs: build | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Run E2E smoke test | ||
| run: ./scripts/e2e-smoke.sh | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Full-stack cross-service E2E smoke test for QuickApp microservices. | ||
| # | ||
| # Brings up the entire docker compose stack (postgres, rabbitmq, the 5 services | ||
| # and the YARP API gateway), waits for readiness, then asserts end-to-end that: | ||
| # * every service answers health 200 through the gateway | ||
| # * every protected resource returns 401 without a token through the gateway | ||
| # * Identity issues a JWT via the password grant through the gateway | ||
| # * that JWT is accepted (200) by Customer, Product and Order through the gateway | ||
| # * a tampered/invalid token is rejected (401) | ||
| # | ||
| # The stack is always torn down on exit. The script exits non-zero on any failure | ||
| # and is safe to re-run (idempotent: it tears down any prior stack first). | ||
| set -euo pipefail | ||
|
|
||
| # --- configuration ---------------------------------------------------------- | ||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" | ||
| COMPOSE_FILE="${REPO_ROOT}/src/docker-compose.yml" | ||
| GATEWAY="${GATEWAY_URL:-http://localhost:5000}" | ||
|
|
||
| # Demo credentials (dev-only; see canonical JWT contract). | ||
| LOGIN_USER="${LOGIN_USER:-admin@quickapp.local}" | ||
| LOGIN_PASS="${LOGIN_PASS:-Pa\$\$w0rd!}" | ||
|
|
||
| # Services exposed through the gateway: <route-prefix> | ||
| SERVICES=(identity customers orders products notifications) | ||
| # Services that must accept the cross-service JWT. | ||
| AUTHED_SERVICES=(customers products orders) | ||
|
|
||
| READY_TIMEOUT="${READY_TIMEOUT:-180}" | ||
|
|
||
| # --- helpers ---------------------------------------------------------------- | ||
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' | ||
| FAILURES=0 | ||
|
|
||
| log() { printf '%b\n' "$*"; } | ||
| pass() { log "${GREEN}PASS${NC} $*"; } | ||
| fail() { log "${RED}FAIL${NC} $*"; FAILURES=$((FAILURES + 1)); } | ||
| info() { log "${YELLOW}==>${NC} $*"; } | ||
|
|
||
| compose() { docker compose -f "${COMPOSE_FILE}" "$@"; } | ||
|
|
||
| http_code() { | ||
| # http_code <url> [auth-header-value] | ||
| local url="$1"; shift || true | ||
| if [[ $# -gt 0 && -n "$1" ]]; then | ||
| curl -s -o /dev/null -w '%{http_code}' -H "Authorization: $1" "${url}" | ||
| else | ||
| curl -s -o /dev/null -w '%{http_code}' "${url}" | ||
| fi | ||
| } | ||
|
|
||
| assert_code() { | ||
| # assert_code <description> <expected> <actual> | ||
| local desc="$1" expected="$2" actual="$3" | ||
| if [[ "${actual}" == "${expected}" ]]; then | ||
| pass "${desc} (${actual})" | ||
| else | ||
| fail "${desc} — expected ${expected}, got ${actual}" | ||
| fi | ||
| } | ||
|
|
||
| cleanup() { | ||
| info "Tearing down stack" | ||
| compose down -v --remove-orphans >/dev/null 2>&1 || true | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # --- bring up --------------------------------------------------------------- | ||
| info "Tearing down any existing stack (idempotency)" | ||
| compose down -v --remove-orphans >/dev/null 2>&1 || true | ||
|
|
||
| info "Building and starting full stack" | ||
| compose up --build -d | ||
|
|
||
| info "Waiting for gateway + all services to be ready (timeout ${READY_TIMEOUT}s)" | ||
| deadline=$(( $(date +%s) + READY_TIMEOUT )) | ||
| until [[ "$(http_code "${GATEWAY}/api/identity/healthz")" == "200" ]]; do | ||
| if [[ $(date +%s) -ge ${deadline} ]]; then | ||
| fail "Gateway did not become ready within ${READY_TIMEOUT}s" | ||
| compose ps | ||
| compose logs --tail=50 || true | ||
| exit 1 | ||
| fi | ||
| sleep 3 | ||
| done | ||
| # Give the remaining services a brief moment to finish migrations/startup. | ||
| for s in "${SERVICES[@]}"; do | ||
| until [[ "$(http_code "${GATEWAY}/api/${s}/healthz")" == "200" ]]; do | ||
| if [[ $(date +%s) -ge ${deadline} ]]; then break; fi | ||
| sleep 2 | ||
| done | ||
| done | ||
|
|
||
| # --- assertions: health 200 ------------------------------------------------- | ||
| info "Asserting per-service health (200) through the gateway" | ||
| for s in "${SERVICES[@]}"; do | ||
| assert_code "health ${s}" 200 "$(http_code "${GATEWAY}/api/${s}/healthz")" | ||
| done | ||
|
|
||
| # --- assertions: unauth 401 ------------------------------------------------- | ||
| info "Asserting per-service unauthenticated access (401) through the gateway" | ||
| for s in "${SERVICES[@]}"; do | ||
| assert_code "unauth ${s}" 401 "$(http_code "${GATEWAY}/api/${s}")" | ||
| done | ||
|
|
||
| # --- login: obtain JWT ------------------------------------------------------ | ||
| info "Logging in via Identity (password grant) through the gateway" | ||
| LOGIN_RESPONSE="$(curl -s -w '\n%{http_code}' -X POST "${GATEWAY}/api/identity/connect/token" \ | ||
| -H 'Content-Type: application/x-www-form-urlencoded' \ | ||
| --data-urlencode 'grant_type=password' \ | ||
| --data-urlencode "username=${LOGIN_USER}" \ | ||
| --data-urlencode "password=${LOGIN_PASS}")" | ||
| LOGIN_STATUS="$(printf '%s' "${LOGIN_RESPONSE}" | tail -n1)" | ||
| LOGIN_BODY="$(printf '%s' "${LOGIN_RESPONSE}" | sed '$d')" | ||
| assert_code "identity login" 200 "${LOGIN_STATUS}" | ||
|
|
||
| TOKEN="$(printf '%s' "${LOGIN_BODY}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')" | ||
| if [[ -n "${TOKEN}" ]]; then | ||
| pass "received access_token" | ||
| else | ||
| fail "no access_token in login response: ${LOGIN_BODY}" | ||
| fi | ||
|
|
||
| # --- assertions: cross-service authed 200 ----------------------------------- | ||
| info "Asserting cross-service authenticated access (200) through the gateway" | ||
| for s in "${AUTHED_SERVICES[@]}"; do | ||
| assert_code "authed ${s}" 200 "$(http_code "${GATEWAY}/api/${s}" "Bearer ${TOKEN}")" | ||
| done | ||
|
|
||
| # --- assertions: invalid token 401 ------------------------------------------ | ||
| info "Asserting tampered/invalid token rejected (401) through the gateway" | ||
| INVALID_TOKEN="${TOKEN%?}X" # corrupt the signature | ||
| [[ "${INVALID_TOKEN}" == "${TOKEN}" || -z "${TOKEN}" ]] && INVALID_TOKEN="not.a.real.token" | ||
| for s in "${AUTHED_SERVICES[@]}"; do | ||
| assert_code "invalid-token ${s}" 401 "$(http_code "${GATEWAY}/api/${s}" "Bearer ${INVALID_TOKEN}")" | ||
| done | ||
|
|
||
| # --- result ----------------------------------------------------------------- | ||
| echo | ||
| if [[ ${FAILURES} -eq 0 ]]; then | ||
| log "${GREEN}E2E SMOKE PASSED${NC} — all assertions succeeded." | ||
| exit 0 | ||
| else | ||
| log "${RED}E2E SMOKE FAILED${NC} — ${FAILURES} assertion(s) failed." | ||
| compose ps | ||
| compose logs --tail=80 || true | ||
| exit 1 | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.