From e0844f0f34147c25cde115dbb5d0c0f860d409de Mon Sep 17 00:00:00 2001 From: le king fu Date: Tue, 28 Apr 2026 21:31:27 -0400 Subject: [PATCH] feat(prices): commit smoke test scaffold for /v1/prices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A of #161: ships the smoke script and README before the maximus-api prices-proxy endpoint is live in prod. The script will fail on case 1 (HTTP 404) until /v1/prices is implemented and deployed — that is expected; running it is gated to Phase B (after prices-proxy ships, before cutting v0.9.0). Script covers 4 cases: 1. Stock happy path (AAPL) — HTTP 200, .price > 0 2. Crypto happy path (BTC) — HTTP 200, .price > 0 3. Invalid symbol — HTTP 404, error.code=symbol_not_found 4. Missing auth — HTTP 401, error.code=missing_token `set -euo pipefail`, exits non-zero on first failure. Reads token from MAXIMUS_API_TEST_TOKEN env var (never committed). README documents env vars and the two paths for obtaining a premium test token (admin endpoint TODO in maximus-api, manual JWT signing as current workaround). CSP whitelist for https://api.lacompagniemaximus.com is already in place in src-tauri/tauri.conf.json — verified, no change needed. No application code touched; npm test (492) and cargo test --lib (69) remain green. Phase A only (#161) --- tests/smoke/README.md | 87 +++++++++++++++++++++++++++++++++++ tests/smoke/prices.sh | 104 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 tests/smoke/README.md create mode 100755 tests/smoke/prices.sh diff --git a/tests/smoke/README.md b/tests/smoke/README.md new file mode 100644 index 0000000..0ba0f6d --- /dev/null +++ b/tests/smoke/README.md @@ -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= +./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-"`, `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 +``` diff --git a/tests/smoke/prices.sh b/tests/smoke/prices.sh new file mode 100755 index 0000000..9d49d6e --- /dev/null +++ b/tests/smoke/prices.sh @@ -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= ./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"