@ -462,3 +462,584 @@ func TestCountEvents(t *testing.T) {
@@ -462,3 +462,584 @@ func TestCountEvents(t *testing.T) {
t . Logf ( "✓ Count events returned correct count: %d" , count )
}
// TestQueryEventsByTagWithHashPrefix tests that tag filters with "#" prefix work correctly.
// This is a regression test for a bug where filter tags like "#d" were not being matched
// because the "#" prefix wasn't being stripped before comparison with stored tags.
func TestQueryEventsByTagWithHashPrefix ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create events with d-tags (parameterized replaceable kind)
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d=id1" ,
tag . NewS ( tag . NewFromAny ( "d" , "id1" ) ) , baseTs )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d=id2" ,
tag . NewS ( tag . NewFromAny ( "d" , "id2" ) ) , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d=id3" ,
tag . NewS ( tag . NewFromAny ( "d" , "id3" ) ) , baseTs + 2 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d=other" ,
tag . NewS ( tag . NewFromAny ( "d" , "other" ) ) , baseTs + 3 )
// Query with "#d" prefix (as clients send it) - should match events with d=id1
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30382 ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "id1" ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query events with #d tag: %v" , err )
}
if len ( evs ) != 1 {
t . Fatalf ( "Expected 1 event with d=id1, got %d" , len ( evs ) )
}
// Verify the returned event has the correct d-tag
dTag := evs [ 0 ] . Tags . GetFirst ( [ ] byte ( "d" ) )
if dTag == nil || string ( dTag . Value ( ) ) != "id1" {
t . Fatalf ( "Expected d=id1, got d=%s" , dTag . Value ( ) )
}
t . Logf ( "✓ Query with #d prefix returned correct event" )
}
// TestQueryEventsByTagMultipleValues tests that tag filters with multiple values
// use OR logic (match events with ANY of the values).
func TestQueryEventsByTagMultipleValues ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create events with different d-tags
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event A" ,
tag . NewS ( tag . NewFromAny ( "d" , "target-1" ) ) , baseTs )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event B" ,
tag . NewS ( tag . NewFromAny ( "d" , "target-2" ) ) , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event C" ,
tag . NewS ( tag . NewFromAny ( "d" , "target-3" ) ) , baseTs + 2 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event D (not target)" ,
tag . NewS ( tag . NewFromAny ( "d" , "other-value" ) ) , baseTs + 3 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event E (no match)" ,
tag . NewS ( tag . NewFromAny ( "d" , "different" ) ) , baseTs + 4 )
// Query with multiple d-tag values using "#d" prefix
// Should match events with d=target-1 OR d=target-2 OR d=target-3
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30382 ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "target-1" , "target-2" , "target-3" ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query events with multiple #d values: %v" , err )
}
if len ( evs ) != 3 {
t . Fatalf ( "Expected 3 events matching the d-tag values, got %d" , len ( evs ) )
}
// Verify returned events have correct d-tags
validDTags := map [ string ] bool { "target-1" : false , "target-2" : false , "target-3" : false }
for _ , ev := range evs {
dTag := ev . Tags . GetFirst ( [ ] byte ( "d" ) )
if dTag == nil {
t . Fatalf ( "Event missing d-tag" )
}
dValue := string ( dTag . Value ( ) )
if _ , ok := validDTags [ dValue ] ; ! ok {
t . Fatalf ( "Unexpected d-tag value: %s" , dValue )
}
validDTags [ dValue ] = true
}
// Verify all expected d-tags were found
for dValue , found := range validDTags {
if ! found {
t . Fatalf ( "Expected to find event with d=%s" , dValue )
}
}
t . Logf ( "✓ Query with multiple #d values returned correct events" )
}
// TestQueryEventsByTagNoMatch tests that tag filters correctly return no results
// when no events match the filter.
func TestQueryEventsByTagNoMatch ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create events with d-tags
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event" ,
tag . NewS ( tag . NewFromAny ( "d" , "existing-value" ) ) , baseTs )
// Query for d-tag value that doesn't exist
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30382 ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "non-existent-value" ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query events: %v" , err )
}
if len ( evs ) != 0 {
t . Fatalf ( "Expected 0 events for non-matching d-tag, got %d" , len ( evs ) )
}
t . Logf ( "✓ Query with non-matching #d value returned no events" )
}
// TestQueryEventsByTagWithKindAndAuthor tests the combination of kind, author, and tag filters.
// This is the specific case reported by the user with kind 30382.
func TestQueryEventsByTagWithKindAndAuthor ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
alice := createTestSignerLocal ( t )
bob := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create events from different authors with d-tags
createAndSaveEventLocal ( t , ctx , alice , 30382 , "Alice target 1" ,
tag . NewS ( tag . NewFromAny ( "d" , "card-1" ) ) , baseTs )
createAndSaveEventLocal ( t , ctx , alice , 30382 , "Alice target 2" ,
tag . NewS ( tag . NewFromAny ( "d" , "card-2" ) ) , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , alice , 30382 , "Alice other" ,
tag . NewS ( tag . NewFromAny ( "d" , "other-card" ) ) , baseTs + 2 )
createAndSaveEventLocal ( t , ctx , bob , 30382 , "Bob target 1" ,
tag . NewS ( tag . NewFromAny ( "d" , "card-1" ) ) , baseTs + 3 ) // Same d-tag as Alice but different author
// Query for Alice's events with specific d-tags
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30382 ) ) ,
Authors : tag . NewFromBytesSlice ( alice . Pub ( ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "card-1" , "card-2" ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query events: %v" , err )
}
// Should only return Alice's 2 events, not Bob's even though he has card-1
if len ( evs ) != 2 {
t . Fatalf ( "Expected 2 events from Alice with matching d-tags, got %d" , len ( evs ) )
}
alicePubkey := hex . Enc ( alice . Pub ( ) )
for _ , ev := range evs {
if hex . Enc ( ev . Pubkey [ : ] ) != alicePubkey {
t . Fatalf ( "Expected author %s, got %s" , alicePubkey , hex . Enc ( ev . Pubkey [ : ] ) )
}
dTag := ev . Tags . GetFirst ( [ ] byte ( "d" ) )
dValue := string ( dTag . Value ( ) )
if dValue != "card-1" && dValue != "card-2" {
t . Fatalf ( "Expected d=card-1 or card-2, got d=%s" , dValue )
}
}
t . Logf ( "✓ Query with kind, author, and #d filter returned correct events" )
}
// TestBinaryTagFilterRegression tests that queries with #e and #p tags work correctly
// even when tags are stored with binary-encoded values but filters come as hex strings.
// This mirrors the Badger database test for binary tag handling.
func TestBinaryTagFilterRegression ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
author := createTestSignerLocal ( t )
referenced := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create a referenced event to get a valid event ID for e-tag
refEvent := createAndSaveEventLocal ( t , ctx , referenced , 1 , "Referenced event" , nil , baseTs )
// Get hex representations
refEventIdHex := hex . Enc ( refEvent . ID )
refPubkeyHex := hex . Enc ( referenced . Pub ( ) )
// Create test event with e, p, d, and other tags
testEvent := createAndSaveEventLocal ( t , ctx , author , 30520 , "Event with binary tags" ,
tag . NewS (
tag . NewFromAny ( "d" , "test-d-value" ) ,
tag . NewFromAny ( "p" , string ( refPubkeyHex ) ) ,
tag . NewFromAny ( "e" , string ( refEventIdHex ) ) ,
tag . NewFromAny ( "t" , "test-topic" ) ,
) , baseTs + 1 )
testEventIdHex := hex . Enc ( testEvent . ID )
// Test case 1: Query WITHOUT #e/#p tags (baseline - should work)
t . Run ( "QueryWithoutEPTags" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30520 ) ) ,
Authors : tag . NewFromBytesSlice ( author . Pub ( ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "test-d-value" ) ) ,
} )
if err != nil {
t . Fatalf ( "Query without e/p tags failed: %v" , err )
}
if len ( evs ) == 0 {
t . Fatal ( "Expected to find event with d tag filter, got 0 results" )
}
found := false
for _ , ev := range evs {
if hex . Enc ( ev . ID ) == testEventIdHex {
found = true
break
}
}
if ! found {
t . Errorf ( "Expected event ID %s not found" , testEventIdHex )
}
} )
// Test case 2: Query WITH #p tag
t . Run ( "QueryWithPTag" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30520 ) ) ,
Authors : tag . NewFromBytesSlice ( author . Pub ( ) ) ,
Tags : tag . NewS (
tag . NewFromAny ( "#d" , "test-d-value" ) ,
tag . NewFromAny ( "#p" , string ( refPubkeyHex ) ) ,
) ,
} )
if err != nil {
t . Fatalf ( "Query with #p tag failed: %v" , err )
}
if len ( evs ) == 0 {
t . Fatalf ( "REGRESSION: Expected to find event with #p tag filter, got 0 results" )
}
} )
// Test case 3: Query WITH #e tag
t . Run ( "QueryWithETag" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30520 ) ) ,
Authors : tag . NewFromBytesSlice ( author . Pub ( ) ) ,
Tags : tag . NewS (
tag . NewFromAny ( "#d" , "test-d-value" ) ,
tag . NewFromAny ( "#e" , string ( refEventIdHex ) ) ,
) ,
} )
if err != nil {
t . Fatalf ( "Query with #e tag failed: %v" , err )
}
if len ( evs ) == 0 {
t . Fatalf ( "REGRESSION: Expected to find event with #e tag filter, got 0 results" )
}
} )
// Test case 4: Query WITH BOTH #e AND #p tags
t . Run ( "QueryWithBothEAndPTags" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30520 ) ) ,
Authors : tag . NewFromBytesSlice ( author . Pub ( ) ) ,
Tags : tag . NewS (
tag . NewFromAny ( "#d" , "test-d-value" ) ,
tag . NewFromAny ( "#e" , string ( refEventIdHex ) ) ,
tag . NewFromAny ( "#p" , string ( refPubkeyHex ) ) ,
) ,
} )
if err != nil {
t . Fatalf ( "Query with both #e and #p tags failed: %v" , err )
}
if len ( evs ) == 0 {
t . Fatalf ( "REGRESSION: Expected to find event with #e and #p tag filters, got 0 results" )
}
} )
t . Logf ( "✓ Binary tag filter regression tests passed" )
}
// TestParameterizedReplaceableEvents tests that parameterized replaceable events (kind 30000+)
// are handled correctly - only the newest version should be returned in queries by kind/author/d-tag.
func TestParameterizedReplaceableEvents ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create older parameterized replaceable event
createAndSaveEventLocal ( t , ctx , signer , 30000 , "Original event" ,
tag . NewS ( tag . NewFromAny ( "d" , "test-param" ) ) , baseTs - 7200 ) // 2 hours ago
// Create newer event with same kind/author/d-tag
createAndSaveEventLocal ( t , ctx , signer , 30000 , "Newer event" ,
tag . NewS ( tag . NewFromAny ( "d" , "test-param" ) ) , baseTs - 3600 ) // 1 hour ago
// Create newest event with same kind/author/d-tag
newestEvent := createAndSaveEventLocal ( t , ctx , signer , 30000 , "Newest event" ,
tag . NewS ( tag . NewFromAny ( "d" , "test-param" ) ) , baseTs ) // Now
// Query for events - should only return the newest one
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30000 ) ) ,
Authors : tag . NewFromBytesSlice ( signer . Pub ( ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#d" , "test-param" ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query parameterized replaceable events: %v" , err )
}
// Note: Neo4j backend may or may not automatically deduplicate replaceable events
// depending on implementation. The important thing is that the newest is returned first.
if len ( evs ) == 0 {
t . Fatal ( "Expected at least 1 event" )
}
// Verify the first (most recent) event is the newest one
if hex . Enc ( evs [ 0 ] . ID ) != hex . Enc ( newestEvent . ID ) {
t . Logf ( "Note: Expected newest event first, got different order" )
}
t . Logf ( "✓ Parameterized replaceable events test returned %d events" , len ( evs ) )
}
// TestQueryForIds tests the QueryForIds method
func TestQueryForIds ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create test events
ev1 := createAndSaveEventLocal ( t , ctx , signer , 1 , "Event 1" , nil , baseTs )
ev2 := createAndSaveEventLocal ( t , ctx , signer , 1 , "Event 2" , nil , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , signer , 7 , "Reaction" , nil , baseTs + 2 )
// Query for IDs of kind 1 events
idPkTs , err := testDB . QueryForIds ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 1 ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query for IDs: %v" , err )
}
if len ( idPkTs ) != 2 {
t . Fatalf ( "Expected 2 IDs for kind 1 events, got %d" , len ( idPkTs ) )
}
// Verify IDs match our events
foundIds := make ( map [ string ] bool )
for _ , r := range idPkTs {
foundIds [ hex . Enc ( r . Id ) ] = true
}
if ! foundIds [ hex . Enc ( ev1 . ID ) ] {
t . Error ( "Event 1 ID not found in results" )
}
if ! foundIds [ hex . Enc ( ev2 . ID ) ] {
t . Error ( "Event 2 ID not found in results" )
}
t . Logf ( "✓ QueryForIds returned correct IDs" )
}
// TestQueryForSerials tests the QueryForSerials method
func TestQueryForSerials ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create test events
createAndSaveEventLocal ( t , ctx , signer , 1 , "Event 1" , nil , baseTs )
createAndSaveEventLocal ( t , ctx , signer , 1 , "Event 2" , nil , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , signer , 1 , "Event 3" , nil , baseTs + 2 )
// Query for serials
serials , err := testDB . QueryForSerials ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 1 ) ) ,
} )
if err != nil {
t . Fatalf ( "Failed to query for serials: %v" , err )
}
if len ( serials ) != 3 {
t . Fatalf ( "Expected 3 serials, got %d" , len ( serials ) )
}
t . Logf ( "✓ QueryForSerials returned %d serials" , len ( serials ) )
}
// TestQueryEventsComplex tests complex filter combinations
func TestQueryEventsComplex ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
alice := createTestSignerLocal ( t )
bob := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create diverse set of events
createAndSaveEventLocal ( t , ctx , alice , 1 , "Alice note with bitcoin tag" ,
tag . NewS ( tag . NewFromAny ( "t" , "bitcoin" ) ) , baseTs )
createAndSaveEventLocal ( t , ctx , alice , 1 , "Alice note with nostr tag" ,
tag . NewS ( tag . NewFromAny ( "t" , "nostr" ) ) , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , alice , 7 , "Alice reaction" ,
nil , baseTs + 2 )
createAndSaveEventLocal ( t , ctx , bob , 1 , "Bob note with bitcoin tag" ,
tag . NewS ( tag . NewFromAny ( "t" , "bitcoin" ) ) , baseTs + 3 )
// Test: kinds + tags (no authors)
t . Run ( "KindsAndTags" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 1 ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#t" , "bitcoin" ) ) ,
} )
if err != nil {
t . Fatalf ( "Query failed: %v" , err )
}
if len ( evs ) != 2 {
t . Fatalf ( "Expected 2 events with kind=1 and #t=bitcoin, got %d" , len ( evs ) )
}
} )
// Test: authors + tags (no kinds)
t . Run ( "AuthorsAndTags" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Authors : tag . NewFromBytesSlice ( alice . Pub ( ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#t" , "bitcoin" ) ) ,
} )
if err != nil {
t . Fatalf ( "Query failed: %v" , err )
}
if len ( evs ) != 1 {
t . Fatalf ( "Expected 1 event from Alice with #t=bitcoin, got %d" , len ( evs ) )
}
} )
// Test: kinds + authors (no tags)
t . Run ( "KindsAndAuthors" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 1 ) ) ,
Authors : tag . NewFromBytesSlice ( alice . Pub ( ) ) ,
} )
if err != nil {
t . Fatalf ( "Query failed: %v" , err )
}
if len ( evs ) != 2 {
t . Fatalf ( "Expected 2 kind=1 events from Alice, got %d" , len ( evs ) )
}
} )
// Test: all three filters
t . Run ( "AllFilters" , func ( t * testing . T ) {
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 1 ) ) ,
Authors : tag . NewFromBytesSlice ( alice . Pub ( ) ) ,
Tags : tag . NewS ( tag . NewFromAny ( "#t" , "nostr" ) ) ,
} )
if err != nil {
t . Fatalf ( "Query failed: %v" , err )
}
if len ( evs ) != 1 {
t . Fatalf ( "Expected 1 event (Alice kind=1 #t=nostr), got %d" , len ( evs ) )
}
} )
t . Logf ( "✓ Complex filter combination tests passed" )
}
// TestQueryEventsMultipleTagTypes tests filtering with multiple different tag types
func TestQueryEventsMultipleTagTypes ( t * testing . T ) {
if testDB == nil {
t . Skip ( "Neo4j not available" )
}
cleanTestDatabase ( )
ctx := context . Background ( )
signer := createTestSignerLocal ( t )
baseTs := timestamp . Now ( ) . V
// Create events with multiple tag types
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d and client tags" ,
tag . NewS (
tag . NewFromAny ( "d" , "user-1" ) ,
tag . NewFromAny ( "client" , "app-a" ) ,
) , baseTs )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with d and different client" ,
tag . NewS (
tag . NewFromAny ( "d" , "user-2" ) ,
tag . NewFromAny ( "client" , "app-b" ) ,
) , baseTs + 1 )
createAndSaveEventLocal ( t , ctx , signer , 30382 , "Event with only d tag" ,
tag . NewS (
tag . NewFromAny ( "d" , "user-3" ) ,
) , baseTs + 2 )
// Query with multiple tag types (should AND them together)
evs , err := testDB . QueryEvents ( ctx , & filter . F {
Kinds : kind . NewS ( kind . New ( 30382 ) ) ,
Tags : tag . NewS (
tag . NewFromAny ( "#d" , "user-1" , "user-2" ) ,
tag . NewFromAny ( "#client" , "app-a" ) ,
) ,
} )
if err != nil {
t . Fatalf ( "Query with multiple tag types failed: %v" , err )
}
// Should match only the first event (user-1 with app-a)
if len ( evs ) != 1 {
t . Fatalf ( "Expected 1 event matching both #d and #client, got %d" , len ( evs ) )
}
dTag := evs [ 0 ] . Tags . GetFirst ( [ ] byte ( "d" ) )
if string ( dTag . Value ( ) ) != "user-1" {
t . Fatalf ( "Expected d=user-1, got d=%s" , dTag . Value ( ) )
}
t . Logf ( "✓ Multiple tag types filter test passed" )
}