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

@@ -1,15 +1,246 @@
package neo4j
import (
"context"
"os"
"testing"
"time"
"next.orly.dev/pkg/database"
)
// skipIfNeo4jNotAvailable skips the test if Neo4j is not available
func skipIfNeo4jNotAvailable(t *testing.T) {
// Check if Neo4j connection details are provided
uri := os.Getenv("ORLY_NEO4J_URI")
if uri == "" {
t.Skip("Neo4j not available (set ORLY_NEO4J_URI to enable tests)")
// testDB is the shared database instance for tests
var testDB *N
// TestMain sets up and tears down the test database
func TestMain(m *testing.M) {
// Skip integration tests if NEO4J_TEST_URI is not set
neo4jURI := os.Getenv("NEO4J_TEST_URI")
if neo4jURI == "" {
neo4jURI = "bolt://localhost:7687"
}
neo4jUser := os.Getenv("NEO4J_TEST_USER")
if neo4jUser == "" {
neo4jUser = "neo4j"
}
neo4jPassword := os.Getenv("NEO4J_TEST_PASSWORD")
if neo4jPassword == "" {
neo4jPassword = "testpassword"
}
// Try to connect to Neo4j
ctx, cancel := context.WithCancel(context.Background())
cfg := &database.DatabaseConfig{
DataDir: os.TempDir(),
Neo4jURI: neo4jURI,
Neo4jUser: neo4jUser,
Neo4jPassword: neo4jPassword,
}
var err error
testDB, err = NewWithConfig(ctx, cancel, cfg)
if err != nil {
// If Neo4j is not available, skip integration tests
os.Stderr.WriteString("Neo4j not available, skipping integration tests: " + err.Error() + "\n")
os.Stderr.WriteString("Start Neo4j with: docker compose -f pkg/neo4j/docker-compose.yaml up -d\n")
os.Exit(0)
}
// Wait for database to be ready
select {
case <-testDB.Ready():
// Database is ready
case <-time.After(30 * time.Second):
os.Stderr.WriteString("Timeout waiting for Neo4j to be ready\n")
os.Exit(1)
}
// Clean database before running tests
cleanTestDatabase()
// Run tests
code := m.Run()
// Clean up
cleanTestDatabase()
testDB.Close()
cancel()
os.Exit(code)
}
// cleanTestDatabase removes all nodes and relationships
func cleanTestDatabase() {
ctx := context.Background()
// Delete all nodes and relationships
_, _ = testDB.ExecuteWrite(ctx, "MATCH (n) DETACH DELETE n", nil)
// Clear migration markers so migrations can run fresh
_, _ = testDB.ExecuteWrite(ctx, "MATCH (m:Migration) DELETE m", nil)
}
// setupTestEvent creates a test event directly in Neo4j for testing queries
func setupTestEvent(t *testing.T, eventID, pubkey string, kind int64, tags string) {
t.Helper()
ctx := context.Background()
cypher := `
MERGE (a:NostrUser {pubkey: $pubkey})
CREATE (e:Event {
id: $eventId,
serial: $serial,
kind: $kind,
created_at: $createdAt,
content: $content,
sig: $sig,
pubkey: $pubkey,
tags: $tags,
expiration: 0
})
CREATE (e)-[:AUTHORED_BY]->(a)
`
params := map[string]any{
"eventId": eventID,
"serial": time.Now().UnixNano(),
"kind": kind,
"createdAt": time.Now().Unix(),
"content": "test content",
"sig": "0000000000000000000000000000000000000000000000000000000000000000" +
"0000000000000000000000000000000000000000000000000000000000000000",
"pubkey": pubkey,
"tags": tags,
}
_, err := testDB.ExecuteWrite(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to setup test event: %v", err)
}
}
// setupInvalidNostrUser creates a NostrUser with an invalid (binary) pubkey for testing migrations
func setupInvalidNostrUser(t *testing.T, invalidPubkey string) {
t.Helper()
ctx := context.Background()
cypher := `CREATE (u:NostrUser {pubkey: $pubkey, created_at: timestamp()})`
params := map[string]any{"pubkey": invalidPubkey}
_, err := testDB.ExecuteWrite(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to setup invalid NostrUser: %v", err)
}
}
// setupInvalidEvent creates an Event with an invalid pubkey/ID for testing migrations
func setupInvalidEvent(t *testing.T, invalidID, invalidPubkey string) {
t.Helper()
ctx := context.Background()
cypher := `
CREATE (e:Event {
id: $id,
pubkey: $pubkey,
kind: 1,
created_at: timestamp(),
content: 'test',
sig: 'invalid',
tags: '[]',
serial: $serial,
expiration: 0
})
`
params := map[string]any{
"id": invalidID,
"pubkey": invalidPubkey,
"serial": time.Now().UnixNano(),
}
_, err := testDB.ExecuteWrite(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to setup invalid Event: %v", err)
}
}
// setupInvalidTag creates a Tag node with invalid value for testing migrations
func setupInvalidTag(t *testing.T, tagType string, invalidValue string) {
t.Helper()
ctx := context.Background()
cypher := `CREATE (tag:Tag {type: $type, value: $value})`
params := map[string]any{
"type": tagType,
"value": invalidValue,
}
_, err := testDB.ExecuteWrite(ctx, cypher, params)
if err != nil {
t.Fatalf("Failed to setup invalid Tag: %v", err)
}
}
// countNodes counts nodes with a given label
func countNodes(t *testing.T, label string) int64 {
t.Helper()
ctx := context.Background()
cypher := "MATCH (n:" + label + ") RETURN count(n) AS count"
result, err := testDB.ExecuteRead(ctx, cypher, nil)
if err != nil {
t.Fatalf("Failed to count nodes: %v", err)
}
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
return count
}
}
return 0
}
// countInvalidNostrUsers counts NostrUser nodes with invalid pubkeys
func countInvalidNostrUsers(t *testing.T) int64 {
t.Helper()
ctx := context.Background()
cypher := `
MATCH (u:NostrUser)
WHERE size(u.pubkey) <> 64
OR NOT u.pubkey =~ '^[0-9a-f]{64}$'
RETURN count(u) AS count
`
result, err := testDB.ExecuteRead(ctx, cypher, nil)
if err != nil {
t.Fatalf("Failed to count invalid NostrUsers: %v", err)
}
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
return count
}
}
return 0
}
// countInvalidTags counts Tag nodes (e/p type) with invalid values
func countInvalidTags(t *testing.T) int64 {
t.Helper()
ctx := context.Background()
cypher := `
MATCH (t:Tag)
WHERE t.type IN ['e', 'p']
AND (size(t.value) <> 64 OR NOT t.value =~ '^[0-9a-f]{64}$')
RETURN count(t) AS count
`
result, err := testDB.ExecuteRead(ctx, cypher, nil)
if err != nil {
t.Fatalf("Failed to count invalid Tags: %v", err)
}
if result.Next(ctx) {
if count, ok := result.Record().Values[0].(int64); ok {
return count
}
}
return 0
}