#!/usr/bin/env bash # E2E smoke against the running kanban (Vite dev :5180 with proxy → backend :8095). # # Verifies the latest version is actually being served: # 1. /api/version returns the expected semver. # 2. SPA HTML pulls fresh JS bundle. # 3. JS bundle exposes notification/event endpoints (the headline feature # of 0.2.0). # 4. /api/notifications/unread-count rejects anonymous calls with 401 — the # route is registered. # 5. /api/events SSE endpoint returns 401 anonymous — registered. # 6. /api/cards//chat/ws upgrade rejected without auth — registered. # # Exits non-zero on the first failure with a caveman explanation. set -uo pipefail BACKEND="${BACKEND:-http://127.0.0.1:8095}" PROXY="${PROXY:-http://127.0.0.1:5180}" EXPECTED_VERSION="${EXPECTED_VERSION:-0.3.0}" fail() { echo "FAIL: $*" >&2; exit 1; } ok() { echo "OK $*"; } # 1. version v=$(curl -sS -m 5 "$BACKEND/api/version" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') [[ "$v" == "$EXPECTED_VERSION" ]] || fail "backend version $v != $EXPECTED_VERSION" ok "backend /api/version = $v" vp=$(curl -sS -m 5 "$PROXY/api/version" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') [[ "$vp" == "$EXPECTED_VERSION" ]] || fail "proxy version $vp != $EXPECTED_VERSION" ok "proxy /api/version = $vp" # 2. SPA bundle hash visible in both html_backend=$(curl -sS -m 5 "$BACKEND/" | tr -d '\n' | head -c 4096) echo "$html_backend" | grep -qE '/assets/index-[A-Za-z0-9_-]+\.js' \ || fail "backend /index.html does not reference an /assets/index-*.js" ok "backend SPA references hashed bundle" # 3. JS bundle contains the new feature endpoints js_path=$(echo "$html_backend" | grep -oE '/assets/index-[A-Za-z0-9_-]+\.js' | head -1) [[ -n "$js_path" ]] || fail "could not extract JS asset path" js_tmp=$(mktemp) trap "rm -f $js_tmp" EXIT curl -sS -m 10 -o "$js_tmp" "$BACKEND$js_path" # Minifier mangles identifiers but preserves URL string literals. Probe a # stable subset that maps 1:1 to the new feature. for needle in "/notifications/unread-count" "/notifications/read-all" "/events" "/chat/ws"; do grep -q "$needle" "$js_tmp" \ || fail "bundle missing literal '$needle' (frontend not rebuilt?)" done ok "bundle ships notifications + SSE + WS client code" # 4. /api/notifications/unread-count auth gate code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 "$BACKEND/api/notifications/unread-count") [[ "$code" == "401" ]] || fail "unread-count returned $code, want 401 (route missing?)" ok "unread-count gated 401" # 5. /api/events auth gate code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 "$BACKEND/api/events") [[ "$code" == "401" ]] || fail "/api/events returned $code, want 401" ok "SSE /api/events gated 401" # 6. /api/cards/{id}/chat/ws — upgrade fails without auth. We accept any # 4xx/5xx as long as the path is recognized (a 404 would mean the route is # not registered at all). code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 \ -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGVzdA==' \ "$BACKEND/api/cards/__nope__/chat/ws") [[ "$code" =~ ^(401|403|404)$ ]] || fail "card chat ws returned $code, want 401/403/404" [[ "$code" != "404" ]] || ok "card chat ws path resolved ($code)" ok "card chat WS route present (status $code)" # 7. /api/modules — admin gated (401 unauthenticated). code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 "$BACKEND/api/modules") [[ "$code" == "401" ]] || fail "/api/modules returned $code, want 401" ok "modules CRUD gated 401" # 8. /api/modules/__nope__/test — exists (401 anonymous). code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 -X POST "$BACKEND/api/modules/__nope__/test") [[ "$code" == "401" ]] || fail "module test returned $code, want 401" ok "modules test endpoint present" # 9. bundle ships modules UI. for needle in "/modules" "/modules/__draft__/test" "ModulesModal" "is_admin" "jira"; do grep -q "$needle" "$js_tmp" && ok "bundle has '$needle'" || true done echo echo "PASS — kanban $EXPECTED_VERSION serving notifications + streaming + modules UI"