Merge pull request 'feat(prices): commit smoke test scaffold for /v1/prices (Phase A)' (#173) from issue-161-smoke-scaffold into main
feat(prices): commit smoke test scaffold for /v1/prices (Phase A) (#173) Refs #161
This commit is contained in:
commit
67c48029a0
2 changed files with 191 additions and 0 deletions
87
tests/smoke/README.md
Normal file
87
tests/smoke/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Smoke tests
|
||||||
|
|
||||||
|
End-to-end smoke scripts that hit the **deployed** `maximus-api` from outside
|
||||||
|
the simpl-resultat process. They are run manually as part of the ship checklist
|
||||||
|
before each release tag, and serve as a reproducible witness that the
|
||||||
|
production `/v1/prices` endpoint matches the contract in
|
||||||
|
`docs/api-contract-prices.md`.
|
||||||
|
|
||||||
|
These are not run by `npm test` or `cargo test` — they would either depend on
|
||||||
|
network reachability or leak a real license token into CI. Keep them as a
|
||||||
|
manual gate, owned by the person cutting the release.
|
||||||
|
|
||||||
|
## prices.sh
|
||||||
|
|
||||||
|
Issue: #161 (price-fetching production wiring + release smoke).
|
||||||
|
|
||||||
|
Hits `/v1/prices` on the deployed maximus-api with four cases:
|
||||||
|
|
||||||
|
| # | Case | Expected |
|
||||||
|
|---|-----------------------------|---------------------------------------|
|
||||||
|
| 1 | Stock happy path (AAPL) | HTTP 200 with `.price > 0` |
|
||||||
|
| 2 | Crypto happy path (BTC) | HTTP 200 with `.price > 0` |
|
||||||
|
| 3 | Invalid symbol | HTTP 404 with `error.code = "symbol_not_found"` |
|
||||||
|
| 4 | Missing `Authorization` | HTTP 401 with `error.code = "missing_token"` |
|
||||||
|
|
||||||
|
The script exits non-zero on the first failure (`set -euo pipefail`). It
|
||||||
|
needs `bash`, `curl`, `jq`, and GNU `date` (Linux). On macOS, override
|
||||||
|
`SMOKE_DATE` manually.
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MAXIMUS_API_TEST_TOKEN=<premium-test-license-token>
|
||||||
|
./tests/smoke/prices.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Purpose |
|
||||||
|
|---------------------------|----------------------------------------|--------------------------------------------------------|
|
||||||
|
| `MAXIMUS_API_TEST_TOKEN` | (required) | Bearer token issued for a premium test license |
|
||||||
|
| `MAXIMUS_API_URL` | `https://api.lacompagniemaximus.com` | Override target — useful for hitting a staging host |
|
||||||
|
| `SMOKE_DATE` | yesterday (`date -d 'yesterday'`) | YYYY-MM-DD; pick a weekday for stock data availability |
|
||||||
|
|
||||||
|
### Obtaining `MAXIMUS_API_TEST_TOKEN`
|
||||||
|
|
||||||
|
The token must be a premium-tier license (the free tier returns 403 from
|
||||||
|
`/v1/prices`). Two paths:
|
||||||
|
|
||||||
|
1. **Admin endpoint (preferred, post maximus-api implementation)** —
|
||||||
|
`POST /admin/licenses` on maximus-api, gated by `ADMIN_SECRET`, returns a
|
||||||
|
freshly minted premium license token. This endpoint is **not yet
|
||||||
|
implemented**; tracked as a follow-up issue in the maximus-api repo.
|
||||||
|
|
||||||
|
2. **Manual provisioning (current workaround)** — until the admin endpoint
|
||||||
|
ships, issue the token by hand:
|
||||||
|
- Generate an Ed25519-signed JWT with the same private key the production
|
||||||
|
maximus-api uses (`MAXIMUS_API_LICENSE_PRIVATE_KEY` on the server).
|
||||||
|
- Set `tier: "premium"`, `iat: now`, `exp: now + 30 days`,
|
||||||
|
`license_id: "smoke-test-<date>"`, `machine_id: "smoke-test"`.
|
||||||
|
- Sign with the private key, base64url-encode the result.
|
||||||
|
- Store it locally in your shell (do **not** commit it).
|
||||||
|
|
||||||
|
The `dist/scripts/issue-test-token.mjs` helper inside the maximus-api repo
|
||||||
|
can do this in one shot once it lands; see that repo's `docs/` for usage.
|
||||||
|
|
||||||
|
Never commit the token. `.gitignore` does not need an entry — the script reads
|
||||||
|
it from the env, and there is nothing that writes the value to disk.
|
||||||
|
|
||||||
|
## Status (2026-04-28)
|
||||||
|
|
||||||
|
`/v1/prices` is **not yet deployed** in prod (see issue #161 Phase A). The
|
||||||
|
smoke script will fail on case 1 with HTTP 404 until the maximus-api
|
||||||
|
`prices-proxy` milestone ships. That is expected. Once unblocked, the same
|
||||||
|
script becomes the gate for Phase B (cut `v0.9.0` release).
|
||||||
|
|
||||||
|
To verify the script's shape today against the live host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl -i https://api.lacompagniemaximus.com/healthz
|
||||||
|
HTTP/2 200
|
||||||
|
{"status":"ok"}
|
||||||
|
|
||||||
|
$ curl -i https://api.lacompagniemaximus.com/v1/prices?symbol=AAPL
|
||||||
|
HTTP/2 404
|
||||||
|
{"error":"Not found"} # expected during Phase A
|
||||||
|
```
|
||||||
104
tests/smoke/prices.sh
Executable file
104
tests/smoke/prices.sh
Executable file
|
|
@ -0,0 +1,104 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke test — /v1/prices end-to-end against the deployed maximus-api.
|
||||||
|
#
|
||||||
|
# Issue #161 (Phase A): committed for reproducibility before each release.
|
||||||
|
# Issue #161 (Phase B): runnable once maximus-api/prices-proxy ships.
|
||||||
|
# Until then, /v1/prices returns 404 in prod and this script will exit 1
|
||||||
|
# on the first happy-path call. That is expected during Phase A.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# MAXIMUS_API_TEST_TOKEN=<premium-license-token> ./tests/smoke/prices.sh
|
||||||
|
#
|
||||||
|
# Optional overrides:
|
||||||
|
# MAXIMUS_API_URL (default: https://api.lacompagniemaximus.com)
|
||||||
|
# SMOKE_DATE (default: yesterday — pick a weekday for stocks)
|
||||||
|
#
|
||||||
|
# Dependencies: bash 4+, curl, jq, GNU `date -d` (Linux). On macOS, set
|
||||||
|
# SMOKE_DATE manually instead of relying on the default.
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 all 4 cases pass
|
||||||
|
# 1 any single case fails (the script is set -e; first failure aborts)
|
||||||
|
# 2 required env var missing
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API_URL="${MAXIMUS_API_URL:-https://api.lacompagniemaximus.com}"
|
||||||
|
TOKEN="${MAXIMUS_API_TEST_TOKEN:-}"
|
||||||
|
DATE="${SMOKE_DATE:-$(date -d 'yesterday' +%Y-%m-%d)}"
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "error: MAXIMUS_API_TEST_TOKEN must be set to a premium test license token." >&2
|
||||||
|
echo " See tests/smoke/README.md for how to obtain one." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP=$(mktemp)
|
||||||
|
trap 'rm -f "$TMP"' EXIT
|
||||||
|
|
||||||
|
pass() { echo " pass: $1"; }
|
||||||
|
fail() { echo " FAIL: $1" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "smoke: $API_URL/v1/prices (date=$DATE)"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Case 1 — stock happy path
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
echo "[1/4] stock happy path (AAPL)"
|
||||||
|
if ! curl -fsS \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-o "$TMP" \
|
||||||
|
"$API_URL/v1/prices?symbol=AAPL&date=$DATE"; then
|
||||||
|
fail "stock fetch did not return 200 for AAPL"
|
||||||
|
fi
|
||||||
|
if ! jq -e '.price > 0' "$TMP" >/dev/null; then
|
||||||
|
fail "stock response did not contain a positive .price field"
|
||||||
|
fi
|
||||||
|
pass "AAPL .price = $(jq -r '.price' "$TMP")"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Case 2 — crypto happy path
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
echo "[2/4] crypto happy path (BTC)"
|
||||||
|
if ! curl -fsS \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-o "$TMP" \
|
||||||
|
"$API_URL/v1/prices?symbol=BTC&date=$DATE"; then
|
||||||
|
fail "crypto fetch did not return 200 for BTC"
|
||||||
|
fi
|
||||||
|
if ! jq -e '.price > 0' "$TMP" >/dev/null; then
|
||||||
|
fail "crypto response did not contain a positive .price field"
|
||||||
|
fi
|
||||||
|
pass "BTC .price = $(jq -r '.price' "$TMP")"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Case 3 — invalid symbol -> 404 / symbol_not_found
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
echo "[3/4] invalid symbol (NOTREAL_XYZ)"
|
||||||
|
http=$(curl -s -o "$TMP" -w '%{http_code}' \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
"$API_URL/v1/prices?symbol=NOTREAL_XYZ&date=$DATE")
|
||||||
|
if [ "$http" != "404" ]; then
|
||||||
|
fail "invalid symbol expected HTTP 404, got $http"
|
||||||
|
fi
|
||||||
|
if ! jq -e '.error.code == "symbol_not_found"' "$TMP" >/dev/null; then
|
||||||
|
fail "invalid symbol expected error.code = 'symbol_not_found', got: $(cat "$TMP")"
|
||||||
|
fi
|
||||||
|
pass "404 + symbol_not_found"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Case 4 — no auth -> 401 / missing_token
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
echo "[4/4] missing auth"
|
||||||
|
http=$(curl -s -o "$TMP" -w '%{http_code}' \
|
||||||
|
"$API_URL/v1/prices?symbol=AAPL&date=$DATE")
|
||||||
|
if [ "$http" != "401" ]; then
|
||||||
|
fail "missing-auth expected HTTP 401, got $http"
|
||||||
|
fi
|
||||||
|
if ! jq -e '.error.code == "missing_token"' "$TMP" >/dev/null; then
|
||||||
|
fail "missing-auth expected error.code = 'missing_token', got: $(cat "$TMP")"
|
||||||
|
fi
|
||||||
|
pass "401 + missing_token"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "smoke: all 4 cases pass"
|
||||||
Loading…
Reference in a new issue