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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user