From d5eb7d4a5510dd7f5cb35dcceeb2c532df72f22f Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Oct 2025 04:52:40 -0400 Subject: [PATCH] v0.4.4 - Just waking up --- README.md | 2 +- c-relay.code-workspace | 8 + relay.pid | 2 +- src/main.c | 42 ++++- src/main.h | 4 +- src/websockets.c | 259 +++++++++++++++++++++++++++++ tests/45_nip_test.sh | 360 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 672 insertions(+), 5 deletions(-) create mode 100644 c-relay.code-workspace create mode 100755 tests/45_nip_test.sh diff --git a/README.md b/README.md index cd7e794..a4debbc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo - [x] NIP-33: Parameterized Replaceable Events - [x] NIP-40: Expiration Timestamp - [x] NIP-42: Authentication of clients to relays -- [ ] NIP-45: Counting results +- [x] NIP-45: Counting results - [ ] NIP-50: Keywords filter - [ ] NIP-70: Protected Events diff --git a/c-relay.code-workspace b/c-relay.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/c-relay.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/relay.pid b/relay.pid index d38a191..ed4280f 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -2356846 +63206 diff --git a/src/main.c b/src/main.c index 669dc55..7b077f5 100644 --- a/src/main.c +++ b/src/main.c @@ -120,6 +120,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length); // Forward declaration for admin event processing (kind 23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); +// Forward declaration for NIP-45 COUNT message handling +int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss); + // Forward declaration for enhanced admin event authorization int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size); @@ -881,7 +884,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru } // Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries - char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind < 20000"; + char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)"; char* sql_ptr = sql + strlen(sql); int remaining = sizeof(sql) - strlen(sql); @@ -972,6 +975,43 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru } } + // Handle tag filters (#e, #p, #t, etc.) + cJSON* filter_item = NULL; + cJSON_ArrayForEach(filter_item, filter) { + const char* filter_key = filter_item->string; + if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) { + // This is a tag filter like "#e", "#p", etc. + const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.) + + if (cJSON_IsArray(filter_item)) { + int tag_value_count = cJSON_GetArraySize(filter_item); + if (tag_value_count > 0) { + // Use EXISTS with LIKE to check for matching tags + snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + + for (int i = 0; i < tag_value_count; i++) { + cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); + if (cJSON_IsString(tag_value)) { + if (i > 0) { + snprintf(sql_ptr, remaining, ","); + sql_ptr++; + remaining--; + } + snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + snprintf(sql_ptr, remaining, "))"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + } + } + // Handle since filter cJSON* since = cJSON_GetObjectItem(filter, "since"); if (since && cJSON_IsNumber(since)) { diff --git a/src/main.h b/src/main.h index 5c5d215..acb104a 100644 --- a/src/main.h +++ b/src/main.h @@ -12,10 +12,10 @@ #define MAIN_H // Version information (auto-updated by build_and_push.sh) -#define VERSION "v0.4.3" +#define VERSION "v0.4.4" #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 3 +#define VERSION_PATCH 4 // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/src/websockets.c b/src/websockets.c index 048c935..9784306 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -74,6 +74,7 @@ int is_event_expired(cJSON* event, time_t current_time); // Forward declarations for subscription handling int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss); +int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss); // Forward declarations for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); @@ -619,6 +620,41 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso } cJSON_Delete(eose_response); } + } else if (strcmp(msg_type, "COUNT") == 0) { + // Check NIP-42 authentication for COUNT requests if required + if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { + if (!pss->auth_challenge_sent) { + send_nip42_auth_challenge(wsi, pss); + } else { + send_notice_message(wsi, "NIP-42 authentication required for count requests"); + log_warning("COUNT rejected: NIP-42 authentication required"); + } + cJSON_Delete(json); + free(message); + return 0; + } + + // Handle COUNT message + cJSON* sub_id = cJSON_GetArrayItem(json, 1); + + if (sub_id && cJSON_IsString(sub_id)) { + const char* subscription_id = cJSON_GetStringValue(sub_id); + + // Create array of filter objects from position 2 onwards + cJSON* filters = cJSON_CreateArray(); + int json_size = cJSON_GetArraySize(json); + for (int i = 2; i < json_size; i++) { + cJSON* filter = cJSON_GetArrayItem(json, i); + if (filter) { + cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); + } + } + + handle_count_message(subscription_id, filters, wsi, pss); + + // Clean up the filters array we created + cJSON_Delete(filters); + } } else if (strcmp(msg_type, "CLOSE") == 0) { // Handle CLOSE message cJSON* sub_id = cJSON_GetArrayItem(json, 1); @@ -899,3 +935,226 @@ int start_websocket_relay(int port_override, int strict_port) { log_success("WebSocket relay shut down cleanly"); return 0; } + +// Handle NIP-45 COUNT message +int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) { + (void)pss; // Suppress unused parameter warning + log_info("Handling COUNT message for subscription"); + + if (!cJSON_IsArray(filters)) { + log_error("COUNT filters is not an array"); + return 0; + } + + int total_count = 0; + + // Process each filter in the array + for (int i = 0; i < cJSON_GetArraySize(filters); i++) { + cJSON* filter = cJSON_GetArrayItem(filters, i); + if (!filter || !cJSON_IsObject(filter)) { + log_warning("Invalid filter object in COUNT"); + continue; + } + + // Build SQL COUNT query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries + char sql[1024] = "SELECT COUNT(*) FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)"; + char* sql_ptr = sql + strlen(sql); + int remaining = sizeof(sql) - strlen(sql); + + // Note: Expiration filtering will be done at application level + // after retrieving events to ensure compatibility with all SQLite versions + + // Handle kinds filter + cJSON* kinds = cJSON_GetObjectItem(filter, "kinds"); + if (kinds && cJSON_IsArray(kinds)) { + int kind_count = cJSON_GetArraySize(kinds); + if (kind_count > 0) { + snprintf(sql_ptr, remaining, " AND kind IN ("); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + + for (int k = 0; k < kind_count; k++) { + cJSON* kind = cJSON_GetArrayItem(kinds, k); + if (cJSON_IsNumber(kind)) { + if (k > 0) { + snprintf(sql_ptr, remaining, ","); + sql_ptr++; + remaining--; + } + snprintf(sql_ptr, remaining, "%d", (int)cJSON_GetNumberValue(kind)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + snprintf(sql_ptr, remaining, ")"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + + // Handle authors filter + cJSON* authors = cJSON_GetObjectItem(filter, "authors"); + if (authors && cJSON_IsArray(authors)) { + int author_count = cJSON_GetArraySize(authors); + if (author_count > 0) { + snprintf(sql_ptr, remaining, " AND pubkey IN ("); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + + for (int a = 0; a < author_count; a++) { + cJSON* author = cJSON_GetArrayItem(authors, a); + if (cJSON_IsString(author)) { + if (a > 0) { + snprintf(sql_ptr, remaining, ","); + sql_ptr++; + remaining--; + } + snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + snprintf(sql_ptr, remaining, ")"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + + // Handle ids filter + cJSON* ids = cJSON_GetObjectItem(filter, "ids"); + if (ids && cJSON_IsArray(ids)) { + int id_count = cJSON_GetArraySize(ids); + if (id_count > 0) { + snprintf(sql_ptr, remaining, " AND id IN ("); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + + for (int i = 0; i < id_count; i++) { + cJSON* id = cJSON_GetArrayItem(ids, i); + if (cJSON_IsString(id)) { + if (i > 0) { + snprintf(sql_ptr, remaining, ","); + sql_ptr++; + remaining--; + } + snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + snprintf(sql_ptr, remaining, ")"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + + // Handle tag filters (#e, #p, #t, etc.) + cJSON* filter_item = NULL; + cJSON_ArrayForEach(filter_item, filter) { + const char* filter_key = filter_item->string; + if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) { + // This is a tag filter like "#e", "#p", etc. + const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.) + + if (cJSON_IsArray(filter_item)) { + int tag_value_count = cJSON_GetArraySize(filter_item); + if (tag_value_count > 0) { + // Use EXISTS with JSON extraction to check for matching tags + snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + + for (int i = 0; i < tag_value_count; i++) { + cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); + if (cJSON_IsString(tag_value)) { + if (i > 0) { + snprintf(sql_ptr, remaining, ","); + sql_ptr++; + remaining--; + } + snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + snprintf(sql_ptr, remaining, "))"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + } + } + } + + // Handle since filter + cJSON* since = cJSON_GetObjectItem(filter, "since"); + if (since && cJSON_IsNumber(since)) { + snprintf(sql_ptr, remaining, " AND created_at >= %ld", (long)cJSON_GetNumberValue(since)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + + // Handle until filter + cJSON* until = cJSON_GetObjectItem(filter, "until"); + if (until && cJSON_IsNumber(until)) { + snprintf(sql_ptr, remaining, " AND created_at <= %ld", (long)cJSON_GetNumberValue(until)); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + } + + // Debug: Log the SQL query being executed + char debug_msg[1280]; + snprintf(debug_msg, sizeof(debug_msg), "Executing COUNT SQL: %s", sql); + log_info(debug_msg); + + // Execute count query + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "Failed to prepare COUNT query: %s", sqlite3_errmsg(g_db)); + log_error(error_msg); + continue; + } + + int filter_count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + filter_count = sqlite3_column_int(stmt, 0); + } + + char count_debug[128]; + snprintf(count_debug, sizeof(count_debug), "Filter %d returned count: %d", i + 1, filter_count); + log_info(count_debug); + + sqlite3_finalize(stmt); + total_count += filter_count; + } + + char total_debug[128]; + snprintf(total_debug, sizeof(total_debug), "Total COUNT result: %d", total_count); + log_info(total_debug); + + // Send COUNT response - NIP-45 format: ["COUNT", , {"count": }] + cJSON* count_response = cJSON_CreateArray(); + cJSON_AddItemToArray(count_response, cJSON_CreateString("COUNT")); + cJSON_AddItemToArray(count_response, cJSON_CreateString(sub_id)); + + // Create count object as per NIP-45 specification + cJSON* count_obj = cJSON_CreateObject(); + cJSON_AddNumberToObject(count_obj, "count", total_count); + cJSON_AddItemToArray(count_response, count_obj); + + char *count_str = cJSON_Print(count_response); + if (count_str) { + size_t count_len = strlen(count_str); + unsigned char *buf = malloc(LWS_PRE + count_len); + if (buf) { + memcpy(buf + LWS_PRE, count_str, count_len); + lws_write(wsi, buf + LWS_PRE, count_len, LWS_WRITE_TEXT); + free(buf); + } + free(count_str); + } + cJSON_Delete(count_response); + + return total_count; +} diff --git a/tests/45_nip_test.sh b/tests/45_nip_test.sh new file mode 100755 index 0000000..55e3451 --- /dev/null +++ b/tests/45_nip_test.sh @@ -0,0 +1,360 @@ +#!/bin/bash + +# NIP-45 COUNT Message Test - Test counting functionality +# Tests COUNT messages with various filters to verify correct event counting + +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 counting 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 send COUNT message and check response +test_count() { + local sub_id="$1" + local filter="$2" + local description="$3" + local expected_count="$4" + + print_step "Testing COUNT: $description" + + # Create COUNT message + local count_message="[\"COUNT\",\"$sub_id\",$filter]" + + print_info "Sending filter: $filter" + + # Send COUNT message and get response + local response="" + if command -v websocat &> /dev/null; then + response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "") + fi + + # Parse COUNT response + local count_result="" + if [[ -n "$response" ]]; then + # Look for COUNT response: ["COUNT","sub_id",{"count":N}] + count_result=$(echo "$response" | grep '"COUNT"' | head -1) + if [[ -n "$count_result" ]]; then + local actual_count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null) + if [[ "$actual_count" =~ ^[0-9]+$ ]]; then + print_info "Received count: $actual_count" + + # Check if count matches expected + if [[ "$expected_count" == "any" ]]; then + print_success "$description - Count: $actual_count" + return 0 + elif [[ "$actual_count" -eq "$expected_count" ]]; then + print_success "$description - Expected: $expected_count, Got: $actual_count" + return 0 + else + print_error "$description - Expected: $expected_count, Got: $actual_count" + return 1 + fi + else + print_error "$description - Invalid count response: $count_result" + return 1 + fi + else + print_error "$description - No COUNT response received" + print_error "Raw response: $response" + return 1 + fi + else + print_error "$description - No response from relay" + return 1 + fi +} + +# Main test function +run_count_test() { + print_header "NIP-45 COUNT Message 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 Test Events" + + # 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 for counting" -k 1 --ts $(($(date +%s) - 100)) -t "type=regular" -t "test=nip45" 2>/dev/null) + local regular2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #2 for counting" -k 1 --ts $(($(date +%s) - 90)) -t "type=regular" -t "test=nip45" 2>/dev/null) + local regular3=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #3 for counting" -k 1 --ts $(($(date +%s) - 80)) -t "type=regular" -t "test=nip45" 2>/dev/null) + + publish_event "$regular1" "regular" "Regular event #1" + publish_event "$regular2" "regular" "Regular event #2" + publish_event "$regular3" "regular" "Regular event #3" + + # 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 NIP-45 COUNT"}' -k 0 --ts $(($(date +%s) - 70)) -t "type=replaceable" 2>/dev/null) + local replaceable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User Updated","about":"Updated for NIP-45"}' -k 0 --ts $(($(date +%s) - 60)) -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+) - should NOT be counted + print_step "Creating ephemeral events (kind 20001)..." + local ephemeral1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Ephemeral event - should not be counted" -k 20001 --ts $(date +%s) -t "type=ephemeral" 2>/dev/null) + + publish_event "$ephemeral1" "ephemeral" "Ephemeral event (should not be counted)" + + # Test 4: Addressable Events (kind 30000+) + print_step "Creating addressable events (kind 30001)..." + local addressable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event #1" -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 "Addressable event #2" -k 30001 --ts $(($(date +%s) - 40)) -t "d=test-article" -t "type=addressable" 2>/dev/null) + + publish_event "$addressable1" "addressable" "Addressable event #1" + publish_event "$addressable2" "addressable" "Addressable event #2" + + # Brief pause to let events settle + sleep 2 + + print_header "PHASE 2: Testing COUNT Messages" + + local test_failures=0 + + # Test 1: Count all events + if ! test_count "count_all" '{}' "Count all events" "any"; then + ((test_failures++)) + fi + + # Test 2: Count events by kind + if ! test_count "count_kind1" '{"kinds":[1]}' "Count kind 1 events" "3"; then + ((test_failures++)) + fi + if ! test_count "count_kind0" '{"kinds":[0]}' "Count kind 0 events" "1"; then + ((test_failures++)) + fi + if ! test_count "count_kind30001" '{"kinds":[30001]}' "Count kind 30001 events" "1"; then + ((test_failures++)) + fi + + # Test 3: Count events by author (pubkey) + local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null) + if ! test_count "count_author" "{\"authors\":[\"$test_pubkey\"]}" "Count events by specific author" "5"; then + ((test_failures++)) + fi + + # Test 4: Count recent events (time-based) + local recent_timestamp=$(($(date +%s) - 200)) + if ! test_count "count_recent" "{\"since\":$recent_timestamp}" "Count recent events" "any"; then + ((test_failures++)) + fi + + # Test 5: Count events with specific tags + if ! test_count "count_tag_type" '{"#type":["regular"]}' "Count events with type=regular tag" "3"; then + ((test_failures++)) + fi + if ! test_count "count_tag_test" '{"#test":["nip45"]}' "Count events with test=nip45 tag" "3"; then + ((test_failures++)) + fi + + # Test 6: Count multiple kinds + if ! test_count "count_multi_kinds" '{"kinds":[0,1]}' "Count multiple kinds (0,1)" "4"; then + ((test_failures++)) + fi + + # Test 7: Count with time range + local start_time=$(($(date +%s) - 120)) + local end_time=$(($(date +%s) - 60)) + if ! test_count "count_time_range" "{\"since\":$start_time,\"until\":$end_time}" "Count events in time range" "any"; then + ((test_failures++)) + fi + + # Test 8: Count specific event IDs + if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then + local test_event_id="${REGULAR_EVENT_IDS[0]}" + if ! test_count "count_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Count specific event ID" "1"; then + ((test_failures++)) + fi + fi + + # Test 9: Count with multiple filters combined + if ! test_count "count_combined" '{"kinds":[1],"#type":["regular"],"#test":["nip45"]}' "Count with combined filters" "3"; then + ((test_failures++)) + fi + + # Test 10: Count ephemeral events (should be 0 since they're not stored) + if ! test_count "count_ephemeral" '{"kinds":[20001]}' "Count ephemeral events (should be 0)" "0"; then + ((test_failures++)) + fi + + # Test 11: Count with limit (should still count all matching, ignore limit) + if ! test_count "count_with_limit" '{"kinds":[1],"limit":1}' "Count with limit (should ignore limit)" "3"; then + ((test_failures++)) + fi + + # Test 12: Count non-existent kind + if ! test_count "count_nonexistent" '{"kinds":[99999]}' "Count non-existent kind" "0"; then + ((test_failures++)) + fi + + # Test 13: Count with empty filter + if ! test_count "count_empty_filter" '{}' "Count with empty filter" "any"; then + ((test_failures++)) + fi + + # Report test results + if [[ $test_failures -gt 0 ]]; then + print_error "COUNT TESTS FAILED: $test_failures test(s) failed" + return 1 + else + print_success "All COUNT tests passed" + fi + + print_header "PHASE 3: 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 "Total events in database:" + sqlite3 "$db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null + + 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 COUNT test +print_header "Starting NIP-45 COUNT Message Test Suite" +echo + +if run_count_test; then + echo + print_success "All NIP-45 COUNT tests completed successfully!" + print_info "The C-Relay COUNT functionality is working correctly" + print_info "✅ COUNT messages are processed and return correct event counts" + echo + exit 0 +else + echo + print_error "❌ NIP-45 COUNT TESTS FAILED!" + print_error "The COUNT functionality has issues that need to be fixed" + echo + exit 1 +fi \ No newline at end of file