veza/tools/tests/http/http_matrix.sh

576 lines
20 KiB
Bash
Raw Normal View History

#!/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 "$@"