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.
315 lines
8.4 KiB
Go
315 lines
8.4 KiB
Go
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))
|
|
}
|
|
})
|
|
}
|