Refactor Neo4j tests and improve tag handling in Cypher

Replaces outdated Neo4j test setup with a robust TestMain, shared test database, and utility functions for test data and migrations. Improves Cypher generation for processing e-tags, p-tags, and other tags to ensure compliance with Neo4j syntax. Added integration test script and updated benchmark reports for Badger backend.
This commit is contained in:
2025-12-04 20:09:24 +00:00
parent 6b98c23606
commit 1e9c447fe6
15 changed files with 1511 additions and 90 deletions

View File

@@ -0,0 +1,314 @@
package neo4j
import (
"context"
"testing"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/kind"
"git.mleku.dev/mleku/nostr/encoders/tag"
"git.mleku.dev/mleku/nostr/encoders/timestamp"
)
// Valid test pubkeys and event IDs (64-character lowercase hex)
const (
validPubkey1 = "0000000000000000000000000000000000000000000000000000000000000001"
validPubkey2 = "0000000000000000000000000000000000000000000000000000000000000002"
validPubkey3 = "0000000000000000000000000000000000000000000000000000000000000003"
validEventID1 = "1111111111111111111111111111111111111111111111111111111111111111"
validEventID2 = "2222222222222222222222222222222222222222222222222222222222222222"
validEventID3 = "3333333333333333333333333333333333333333333333333333333333333333"
)
// TestQueryEventsWithNilFilter tests that QueryEvents handles nil filter fields gracefully
// This test covers the nil pointer fix in query-events.go
func TestQueryEventsWithNilFilter(t *testing.T) {
if testDB == nil {
t.Skip("Neo4j not available")
}
// Clean up before test
cleanTestDatabase()
// Setup some test events
setupTestEvent(t, validEventID1, validPubkey1, 1, "[]")
setupTestEvent(t, validEventID2, validPubkey2, 1, "[]")
ctx := context.Background()
// Test 1: Completely empty filter (all nil fields)
t.Run("EmptyFilter", func(t *testing.T) {
f := &filter.F{}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with empty filter should not panic: %v", err)
}
if len(events) == 0 {
t.Error("Expected to find events with empty filter")
}
})
// Test 2: Filter with nil Ids
t.Run("NilIds", func(t *testing.T) {
f := &filter.F{
Ids: nil, // Explicitly nil
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with nil Ids should not panic: %v", err)
}
})
// Test 3: Filter with nil Authors
t.Run("NilAuthors", func(t *testing.T) {
f := &filter.F{
Authors: nil, // Explicitly nil
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with nil Authors should not panic: %v", err)
}
})
// Test 4: Filter with nil Kinds
t.Run("NilKinds", func(t *testing.T) {
f := &filter.F{
Kinds: nil, // Explicitly nil
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with nil Kinds should not panic: %v", err)
}
})
// Test 5: Filter with empty Ids slice
t.Run("EmptyIds", func(t *testing.T) {
f := &filter.F{
Ids: &tag.S{T: [][]byte{}},
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with empty Ids should not panic: %v", err)
}
})
// Test 6: Filter with empty Authors slice
t.Run("EmptyAuthors", func(t *testing.T) {
f := &filter.F{
Authors: &tag.S{T: [][]byte{}},
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with empty Authors should not panic: %v", err)
}
})
// Test 7: Filter with empty Kinds slice
t.Run("EmptyKinds", func(t *testing.T) {
f := &filter.F{
Kinds: &kind.S{K: []*kind.T{}},
}
_, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents with empty Kinds should not panic: %v", err)
}
})
}
// TestQueryEventsWithValidFilters tests that QueryEvents works correctly with valid filters
func TestQueryEventsWithValidFilters(t *testing.T) {
if testDB == nil {
t.Skip("Neo4j not available")
}
// Clean up before test
cleanTestDatabase()
// Setup test events
setupTestEvent(t, validEventID1, validPubkey1, 1, "[]")
setupTestEvent(t, validEventID2, validPubkey2, 3, "[]")
setupTestEvent(t, validEventID3, validPubkey1, 1, "[]")
ctx := context.Background()
// Test 1: Filter by ID
t.Run("FilterByID", func(t *testing.T) {
f := &filter.F{
Ids: tag.NewFromBytesSlice([]byte(validEventID1)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("Expected 1 event, got %d", len(events))
}
})
// Test 2: Filter by Author
t.Run("FilterByAuthor", func(t *testing.T) {
f := &filter.F{
Authors: tag.NewFromBytesSlice([]byte(validPubkey1)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 2 {
t.Errorf("Expected 2 events from pubkey1, got %d", len(events))
}
})
// Test 3: Filter by Kind
t.Run("FilterByKind", func(t *testing.T) {
f := &filter.F{
Kinds: kind.NewS(kind.New(1)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 2 {
t.Errorf("Expected 2 kind-1 events, got %d", len(events))
}
})
// Test 4: Combined filters (kind + author)
t.Run("FilterByKindAndAuthor", func(t *testing.T) {
f := &filter.F{
Kinds: kind.NewS(kind.New(1)),
Authors: tag.NewFromBytesSlice([]byte(validPubkey1)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 2 {
t.Errorf("Expected 2 kind-1 events from pubkey1, got %d", len(events))
}
})
// Test 5: Filter with limit
t.Run("FilterWithLimit", func(t *testing.T) {
limit := 1
f := &filter.F{
Kinds: kind.NewS(kind.New(1)),
Limit: &limit,
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("Expected 1 event due to limit, got %d", len(events))
}
})
}
// TestBuildCypherQueryWithNilFields tests the buildCypherQuery function with nil fields
func TestBuildCypherQueryWithNilFields(t *testing.T) {
if testDB == nil {
t.Skip("Neo4j not available")
}
// Test that buildCypherQuery doesn't panic with nil fields
t.Run("AllNilFields", func(t *testing.T) {
f := &filter.F{
Ids: nil,
Authors: nil,
Kinds: nil,
Since: nil,
Until: nil,
Tags: nil,
Limit: nil,
}
cypher, params := testDB.buildCypherQuery(f, false)
if cypher == "" {
t.Error("Expected non-empty Cypher query")
}
if params == nil {
t.Error("Expected non-nil params map")
}
})
// Test with empty slices
t.Run("EmptySlices", func(t *testing.T) {
f := &filter.F{
Ids: &tag.S{T: [][]byte{}},
Authors: &tag.S{T: [][]byte{}},
Kinds: &kind.S{K: []*kind.T{}},
}
cypher, params := testDB.buildCypherQuery(f, false)
if cypher == "" {
t.Error("Expected non-empty Cypher query")
}
if params == nil {
t.Error("Expected non-nil params map")
}
})
// Test with time filters
t.Run("TimeFilters", func(t *testing.T) {
since := timestamp.Now()
until := timestamp.Now()
f := &filter.F{
Since: &since,
Until: &until,
}
cypher, params := testDB.buildCypherQuery(f, false)
if _, ok := params["since"]; !ok {
t.Error("Expected 'since' param")
}
if _, ok := params["until"]; !ok {
t.Error("Expected 'until' param")
}
_ = cypher
})
}
// TestQueryEventsUppercaseHexNormalization tests that uppercase hex in filters is normalized
func TestQueryEventsUppercaseHexNormalization(t *testing.T) {
if testDB == nil {
t.Skip("Neo4j not available")
}
// Clean up before test
cleanTestDatabase()
// Setup test event with lowercase pubkey (as Neo4j stores)
lowercasePubkey := "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
lowercaseEventID := "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
setupTestEvent(t, lowercaseEventID, lowercasePubkey, 1, "[]")
ctx := context.Background()
// Test query with uppercase pubkey - should be normalized and still match
t.Run("UppercaseAuthor", func(t *testing.T) {
uppercasePubkey := "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789"
f := &filter.F{
Authors: tag.NewFromBytesSlice([]byte(uppercasePubkey)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("Expected to find 1 event with uppercase pubkey filter, got %d", len(events))
}
})
// Test query with uppercase event ID - should be normalized and still match
t.Run("UppercaseEventID", func(t *testing.T) {
uppercaseEventID := "FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210"
f := &filter.F{
Ids: tag.NewFromBytesSlice([]byte(uppercaseEventID)),
}
events, err := testDB.QueryEvents(ctx, f)
if err != nil {
t.Fatalf("QueryEvents failed: %v", err)
}
if len(events) != 1 {
t.Errorf("Expected to find 1 event with uppercase ID filter, got %d", len(events))
}
})
}