Files
canteen-asset-tracker/smoke_test.sh
T
shawn 25da436bba Fix: seed admin/changeme user for frontend E2E tests
The test suite login as 'admin'/'changeme' but only 'shawn' was seeded.
Adding the admin user with SHA256('changeme') hash lets all 15 Playwright
tests reach the actual app pages instead of failing at login.
2026-05-22 15:08:39 -04:00

279 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# smoke_test.sh — End-to-end smoke test for Canteen Asset Tracker
# Exercises the full workflow: health, CRUD, check-in, stats, export.
#
# Usage: ./smoke_test.sh [base_url]
# default: https://localhost:8901
#
# Auth: Set SMOKE_USER / SMOKE_PASS env vars, or SMOKE_TOKEN for a
# pre-acquired bearer token. Defaults: admin / changeme.
#
set -euo pipefail
BASE="${1:-https://localhost:8901}"
SMOKE_USER="${SMOKE_USER:-}"
SMOKE_PASS="${SMOKE_PASS:-}"
TOKEN="${SMOKE_TOKEN:-}"
# Helper: raw curl with status appended
_curl() {
curl -sk -w '\n%{http_code}' "$@"
}
# Helper: curl with Bearer auth header (no-op if TOKEN empty)
_auth_curl() {
if [ -n "$TOKEN" ]; then
_curl -H "Authorization: Bearer $TOKEN" "$@"
else
_curl "$@"
fi
}
# Login and capture a bearer token
_login() {
local user="${SMOKE_USER:-admin}"
local pass="${SMOKE_PASS:-changeme}"
echo "── Auth: logging in as $user ──"
local RAW
RAW=$(_curl -X POST "$BASE/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"$user\",\"password\":\"$pass\"}")
local BODY
BODY=$(_body "$RAW")
local STATUS
STATUS=$(_status "$RAW")
if [ "$STATUS" = "200" ]; then
TOKEN=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null || echo "")
if [ -n "$TOKEN" ]; then
pass "login successful, got token ${TOKEN:0:10}..."
return 0
fi
fi
echo " ⚠ Login failed (HTTP $STATUS). API calls requiring auth will fail."
echo " Set SMOKE_USER / SMOKE_PASS env vars, or SMOKE_TOKEN for a direct token."
return 1
}
# Extract status code (last line) and body (everything before it)
_status() { echo "$1" | tail -1; }
_body() { echo "$1" | sed '$d'; }
PASS=0
FAIL=0
pass() { echo "$1"; PASS=$((PASS + 1)); }
fail() { echo "$1 (expected $2, got $3)"; FAIL=$((FAIL + 1)); }
echo "══════════════════════════════════════════════════"
echo " Canteen Asset Tracker — E2E Smoke Test"
echo " Target: $BASE"
echo "══════════════════════════════════════════════════"
echo ""
# ─── 0. Auth —────────────────────────────────────────────────────────────────
if [ -z "$TOKEN" ]; then
_login || true
fi
echo ""
# ─── 1. Health check ────────────────────────────────────────────────────────
echo "── 1. Health Check ──"
RAW=$(_curl "$BASE/health")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
echo " $BODY"
[ "$STATUS" = "200" ] && pass "GET /health returns 200" || fail "GET /health" "200" "$STATUS"
[[ "$BODY" == *'"status":"ok"'* ]] && pass "health body has status:ok" || fail "health body" '"status":"ok"' "$BODY"
echo ""
# ─── 2. Create asset ────────────────────────────────────────────────────────
echo "── 2. Create Asset ──"
RAW=$(_auth_curl -X POST "$BASE/api/assets" \
-H "Content-Type: application/json" \
-d '{"machine_id":"SMOKE001","name":"Smoke Refrigerator","category":"Appliances","status":"active"}')
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
echo " $BODY"
[ "$STATUS" = "201" ] && pass "POST /api/assets returns 201" || fail "POST /api/assets" "201" "$STATUS"
ASSET_ID=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
[ -n "$ASSET_ID" ] && pass "asset created with id=$ASSET_ID" || fail "asset create" "got id" "no id"
echo ""
# ─── 3. Duplicate machine_id (should 409) ─────────────────────────────────────
echo "── 3. Duplicate machine_id ──"
RAW=$(_auth_curl -X POST "$BASE/api/assets" \
-H "Content-Type: application/json" \
-d '{"machine_id":"SMOKE001","name":"Duplicate"}')
STATUS=$(_status "$RAW")
[ "$STATUS" = "409" ] && pass "duplicate machine_id returns 409" || fail "duplicate machine_id" "409" "$STATUS"
echo ""
# ─── 4. Lookup ──────────────────────────────────────────────────────────────
echo "── 4. Search ──"
RAW=$(_auth_curl "$BASE/api/assets/search?q=SMOKE001")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/assets/search returns 200" || fail "GET /api/assets/search" "200" "$STATUS"
[[ "$BODY" == *"Smoke Refrigerator"* ]] && pass "search returns correct name" || fail "search name" "Smoke Refrigerator" "$BODY"
echo ""
# ─── 5. Get single asset ────────────────────────────────────────────────────
echo "── 5. Get Single Asset ──"
RAW=$(_auth_curl "$BASE/api/assets/$ASSET_ID")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/assets/$ASSET_ID returns 200" || fail "GET /api/assets/$ASSET_ID" "200" "$STATUS"
echo ""
# ─── 6. Update asset ────────────────────────────────────────────────────────
echo "── 6. Update Asset ──"
RAW=$(_auth_curl -X PUT "$BASE/api/assets/$ASSET_ID" \
-H "Content-Type: application/json" \
-d '{"name":"Smoke Refrigerator v2","status":"maintenance"}')
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "PUT /api/assets/$ASSET_ID returns 200" || fail "PUT" "200" "$STATUS"
[[ "$BODY" == *"Smoke Refrigerator v2"* ]] && pass "update applied name" || fail "update name" "Smoke Refrigerator v2" "$BODY"
[[ "$BODY" == *"maintenance"* ]] && pass "update applied status" || fail "update status" "maintenance" "$BODY"
echo ""
# ─── 7. List assets ─────────────────────────────────────────────────────────
echo "── 7. List Assets ──"
RAW=$(_auth_curl "$BASE/api/assets")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/assets returns 200" || fail "GET /api/assets" "200" "$STATUS"
[[ "$BODY" == *"Smoke Refrigerator v2"* ]] && pass "list includes updated asset" || fail "list content" "Smoke Refrigerator v2" "$BODY"
echo ""
# ─── 8. Filter by category ──────────────────────────────────────────────────
echo "── 8. Filter by Category ──"
RAW=$(_auth_curl "$BASE/api/assets?category=Appliances")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/assets?category=Appliances returns 200" || fail "filter category" "200" "$STATUS"
COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
[ "$COUNT" = "1" ] && pass "category filter returns 1 result" || fail "category filter count" "1" "$COUNT"
echo ""
# ─── 9. Create check-in ─────────────────────────────────────────────────────
echo "── 9. Create Check-in ──"
RAW=$(_auth_curl -X POST "$BASE/api/checkins" \
-H "Content-Type: application/json" \
-d "{\"asset_id\":$ASSET_ID,\"latitude\":40.7128,\"longitude\":-74.006,\"accuracy\":15.0,\"notes\":\"Found in kitchen\"}")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "201" ] && pass "POST /api/checkins returns 201" || fail "POST /api/checkins" "201" "$STATUS"
CHECKIN_ID=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
[ -n "$CHECKIN_ID" ] && pass "check-in created with id=$CHECKIN_ID" || fail "checkin create" "got id" "no id"
echo ""
# ─── 10. Check-in without GPS (allowed) ─────────────────────────────────────
echo "── 10. Check-in Without GPS ──"
RAW=$(_auth_curl -X POST "$BASE/api/checkins" \
-H "Content-Type: application/json" \
-d "{\"asset_id\":$ASSET_ID,\"notes\":\"Quick sighting\"}")
STATUS=$(_status "$RAW")
[ "$STATUS" = "201" ] && pass "check-in without GPS returns 201" || fail "check-in no GPS" "201" "$STATUS"
echo ""
# ─── 11. List check-ins for asset ───────────────────────────────────────────
echo "── 11. List Check-ins ──"
RAW=$(_auth_curl "$BASE/api/checkins?asset_id=$ASSET_ID")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/checkins?asset_id= returns 200" || fail "list checkins" "200" "$STATUS"
COUNT=$(echo "$BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
[ "$COUNT" = "2" ] && pass "asset has 2 check-ins" || fail "checkin count" "2" "$COUNT"
echo ""
# ─── 12. Stats ──────────────────────────────────────────────────────────────
echo "── 12. Stats ──"
RAW=$(_auth_curl "$BASE/api/stats")
BODY=$(_body "$RAW")
STATUS=$(_status "$RAW")
[ "$STATUS" = "200" ] && pass "GET /api/stats returns 200" || fail "stats" "200" "$STATUS"
[[ "$BODY" == *'"total_assets":1'* ]] && pass "stats: total_assets=1" || fail "stats total_assets" "1" "$BODY"
[[ "$BODY" == *'"total_checkins":2'* ]] && pass "stats: total_checkins=2" || fail "stats total_checkins" "2" "$BODY"
echo ""
# ─── 13. CSV Export ─────────────────────────────────────────────────────────
echo "── 13. CSV Export ──"
RAW=$(_auth_curl "$BASE/api/export/assets")
CSV_BODY=$(_body "$RAW")
CSV_STATUS=$(_status "$RAW")
[ "$CSV_STATUS" = "200" ] && pass "GET /api/export/assets returns 200" || fail "export assets" "200" "$CSV_STATUS"
[[ "$CSV_BODY" == *"Smoke Refrigerator"* ]] && pass "CSV contains asset name" || fail "CSV content" "Smoke Refrigerator" "$CSV_BODY"
echo ""
RAW=$(_auth_curl "$BASE/api/export/checkins?asset_id=$ASSET_ID")
CSV2_BODY=$(_body "$RAW")
CSV2_STATUS=$(_status "$RAW")
[ "$CSV2_STATUS" = "200" ] && pass "GET /api/export/checkins returns 200" || fail "export checkins" "200" "$CSV2_STATUS"
[[ "$CSV2_BODY" == *"kitchen"* ]] && pass "checkin CSV contains note" || fail "checkin CSV" "kitchen" "$CSV2_BODY"
echo ""
# ─── 14. 404 on non-existent asset ──────────────────────────────────────────
echo "── 14. 404 Handling ──"
NOTFOUND=$(_status "$(_auth_curl -o /dev/null "$BASE/api/assets/99999")")
[ "$NOTFOUND" = "404" ] && pass "GET /api/assets/99999 returns 404" || fail "404 asset" "404" "$NOTFOUND"
NOTFOUND2=$(_status "$(_auth_curl "$BASE/api/assets/search?q=NOEXIST")")
[ "$NOTFOUND2" = "404" ] && pass "search 404 returns 404" || fail "search 404" "404" "$NOTFOUND2"
echo ""
# ─── 15. 422 on invalid input ───────────────────────────────────────────────
echo "── 15. Input Validation ──"
VAL1=$(_status "$(_auth_curl -X POST "$BASE/api/assets" \
-H "Content-Type: application/json" \
-d '{"machine_id":"","name":""}')")
[ "$VAL1" = "422" ] && pass "empty machine_id/name returns 422" || fail "empty fields" "422" "$VAL1"
VAL2=$(_status "$(_auth_curl -X POST "$BASE/api/assets" \
-H "Content-Type: application/json" \
-d '{"machine_id":" ","name":"Test"}')")
[ "$VAL2" = "422" ] && pass "whitespace-only machine_id returns 422" || fail "whitespace machine_id" "422" "$VAL2"
echo ""
# ─── 16. Delete asset ───────────────────────────────────────────────────────
echo "── 16. Delete Asset ──"
DEL=$(_status "$(_auth_curl -X DELETE "$BASE/api/assets/$ASSET_ID")")
[ "$DEL" = "204" ] && pass "DELETE /api/assets/$ASSET_ID returns 204" || fail "DELETE" "204" "$DEL"
# Verify gone
DELVERIFY=$(_status "$(_auth_curl "$BASE/api/assets/$ASSET_ID")")
[ "$DELVERIFY" = "404" ] && pass "deleted asset returns 404" || fail "deleted verify" "404" "$DELVERIFY"
# Verify check-ins cascade-deleted
RAW=$(_auth_curl "$BASE/api/checkins?asset_id=$ASSET_ID")
CKC_BODY=$(_body "$RAW")
COUNT=$(echo "$CKC_BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
[ "$COUNT" = "0" ] && pass "check-ins cascade-deleted (0 remaining)" || fail "cascade delete" "0" "$COUNT"
echo ""
# ─── Summary ────────────────────────────────────────────────────────────────
echo "══════════════════════════════════════════════════"
echo " Results: $PASS passed, $FAIL failed"
echo "══════════════════════════════════════════════════"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0