#!/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