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.
This commit is contained in:
2026-05-22 15:08:39 -04:00
parent 72003e2742
commit 25da436bba
4 changed files with 184 additions and 43 deletions
+59
View File
@@ -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 ====="
+9 -7
View File
@@ -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"),
)
+80 -34
View File
@@ -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"
+36 -2
View File
@@ -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"],
)