Skip to content

Commit 8d25926

Browse files
viloforgeviloforge
authored andcommitted
test(lab): add make smoke — fast auth + ingest check of the running lab
scripts/smoke.sh (run via `make smoke`; requires `make lab`): over plain curl + one psql query, asserts the API booted against the TOKEN_TRACKER_OIDC_* config, reached the lab IdP and verified its access tokens, mapped roles (admin / user), 401'd the no-token and bad-token paths, and ingested an OTLP span via /v1/traces whose row took user_id from the verified token (the span's agent.user.id is ignored — D8). No browser, no LLM key. Gets the tokens from the lab IdP's /lab/token shortcut; honours LAB_API / LAB_IDP / PG_CONTAINER. Also refreshed `make help` / `make lab`'s output — it still said "Dex", "login at /auth/login", "lab users … / lab"; the lab now runs the purpose-built IdP with passwordless lab-admin / lab-user / lab-viewer identities. `bash -n` clean; the embedded OTLP payload validates as JSON; `npm run check` (88 tests) unaffected. Not a substitute for the browser MSAL smoke or the pi-turn scenario — see the lab IdP rework commit's notes.
1 parent 1c04a34 commit 8d25926

2 files changed

Lines changed: 109 additions & 9 deletions

File tree

Makefile

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,25 @@ COMPOSE := docker compose --project-directory $(COMPOSE_DIR) \
1010
-f $(COMPOSE_DIR)/compose.yml \
1111
-f $(COMPOSE_DIR)/compose.override.yml
1212

13-
.PHONY: help lab lab-up lab-down reset seed psql logs ps wait-healthy clean
13+
.PHONY: help lab lab-up lab-down reset seed psql smoke logs ps wait-healthy clean
1414

1515
help: ## Show this help.
1616
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-18s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
1717

18-
lab: lab-up wait-healthy ## Bring the lab up (Postgres + Grafana + api + Dex; Collector + bridge as regression fixtures), wait for healthy.
18+
lab: lab-up wait-healthy ## Bring the lab up (Postgres + Grafana + api + SPA + lab IdP; Collector + bridge as regression fixtures), wait for healthy.
1919
@echo
2020
@echo "Lab is up:"
2121
@echo " Grafana: http://localhost:7000 (anonymous viewer; admin/admin to log in)"
22-
@echo " API: http://localhost:7080 (login at /auth/login → Dex; /v1/traces is the production OTLP path)"
22+
@echo " API + SPA: http://localhost:7080 (sign in via the lab IdP; /v1/traces is the OTLP ingest path)"
23+
@echo " lab IdP: http://idp.localhost:7019 (identities: lab-admin / lab-user / lab-viewer — no passwords)"
2324
@echo
2425
@echo " For Postgres: make psql (no host port mapping by design)"
25-
@echo " Dex (lab IdP): http://idp.localhost:7019"
2626
@echo
27-
@echo " Regression fixtures (lab-only since phase 0.3.8 / D6 sunset):"
27+
@echo " Regression fixtures (lab-only — exercise the standard OTel pipeline):"
2828
@echo " Collector OTLP/HTTP: http://localhost:7018"
2929
@echo " bridge: tails Collector JSONL → Postgres (catches OTel-pipeline regressions only)"
3030
@echo
31-
@echo " Lab users: lab-admin@example.invalid / lab"
32-
@echo " lab-user@example.invalid / lab"
33-
@echo
34-
@echo "Run 'make seed' to populate with synthetic data via the regression-fixture path."
31+
@echo "Next: 'make seed' for synthetic data, 'make smoke' for a fast auth + ingest check."
3532

3633
lab-up: ## docker compose up -d
3734
$(COMPOSE) up -d --build postgres collector bridge grafana api idp
@@ -50,6 +47,9 @@ seed: ## Run the synthetic emitter against the local Collector.
5047
psql: ## Open psql against the lab Postgres.
5148
$(COMPOSE) exec postgres psql -U token_tracker -d token_tracker
5249

50+
smoke: ## Fast end-to-end check of the running lab (auth + /v1/traces ingest; no browser/LLM). Requires `make lab`.
51+
@bash scripts/smoke.sh
52+
5353
logs: ## Tail logs (use S=<service> to scope, e.g. make logs S=bridge).
5454
@$(COMPOSE) logs -f $(S)
5555

scripts/smoke.sh

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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

Comments
 (0)