#!/bin/bash # Comprehensive C-Relay Test - Test event types and subscriptions # Uses nak to generate and publish various event types, then tests subscriptions set -e # Exit on any error # Color constants RED='\033[31m' GREEN='\033[32m' YELLOW='\033[33m' BLUE='\033[34m' BOLD='\033[1m' RESET='\033[0m' # Test configuration RELAY_URL="ws://127.0.0.1:8888" TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99" # Print functions print_header() { echo -e "${BLUE}${BOLD}=== $1 ===${RESET}" } print_step() { echo -e "${YELLOW}[STEP]${RESET} $1" } print_success() { echo -e "${GREEN}✓${RESET} $1" } print_error() { echo -e "${RED}✗${RESET} $1" } print_info() { echo -e "${BLUE}[INFO]${RESET} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${RESET} $1" } # Global arrays to store event IDs for subscription tests declare -a REGULAR_EVENT_IDS=() declare -a REPLACEABLE_EVENT_IDS=() declare -a EPHEMERAL_EVENT_IDS=() declare -a ADDRESSABLE_EVENT_IDS=() # Helper function to publish event and extract ID publish_event() { local event_json="$1" local event_type="$2" local description="$3" # Extract event ID local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null) if [[ "$event_id" == "null" || -z "$event_id" ]]; then print_error "Could not extract event ID from $description" return 1 fi print_info "Publishing $description..." # Create EVENT message in Nostr format local event_message="[\"EVENT\",$event_json]" # Publish to relay local response="" if command -v websocat &> /dev/null; then response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") else print_error "websocat not found - required for testing" return 1 fi # Check response if [[ "$response" == *"Connection failed"* ]]; then print_error "Failed to connect to relay for $description" return 1 elif [[ "$response" == *"true"* ]]; then print_success "$description uploaded (ID: ${event_id:0:16}...)" # Store event ID in appropriate array case "$event_type" in "regular") REGULAR_EVENT_IDS+=("$event_id") ;; "replaceable") REPLACEABLE_EVENT_IDS+=("$event_id") ;; "ephemeral") EPHEMERAL_EVENT_IDS+=("$event_id") ;; "addressable") ADDRESSABLE_EVENT_IDS+=("$event_id") ;; esac echo # Add blank line for readability return 0 else print_warning "$description might have failed: $response" echo # Add blank line for readability return 1 fi } # Helper function to publish invalid event and expect rejection publish_invalid_event() { local event_json="$1" local description="$2" local expected_error="$3" print_info "Publishing invalid $description..." # Create EVENT message in Nostr format local event_message="[\"EVENT\",$event_json]" # Publish to relay local response="" if command -v websocat &> /dev/null; then response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") else print_error "websocat not found - required for testing" return 1 fi # Check response - should contain "false" and error message if [[ "$response" == *"Connection failed"* ]]; then print_error "Failed to connect to relay for $description" return 1 elif [[ "$response" == *"false"* ]]; then # Extract error message local error_msg=$(echo "$response" | grep -o '"[^"]*invalid[^"]*"' | head -1 | sed 's/"//g' 2>/dev/null || echo "rejected") print_success "$description correctly rejected: $error_msg" echo # Add blank line for readability return 0 elif [[ "$response" == *"true"* ]]; then print_error "$description was incorrectly accepted (should have been rejected)" echo # Add blank line for readability return 1 else print_warning "$description response unclear: $response" echo # Add blank line for readability return 1 fi } # Test subscription with filters test_subscription() { local sub_id="$1" local filter="$2" local description="$3" local expected_count="$4" print_step "Testing subscription: $description" # Create REQ message local req_message="[\"REQ\",\"$sub_id\",$filter]" print_info "Testing filter: $filter" # Send subscription and collect events local response="" if command -v websocat &> /dev/null; then response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "") fi # Count EVENT responses (lines containing ["EVENT","sub_id",...]) local event_count=0 local filter_mismatch_count=0 if [[ -n "$response" ]]; then event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0") filter_mismatch_count=$(echo "$response" | grep -c "filter does not match" 2>/dev/null || echo "0") fi # Clean up the filter_mismatch_count (remove any extra spaces/newlines) filter_mismatch_count=$(echo "$filter_mismatch_count" | tr -d '[:space:]' | sed 's/[^0-9]//g') if [[ -z "$filter_mismatch_count" ]]; then filter_mismatch_count=0 fi # Debug: Show what we found print_info "Found $event_count events, $filter_mismatch_count filter mismatches" # Check for filter mismatches (protocol violation) if [[ "$filter_mismatch_count" -gt 0 ]]; then print_error "$description - PROTOCOL VIOLATION: Relay sent $filter_mismatch_count events that don't match filter!" print_error "Filter: $filter" print_error "This indicates improper server-side filtering - relay should only send matching events" return 1 fi # Additional check: Analyze returned events against filter criteria local filter_violation_count=0 if [[ -n "$response" && "$event_count" -gt 0 ]]; then # Parse filter to check for violations if echo "$filter" | grep -q '"kinds":\['; then # Kind filter - check that all returned events have matching kinds local allowed_kinds=$(echo "$filter" | sed 's/.*"kinds":\[\([^]]*\)\].*/\1/' | sed 's/[^0-9,]//g') echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do local event_kind=$(echo "$event_line" | jq -r '.[2].kind' 2>/dev/null) if [[ -n "$event_kind" && "$event_kind" =~ ^[0-9]+$ ]]; then local kind_matches=0 IFS=',' read -ra KIND_ARRAY <<< "$allowed_kinds" for kind in "${KIND_ARRAY[@]}"; do if [[ "$event_kind" == "$kind" ]]; then kind_matches=1 break fi done if [[ "$kind_matches" == "0" ]]; then ((filter_violation_count++)) fi fi done elif echo "$filter" | grep -q '"ids":\['; then # ID filter - check that all returned events have matching IDs local allowed_ids=$(echo "$filter" | sed 's/.*"ids":\[\([^]]*\)\].*/\1/' | sed 's/"//g' | sed 's/[][]//g') echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do local event_id=$(echo "$event_line" | jq -r '.[2].id' 2>/dev/null) if [[ -n "$event_id" ]]; then local id_matches=0 IFS=',' read -ra ID_ARRAY <<< "$allowed_ids" for id in "${ID_ARRAY[@]}"; do if [[ "$event_id" == "$id" ]]; then id_matches=1 break fi done if [[ "$id_matches" == "0" ]]; then ((filter_violation_count++)) fi fi done fi fi # Report filter violations if [[ "$filter_violation_count" -gt 0 ]]; then print_error "$description - FILTER VIOLATION: $filter_violation_count events don't match the filter criteria!" print_error "Filter: $filter" print_error "Expected only events matching the filter, but received non-matching events" print_error "This indicates improper server-side filtering" return 1 fi # Also fail on count mismatches for strict filters (like specific IDs and kinds with expected counts) if [[ "$expected_count" != "any" && "$event_count" != "$expected_count" ]]; then if echo "$filter" | grep -q '"ids":\['; then print_error "$description - CRITICAL VIOLATION: ID filter should return exactly $expected_count event(s), got $event_count" print_error "Filter: $filter" print_error "ID queries must return exactly the requested event or none" return 1 elif echo "$filter" | grep -q '"kinds":\[' && [[ "$expected_count" =~ ^[0-9]+$ ]]; then print_error "$description - FILTER VIOLATION: Kind filter expected $expected_count event(s), got $event_count" print_error "Filter: $filter" print_error "This suggests improper filtering - events of wrong kinds are being returned" return 1 fi fi if [[ "$expected_count" == "any" ]]; then if [[ $event_count -gt 0 ]]; then print_success "$description - Found $event_count events" else print_warning "$description - No events found" fi elif [[ $event_count -eq $expected_count ]]; then print_success "$description - Found expected $event_count events" else print_warning "$description - Expected $expected_count events, found $event_count" fi # Show a few sample events for verification (first 2) if [[ $event_count -gt 0 && "$description" == "All events" ]]; then print_info "Sample events (first 2):" echo "$response" | grep "\"EVENT\"" | head -2 | while IFS= read -r line; do local event_content=$(echo "$line" | jq -r '.[2].content' 2>/dev/null || echo "N/A") local event_kind=$(echo "$line" | jq -r '.[2].kind' 2>/dev/null || echo "N/A") local event_id=$(echo "$line" | jq -r '.[2].id' 2>/dev/null || echo "N/A") echo " - ID: ${event_id:0:16}... Kind: $event_kind Content: ${event_content:0:30}..." done fi echo # Add blank line for readability return 0 } # Main test function run_comprehensive_test() { print_header "C-Relay Comprehensive Test" # Check dependencies print_step "Checking dependencies..." if ! command -v nak &> /dev/null; then print_error "nak command not found" print_info "Please install nak: go install github.com/fiatjaf/nak@latest" return 1 fi if ! command -v websocat &> /dev/null; then print_error "websocat command not found" print_info "Please install websocat for testing" return 1 fi if ! command -v jq &> /dev/null; then print_error "jq command not found" print_info "Please install jq for JSON processing" return 1 fi print_success "All dependencies found" print_header "PHASE 1: Publishing Various Event Types" # Test 1: Regular Events (kind 1) print_step "Creating regular events (kind 1)..." local regular1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #1" -k 1 --ts $(($(date +%s) - 100)) -t "type=regular" -t "test=phase1" 2>/dev/null) local regular2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #2 with tags" -k 1 --ts $(($(date +%s) - 90)) -e "previous_event_id" -p "test_pubkey" -t "type=regular" -t "test=phase1" 2>/dev/null) publish_event "$regular1" "regular" "Regular event #1" publish_event "$regular2" "regular" "Regular event #2" # Test 2: Replaceable Events (kind 0 - metadata) print_step "Creating replaceable events (kind 0)..." local replaceable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User","about":"Testing C-Relay"}' -k 0 --ts $(($(date +%s) - 80)) -t "type=replaceable" 2>/dev/null) local replaceable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User Updated","about":"Updated profile"}' -k 0 --ts $(($(date +%s) - 70)) -t "type=replaceable" 2>/dev/null) publish_event "$replaceable1" "replaceable" "Replaceable event #1 (metadata)" publish_event "$replaceable2" "replaceable" "Replaceable event #2 (metadata update)" # Test 3: Ephemeral Events (kind 20000+) print_step "Creating ephemeral events (kind 20001)..." local ephemeral1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Ephemeral event - should not be stored permanently" -k 20001 --ts $(date +%s) -t "type=ephemeral" 2>/dev/null) publish_event "$ephemeral1" "ephemeral" "Ephemeral event" # Test 4: Addressable Events (kind 30000+) print_step "Creating addressable events (kind 30001)..." local addressable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event with d-tag" -k 30001 --ts $(($(date +%s) - 50)) -t "d=test-article" -t "type=addressable" 2>/dev/null) local addressable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Updated addressable event" -k 30001 --ts $(($(date +%s) - 40)) -t "d=test-article" -t "type=addressable" -t "updated=true" 2>/dev/null) publish_event "$addressable1" "addressable" "Addressable event #1" publish_event "$addressable2" "addressable" "Addressable event #2 (update)" # Brief pause to let events settle sleep 2 print_header "PHASE 2: Testing Invalid Events (NIP-01 Validation)" print_step "Testing various invalid events that should be rejected..." # Test 1: Event with invalid JSON structure (malformed) local malformed_event='{"id":"invalid","pubkey":"invalid_pubkey","created_at":"not_a_number","kind":1,"tags":[],"content":"test"}' publish_invalid_event "$malformed_event" "malformed event with invalid created_at" "invalid" # Test 2: Event with missing required fields local missing_field_event='{"id":"test123","pubkey":"valid_pubkey","kind":1,"tags":[],"content":"test"}' publish_invalid_event "$missing_field_event" "event missing created_at and sig" "invalid" # Test 3: Event with invalid pubkey format (not hex) local invalid_pubkey_event='{"id":"abc123","pubkey":"not_valid_hex_pubkey","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"fake_sig"}' publish_invalid_event "$invalid_pubkey_event" "event with invalid pubkey format" "invalid" # Test 4: Event with invalid event ID format local invalid_id_event='{"id":"not_64_char_hex","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}' publish_invalid_event "$invalid_id_event" "event with invalid ID format" "invalid" # Test 5: Event with invalid signature local invalid_sig_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"invalid_signature_format"}' publish_invalid_event "$invalid_sig_event" "event with invalid signature format" "invalid" # Test 6: Event with invalid kind (negative) local invalid_kind_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":-1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}' publish_invalid_event "$invalid_kind_event" "event with negative kind" "invalid" # Test 7: Event with invalid tags format (not array) local invalid_tags_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":"not_an_array","content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}' publish_invalid_event "$invalid_tags_event" "event with invalid tags format" "invalid" print_success "Invalid event tests completed - all should have been rejected" print_header "PHASE 3: Testing Subscriptions and Filters" # Test subscription filters print_step "Testing various subscription filters..." local test_failures=0 # Test 1: Get all events if ! test_subscription "test_all" '{}' "All events" "any"; then ((test_failures++)) fi # Test 2: Get events by kind if ! test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "any"; then ((test_failures++)) fi if ! test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"; then ((test_failures++)) fi # Test 3: Get events by author (pubkey) local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null) if ! test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"; then ((test_failures++)) fi # Test 4: Get recent events (time-based) local recent_timestamp=$(($(date +%s) - 200)) if ! test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"; then ((test_failures++)) fi # Test 5: Get events with specific tags if ! test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"; then ((test_failures++)) fi # Test 6: Multiple kinds if ! test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"; then ((test_failures++)) fi # Test 7: Limit results if ! test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"; then ((test_failures++)) fi # Test 8: Specific event ID query (tests for "filter does not match" bug) if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then local test_event_id="${REGULAR_EVENT_IDS[0]}" if ! test_subscription "test_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Specific event ID query" "1"; then ((test_failures++)) fi fi # Report subscription test results if [[ $test_failures -gt 0 ]]; then print_error "SUBSCRIPTION TESTS FAILED: $test_failures test(s) detected protocol violations" return 1 else print_success "All subscription tests passed" fi print_header "PHASE 4: Database Verification" # Check what's actually stored in the database print_step "Verifying database contents..." if command -v sqlite3 &> /dev/null; then # Find the database file (should be in build/ directory with relay pubkey as filename) local db_file="" if [[ -d "../build" ]]; then db_file=$(find ../build -name "*.db" -type f | head -1) fi if [[ -n "$db_file" && -f "$db_file" ]]; then print_info "Events by type in database ($db_file):" sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do echo " $line" done print_info "Recent events in database:" sqlite3 "$db_file" "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null | while read line; do echo " $line" done print_success "Database verification complete" else print_warning "Database file not found in build/ directory" print_info "Expected database files: build/*.db (named after relay pubkey)" fi else print_warning "sqlite3 not available for database verification" fi return 0 } # Run the comprehensive test print_header "Starting C-Relay Comprehensive Test Suite with NIP-01 Validation" echo if run_comprehensive_test; then echo print_success "All tests completed successfully!" print_info "The C-Relay with full NIP-01 validation is working correctly" print_info "✅ Event validation, signature verification, and error handling all working" echo exit 0 else echo print_error "❌ TESTS FAILED: Protocol violations detected!" print_error "The C-Relay has critical issues that need to be fixed:" print_error " - Server-side filtering is not implemented properly" print_error " - Events are sent to clients regardless of subscription filters" print_error " - This violates the Nostr protocol specification" echo exit 1 fi