#!/bin/bash # NIP-40 Expiration Timestamp Test Suite for C Nostr Relay # Tests expiration timestamp handling in the relay's event processing pipeline set -e # Exit on 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" HTTP_URL="http://127.0.0.1:8888" TEST_COUNT=0 PASSED_COUNT=0 FAILED_COUNT=0 # Test results tracking declare -a TEST_RESULTS=() print_info() { echo -e "${BLUE}[INFO]${RESET} $1" } print_success() { echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${RESET} $1" } print_error() { echo -e "${RED}${BOLD}[ERROR]${RESET} $1" } print_test_header() { TEST_COUNT=$((TEST_COUNT + 1)) echo "" echo -e "${BOLD}=== TEST $TEST_COUNT: $1 ===${RESET}" } record_test_result() { local test_name="$1" local result="$2" local details="$3" TEST_RESULTS+=("$test_name|$result|$details") if [ "$result" = "PASS" ]; then PASSED_COUNT=$((PASSED_COUNT + 1)) print_success "PASS: $test_name" else FAILED_COUNT=$((FAILED_COUNT + 1)) print_error "FAIL: $test_name" if [ -n "$details" ]; then echo " Details: $details" fi fi } # Check if relay is running check_relay_running() { print_info "Checking if relay is running..." if ! curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" >/dev/null 2>&1; then print_error "Relay is not running or not accessible at $HTTP_URL" print_info "Please start the relay with: ./make_and_restart_relay.sh" exit 1 fi print_success "Relay is running and accessible" } # Test NIP-11 relay information includes NIP-40 test_nip11_expiration_support() { print_test_header "NIP-11 Expiration Support Advertisement" print_info "Fetching relay information..." RELAY_INFO=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL/") echo "Relay Info Response:" echo "$RELAY_INFO" | jq '.' echo "" # Check if NIP-40 is in supported_nips if echo "$RELAY_INFO" | jq -e '.supported_nips | index(40)' >/dev/null 2>&1; then print_success "✓ NIP-40 found in supported_nips array" NIP40_SUPPORTED=true else print_error "✗ NIP-40 not found in supported_nips array" NIP40_SUPPORTED=false fi if [ "$NIP40_SUPPORTED" = true ]; then record_test_result "NIP-11 Expiration Support Advertisement" "PASS" "NIP-40 advertised in relay info" return 0 else record_test_result "NIP-11 Expiration Support Advertisement" "FAIL" "NIP-40 not advertised" return 1 fi } # Helper function to create event with expiration tag create_event_with_expiration() { local content="$1" local expiration_timestamp="$2" local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" if ! command -v nak &> /dev/null; then echo "" return 1 fi # Create event with expiration tag nak event --sec "$private_key" -c "$content" -t "expiration=$expiration_timestamp" --ts $(date +%s) } # Helper function to send event and check response send_event_and_check() { local event_json="$1" local expected_result="$2" # "accept" or "reject" local description="$3" if [ -z "$event_json" ]; then return 1 fi # Create EVENT message local event_message="[\"EVENT\",$event_json]" # Send to relay if command -v websocat &> /dev/null; then local response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed") print_info "Relay response: $response" if [[ "$response" == *"Connection failed"* ]]; then print_error "✗ Failed to connect to relay" return 1 elif [[ "$expected_result" == "accept" && "$response" == *"true"* ]]; then print_success "✓ $description accepted as expected" return 0 elif [[ "$expected_result" == "reject" && "$response" == *"false"* ]]; then print_success "✓ $description rejected as expected" return 0 elif [[ "$expected_result" == "accept" && "$response" == *"false"* ]]; then print_error "✗ $description unexpectedly rejected: $response" return 1 elif [[ "$expected_result" == "reject" && "$response" == *"true"* ]]; then print_error "✗ $description unexpectedly accepted: $response" return 1 else print_warning "? Unclear response for $description: $response" return 1 fi else print_error "websocat not found - required for testing" return 1 fi } # Test event without expiration tag test_event_without_expiration() { print_test_header "Event Submission Without Expiration Tag" if ! command -v nak &> /dev/null; then print_warning "nak command not found - skipping expiration tests" record_test_result "Event Submission Without Expiration Tag" "SKIP" "nak not available" return 0 fi print_info "Creating event without expiration tag..." local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" local event_json=$(nak event --sec "$private_key" -c "Test event without expiration" --ts $(date +%s)) print_info "Generated event:" echo "$event_json" | jq '.' echo "" if send_event_and_check "$event_json" "accept" "Event without expiration tag"; then record_test_result "Event Submission Without Expiration Tag" "PASS" "Non-expiring event accepted" return 0 else record_test_result "Event Submission Without Expiration Tag" "FAIL" "Non-expiring event handling failed" return 1 fi } # Test event with future expiration (should be accepted) test_event_with_future_expiration() { print_test_header "Event Submission With Future Expiration" if ! command -v nak &> /dev/null; then record_test_result "Event Submission With Future Expiration" "SKIP" "nak not available" return 0 fi print_info "Creating event with future expiration (1 hour from now)..." local future_timestamp=$(($(date +%s) + 3600)) # 1 hour from now local event_json=$(create_event_with_expiration "Test event expiring in 1 hour" "$future_timestamp") if [ -z "$event_json" ]; then record_test_result "Event Submission With Future Expiration" "FAIL" "Failed to create event" return 1 fi print_info "Generated event (expires at $future_timestamp):" echo "$event_json" | jq '.' echo "" if send_event_and_check "$event_json" "accept" "Event with future expiration"; then record_test_result "Event Submission With Future Expiration" "PASS" "Future-expiring event accepted" return 0 else record_test_result "Event Submission With Future Expiration" "FAIL" "Future-expiring event rejected" return 1 fi } # Test event with past expiration (should be rejected in strict mode) test_event_with_past_expiration() { print_test_header "Event Submission With Past Expiration" if ! command -v nak &> /dev/null; then record_test_result "Event Submission With Past Expiration" "SKIP" "nak not available" return 0 fi print_info "Creating event with past expiration (1 hour ago)..." local past_timestamp=$(($(date +%s) - 3600)) # 1 hour ago local event_json=$(create_event_with_expiration "Test event expired 1 hour ago" "$past_timestamp") if [ -z "$event_json" ]; then record_test_result "Event Submission With Past Expiration" "FAIL" "Failed to create event" return 1 fi print_info "Generated event (expired at $past_timestamp):" echo "$event_json" | jq '.' echo "" # In strict mode (default), this should be rejected if send_event_and_check "$event_json" "reject" "Event with past expiration"; then record_test_result "Event Submission With Past Expiration" "PASS" "Expired event correctly rejected in strict mode" return 0 else record_test_result "Event Submission With Past Expiration" "FAIL" "Expired event handling failed" return 1 fi } # Test event with expiration within grace period test_event_within_grace_period() { print_test_header "Event Submission Within Grace Period" if ! command -v nak &> /dev/null; then record_test_result "Event Submission Within Grace Period" "SKIP" "nak not available" return 0 fi print_info "Creating event with expiration within grace period (2 minutes ago, grace period is 5 minutes)..." local grace_timestamp=$(($(date +%s) - 120)) # 2 minutes ago (within 5 minute grace period) local event_json=$(create_event_with_expiration "Test event within grace period" "$grace_timestamp") if [ -z "$event_json" ]; then record_test_result "Event Submission Within Grace Period" "FAIL" "Failed to create event" return 1 fi print_info "Generated event (expired at $grace_timestamp, within grace period):" echo "$event_json" | jq '.' echo "" # Should be accepted due to grace period if send_event_and_check "$event_json" "accept" "Event within grace period"; then record_test_result "Event Submission Within Grace Period" "PASS" "Event within grace period accepted" return 0 else record_test_result "Event Submission Within Grace Period" "FAIL" "Grace period handling failed" return 1 fi } # Test event filtering in subscriptions test_expiration_filtering_in_subscriptions() { print_test_header "Expiration Filtering in Subscriptions" if ! command -v nak &> /dev/null || ! command -v websocat &> /dev/null; then record_test_result "Expiration Filtering in Subscriptions" "SKIP" "Required tools not available" return 0 fi print_info "Setting up short-lived events for proper expiration filtering test..." local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" # Event 1: No expiration (should always be returned) local event1=$(nak event --sec "$private_key" -c "Event without expiration for filtering test" --ts $(date +%s)) # Event 2: Future expiration (should be returned) local future_timestamp=$(($(date +%s) + 1800)) # 30 minutes from now local event2=$(create_event_with_expiration "Event with future expiration for filtering test" "$future_timestamp") # Event 3: SHORT-LIVED EVENT - expires in 3 seconds local short_expiry=$(($(date +%s) + 3)) # 3 seconds from now local event3=$(create_event_with_expiration "Short-lived event for filtering test" "$short_expiry") print_info "Publishing test events (including one that expires in 3 seconds)..." # Submit all events - they should all be accepted initially local response1=$(echo "[\"EVENT\",$event1]" | timeout 5s websocat "$RELAY_URL" 2>&1) local response2=$(echo "[\"EVENT\",$event2]" | timeout 5s websocat "$RELAY_URL" 2>&1) local response3=$(echo "[\"EVENT\",$event3]" | timeout 5s websocat "$RELAY_URL" 2>&1) print_info "Event submission responses:" echo "Event 1 (no expiry): $response1" echo "Event 2 (future expiry): $response2" echo "Event 3 (expires in 3s): $response3" echo "" # Verify all events were accepted if [[ "$response1" != *"true"* ]] || [[ "$response2" != *"true"* ]] || [[ "$response3" != *"true"* ]]; then record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Events not properly accepted during submission" return 1 fi print_success "✓ All events accepted during submission" # Test 1: Query immediately - all events should be present print_info "Testing immediate subscription (before expiration)..." local req_message='["REQ","filter_immediate",{"kinds":[1],"limit":10}]' local immediate_response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_immediate\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "") local immediate_count=0 if echo "$immediate_response" | grep -q "Event without expiration for filtering test"; then immediate_count=$((immediate_count + 1)) fi if echo "$immediate_response" | grep -q "Event with future expiration for filtering test"; then immediate_count=$((immediate_count + 1)) fi if echo "$immediate_response" | grep -q "Short-lived event for filtering test"; then immediate_count=$((immediate_count + 1)) fi print_info "Immediate response found $immediate_count/3 events" # Wait for the short-lived event to expire (5 seconds total wait) print_info "Waiting 5 seconds for short-lived event to expire..." sleep 5 # Test 2: Query after expiration - short-lived event should be filtered out print_info "Testing subscription after expiration (short-lived event should be filtered)..." req_message='["REQ","filter_after_expiry",{"kinds":[1],"limit":10}]' local expired_response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_after_expiry\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "") print_info "Post-expiration subscription response:" echo "$expired_response" echo "" # Count events in the expired response local no_exp_count=0 local future_exp_count=0 local expired_event_count=0 if echo "$expired_response" | grep -q "Event without expiration for filtering test"; then no_exp_count=1 print_success "✓ Event without expiration found in post-expiration results" fi if echo "$expired_response" | grep -q "Event with future expiration for filtering test"; then future_exp_count=1 print_success "✓ Event with future expiration found in post-expiration results" fi if echo "$expired_response" | grep -q "Short-lived event for filtering test"; then expired_event_count=1 print_error "✗ EXPIRED short-lived event found in subscription results (should be filtered!)" else print_success "✓ Expired short-lived event properly filtered from subscription results" fi # Evaluate results local expected_active_events=$((no_exp_count + future_exp_count)) if [ $expected_active_events -ge 2 ] && [ $expired_event_count -eq 0 ]; then record_test_result "Expiration Filtering in Subscriptions" "PASS" "Expired events properly filtered from subscriptions" return 0 else local details="Found $expected_active_events active events, $expired_event_count expired events (should be 0)" record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Expiration filtering not working properly in subscriptions - $details" return 1 fi } # Test malformed expiration tags test_malformed_expiration_tags() { print_test_header "Handling of Malformed Expiration Tags" if ! command -v nak &> /dev/null; then record_test_result "Handling of Malformed Expiration Tags" "SKIP" "nak not available" return 0 fi print_info "Testing events with malformed expiration tags..." local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe" # Test 1: Non-numeric expiration value local event1=$(nak event --sec "$private_key" -c "Event with non-numeric expiration" -t "expiration=not_a_number" --ts $(date +%s)) # Test 2: Empty expiration value local event2=$(nak event --sec "$private_key" -c "Event with empty expiration" -t "expiration=" --ts $(date +%s)) print_info "Testing non-numeric expiration value..." if send_event_and_check "$event1" "accept" "Event with non-numeric expiration (should be treated as no expiration)"; then print_success "✓ Non-numeric expiration handled gracefully" malformed_test1=true else malformed_test1=false fi print_info "Testing empty expiration value..." if send_event_and_check "$event2" "accept" "Event with empty expiration (should be treated as no expiration)"; then print_success "✓ Empty expiration handled gracefully" malformed_test2=true else malformed_test2=false fi if [ "$malformed_test1" = true ] && [ "$malformed_test2" = true ]; then record_test_result "Handling of Malformed Expiration Tags" "PASS" "Malformed expiration tags handled gracefully" return 0 else record_test_result "Handling of Malformed Expiration Tags" "FAIL" "Malformed expiration tag handling failed" return 1 fi } # Test configuration via environment variables test_expiration_configuration() { print_test_header "Expiration Configuration Via Environment Variables" print_info "Testing expiration configuration from relay logs..." if [ -f "relay.log" ]; then print_info "Current configuration from logs:" grep "Expiration Configuration:" relay.log | tail -1 || print_warning "No expiration configuration found in logs" else print_warning "No relay.log found" fi # The relay should be running with default configuration print_info "Default configuration should be:" print_info " enabled=true" print_info " strict_mode=true (rejects expired events on submission)" print_info " filter_responses=true (filters expired events from responses)" print_info " grace_period=300 seconds (5 minutes)" # Test current behavior matches expected default configuration print_info "Configuration test based on observed behavior:" # Check if NIP-40 is advertised (indicates enabled=true) if curl -s -H "Accept: application/nostr+json" "$HTTP_URL/" | jq -e '.supported_nips | index(40)' >/dev/null 2>&1; then print_success "✓ NIP-40 support advertised (enabled=true)" config_test=true else print_error "✗ NIP-40 not advertised (may be disabled)" config_test=false fi if [ "$config_test" = true ]; then record_test_result "Expiration Configuration Via Environment Variables" "PASS" "Expiration configuration is accessible and working" return 0 else record_test_result "Expiration Configuration Via Environment Variables" "FAIL" "Expiration configuration issues detected" return 1 fi } # Print test summary print_test_summary() { echo "" echo -e "${BOLD}=== TEST SUMMARY ===${RESET}" echo "Total tests run: $TEST_COUNT" echo -e "${GREEN}Passed: $PASSED_COUNT${RESET}" echo -e "${RED}Failed: $FAILED_COUNT${RESET}" if [ $FAILED_COUNT -gt 0 ]; then echo "" echo -e "${RED}${BOLD}Failed tests:${RESET}" for result in "${TEST_RESULTS[@]}"; do IFS='|' read -r name status details <<< "$result" if [ "$status" = "FAIL" ]; then echo -e " ${RED}✗ $name${RESET}" if [ -n "$details" ]; then echo " $details" fi fi done fi echo "" if [ $FAILED_COUNT -eq 0 ]; then echo -e "${GREEN}${BOLD}🎉 ALL TESTS PASSED!${RESET}" echo -e "${GREEN}✅ NIP-40 Expiration Timestamp support is working correctly in the relay${RESET}" return 0 else echo -e "${RED}${BOLD}❌ SOME TESTS FAILED${RESET}" echo "Please review the output above and check relay logs for more details." return 1 fi } # Main test execution main() { echo -e "${BOLD}=== NIP-40 Expiration Timestamp Relay Test Suite ===${RESET}" echo "Testing NIP-40 Expiration Timestamp support in the C Nostr Relay" echo "Relay URL: $RELAY_URL" echo "" # Check prerequisites if ! command -v curl &> /dev/null; then print_error "curl is required but not installed" exit 1 fi if ! command -v jq &> /dev/null; then print_error "jq is required but not installed" exit 1 fi if ! command -v websocat &> /dev/null; then print_warning "websocat not found - WebSocket tests will be skipped" fi if ! command -v nak &> /dev/null; then print_warning "nak not found - Event generation tests will be skipped" print_info "Install with: go install github.com/fiatjaf/nak@latest" fi # Run tests check_relay_running test_nip11_expiration_support test_event_without_expiration test_event_with_future_expiration test_event_with_past_expiration test_event_within_grace_period test_expiration_filtering_in_subscriptions test_malformed_expiration_tags test_expiration_configuration # Print summary print_test_summary exit $? } # Run main function main "$@"