Source-agnostic license issuance service. FastAPI app fronts a Postgres `licenses` table; the only currently-wired source is `manual` (operator mints via /internal/mint). Gumroad webhook adapter lands in PR 2. Key design points: - Signing reuses src/license/crypto.py via a COPY into the image (single source of truth — blobs minted server-side verify against the same embedded pubkey on the buyer's machine). - Source adapter Protocol (app/adapters/base.py) is the seam for Gumroad / Lemon Squeezy / Stripe in later PRs; Mint API speaks only SaleEvent / RefundEvent. - (source, source_order_id) UNIQUE composite gives idempotent webhook retries without double-mint. - JSONB type uses with_variant(JSON, 'sqlite') so the same models drive both Postgres prod and SQLite tests (no testcontainers dep). - Bearer-token auth on /internal/*; the IP-loopback guard was removed after the docker bridge made it fight legitimate prod traffic (nginx defense + Bearer remain). - Secrets resolved via *_FILE env vars pointing at /run/secrets/<name>, so passwords never appear in `docker inspect`. 21 unit tests (SQLite in-memory, StaticPool) plus a real-Postgres docker-compose smoke test in server/scripts/smoke.sh that builds the image, runs the alembic migration, mints a license, verifies the signature against the host dev pubkey, and checks the DB row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
2.6 KiB
Bash
Executable File
85 lines
2.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end smoke test for the license server.
|
|
#
|
|
# Builds the API image, brings up Postgres + API, runs the Alembic
|
|
# migration, mints a license through /internal/mint, verifies the
|
|
# resulting blob's Ed25519 signature against the dev pubkey, and
|
|
# confirms the row landed in the DB. Tears everything down at exit.
|
|
#
|
|
# Run from the server/ directory: ./scripts/smoke.sh
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
PROJECT=dt-license-smoke
|
|
COMPOSE=(docker compose -p "$PROJECT" -f compose.test.yml)
|
|
|
|
cleanup() {
|
|
echo "--- Tearing down ---"
|
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
echo "--- Building image ---"
|
|
"${COMPOSE[@]}" build
|
|
|
|
echo "--- Starting stack ---"
|
|
"${COMPOSE[@]}" up -d
|
|
|
|
echo "--- Waiting for API health (max 60s) ---"
|
|
for i in $(seq 1 60); do
|
|
if curl -sf http://127.0.0.1:18090/health 2>/dev/null | grep -q '"status":"ok"'; then
|
|
echo "API up after ${i}s"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "--- Running migrations ---"
|
|
"${COMPOSE[@]}" exec -T api alembic upgrade head
|
|
|
|
echo "--- Re-checking health post-migration ---"
|
|
curl -sf http://127.0.0.1:18090/health | tee /dev/stderr | grep -q '"db":"ok"'
|
|
|
|
echo "--- POST /internal/mint ---"
|
|
RESP=$(curl -s -w "\nHTTP=%{http_code}" -X POST http://127.0.0.1:18090/internal/mint \
|
|
-H "Authorization: Bearer test-admin-token" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"Smoke Test","email":"smoke@example.com","tier":"core","source":"manual"}')
|
|
echo "$RESP"
|
|
HTTP_CODE=$(echo "$RESP" | tail -n1 | sed 's/HTTP=//')
|
|
RESP=$(echo "$RESP" | sed '$d')
|
|
if [ "$HTTP_CODE" != "201" ]; then
|
|
echo "MINT FAILED (HTTP $HTTP_CODE)"
|
|
"${COMPOSE[@]}" logs --tail 50 api
|
|
exit 1
|
|
fi
|
|
echo "$RESP" | python3 -m json.tool | head -8
|
|
|
|
BLOB=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["blob"])')
|
|
|
|
echo "--- Verifying blob signature against host dev pubkey ---"
|
|
python3 - <<EOF
|
|
import sys
|
|
sys.path.insert(0, "..")
|
|
from src.license.crypto import decode_blob, verify
|
|
payload = decode_blob("$BLOB")
|
|
sig = payload.pop("signature")
|
|
assert verify(payload, sig), "signature must verify"
|
|
assert payload["name"] == "Smoke Test"
|
|
assert payload["email"] == "smoke@example.com"
|
|
assert payload["tier"] == "core"
|
|
print("OK: signature verifies, payload matches")
|
|
EOF
|
|
|
|
echo "--- Verifying DB row ---"
|
|
"${COMPOSE[@]}" exec -T postgres \
|
|
psql -U dt_test -d dt_test -t -c \
|
|
"SELECT license_key, email, tier, source FROM licenses;" \
|
|
| grep -q smoke@example.com
|
|
|
|
echo
|
|
echo "===================================="
|
|
echo " SMOKE TEST PASSED"
|
|
echo "===================================="
|