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.
247 lines
5.9 KiB
Go
247 lines
5.9 KiB
Go
package neo4j
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"next.orly.dev/pkg/database"
|
|
)
|
|
|
|
// 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
|
|
}
|