From c00c807bb222dea1d8f1b2a8f28eadb3456a5221 Mon Sep 17 00:00:00 2001 From: "@tanya_r" Date: Fri, 19 Jun 2026 20:03:31 -0300 Subject: [PATCH] feat(sandbox): drive the full payment lifecycle in the compose smoke Add a demo that exercises a payment end to end against the sandbox: it obtains a client-credentials token, initiates a payment, waits for it to reach SUBMITTED on its own, then delivers an HMAC-signed webhook and asserts the payment settles. The webhook secret is wired into the payments service, the signature is computed in-script over the exact posted body, and the compose smoke runs the journey so a regression in orchestration or webhook verification fails CI. Secrets come from the environment with documented sandbox defaults. Closes #236 --- .github/workflows/ci.yml | 7 ++++ .gitleaks.toml | 6 ++- docker-compose.yml | 1 + scripts/demo-payment.sh | 84 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 scripts/demo-payment.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38f3e97..f5dfa79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,6 +142,10 @@ jobs: permissions: contents: read + # Shared by `docker compose up` (the payments service) and demo-payment.sh so both sign/verify with the same secret. + env: + FINCORE_PAYMENTS_WEBHOOK_HMACSECRET: sandbox-webhook-secret + # Step order is load-bearing: smoke -> logs (on failure) -> teardown (always). # `docker compose down` removes containers, so logs must be dumped before it. steps: @@ -159,6 +163,9 @@ jobs: - name: Auth chain proof (token -> protected endpoint -> 200) run: bash scripts/demo-auth.sh + - name: Payment lifecycle demo (initiate -> SUBMITTED -> webhook -> SETTLED) + run: bash scripts/demo-payment.sh + - name: Dump container logs on failure if: failure() run: docker compose logs --no-color diff --git a/.gitleaks.toml b/.gitleaks.toml index 1e7483b..859e852 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -3,8 +3,12 @@ useDefault = true [allowlist] -description = "Sandbox demo Keycloak realm + token-proof script: a documented, intentionally public demo client secret, not a real credential" +description = "Documented, intentionally public sandbox demo secrets (Keycloak realm + demo scripts + compose webhook secret), not real credentials" paths = [ '''^deploy/keycloak/fincore-realm\.json$''', '''^scripts/demo-auth\.sh$''', + '''^scripts/demo-payment\.sh$''', +] +regexes = [ + '''sandbox-webhook-secret''', ] diff --git a/docker-compose.yml b/docker-compose.yml index 234a8ab..912a67d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -73,6 +73,7 @@ services: SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-ledger} KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/fincore FINCORE_PAYMENTS_BANK_SANDBOX_ENABLED: "true" + FINCORE_PAYMENTS_WEBHOOK_HMACSECRET: ${FINCORE_PAYMENTS_WEBHOOK_HMACSECRET:-sandbox-webhook-secret} OTLP_TRACING_ENDPOINT: http://otel-collector:4318/v1/traces web: diff --git a/scripts/demo-payment.sh b/scripts/demo-payment.sh new file mode 100644 index 0000000..7b4492c --- /dev/null +++ b/scripts/demo-payment.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: BUSL-1.1 +# SPDX-FileCopyrightText: 2026 FinCore Engine Authors +set -euo pipefail + +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8085}" +PAYMENTS_URL="${PAYMENTS_URL:-http://localhost:8081}" +CLIENT_ID="${FINCORE_DEMO_CLIENT_ID:-fincore-demo}" +CLIENT_SECRET="${FINCORE_DEMO_CLIENT_SECRET:-fincore-demo-secret}" +WEBHOOK_SECRET="${FINCORE_PAYMENTS_WEBHOOK_HMACSECRET:-sandbox-webhook-secret}" +TOKEN_ENDPOINT="$KEYCLOAK_URL/realms/fincore/protocol/openid-connect/token" + +uuid() { uuidgen | tr '[:upper:]' '[:lower:]'; } +extract() { sed -n "s/.*\"$1\":\"\\([^\"]*\\)\".*/\\1/p"; } + +status_of() { + curl -fsS -H "Authorization: Bearer $token" "$PAYMENTS_URL/v1/payments/$1" | extract status +} + +token=$( + curl -fsS -X POST "$TOKEN_ENDPOINT" \ + -d grant_type=client_credentials -d "client_id=$CLIENT_ID" -d "client_secret=$CLIENT_SECRET" | + extract access_token +) +[ -n "$token" ] || { + echo "FAIL: could not obtain a token" + exit 1 +} + +created=$( + curl -fsS -X POST "$PAYMENTS_URL/v1/payments" \ + -H "Authorization: Bearer $token" \ + -H "Idempotency-Key: $(uuid)" \ + -H "Content-Type: application/json" \ + --data-binary '{"amount":"100.00","currency":"USD","reference":"demo-e2e"}' +) +payment_id=$(printf '%s' "$created" | extract id) +[ -n "$payment_id" ] || { + echo "FAIL: no payment id in response: $created" + exit 1 +} +echo "initiated $payment_id" + +submitted=false +for _ in $(seq 1 30); do + status=$(status_of "$payment_id") + [ "$status" = "SUBMITTED" ] && { + submitted=true + break + } + [ "$status" = "FAILED" ] && { + echo "FAIL: payment FAILED during orchestration" + exit 1 + } + sleep 1 +done +[ "$submitted" = true ] || { + echo "FAIL: payment did not reach SUBMITTED" + exit 1 +} +echo "submitted $payment_id" + +body="{\"deliveryId\":\"$(uuid)\",\"providerReference\":\"sbx-$payment_id\",\"outcome\":\"SETTLED\"}" +signature=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.*= //') +[ -n "$signature" ] || { + echo "FAIL: could not compute the webhook signature" + exit 1 +} +curl -fsS -X POST "$PAYMENTS_URL/v1/payments/webhooks" \ + -H "X-Webhook-Signature: $signature" -H "Content-Type: application/json" --data-binary "$body" >/dev/null + +settled=false +for _ in $(seq 1 15); do + [ "$(status_of "$payment_id")" = "SETTLED" ] && { + settled=true + break + } + sleep 1 +done +[ "$settled" = true ] || { + echo "FAIL: payment did not reach SETTLED after the webhook" + exit 1 +} +echo "payment lifecycle OK: $payment_id INITIATED -> SUBMITTED -> SETTLED"