You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
5.9 KiB
246 lines
5.9 KiB
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 |
|
}
|
|
|