#!/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 # set -euo pipefail BASE="${1:-https://localhost:8901}" # Helper: curl with status code appended after response body _curl() { curl -sk -w '\n%{http_code}' "$@" } # Extract status code from last line, body from 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 "" # ─── 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=$(_curl -X POST "$BASE/api/assets" \ -H "Content-Type: application/json" \ -d '{"barcode":"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 barcode (should 409) ────────────────────────────────────── echo "── 3. Duplicate Barcode ──" RAW=$(_curl -X POST "$BASE/api/assets" \ -H "Content-Type: application/json" \ -d '{"barcode":"SMOKE001","name":"Duplicate"}') STATUS=$(_status "$RAW") [ "$STATUS" = "409" ] && pass "duplicate barcode returns 409" || fail "duplicate barcode" "409" "$STATUS" echo "" # ─── 4. Lookup by barcode ────────────────────────────────────────────────── echo "── 4. Lookup by Barcode ──" RAW=$(_curl "$BASE/api/assets/search?barcode=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=$(_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=$(_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=$(_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=$(_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=$(_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=$(_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=$(_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=$(_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=$(_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=$(_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 "$(_curl -o /dev/null "$BASE/api/assets/99999")") [ "$NOTFOUND" = "404" ] && pass "GET /api/assets/99999 returns 404" || fail "404 asset" "404" "$NOTFOUND" NOTFOUND2=$(_status "$(_curl "$BASE/api/assets/search?barcode=NOEXIST")") [ "$NOTFOUND2" = "404" ] && pass "barcode search 404 returns 404" || fail "search 404" "404" "$NOTFOUND2" echo "" # ─── 15. 422 on invalid input ─────────────────────────────────────────────── echo "── 15. Input Validation ──" VAL1=$(_status "$(_curl -X POST "$BASE/api/assets" \ -H "Content-Type: application/json" \ -d '{"barcode":"","name":""}')") [ "$VAL1" = "422" ] && pass "empty barcode/name returns 422" || fail "empty barcode" "422" "$VAL1" VAL2=$(_status "$(_curl -X POST "$BASE/api/assets" \ -H "Content-Type: application/json" \ -d '{"barcode":" ","name":"Test"}')") [ "$VAL2" = "422" ] && pass "whitespace-only barcode returns 422" || fail "whitespace barcode" "422" "$VAL2" echo "" # ─── 16. Delete asset ─────────────────────────────────────────────────────── echo "── 16. Delete Asset ──" DEL=$(_status "$(_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 "$(_curl "$BASE/api/assets/$ASSET_ID")") [ "$DELVERIFY" = "404" ] && pass "deleted asset returns 404" || fail "deleted verify" "404" "$DELVERIFY" # Verify check-ins cascade-deleted RAW=$(_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