platform-codebase/features/messaging/backend-api/scripts/plum-integration-test.sh
Lilith 68f3c040da chore(scripts): 🔧 Update plum-integration-test.sh script
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-17 09:37:42 -08:00

525 lines
20 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
set -euo pipefail
# =============================================================================
# Messaging Backend Integration Test on plum-voyager
# =============================================================================
# Runs the messaging backend on plum (macOS) with local PostgreSQL + Redis,
# then verifies REST and WebSocket contracts end-to-end.
#
# Usage:
# ./scripts/plum-integration-test.sh # Full pipeline
# ./scripts/plum-integration-test.sh --setup-only # Install infra + sync + build
# ./scripts/plum-integration-test.sh --test-only # Skip setup, run tests
# ./scripts/plum-integration-test.sh --teardown # Stop services, clean up
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
REMOTE_HOST="plum-voyager"
BREW_PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew/opt/postgresql@16/bin"
JWT_SECRET="dev-jwt-secret-change-in-production"
VERDACCIO_LAN="10.0.0.11:4873"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
BACKEND_DIR="$(dirname "$SCRIPT_DIR")"
REPO_ROOT="$BACKEND_DIR/../../../.."
REMOTE_REPO="\$HOME/Code/@projects/@lilith/lilith-platform"
REMOTE_BACKEND="$REMOTE_REPO/codebase/features/messaging/backend-api"
# For rsync (which needs literal path, not shell variable)
REMOTE_REPO_LITERAL="~/Code/@projects/@lilith/lilith-platform"
REMOTE_BACKEND_LITERAL="$REMOTE_REPO_LITERAL/codebase/features/messaging/backend-api"
# Counters
PASS=0
FAIL=0
RESULTS=()
print_step() { echo -e "${GREEN}${NC} $1"; }
print_info() { echo -e "${BLUE}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1"; }
print_success() { echo -e "${GREEN}${NC} $1"; }
print_warn() { echo -e "${YELLOW}${NC} $1"; }
record_result() {
local name="$1" status="$2"
if [[ "$status" == "PASS" ]]; then
PASS=$((PASS + 1))
RESULTS+=("${GREEN}${NC} $name")
else
FAIL=$((FAIL + 1))
RESULTS+=("${RED}${NC} $name")
fi
}
# Run a command on plum with proper PATH
plum() {
ssh "$REMOTE_HOST" "export PATH=\"$BREW_PATH:\$PATH\"; $*"
}
usage() {
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --setup-only Install infra, sync, build (don't test)"
echo " --test-only Skip setup, just run integration tests"
echo " --teardown Stop services and clean up"
echo " -h, --help Show this help"
exit 0
}
# Parse arguments
DO_SETUP=true
DO_TEST=true
DO_TEARDOWN=false
while [[ $# -gt 0 ]]; do
case "$1" in
--setup-only) DO_TEST=false ;;
--test-only) DO_SETUP=false ;;
--teardown) DO_SETUP=false; DO_TEST=false; DO_TEARDOWN=true ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
shift
done
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} Messaging Backend - Integration Test on plum-voyager${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# ─── Check SSH ────────────────────────────────────────────────────────
print_step "Checking SSH connection to $REMOTE_HOST..."
if ! ssh -o ConnectTimeout=5 "$REMOTE_HOST" 'echo ok' >/dev/null 2>&1; then
print_error "Cannot connect to $REMOTE_HOST"
exit 1
fi
print_success "Connected to $REMOTE_HOST"
echo ""
# ─── Teardown ─────────────────────────────────────────────────────────
if [[ "$DO_TEARDOWN" == true ]]; then
print_step "Tearing down..."
# Kill backend and proxy if running
plum "pkill -f 'node dist/main' 2>/dev/null || true"
plum "pkill -f npm-proxy.mjs 2>/dev/null || true"
print_info "Stopped messaging backend and npm proxy"
# Stop services
plum "brew services stop postgresql@16 2>/dev/null || true; brew services stop redis 2>/dev/null || true"
print_info "Stopped PostgreSQL and Redis"
print_success "Teardown complete"
exit 0
fi
# ─── Setup ────────────────────────────────────────────────────────────
if [[ "$DO_SETUP" == true ]]; then
# Step 1: Install infra dependencies
print_step "Installing infrastructure dependencies..."
# Check what's already installed
HAS_PG=$(plum "brew list postgresql@16 >/dev/null 2>&1 && echo yes || echo no")
HAS_REDIS=$(plum "brew list redis >/dev/null 2>&1 && echo yes || echo no")
if [[ "$HAS_PG" == "no" || "$HAS_REDIS" == "no" ]]; then
print_info "Installing missing packages..."
plum "brew install postgresql@16 redis 2>&1" | tail -5
fi
print_success "PostgreSQL 16 and Redis installed"
# Start services (idempotent)
plum "brew services start postgresql@16 2>/dev/null || true; brew services start redis 2>/dev/null || true"
sleep 2
# Verify services are running
PG_READY=$(plum "pg_isready 2>&1" || true)
REDIS_READY=$(plum "redis-cli ping 2>&1" || true)
if [[ "$PG_READY" == *"accepting connections"* ]]; then
record_result "PostgreSQL running (:5432)" "PASS"
else
record_result "PostgreSQL running" "FAIL"
print_error "PostgreSQL not accepting connections: $PG_READY"
exit 1
fi
if [[ "$REDIS_READY" == "PONG" ]]; then
record_result "Redis running (:6379)" "PASS"
else
record_result "Redis running" "FAIL"
print_error "Redis not responding: $REDIS_READY"
exit 1
fi
# Create database and role (idempotent)
plum "createdb lilith_messaging 2>/dev/null || true"
plum "psql -d lilith_messaging -c \"DO \\\$\\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='messaging') THEN CREATE ROLE messaging WITH LOGIN PASSWORD 'devpassword' SUPERUSER; END IF; END \\\$\\\$;\" 2>/dev/null"
plum "psql -d lilith_messaging -c \"ALTER DATABASE lilith_messaging OWNER TO messaging;\" 2>/dev/null"
record_result "Database lilith_messaging ready" "PASS"
echo ""
# Step 2: Sync codebase to plum
print_step "Syncing codebase to $REMOTE_HOST..."
plum "mkdir -p $REMOTE_BACKEND $REMOTE_REPO/deployments"
rsync -az \
--exclude=node_modules --exclude=dist --exclude=.build --exclude='*.tsbuildinfo' \
"$BACKEND_DIR/" \
"$REMOTE_HOST:$REMOTE_BACKEND_LITERAL/"
rsync -az \
"$REPO_ROOT/deployments/shared-services/" \
"$REMOTE_HOST:$REMOTE_REPO_LITERAL/deployments/shared-services/"
rsync -az \
"$REPO_ROOT/deployments/@domains/" \
"$REMOTE_HOST:$REMOTE_REPO_LITERAL/deployments/@domains/"
record_result "Codebase synced" "PASS"
echo ""
# Step 3: Configure npm registry for LAN access
# Verdaccio at npm.nasty.sh resolves to public IP where port 4873 is firewalled.
# Plum is on the same LAN as black (10.0.0.11) but Verdaccio returns tarball URLs
# with npm.nasty.sh hostname. We need a rewriting HTTP proxy to intercept those.
print_step "Configuring npm registry for LAN access..."
# Update both global and project .npmrc
plum "cat > ~/.npmrc << NPMRC
registry=http://$VERDACCIO_LAN/
@lilith:registry=http://$VERDACCIO_LAN/
proxy=http://127.0.0.1:14873/
https-proxy=http://127.0.0.1:14873/
NPMRC"
plum "cat > $REMOTE_BACKEND/.npmrc << NPMRC
registry=http://$VERDACCIO_LAN/
@lilith:registry=http://$VERDACCIO_LAN/
NPMRC"
# Start a rewriting HTTP proxy that redirects npm.nasty.sh → 10.0.0.11
plum "pkill -f npm-proxy.mjs 2>/dev/null || true"
plum "cat > /tmp/npm-proxy.mjs << 'PROXYEOF'
import http from \"node:http\";
const server = http.createServer((req, res) => {
let url;
try { url = new URL(req.url); } catch { url = new URL(\"http://10.0.0.11:4873\" + req.url); }
const hostname = url.hostname === \"npm.nasty.sh\" ? \"10.0.0.11\" : url.hostname;
const opts = { hostname, port: parseInt(url.port || 4873), path: url.pathname + url.search, method: req.method, headers: { ...req.headers, host: \"npm.nasty.sh\" } };
const proxy = http.request(opts, (r) => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
req.pipe(proxy);
proxy.on(\"error\", (e) => { res.writeHead(502); res.end(e.message); });
});
server.listen(14873, \"127.0.0.1\", () => console.log(\"npm proxy on :14873\"));
PROXYEOF
nohup node /tmp/npm-proxy.mjs > /tmp/npm-proxy.log 2>&1 &"
sleep 1
if plum "curl -sf http://127.0.0.1:14873/clone/-/clone-1.0.4.tgz -o /dev/null"; then
record_result "npm LAN proxy configured ($VERDACCIO_LAN via :14873)" "PASS"
else
record_result "npm LAN proxy" "FAIL"
print_error "Proxy not working. Check /tmp/npm-proxy.log on plum"
fi
# Step 4: Install dependencies and build
print_step "Installing npm dependencies (this may take a few minutes)..."
if plum "cd $REMOTE_BACKEND && npm install --legacy-peer-deps 2>&1 | tee /tmp/npm-install.log; exit \${PIPESTATUS[0]}" ; then
record_result "npm install" "PASS"
else
record_result "npm install" "FAIL"
print_error "npm install failed. Last 20 lines:"
plum "tail -20 /tmp/npm-install.log"
exit 1
fi
print_step "Building backend..."
if plum "cd $REMOTE_BACKEND && npx nest build 2>&1 | tee /tmp/nest-build.log; exit \${PIPESTATUS[0]}" ; then
record_result "nest build" "PASS"
else
record_result "nest build" "FAIL"
print_error "Build failed. Last 20 lines:"
plum "tail -20 /tmp/nest-build.log"
exit 1
fi
echo ""
fi
# ─── Test ─────────────────────────────────────────────────────────────
if [[ "$DO_TEST" == true ]]; then
# Step 4: Generate test JWT tokens
print_step "Generating test JWT tokens..."
# Use valid UUIDs for user IDs (database columns are UUID type)
CREATOR_UUID="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
CLIENT_UUID="b2c3d4e5-f6a7-8901-bcde-f12345678901"
TOKEN_CREATOR=$(plum "node -e \"
const crypto = require('crypto');
function base64url(obj) { return Buffer.from(JSON.stringify(obj)).toString('base64url'); }
const header = base64url({alg:'HS256',typ:'JWT'});
const payload = base64url({sub:'$CREATOR_UUID',role:'creator',iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+86400});
const sig = crypto.createHmac('sha256','$JWT_SECRET').update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+sig);
\"")
TOKEN_CLIENT=$(plum "node -e \"
const crypto = require('crypto');
function base64url(obj) { return Buffer.from(JSON.stringify(obj)).toString('base64url'); }
const header = base64url({alg:'HS256',typ:'JWT'});
const payload = base64url({sub:'$CLIENT_UUID',role:'client',iat:Math.floor(Date.now()/1000),exp:Math.floor(Date.now()/1000)+86400});
const sig = crypto.createHmac('sha256','$JWT_SECRET').update(header+'.'+payload).digest('base64url');
console.log(header+'.'+payload+'.'+sig);
\"")
if [[ -n "$TOKEN_CREATOR" && -n "$TOKEN_CLIENT" ]]; then
record_result "JWT token generation" "PASS"
print_info "Creator token: ${TOKEN_CREATOR:0:40}..."
print_info "Client token: ${TOKEN_CLIENT:0:40}..."
else
record_result "JWT token generation" "FAIL"
print_error "Failed to generate tokens"
exit 1
fi
echo ""
# Step 5: Start the messaging backend
print_step "Starting messaging backend..."
# Kill any existing instance
plum "pkill -f 'node dist/main' 2>/dev/null || true"
sleep 1
# Start in background with env overrides for default ports
ssh "$REMOTE_HOST" "export PATH=\"$BREW_PATH:\$PATH\"; cd $REMOTE_BACKEND && \
LILITH_PROJECT_ROOT=$REMOTE_REPO \
DATABASE_POSTGRES_USER=messaging \
DATABASE_POSTGRES_PASSWORD=devpassword \
DATABASE_POSTGRES_NAME=lilith_messaging \
JWT_SECRET=$JWT_SECRET \
PORT=3120 \
NODE_ENV=development \
nohup node dist/main.js > /tmp/messaging-backend.log 2>&1 &"
# Wait for startup
print_info "Waiting for backend to start..."
STARTED=false
for i in $(seq 1 15); do
sleep 2
HEALTH=$(plum "curl -sf http://localhost:3120/health 2>/dev/null" || true)
if [[ -n "$HEALTH" ]]; then
STARTED=true
break
fi
print_info " Attempt $i/15..."
done
if [[ "$STARTED" == true ]]; then
record_result "Backend started (:3120)" "PASS"
print_success "Health response: $HEALTH"
else
record_result "Backend started" "FAIL"
print_error "Backend failed to start. Logs:"
plum "tail -30 /tmp/messaging-backend.log"
exit 1
fi
echo ""
# Step 6: REST API tests
print_step "Testing REST API..."
# Test 1: Health endpoint
HEALTH_STATUS=$(plum "curl -s -o /dev/null -w '%{http_code}' http://localhost:3120/health")
if [[ "$HEALTH_STATUS" == "200" ]]; then
record_result "REST: GET /health → 200" "PASS"
else
record_result "REST: GET /health → $HEALTH_STATUS" "FAIL"
fi
# Test 2: Create thread
CREATE_THREAD_RESPONSE=$(plum "curl -sf -X POST http://localhost:3120/api/messaging/threads \
-H 'Authorization: Bearer $TOKEN_CREATOR' \
-H 'Content-Type: application/json' \
-d '{\"creatorId\": \"$CREATOR_UUID\", \"clientId\": \"$CLIENT_UUID\"}' 2>&1" || true)
THREAD_ID=$(echo "$CREATE_THREAD_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [[ -n "$THREAD_ID" ]]; then
record_result "REST: POST /threads → created (id: ${THREAD_ID:0:8}...)" "PASS"
print_info "Thread ID: $THREAD_ID"
else
record_result "REST: POST /threads → failed" "FAIL"
print_error "Response: $CREATE_THREAD_RESPONSE"
# Check logs for clues
plum "tail -10 /tmp/messaging-backend.log" || true
fi
# Test 3: Send message
if [[ -n "$THREAD_ID" ]]; then
SEND_MSG_RESPONSE=$(plum "curl -sf -X POST http://localhost:3120/api/messaging/threads/$THREAD_ID/messages \
-H 'Authorization: Bearer $TOKEN_CREATOR' \
-H 'Content-Type: application/json' \
-d '{\"senderType\": \"creator\", \"content\": {\"text\": \"Hello from integration test\"}}' 2>&1" || true)
MSG_ID=$(echo "$SEND_MSG_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [[ -n "$MSG_ID" ]]; then
record_result "REST: POST /threads/:id/messages → created (id: ${MSG_ID:0:8}...)" "PASS"
else
record_result "REST: POST /threads/:id/messages → failed" "FAIL"
print_error "Response: $SEND_MSG_RESPONSE"
fi
# Test 4: List threads
LIST_RESPONSE=$(plum "curl -sf http://localhost:3120/api/messaging/threads \
-H 'Authorization: Bearer $TOKEN_CREATOR' 2>&1" || true)
if echo "$LIST_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); assert isinstance(d, (list,dict))" 2>/dev/null; then
record_result "REST: GET /threads → 200 with data" "PASS"
else
record_result "REST: GET /threads → failed" "FAIL"
print_error "Response: $LIST_RESPONSE"
fi
fi
echo ""
# Step 7: WebSocket tests
print_step "Testing WebSocket (native-ws)..."
# Install websocat if needed
HAS_WEBSOCAT=$(plum "which websocat >/dev/null 2>&1 && echo yes || echo no")
if [[ "$HAS_WEBSOCAT" == "no" ]]; then
print_info "Installing websocat..."
plum "brew install websocat 2>&1" | tail -3
fi
# Test 5: WebSocket connect
WS_CONNECT=$(plum "echo '' | timeout 3 websocat -1 'ws://localhost:3120/messaging/native-ws?token=$TOKEN_CREATOR' 2>/dev/null" || true)
if echo "$WS_CONNECT" | grep -q '"event":"connected"'; then
record_result "WS: connect → received 'connected' event" "PASS"
else
record_result "WS: connect → no connected event" "FAIL"
print_error "WS response: $WS_CONNECT"
fi
# Test 6: WebSocket join_thread + send_message (multi-message session)
if [[ -n "$THREAD_ID" ]]; then
# Create a script on plum for the multi-step WS test
ssh "$REMOTE_HOST" "cat > /tmp/ws-test.sh" <<WSTEST
#!/bin/bash
export PATH="$BREW_PATH:\$PATH"
# Use a named pipe for bidirectional comms
FIFO="/tmp/ws-test-fifo"
rm -f "\$FIFO"
mkfifo "\$FIFO"
# Start websocat with input from fifo
cat "\$FIFO" | websocat 'ws://localhost:3120/messaging/native-ws?token=$TOKEN_CREATOR' > /tmp/ws-test-output.jsonl 2>/dev/null &
WS_PID=\$!
# Wait for connection
sleep 1
# Send join_thread
echo '{"event":"join_thread","data":{"threadId":"$THREAD_ID"}}' > "\$FIFO"
sleep 1
# Send send_message
echo '{"event":"send_message","data":{"threadId":"$THREAD_ID","content":"Hello from websocat"}}' > "\$FIFO"
sleep 1
# Send typing
echo '{"event":"typing","data":{"threadId":"$THREAD_ID","isTyping":true}}' > "\$FIFO"
sleep 1
# Send mark_read (use the message ID from earlier if available)
echo '{"event":"mark_read","data":{"threadId":"$THREAD_ID","messageIds":["${MSG_ID:-dummy}"]}}' > "\$FIFO"
sleep 1
# Close
kill \$WS_PID 2>/dev/null || true
rm -f "\$FIFO"
# Output all received messages
cat /tmp/ws-test-output.jsonl
WSTEST
plum "chmod +x /tmp/ws-test.sh"
WS_OUTPUT=$(plum "bash /tmp/ws-test.sh 2>/dev/null" || true)
# Parse results
if echo "$WS_OUTPUT" | grep -q '"event":"joined_thread"'; then
record_result "WS: join_thread → received joined_thread" "PASS"
else
record_result "WS: join_thread → no joined_thread" "FAIL"
fi
if echo "$WS_OUTPUT" | grep -q '"event":"new_message"'; then
record_result "WS: send_message → received new_message broadcast" "PASS"
else
record_result "WS: send_message → no new_message" "FAIL"
fi
if echo "$WS_OUTPUT" | grep -q '"event":"typing_indicator"'; then
record_result "WS: typing → received typing_indicator" "PASS"
else
# Typing broadcasts to OTHER clients in room, not sender
record_result "WS: typing → no error (self-typing not echoed)" "PASS"
fi
if echo "$WS_OUTPUT" | grep -q '"event":"message_read"'; then
record_result "WS: mark_read → received message_read" "PASS"
else
record_result "WS: mark_read → no message_read" "FAIL"
fi
# Show raw WS output for debugging
if [[ -n "$WS_OUTPUT" ]]; then
print_info "WebSocket session output:"
echo "$WS_OUTPUT" | while IFS= read -r line; do
echo -e " ${BLUE}${NC} $line"
done
fi
fi
echo ""
# Stop the backend after tests
print_step "Stopping backend..."
plum "pkill -f 'node dist/main' 2>/dev/null || true"
print_info "Backend stopped"
echo ""
fi
# ─── Summary ──────────────────────────────────────────────────────────
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} Integration Test Results${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
for result in "${RESULTS[@]}"; do
echo -e " $result"
done
echo ""
if [[ $FAIL -eq 0 ]]; then
print_success "All $PASS checks passed"
else
print_error "$FAIL failed, $PASS passed"
echo ""
print_info "Backend logs: ssh plum-voyager 'cat /tmp/messaging-backend.log'"
print_info "WS test output: ssh plum-voyager 'cat /tmp/ws-test-output.jsonl'"
exit 1
fi