|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# scripts/smoke.sh — fast end-to-end check of the lab (no browser, no LLM key). |
| 4 | +# |
| 5 | +# Requires the lab to be up (`make lab`). Verifies the things the auth rewrite |
| 6 | +# touched, all over HTTP + one psql query: |
| 7 | +# - the API boots against the new TOKEN_TRACKER_OIDC_* config (/health) |
| 8 | +# - it reaches the lab IdP, fetches its JWKs, and verifies access tokens |
| 9 | +# - role mapping: a TokenTracker.Admin token -> role "admin"; .User -> "user" |
| 10 | +# - the unauthenticated / bad-token paths -> 401 with the right error |
| 11 | +# - POST /v1/traces ingests an OTLP span, and the row's user_id comes from |
| 12 | +# the verified token (the span's agent.user.id is ignored — D8) |
| 13 | +# |
| 14 | +# Run: make smoke (or: bash scripts/smoke.sh) |
| 15 | +# |
| 16 | +# Env knobs: LAB_API (default http://localhost:7080), LAB_IDP |
| 17 | +# (default http://localhost:7019), PG_CONTAINER |
| 18 | +# (default token-tracker-postgres-1). |
| 19 | +set -uo pipefail |
| 20 | + |
| 21 | +LAB_API="${LAB_API:-http://localhost:7080}" |
| 22 | +LAB_IDP="${LAB_IDP:-http://localhost:7019}" |
| 23 | +PG_CONTAINER="${PG_CONTAINER:-token-tracker-postgres-1}" |
| 24 | + |
| 25 | +PASS=0 FAIL=0 |
| 26 | +ok() { printf ' \033[32mok \033[0m %s\n' "$*"; PASS=$((PASS + 1)); } |
| 27 | +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=$((FAIL + 1)); } |
| 28 | +_json_field() { python3 -c 'import sys,json; print(json.load(sys.stdin).get(sys.argv[1],""))' "$1" 2>/dev/null; } |
| 29 | +_status() { curl -sS -o /dev/null -w '%{http_code}' "$@" 2>/dev/null || echo "000"; } |
| 30 | +_pg() { docker exec -i "$PG_CONTAINER" psql -U token_tracker -d token_tracker -At -c "$1" 2>/dev/null; } |
| 31 | + |
| 32 | +echo "== lab smoke — API $LAB_API, IdP $LAB_IDP ==" |
| 33 | + |
| 34 | +# --- get tokens from the lab IdP's /lab/token shortcut --- |
| 35 | +ADMIN="$(curl -sS -X POST "$LAB_IDP/lab/token" -d 'user=lab-admin@example.invalid' 2>/dev/null | _json_field access_token)" |
| 36 | +USER="$(curl -sS -X POST "$LAB_IDP/lab/token" -d 'user=lab-user@example.invalid' 2>/dev/null | _json_field access_token)" |
| 37 | +if [[ -z "$ADMIN" || -z "$USER" ]]; then |
| 38 | + echo " could not get tokens from the lab IdP at $LAB_IDP — is the lab up? (make lab)" >&2 |
| 39 | + exit 1 |
| 40 | +fi |
| 41 | +ok "lab IdP issued admin + user access tokens" |
| 42 | + |
| 43 | +# 1. /health |
| 44 | +if curl -sf "$LAB_API/health" 2>/dev/null | grep -q '"status":"ok"'; then ok "/health -> {status:ok}"; else bad "/health (is the api container up + healthy?)"; fi |
| 45 | + |
| 46 | +# 2. /api/me with the admin token -> role admin |
| 47 | +ME_ADMIN="$(curl -sS "$LAB_API/api/me" -H "Authorization: Bearer $ADMIN" 2>/dev/null)" |
| 48 | +if grep -q '"role":"admin"' <<<"$ME_ADMIN" && grep -q 'lab-admin@example.invalid' <<<"$ME_ADMIN"; then |
| 49 | + ok "/api/me (admin token) -> role=admin, email=lab-admin@example.invalid" |
| 50 | +else bad "/api/me (admin token): $ME_ADMIN"; fi |
| 51 | + |
| 52 | +# 3. /api/me with the user token -> role user |
| 53 | +ME_USER="$(curl -sS "$LAB_API/api/me" -H "Authorization: Bearer $USER" 2>/dev/null)" |
| 54 | +if grep -q '"role":"user"' <<<"$ME_USER" && grep -q 'lab-user@example.invalid' <<<"$ME_USER"; then |
| 55 | + ok "/api/me (user token) -> role=user, email=lab-user@example.invalid" |
| 56 | +else bad "/api/me (user token): $ME_USER"; fi |
| 57 | + |
| 58 | +# 4. no token -> 401 "no token" |
| 59 | +if [[ "$(_status "$LAB_API/api/me")" == "401" ]] && curl -sS "$LAB_API/api/me" 2>/dev/null | grep -q '"no token"'; then |
| 60 | + ok "/api/me (no token) -> 401 {error:\"no token\"}" |
| 61 | +else bad "/api/me (no token) didn't 401 with {error:\"no token\"}"; fi |
| 62 | + |
| 63 | +# 5. garbage token -> 401 "invalid token" |
| 64 | +if [[ "$(_status "$LAB_API/api/me" -H 'Authorization: Bearer not.a.jwt')" == "401" ]] \ |
| 65 | + && curl -sS "$LAB_API/api/me" -H 'Authorization: Bearer not.a.jwt' 2>/dev/null | grep -q '"invalid token"'; then |
| 66 | + ok "/api/me (garbage token) -> 401 {error:\"invalid token\"}" |
| 67 | +else bad "/api/me (garbage token) didn't 401 with {error:\"invalid token\"}"; fi |
| 68 | + |
| 69 | +# 6. POST /v1/traces with the user token — and the span lies about its user_id |
| 70 | +OTLP='{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"smoke"}}]},"scopeSpans":[{"scope":{"name":"smoke"},"spans":[{"startTimeUnixNano":"1700000000000000000","attributes":[ |
| 71 | +{"key":"agent.harness.name","value":{"stringValue":"smoke"}}, |
| 72 | +{"key":"gen_ai.request.model","value":{"stringValue":"glm-4.6"}}, |
| 73 | +{"key":"gen_ai.response.model","value":{"stringValue":"glm-4.6"}}, |
| 74 | +{"key":"gen_ai.provider.name","value":{"stringValue":"z.ai"}}, |
| 75 | +{"key":"agent.api.dialect","value":{"stringValue":"anthropic-messages"}}, |
| 76 | +{"key":"agent.machine.id","value":{"stringValue":"11111111-1111-1111-1111-111111111111"}}, |
| 77 | +{"key":"agent.session.id","value":{"stringValue":"22222222-2222-2222-2222-222222222222"}}, |
| 78 | +{"key":"gen_ai.usage.input_tokens","value":{"intValue":1}}, |
| 79 | +{"key":"gen_ai.usage.output_tokens","value":{"intValue":1}}, |
| 80 | +{"key":"agent.cost.input.usd","value":{"doubleValue":0}}, |
| 81 | +{"key":"agent.cost.output.usd","value":{"doubleValue":0}}, |
| 82 | +{"key":"agent.cost.total.usd","value":{"doubleValue":0}}, |
| 83 | +{"key":"agent.user.id","value":{"stringValue":"forged@example.invalid"}} |
| 84 | +]}]}]}]}' |
| 85 | +TRACES="$(curl -sS -X POST "$LAB_API/v1/traces" -H "Authorization: Bearer $USER" -H 'content-type: application/json' --data "$OTLP" 2>/dev/null)" |
| 86 | +if grep -q 'partialSuccess' <<<"$TRACES"; then ok "POST /v1/traces (user token) -> partialSuccess"; else bad "POST /v1/traces: $TRACES"; fi |
| 87 | + |
| 88 | +# 7. the row landed with user_id from the token, not from agent.user.id |
| 89 | +if command -v docker >/dev/null 2>&1; then |
| 90 | + ROW_USER="$(_pg "SELECT user_id FROM usage_log WHERE harness_name = 'smoke' ORDER BY id DESC LIMIT 1")" |
| 91 | + if [[ "$ROW_USER" == "lab-user@example.invalid" ]]; then |
| 92 | + ok "usage_log: latest smoke row user_id=lab-user@example.invalid (agent.user.id=forged@… ignored — D8)" |
| 93 | + else bad "usage_log latest smoke row user_id is '$ROW_USER' (expected lab-user@example.invalid)"; fi |
| 94 | +else |
| 95 | + echo " (skipped the usage_log check — docker not on PATH; run \`make psql\` and look for harness_name='smoke')" |
| 96 | +fi |
| 97 | + |
| 98 | +echo |
| 99 | +echo "== $PASS passed, $FAIL failed ==" |
| 100 | +[[ "$FAIL" -eq 0 ]] |
0 commit comments