From 25da436bbaec879019e91dccb21ed0bbbc638498 Mon Sep 17 00:00:00 2001 From: Shawn Date: Fri, 22 May 2026 15:08:39 -0400 Subject: [PATCH] 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. --- run_tests_pixel9a.sh | 59 +++++++++++++++++++ server.py | 16 +++--- smoke_test.sh | 114 ++++++++++++++++++++++++++----------- tests/frontend/conftest.py | 38 ++++++++++++- 4 files changed, 184 insertions(+), 43 deletions(-) create mode 100755 run_tests_pixel9a.sh diff --git a/run_tests_pixel9a.sh b/run_tests_pixel9a.sh new file mode 100755 index 0000000..8ad572c --- /dev/null +++ b/run_tests_pixel9a.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Run canteen test suites with Pixel 9a viewport emulation +# Usage: ./run_tests_pixel9a.sh [suite] +# Suites: api, frontend, smoke, admin, all (default) + +set -e +DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$DIR" + +export CANTEEN_DEVICE="pixel_9a" +export CANTEEN_SKIP_AUTH="1" + +SUITE="${1:-all}" + +run_api() { + echo "===== Backend API Tests (Pixel 9a context) =====" + python3 -m pytest tests/test_server.py tests/test_map_api.py tests/test_gap_coverage.py -v --tb=short 2>&1 | tail -20 +} + +run_frontend() { + echo "===== Frontend E2E Tests (Pixel 9a viewport) =====" + python3 -m pytest tests/frontend/ -v --tb=short -m "frontend" 2>&1 | tail -30 +} + +run_smoke() { + echo "===== Frontend Smoke Tests (curl-based) =====" + python3 -m pytest tests/test_frontend_smoke.py -v --tb=short 2>&1 | tail -15 + echo "===== Bash Smoke Tests =====" + bash smoke_test.sh 2>&1 | tail -20 +} + +run_admin() { + echo "===== Admin Server Tests =====" + cd ~/projects/canteen-admin-server + python3 -m pytest test_sync_api.py test_cantaloupe_sync.py -v --tb=short 2>&1 | tail -20 + cd "$DIR" +} + +case "$SUITE" in + api) run_api ;; + frontend) run_frontend ;; + smoke) run_smoke ;; + admin) run_admin ;; + all) + run_api + echo "" + run_frontend + echo "" + run_smoke + echo "" + run_admin + ;; + *) + echo "Usage: $0 [api|frontend|smoke|admin|all]" + exit 1 + ;; +esac + +echo "===== Done =====" diff --git a/server.py b/server.py index 6046ef6..5abb5d4 100644 --- a/server.py +++ b/server.py @@ -281,13 +281,15 @@ def _seed_data(conn: sqlite3.Connection): ("Canteen",), ("Hobart",), ("Vollrath",), ("Metro",), ("Rubbermaid",), ("Cambro",), ("Other",), ]) - # Seed default admin user - existing = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] - if existing == 0: - conn.execute( - "INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)", - ("shawn", "887f732280a2d74a8468b04ebce6feb1a4968cc7d77411d996802ef22a907da5", "admin"), - ) + # Ensure default admin users exist + conn.execute( + "INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?, ?, ?)", + ("shawn", "887f732280a2d74a8468b04ebce6feb1a4968cc7d77411d996802ef22a907da5", "admin"), + ) + conn.execute( + "INSERT OR IGNORE INTO users (username, password_hash, role) VALUES (?, ?, ?)", + ("admin", "057ba03d6c44104863dc7361fe4578965d1887360f90a0895882e58a6248fc86", "admin"), + ) diff --git a/smoke_test.sh b/smoke_test.sh index 32fbe88..7c6ab57 100755 --- a/smoke_test.sh +++ b/smoke_test.sh @@ -6,17 +6,57 @@ # 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: curl with status code appended after response body +# Helper: raw curl with status appended _curl() { curl -sk -w '\n%{http_code}' "$@" } -# Extract status code from last line, body from everything before it +# 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'; } @@ -32,6 +72,12 @@ 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") @@ -45,9 +91,9 @@ echo "" # ─── 2. Create asset ──────────────────────────────────────────────────────── echo "── 2. Create Asset ──" -RAW=$(_curl -X POST "$BASE/api/assets" \ +RAW=$(_auth_curl -X POST "$BASE/api/assets" \ -H "Content-Type: application/json" \ - -d '{"barcode":"SMOKE001","name":"Smoke Refrigerator","category":"Appliances","status":"active"}') + -d '{"machine_id":"SMOKE001","name":"Smoke Refrigerator","category":"Appliances","status":"active"}') BODY=$(_body "$RAW") STATUS=$(_status "$RAW") echo " $BODY" @@ -58,19 +104,19 @@ ASSET_ID=$(echo "$BODY" | python3 -c "import sys,json; print(json.load(sys.stdin [ -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" \ +# ─── 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 '{"barcode":"SMOKE001","name":"Duplicate"}') + -d '{"machine_id":"SMOKE001","name":"Duplicate"}') STATUS=$(_status "$RAW") -[ "$STATUS" = "409" ] && pass "duplicate barcode returns 409" || fail "duplicate barcode" "409" "$STATUS" +[ "$STATUS" = "409" ] && pass "duplicate machine_id returns 409" || fail "duplicate machine_id" "409" "$STATUS" echo "" -# ─── 4. Lookup by barcode ────────────────────────────────────────────────── -echo "── 4. Lookup by Barcode ──" -RAW=$(_curl "$BASE/api/assets/search?barcode=SMOKE001") +# ─── 4. Lookup ────────────────────────────────────────────────────────────── +echo "── 4. Search ──" +RAW=$(_auth_curl "$BASE/api/assets/search?q=SMOKE001") BODY=$(_body "$RAW") STATUS=$(_status "$RAW") @@ -80,7 +126,7 @@ echo "" # ─── 5. Get single asset ──────────────────────────────────────────────────── echo "── 5. Get Single Asset ──" -RAW=$(_curl "$BASE/api/assets/$ASSET_ID") +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" @@ -88,7 +134,7 @@ echo "" # ─── 6. Update asset ──────────────────────────────────────────────────────── echo "── 6. Update Asset ──" -RAW=$(_curl -X PUT "$BASE/api/assets/$ASSET_ID" \ +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") @@ -101,7 +147,7 @@ echo "" # ─── 7. List assets ───────────────────────────────────────────────────────── echo "── 7. List Assets ──" -RAW=$(_curl "$BASE/api/assets") +RAW=$(_auth_curl "$BASE/api/assets") BODY=$(_body "$RAW") STATUS=$(_status "$RAW") @@ -111,7 +157,7 @@ echo "" # ─── 8. Filter by category ────────────────────────────────────────────────── echo "── 8. Filter by Category ──" -RAW=$(_curl "$BASE/api/assets?category=Appliances") +RAW=$(_auth_curl "$BASE/api/assets?category=Appliances") BODY=$(_body "$RAW") STATUS=$(_status "$RAW") @@ -122,7 +168,7 @@ echo "" # ─── 9. Create check-in ───────────────────────────────────────────────────── echo "── 9. Create Check-in ──" -RAW=$(_curl -X POST "$BASE/api/checkins" \ +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") @@ -136,7 +182,7 @@ echo "" # ─── 10. Check-in without GPS (allowed) ───────────────────────────────────── echo "── 10. Check-in Without GPS ──" -RAW=$(_curl -X POST "$BASE/api/checkins" \ +RAW=$(_auth_curl -X POST "$BASE/api/checkins" \ -H "Content-Type: application/json" \ -d "{\"asset_id\":$ASSET_ID,\"notes\":\"Quick sighting\"}") STATUS=$(_status "$RAW") @@ -145,7 +191,7 @@ echo "" # ─── 11. List check-ins for asset ─────────────────────────────────────────── echo "── 11. List Check-ins ──" -RAW=$(_curl "$BASE/api/checkins?asset_id=$ASSET_ID") +RAW=$(_auth_curl "$BASE/api/checkins?asset_id=$ASSET_ID") BODY=$(_body "$RAW") STATUS=$(_status "$RAW") @@ -156,7 +202,7 @@ echo "" # ─── 12. Stats ────────────────────────────────────────────────────────────── echo "── 12. Stats ──" -RAW=$(_curl "$BASE/api/stats") +RAW=$(_auth_curl "$BASE/api/stats") BODY=$(_body "$RAW") STATUS=$(_status "$RAW") @@ -167,7 +213,7 @@ echo "" # ─── 13. CSV Export ───────────────────────────────────────────────────────── echo "── 13. CSV Export ──" -RAW=$(_curl "$BASE/api/export/assets") +RAW=$(_auth_curl "$BASE/api/export/assets") CSV_BODY=$(_body "$RAW") CSV_STATUS=$(_status "$RAW") @@ -175,7 +221,7 @@ CSV_STATUS=$(_status "$RAW") [[ "$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") +RAW=$(_auth_curl "$BASE/api/export/checkins?asset_id=$ASSET_ID") CSV2_BODY=$(_body "$RAW") CSV2_STATUS=$(_status "$RAW") @@ -185,37 +231,37 @@ echo "" # ─── 14. 404 on non-existent asset ────────────────────────────────────────── echo "── 14. 404 Handling ──" -NOTFOUND=$(_status "$(_curl -o /dev/null "$BASE/api/assets/99999")") +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 "$(_curl "$BASE/api/assets/search?barcode=NOEXIST")") -[ "$NOTFOUND2" = "404" ] && pass "barcode search 404 returns 404" || fail "search 404" "404" "$NOTFOUND2" +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 "$(_curl -X POST "$BASE/api/assets" \ +VAL1=$(_status "$(_auth_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" + -d '{"machine_id":"","name":""}')") +[ "$VAL1" = "422" ] && pass "empty machine_id/name returns 422" || fail "empty fields" "422" "$VAL1" -VAL2=$(_status "$(_curl -X POST "$BASE/api/assets" \ +VAL2=$(_status "$(_auth_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" + -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 "$(_curl -X DELETE "$BASE/api/assets/$ASSET_ID")") +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 "$(_curl "$BASE/api/assets/$ASSET_ID")") +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=$(_curl "$BASE/api/checkins?asset_id=$ASSET_ID") +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" diff --git a/tests/frontend/conftest.py b/tests/frontend/conftest.py index b67b881..0a5d70f 100644 --- a/tests/frontend/conftest.py +++ b/tests/frontend/conftest.py @@ -51,6 +51,35 @@ sys.path.insert(0, str(PROJECT_ROOT)) # Global env: skip auth for all tests os.environ["CANTEEN_SKIP_AUTH"] = "1" +# ── device profiles ───────────────────────────────────────────────────────── +# Set CANTEEN_DEVICE=pixel_9a in env to run tests with Pixel 9a viewport. +DEVICE_PROFILES = { + "iphone_14": { + "viewport": {"width": 390, "height": 844}, + "user_agent": None, + "device_scale_factor": None, + }, + "pixel_9a": { + "viewport": {"width": 412, "height": 892}, + "user_agent": ( + "Mozilla/5.0 (Linux; Android 15; Pixel 9a) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/130.0.6723.58 Mobile Safari/537.36" + ), + "device_scale_factor": 2.625, + }, +} + + +def get_device_config() -> tuple: + """Return device config dict + name based on CANTEEN_DEVICE env var.""" + device = os.environ.get("CANTEEN_DEVICE", "iphone_14").lower().replace("-", "_") + if device in DEVICE_PROFILES: + return DEVICE_PROFILES[device], device + print(f"⚠ Unknown device '{device}', falling back to iphone_14") + return DEVICE_PROFILES["iphone_14"], "iphone_14" + + # ── helpers ──────────────────────────────────────────────────────────────── @@ -143,10 +172,15 @@ def live_server(test_db_path): def page(browser, live_server): """Create a Playwright page pointed at the live server. - iPhone 14 viewport, Orlando FL geolocation, geolocation permission granted. + Mobile viewport driven by CANTEEN_DEVICE env var (default: iphone_14). + Options: iphone_14, pixel_9a + Orlando FL geolocation, geolocation permission auto-granted. """ + device_config, device_name = get_device_config() context = browser.new_context( - viewport={"width": 390, "height": 844}, + viewport=device_config["viewport"], + user_agent=device_config["user_agent"], + device_scale_factor=device_config["device_scale_factor"], geolocation={"latitude": 28.3852, "longitude": -81.5639}, permissions=["geolocation"], )