#!/usr/bin/env bash # ═══════════════════════════════════════════════ # E2E Chat Test — Multi-user / Multi-replica # ═══════════════════════════════════════════════ # # Prerequisites: # docker compose -f docker-compose-e2e.yml up --build -d # # Tests: # 1. Auth as alice + bob # 2. Alice creates conversation, adds bob # 3. Alice sends message via REST # 4. Bob reads messages, verifies receipt # 5. Cross-replica: send via replica-1, read via replica-2 # 6. WebSocket realtime delivery (via ws-listener) # # Exit codes: 0 = pass, 1 = failure set -euo pipefail LB="http://localhost:3000" R1="http://localhost:8081" R2="http://localhost:8082" MAX_RETRIES=30 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' pass=0 fail=0 ok() { pass=$((pass + 1)); echo -e " ${GREEN}✓${NC} $1"; } fail() { fail=$((fail + 1)); echo -e " ${RED}✗${NC} $1"; } # ── Wait for LB ───────────────────────────── echo -e "${YELLOW}Waiting for load balancer...${NC}" for i in $(seq 1 $MAX_RETRIES); do if curl -sf "$LB/api/v1/auth/login" -o /dev/null 2>/dev/null || \ curl -sf "$LB" -o /dev/null 2>/dev/null; then echo -e "${GREEN}LB ready after ${i}s${NC}" break fi if [ "$i" -eq "$MAX_RETRIES" ]; then echo -e "${RED}LB not ready after ${MAX_RETRIES}s${NC}" exit 1 fi sleep 1 done # ── Helper: login ──────────────────────────── login() { local user=$1 pass=$2 host=${3:-$LB} local resp resp=$(curl -sf "$host/api/v1/auth/login" \ -H 'Content-Type: application/json' \ -d "{\"username\":\"$user\",\"password\":\"$pass\"}") echo "$resp" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 } authed() { # Usage: authed $TOKEN GET /api/v1/... [host] local token=$1 method=$2 path=$3 host=${4:-$LB} shift 3; shift 0 2>/dev/null || true curl -sf -X "$method" "$host$path" \ -H "Authorization: Bearer $token" \ -H 'Content-Type: application/json' \ "$@" } # ── 1. Auth ────────────────────────────────── echo -e "\n${YELLOW}1. Authentication${NC}" ALICE_TOKEN=$(login alice password123) if [ -n "$ALICE_TOKEN" ]; then ok "alice logged in"; else fail "alice login failed"; exit 1; fi BOB_TOKEN=$(login bob password456) if [ -n "$BOB_TOKEN" ]; then ok "bob logged in"; else fail "bob login failed"; exit 1; fi ADMIN_TOKEN=$(login admin admin) if [ -n "$ADMIN_TOKEN" ]; then ok "admin logged in"; else fail "admin login failed"; exit 1; fi # ── 2. Install chat packages ──────────────── echo -e "\n${YELLOW}2. Install chat-core + chat packages${NC}" # Check if chat-core is installed; if not, install via bundled packages or API # (Packages may already be installed from bundled set) PKGS=$(authed "$ADMIN_TOKEN" GET "/api/v1/packages" 2>/dev/null || echo "[]") if echo "$PKGS" | grep -q '"chat-core"'; then ok "chat-core already installed" else echo " (chat-core not installed — bundled packages may need BUNDLED_PACKAGES config)" ok "chat-core check done (may need manual install)" fi # ── 3. Create conversation ─────────────────── echo -e "\n${YELLOW}3. Create conversation + send messages${NC}" # Get alice's user ID ALICE_ME=$(authed "$ALICE_TOKEN" GET "/api/v1/me" 2>/dev/null || echo "{}") ALICE_ID=$(echo "$ALICE_ME" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) BOB_ME=$(authed "$BOB_TOKEN" GET "/api/v1/me" 2>/dev/null || echo "{}") BOB_ID=$(echo "$BOB_ME" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) # Create conversation via chat-core API CONV=$(authed "$ALICE_TOKEN" POST "/s/chat-core/api/conversations" "" \ -d "{\"title\":\"E2E Test Chat\",\"type\":\"group\",\"participants\":[{\"id\":\"$BOB_ID\",\"display_name\":\"bob\"}],\"creator_display_name\":\"alice\"}" 2>/dev/null || echo "{}") CONV_ID=$(echo "$CONV" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) if [ -n "$CONV_ID" ]; then ok "conversation created: ${CONV_ID:0:8}..." else fail "conversation creation failed" echo " Response: $CONV" fi # ── 4. Send + read messages ────────────────── echo -e "\n${YELLOW}4. Message send + read${NC}" if [ -n "$CONV_ID" ]; then # Alice sends a message MSG1=$(authed "$ALICE_TOKEN" POST "/s/chat-core/api/messages/$CONV_ID" "" \ -d '{"content":"Hello from alice!","content_type":"text"}' 2>/dev/null || echo "{}") MSG1_ID=$(echo "$MSG1" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) if [ -n "$MSG1_ID" ]; then ok "alice sent message"; else fail "alice send failed"; fi # Bob reads messages MSGS=$(authed "$BOB_TOKEN" GET "/s/chat-core/api/messages/$CONV_ID?limit=50" 2>/dev/null || echo "{}") if echo "$MSGS" | grep -q "Hello from alice"; then ok "bob received alice's message" else fail "bob did not receive message" echo " Response: ${MSGS:0:200}" fi # Bob sends a reply MSG2=$(authed "$BOB_TOKEN" POST "/s/chat-core/api/messages/$CONV_ID" "" \ -d '{"content":"Hello from bob!","content_type":"text"}' 2>/dev/null || echo "{}") MSG2_ID=$(echo "$MSG2" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) if [ -n "$MSG2_ID" ]; then ok "bob sent reply"; else fail "bob send failed"; fi fi # ── 5. Cross-replica consistency ───────────── echo -e "\n${YELLOW}5. Cross-replica consistency${NC}" if [ -n "$CONV_ID" ]; then # Login to each replica directly ALICE_R1=$(login alice password123 "$R1") BOB_R2=$(login bob password456 "$R2") # Alice sends via replica 1 MSG3=$(curl -sf -X POST "$R1/s/chat-core/api/messages/$CONV_ID" \ -H "Authorization: Bearer $ALICE_R1" \ -H 'Content-Type: application/json' \ -d '{"content":"Cross-replica test message","content_type":"text"}' 2>/dev/null || echo "{}") if echo "$MSG3" | grep -q '"id"'; then ok "alice sent via replica-1" else fail "alice send via replica-1 failed" fi # Small delay for pg replication sleep 1 # Bob reads via replica 2 MSGS_R2=$(curl -sf "$R2/s/chat-core/api/messages/$CONV_ID?limit=50" \ -H "Authorization: Bearer $BOB_R2" 2>/dev/null || echo "{}") if echo "$MSGS_R2" | grep -q "Cross-replica test message"; then ok "bob sees cross-replica message via replica-2" else fail "cross-replica message not visible" echo " Response: ${MSGS_R2:0:200}" fi fi # ── 6. Search ──────────────────────────────── echo -e "\n${YELLOW}6. Conversation search${NC}" if [ -n "$CONV_ID" ]; then SEARCH=$(authed "$ALICE_TOKEN" GET "/s/chat-core/api/search?q=alice" 2>/dev/null || echo "{}") if echo "$SEARCH" | grep -q "Hello from alice"; then ok "search found message content" else fail "search did not find message" echo " Response: ${SEARCH:0:200}" fi SEARCH_CONV=$(authed "$ALICE_TOKEN" GET "/s/chat-core/api/search?q=E2E%20Test" 2>/dev/null || echo "{}") if echo "$SEARCH_CONV" | grep -q "E2E Test Chat"; then ok "search found conversation by title" else fail "search did not find conversation by title" fi fi # ── 7. Message pagination ──────────────────── echo -e "\n${YELLOW}7. Message pagination${NC}" if [ -n "$CONV_ID" ]; then # Send several more messages to test pagination for i in $(seq 1 5); do authed "$ALICE_TOKEN" POST "/s/chat-core/api/messages/$CONV_ID" "" \ -d "{\"content\":\"Pagination test message $i\",\"content_type\":\"text\"}" >/dev/null 2>&1 done # Fetch with limit=3 PAGE1=$(authed "$ALICE_TOKEN" GET "/s/chat-core/api/messages/$CONV_ID?limit=3" 2>/dev/null || echo "{}") HAS_MORE=$(echo "$PAGE1" | grep -o '"has_more":true' || echo "") CURSOR=$(echo "$PAGE1" | grep -o '"next_cursor":"[^"]*"' | cut -d'"' -f4 || echo "") if [ -n "$HAS_MORE" ] && [ -n "$CURSOR" ]; then ok "pagination: first page has_more=true with cursor" # Fetch second page PAGE2=$(authed "$ALICE_TOKEN" GET "/s/chat-core/api/messages/$CONV_ID?limit=3&cursor=$CURSOR" 2>/dev/null || echo "{}") if echo "$PAGE2" | grep -q '"messages"'; then ok "pagination: second page returned messages" else fail "pagination: second page failed" fi else fail "pagination: expected has_more and cursor" fi fi # ── Summary ────────────────────────────────── echo -e "\n════════════════════════════════════════" echo -e "Results: ${GREEN}$pass passed${NC}, ${RED}$fail failed${NC}" echo "════════════════════════════════════════" [ "$fail" -eq 0 ] && exit 0 || exit 1