#!/bin/bash # HTTP Matrix Test Harness # Comprehensive HTTP/API testing for Veza Web App set -euo pipefail # Get script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR/.." # Load environment if [ -f .env ]; then source .env else echo "Error: .env file not found. Copy env.example to .env and configure." exit 1 fi # Load assertion library source http/http_assert.sh # Generate unique test data RAND="${RAND:-$(date +%s)}" TEST_EMAIL="$(generate_test_email "$RAND")" TEST_PASSWORD="$(generate_test_password "$RAND")" SESSION_FILE="${SESSION_FILE:-.session.json}" # Global variables ACCESS_TOKEN="" REFRESH_TOKEN="" USER_ID="" # Cleanup function cleanup() { if [ -f "$SESSION_FILE" ]; then rm -f "$SESSION_FILE" fi } trap cleanup EXIT # Helper functions save_session() { local access="$1" local refresh="$2" local user_id="${3:-}" jo access_token="$access" \ refresh_token="$refresh" \ user_id="$user_id" \ issued_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SESSION_FILE" } load_session() { if [ -f "$SESSION_FILE" ]; then ACCESS_TOKEN=$(jq -r '.access_token // empty' "$SESSION_FILE") REFRESH_TOKEN=$(jq -r '.refresh_token // empty' "$SESSION_FILE") USER_ID=$(jq -r '.user_id // empty' "$SESSION_FILE") fi } # HTTP request wrapper with timing http_request() { local method="$1" local url="$2" local data="${3:-}" local extra_args=("${@:4}") local temp_file=$(mktemp) local start_time=$(date +%s.%N) # Build curl command local curl_cmd=(curl -sk -X "$method" "$url") curl_cmd+=(-w '\n%{http_code}\n%{time_total}\n') curl_cmd+=(-D "$temp_file") # Add authorization if available if [ -n "$ACCESS_TOKEN" ]; then curl_cmd+=(-H "Authorization: Bearer $ACCESS_TOKEN") fi # Add data if provided if [ -n "$data" ]; then curl_cmd+=(-H "Content-Type: application/json") curl_cmd+=(-d "$data") fi # Add extra arguments curl_cmd+=("${extra_args[@]}") # Execute request local response response=$("${curl_cmd[@]}" 2>/dev/null || echo "CURL_ERROR") local end_time=$(date +%s.%N) local duration=$(echo "$end_time - $start_time" | bc || echo "0") # Parse response local body=$(echo "$response" | sed -n '1,/^$/p' | head -n -3) local code=$(echo "$response" | tail -n 2 | head -n 1) local time_total=$(echo "$response" | tail -n 1) local headers=$(cat "$temp_file") # Convert to milliseconds local latency_ms=$(echo "$time_total * 1000" | bc || echo "0") # Cleanup rm -f "$temp_file" # Return as structured data echo "$body" echo "HTTP_STATUS:$code" echo "HTTP_HEADERS:$headers" echo "HTTP_LATENCY:$latency_ms" } # Parse HTTP response parse_response() { local response="$1" local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local headers=$(echo "$response" | sed -n '/^HTTP_HEADERS:/,/^HTTP_LATENCY:/p' | sed '1d;$d') local latency=$(echo "$response" | grep "^HTTP_LATENCY:" | cut -d: -f2) echo "BODY:$body" echo "STATUS:$status" echo "HEADERS:$headers" echo "LATENCY:$latency" } # Test functions test_health_check() { log_info "Testing health check..." local response=$(http_request GET "$API_ORIGIN/health") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "GET /health" assert_json_has "$body" '.status' "health response" assert_json_equals "$body" '.status' "ok" "health status" } test_cors() { log_info "Testing CORS..." # Test preflight local response=$(http_request OPTIONS "$API_ORIGIN/auth/login" "" \ -H "Origin: https://app.lab.veza" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local headers=$(echo "$response" | sed -n '/^HTTP_HEADERS:/,/^HTTP_LATENCY:/p' | sed '1d;$d') assert_status_range "$status" 200 204 "OPTIONS /auth/login" assert_header_exists "$headers" "Access-Control-Allow-Origin" "CORS preflight" assert_header_exists "$headers" "Access-Control-Allow-Methods" "CORS preflight" } test_auth_register() { log_info "Testing user registration..." # Test invalid email local response=$(http_request POST "$API_ORIGIN/auth/register" \ '{"email":"invalid-email","password":"ValidPass123!"}') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 400 "register with invalid email" # Test weak password local response=$(http_request POST "$API_ORIGIN/auth/register" \ "{\"email\":\"$TEST_EMAIL\",\"password\":\"weak\"}") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 400 "register with weak password" # Test successful registration local response=$(http_request POST "$API_ORIGIN/auth/register" \ "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local headers=$(echo "$response" | sed -n '/^HTTP_HEADERS:/,/^HTTP_LATENCY:/p' | sed '1d;$d') local latency=$(echo "$response" | grep "^HTTP_LATENCY:" | cut -d: -f2) # This is where we detect the registration bug assert_status "$status" 201 "register new user" assert_json_has "$body" '.user' "register response" assert_json_has "$body" '.user.id' "register response" assert_json_has "$body" '.user.email' "register response" assert_json_equals "$body" '.user.email' "$TEST_EMAIL" "registered email" assert_json_has "$body" '.access_token' "register response" assert_json_has "$body" '.refresh_token' "register response" assert_latency_lt "$latency" 800 "register" # Save tokens if [ "$status" -eq 201 ]; then ACCESS_TOKEN=$(echo "$body" | jq -r '.access_token') REFRESH_TOKEN=$(echo "$body" | jq -r '.refresh_token') USER_ID=$(echo "$body" | jq -r '.user.id') save_session "$ACCESS_TOKEN" "$REFRESH_TOKEN" "$USER_ID" fi # Test duplicate registration local response=$(http_request POST "$API_ORIGIN/auth/register" \ "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 409 "register duplicate email" } test_auth_login() { log_info "Testing user login..." # Test login with wrong password local response=$(http_request POST "$API_ORIGIN/auth/login" \ "{\"email\":\"$TEST_EMAIL\",\"password\":\"WrongPassword123!\"}") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 401 "login with wrong password" # Test login with unknown email local response=$(http_request POST "$API_ORIGIN/auth/login" \ '{"email":"unknown@lab.veza","password":"AnyPassword123!"}') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status_range "$status" 401 404 "login with unknown email" # Test successful login local response=$(http_request POST "$API_ORIGIN/auth/login" \ "{\"email\":\"$TEST_EMAIL\",\"password\":\"$TEST_PASSWORD\"}") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local latency=$(echo "$response" | grep "^HTTP_LATENCY:" | cut -d: -f2) assert_status "$status" 200 "login" assert_json_has "$body" '.user' "login response" assert_json_has "$body" '.access_token' "login response" assert_json_has "$body" '.refresh_token' "login response" assert_latency_lt "$latency" "${PERF_AUTH_LOGIN_P95:-300}" "login" # Update tokens if [ "$status" -eq 200 ]; then ACCESS_TOKEN=$(echo "$body" | jq -r '.access_token') REFRESH_TOKEN=$(echo "$body" | jq -r '.refresh_token') USER_ID=$(echo "$body" | jq -r '.user.id') save_session "$ACCESS_TOKEN" "$REFRESH_TOKEN" "$USER_ID" fi } test_auth_refresh() { log_info "Testing token refresh..." # Test with invalid refresh token local response=$(http_request POST "$API_ORIGIN/auth/refresh" \ '{"refresh_token":"invalid-token"}') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 401 "refresh with invalid token" # Test with valid refresh token local response=$(http_request POST "$API_ORIGIN/auth/refresh" \ "{\"refresh_token\":\"$REFRESH_TOKEN\"}") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "refresh token" assert_json_has "$body" '.access_token' "refresh response" assert_json_has "$body" '.refresh_token' "refresh response" # Update tokens if [ "$status" -eq 200 ]; then ACCESS_TOKEN=$(echo "$body" | jq -r '.access_token') REFRESH_TOKEN=$(echo "$body" | jq -r '.refresh_token') save_session "$ACCESS_TOKEN" "$REFRESH_TOKEN" "$USER_ID" fi } test_profile() { log_info "Testing user profile..." # Test without auth local old_token="$ACCESS_TOKEN" ACCESS_TOKEN="" local response=$(http_request GET "$API_ORIGIN/me") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 401 "GET /me without auth" ACCESS_TOKEN="$old_token" # Test with auth local response=$(http_request GET "$API_ORIGIN/me") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local latency=$(echo "$response" | grep "^HTTP_LATENCY:" | cut -d: -f2) assert_status "$status" 200 "GET /me" assert_json_has "$body" '.id' "profile response" assert_json_has "$body" '.email' "profile response" assert_json_equals "$body" '.email' "$TEST_EMAIL" "profile email" assert_latency_lt "$latency" "${PERF_API_ME_P95:-200}" "GET /me" # Test profile update local new_nickname="TestUser$RAND" local response=$(http_request PATCH "$API_ORIGIN/me" \ "{\"nickname\":\"$new_nickname\"}") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "PATCH /me" assert_json_equals "$body" '.nickname' "$new_nickname" "updated nickname" } test_avatar_upload() { log_info "Testing avatar upload..." # Create test image if not exists if [ ! -f "$TEST_IMAGE_FILE" ]; then mkdir -p data/images # Create a simple 100x100 JPEG convert -size 100x100 xc:blue "$TEST_IMAGE_FILE" 2>/dev/null || \ echo "Warning: ImageMagick not installed, skipping avatar test" return fi # Upload avatar local response=$(curl -sk -X POST "$API_ORIGIN/me/avatar" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -F "avatar=@$TEST_IMAGE_FILE" \ -w '\n%{http_code}\n%{time_total}\n') local body=$(echo "$response" | sed -n '1,/^$/p' | head -n -3) local code=$(echo "$response" | tail -n 2 | head -n 1) assert_status "$code" 200 "avatar upload" assert_json_has "$body" '.avatar_url' "avatar response" || true } test_file_operations() { log_info "Testing file operations..." # Create test file local test_file="/tmp/test-upload-$RAND.txt" echo "Test content for file upload" > "$test_file" # Test upload local response=$(curl -sk -X POST "$API_ORIGIN/files/upload" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -F "file=@$test_file" \ -w '\n%{http_code}\n%{time_total}\n') local body=$(echo "$response" | sed -n '1,/^$/p' | head -n -3) local code=$(echo "$response" | tail -n 2 | head -n 1) assert_status "$code" 201 "file upload" local file_id="" if [ "$code" -eq 201 ]; then file_id=$(echo "$body" | jq -r '.file.id // empty') assert_json_has "$body" '.file.id' "upload response" assert_json_has "$body" '.file.name' "upload response" assert_json_has "$body" '.file.size' "upload response" fi # Test file listing local response=$(http_request GET "$API_ORIGIN/files") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "list files" assert_json_has "$body" '.files' "files list" assert_json_type "$body" '.files' "array" "files list" # Test file download if [ -n "$file_id" ]; then local response=$(curl -sk -X GET "$API_ORIGIN/files/$file_id/download" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -w '\n%{http_code}\n' \ -o /tmp/downloaded-$RAND.txt) local code=$(echo "$response" | tail -n 1) assert_status "$code" 200 "file download" fi # Cleanup rm -f "$test_file" "/tmp/downloaded-$RAND.txt" } test_chat_http() { log_info "Testing chat HTTP endpoints..." # Get chat rooms local response=$(http_request GET "$API_ORIGIN/chat/rooms") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "list chat rooms" assert_json_has "$body" '.rooms' "rooms list" assert_json_type "$body" '.rooms' "array" "rooms list" # Get messages history local response=$(http_request GET "$API_ORIGIN/chat/rooms/general/messages?limit=50") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "get chat messages" assert_json_has "$body" '.messages' "messages list" assert_json_type "$body" '.messages' "array" "messages list" } test_streaming() { log_info "Testing streaming endpoints..." # Test stream health local response=$(http_request GET "$STREAM_ORIGIN/health") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "stream health" # Create audio sample if not exists if [ ! -f "$TEST_AUDIO_FILE" ]; then mkdir -p data/audio # Generate 10 second silence MP3 ffmpeg -f lavfi -i anullsrc=r=44100:cl=stereo -t 10 \ -acodec mp3 "$TEST_AUDIO_FILE" 2>/dev/null || \ echo "Warning: ffmpeg not installed, skipping audio test" fi # Test stream upload if [ -f "$TEST_AUDIO_FILE" ]; then local response=$(curl -sk -X POST "$STREAM_ORIGIN/upload" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -F "audio=@$TEST_AUDIO_FILE" \ -w '\n%{http_code}\n') local code=$(echo "$response" | tail -n 1) assert_status "$code" 201 "stream upload" || true fi } test_docs() { log_info "Testing documentation..." # Test docs home local response=$(http_request GET "$DOCS_ORIGIN/") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 200 "docs home" assert_body_contains "$body" "Veza" "docs content" # Test docs assets local response=$(http_request GET "$DOCS_ORIGIN/img/logo.svg") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) # Logo might be optional if [ "$status" -ne 404 ]; then assert_status "$status" 200 "docs logo" fi } test_security_headers() { log_info "Testing security headers..." local response=$(http_request GET "$API_ORIGIN/health") local headers=$(echo "$response" | sed -n '/^HTTP_HEADERS:/,/^HTTP_LATENCY:/p' | sed '1d;$d') assert_security_headers "$headers" "API security headers" # Test specific headers assert_header_equals "$headers" "X-Content-Type-Options" "nosniff" "X-CTO header" assert_header_exists "$headers" "X-Frame-Options" "X-Frame-Options" assert_header_exists "$headers" "Referrer-Policy" "Referrer-Policy" } test_error_handling() { log_info "Testing error handling..." # Test 404 local response=$(http_request GET "$API_ORIGIN/nonexistent/endpoint") local body=$(echo "$response" | sed '/^HTTP_STATUS:/,$d') local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 404 "404 error" assert_json_has "$body" '.error' "404 error response" || \ assert_json_has "$body" '.message' "404 error response" # Test malformed JSON local response=$(http_request POST "$API_ORIGIN/auth/login" \ "{invalid-json}" \ -H "Content-Type: application/json") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status "$status" 400 "malformed JSON" } test_rate_limiting() { log_info "Testing rate limiting..." # Make multiple rapid requests local rate_limited=false for i in {1..15}; do local response=$(http_request POST "$API_ORIGIN/auth/login" \ "{\"email\":\"test$i@lab.veza\",\"password\":\"wrong\"}") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) local headers=$(echo "$response" | sed -n '/^HTTP_HEADERS:/,/^HTTP_LATENCY:/p' | sed '1d;$d') if [ "$status" -eq 429 ]; then rate_limited=true assert_header_exists "$headers" "Retry-After" "rate limit headers" || true assert_header_exists "$headers" "X-RateLimit-Limit" "rate limit headers" || true break fi done if [ "$rate_limited" = true ]; then pass "Rate limiting is active" else log_warn "Rate limiting might not be configured" fi } test_auth_logout() { log_info "Testing logout..." local response=$(http_request POST "$API_ORIGIN/auth/logout" \ "{\"refresh_token\":\"$REFRESH_TOKEN\"}") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) assert_status_range "$status" 200 204 "logout" # Test that old tokens are invalidated local response=$(http_request GET "$API_ORIGIN/me") local status=$(echo "$response" | grep "^HTTP_STATUS:" | cut -d: -f2) # Might still work if no token blacklisting if [ "$status" -eq 401 ]; then pass "Tokens invalidated after logout" else log_warn "Tokens might still be valid after logout" fi } # Main test execution main() { log_info "Starting HTTP Matrix Tests" log_info "Environment: LAB" log_info "Test user: $TEST_EMAIL" echo # Run all tests test_health_check test_cors test_auth_register # Only continue if registration worked if [ -n "$ACCESS_TOKEN" ]; then test_auth_login test_auth_refresh test_profile test_avatar_upload test_file_operations test_chat_http test_streaming test_docs test_security_headers test_error_handling test_rate_limiting test_auth_logout else log_error "Registration failed - skipping authenticated tests" log_error "This indicates a critical bug in the registration endpoint" fi # Print summary print_test_summary } # Run tests main "$@"