Sergent renameSync() rotates reports/ -> reports/archive/ at 07:30 UTC daily, so for ~22h per day the only copy of a fresh scan lives in archive/. The handler now scans both directories and concatenates with top-level priority on filename collision. archive/ missing is a silent skip. Tests : 17/17 in test-curl.sh (11 existing + 6 new for archive coverage). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
9.6 KiB
Bash
Executable file
233 lines
9.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# test-curl.sh — manual smoke test for vps-health-api endpoints.
|
|
#
|
|
# Spins up the server against a temporary REPORTS_DIR populated with
|
|
# scan/run-report fixtures, then runs curl against each endpoint and
|
|
# checks status codes + payload shape. No test runner installed — this
|
|
# script is the authoritative regression suite for the GET /reports/scans
|
|
# endpoint until vitest/jest is added.
|
|
#
|
|
# Usage :
|
|
# bash test-curl.sh
|
|
#
|
|
# Exit 0 if all cases pass, exit 1 on first failure (fail-fast).
|
|
|
|
set -euo pipefail
|
|
|
|
BASE_URL="${BASE_URL:-http://localhost:3099}"
|
|
TOKEN="${TOKEN:-test-token-123}"
|
|
TMP_DIR="$(mktemp -d -t vps-health-api.XXXXXX)"
|
|
trap 'rm -rf "$TMP_DIR"; kill "$SERVER_PID" 2>/dev/null || true' EXIT
|
|
|
|
# Fixtures :
|
|
# - 3 scan reports on 2026-05-07 (booking, simpl-liste, maximus)
|
|
# - 1 defenseur-auto run report on 2026-05-07 (must be filtered out)
|
|
# - 1 booking scan report on 2026-05-06 (must be excluded by date filter)
|
|
# - 1 archived scan report on 2026-05-04 (sergent rotated it post-07:30 UTC)
|
|
# - 1 archived scan report on 2026-05-07 used to assert top-level priority
|
|
# when the same filename also exists in REPORTS_DIR (defensive dedupe).
|
|
mkdir -p "$TMP_DIR/reports"
|
|
mkdir -p "$TMP_DIR/reports/archive"
|
|
|
|
cat > "$TMP_DIR/reports/defenseur-booking_2026-05-07T05-30-11-249Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-booking",
|
|
"timestamp": "2026-05-07T05:30:11.249Z",
|
|
"project": "la-suite-booking",
|
|
"checksRun": 16,
|
|
"checksPassed": 14,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
cat > "$TMP_DIR/reports/defenseur-simpl-liste_2026-05-07T05-32-04-512Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-simpl-liste",
|
|
"timestamp": "2026-05-07T05:32:04.512Z",
|
|
"project": "simpl-liste",
|
|
"checksRun": 12,
|
|
"checksPassed": 11,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
cat > "$TMP_DIR/reports/defenseur-maximus_2026-05-07T05-00-12-100Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-maximus",
|
|
"timestamp": "2026-05-07T05:00:12.100Z",
|
|
"project": "la-compagnie-maximus",
|
|
"checksRun": 8,
|
|
"checksPassed": 8,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
cat > "$TMP_DIR/reports/defenseur-auto_2026-05-07.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-auto",
|
|
"timestamp": "2026-05-07T07:00:00.000Z",
|
|
"status": "ok",
|
|
"actions": [],
|
|
"skipped": [],
|
|
"totalCostUsd": 0
|
|
}
|
|
JSON
|
|
|
|
cat > "$TMP_DIR/reports/defenseur-booking_2026-05-06T05-30-00-000Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-booking",
|
|
"timestamp": "2026-05-06T05:30:00.000Z",
|
|
"project": "la-suite-booking",
|
|
"checksRun": 16,
|
|
"checksPassed": 16,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
# Archived scan report (sergent renameSync at 07:30 UTC moves files here).
|
|
cat > "$TMP_DIR/reports/archive/defenseur-vps_2026-05-04T05-15-00-000Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-vps",
|
|
"timestamp": "2026-05-04T05:15:00.000Z",
|
|
"project": "vps",
|
|
"checksRun": 10,
|
|
"checksPassed": 10,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
# Same filename present at top-level (already created above) AND in archive/.
|
|
# Top-level wins (more recent — the archive copy is the stale one). The
|
|
# archive copy carries agent="defenseur-maximus-STALE" so the dedupe
|
|
# regression case can detect a leak.
|
|
cat > "$TMP_DIR/reports/archive/defenseur-maximus_2026-05-07T05-00-12-100Z.json" <<'JSON'
|
|
{
|
|
"agent": "defenseur-maximus-STALE",
|
|
"timestamp": "2026-05-07T05:00:12.100Z",
|
|
"project": "la-compagnie-maximus",
|
|
"checksRun": 1,
|
|
"checksPassed": 0,
|
|
"findings": []
|
|
}
|
|
JSON
|
|
|
|
# Boot the server with the temp REPORTS_DIR.
|
|
PORT=3099 \
|
|
HEALTH_TOKEN="$TOKEN" \
|
|
REPORTS_DIR="$TMP_DIR/reports" \
|
|
LOGTO_HEALTH_URL="http://127.0.0.1:1/never" \
|
|
node "$(dirname "$0")/index.js" >/dev/null 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
# Wait for the server to be ready.
|
|
for _ in {1..50}; do
|
|
if curl -s -o /dev/null "$BASE_URL/health" 2>/dev/null; then break; fi
|
|
sleep 0.1
|
|
done
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
fail() {
|
|
echo "FAIL: $1"
|
|
FAIL=$((FAIL+1))
|
|
}
|
|
pass() {
|
|
echo "PASS: $1"
|
|
PASS=$((PASS+1))
|
|
}
|
|
|
|
# Case 1 : no auth -> 401
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE_URL/reports/scans?date=2026-05-07")
|
|
[[ "$code" == "401" ]] && pass "no-auth -> 401" || fail "no-auth -> got $code"
|
|
|
|
# Case 2 : wrong token -> 401
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: Bearer wrong" "$BASE_URL/reports/scans?date=2026-05-07")
|
|
[[ "$code" == "401" ]] && pass "wrong-token -> 401" || fail "wrong-token -> got $code"
|
|
|
|
# Case 3 : valid token + date 2026-05-07 -> 200, count=3, sorted asc, no auto report
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-07")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
agents=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).reports.map(r=>r.agent).join(","));});')
|
|
[[ "$count" == "3" ]] && pass "valid date 2026-05-07 -> count=3" || fail "valid date 2026-05-07 -> count=$count"
|
|
[[ "$agents" == "defenseur-maximus,defenseur-booking,defenseur-simpl-liste" ]] \
|
|
&& pass "sort by timestamp asc + auto filtered" \
|
|
|| fail "sort/filter mismatch -> $agents"
|
|
|
|
# Case 4 : invalid date format -> 400
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: Bearer $TOKEN" "$BASE_URL/reports/scans?date=hello")
|
|
[[ "$code" == "400" ]] && pass "invalid date -> 400" || fail "invalid date -> got $code"
|
|
|
|
# Case 5 : missing date param -> 400
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: Bearer $TOKEN" "$BASE_URL/reports/scans")
|
|
[[ "$code" == "400" ]] && pass "missing date -> 400" || fail "missing date -> got $code"
|
|
|
|
# Case 6 : valid token + unknown date (no fixtures) -> 200 count=0
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2025-01-01")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
[[ "$count" == "0" ]] && pass "unknown date -> count=0" || fail "unknown date -> count=$count"
|
|
|
|
# Case 7 : valid token + date 2026-05-06 -> 200 count=1
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-06")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
[[ "$count" == "1" ]] && pass "date 2026-05-06 -> count=1" || fail "date 2026-05-06 -> count=$count"
|
|
|
|
# Case 8 : path traversal -> 400 (regex blocks)
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=../../../etc/passwd")
|
|
[[ "$code" == "400" ]] && pass "path traversal -> 400" || fail "path traversal -> got $code"
|
|
|
|
# Case 9 : POST -> 404 (GET-only)
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-07")
|
|
[[ "$code" == "404" ]] && pass "POST -> 404" || fail "POST -> got $code"
|
|
|
|
# Case 10 : wrong path -> 404
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
-H "Authorization: Bearer $TOKEN" "$BASE_URL/reports/nope")
|
|
[[ "$code" == "404" ]] && pass "wrong path -> 404" || fail "wrong path -> got $code"
|
|
|
|
# Case 11 : archive-only date -> 200 count=1, returns the archived report.
|
|
# Reproduces the post-07:30 UTC window (sergent rotated all reports out of
|
|
# REPORTS_DIR into REPORTS_DIR/archive).
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-04")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
agent=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).reports[0]?.agent||"");});')
|
|
[[ "$count" == "1" ]] && pass "archive-only date 2026-05-04 -> count=1" || fail "archive-only date 2026-05-04 -> count=$count"
|
|
[[ "$agent" == "defenseur-vps" ]] && pass "archive report agent matches" || fail "archive report agent mismatch -> $agent"
|
|
|
|
# Case 12 : top-level + archive same filename -> top-level wins (defensive
|
|
# dedupe). The archive copy carries agent="defenseur-maximus-STALE" — if we
|
|
# see that string in the response we picked the wrong copy.
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-07")
|
|
stale=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s).reports.map(r=>r.agent);console.log(a.includes("defenseur-maximus-STALE")?"yes":"no");});')
|
|
[[ "$stale" == "no" ]] && pass "top-level priority over archive (no STALE)" || fail "archive copy leaked -> reports include STALE"
|
|
# Also assert count is still 3 — no duplication of the maximus report.
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
[[ "$count" == "3" ]] && pass "no dedupe duplication on 2026-05-07" || fail "dedupe duplication -> count=$count"
|
|
|
|
# Case 13 : missing archive/ subdir is OK (silent skip). Remove the directory
|
|
# and re-query 2026-05-07 — should still return the 3 top-level reports.
|
|
rm -rf "$TMP_DIR/reports/archive"
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-07")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
[[ "$count" == "3" ]] && pass "missing archive/ -> still count=3 from top-level" || fail "missing archive/ -> count=$count"
|
|
# And the archive-only date now collapses to 0 silently.
|
|
body=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
|
"$BASE_URL/reports/scans?date=2026-05-04")
|
|
count=$(echo "$body" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).count);});')
|
|
[[ "$count" == "0" ]] && pass "missing archive/ + archive-only date -> count=0" || fail "missing archive/ archive-only -> count=$count"
|
|
|
|
echo
|
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
[[ "$FAIL" == "0" ]] || exit 1
|