Some checks failed
Go / build-and-release (push) Has been cancelled
Introduce comprehensive integration tests for Neo4j bug fixes covering batching, event relationships, and processing logic. Add rate-limiting to Neo4j queries using semaphores and retry policies to prevent authentication rate limiting and connection exhaustion, ensuring system stability under load.
249 lines
5.9 KiB
Go
249 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, then re-initializes
|
|
func cleanTestDatabase() {
|
|
ctx := context.Background()
|
|
// Delete all nodes and relationships
|
|
_, _ = testDB.ExecuteWrite(ctx, "MATCH (n) DETACH DELETE n", nil)
|
|
// Re-apply schema (constraints and indexes)
|
|
_ = testDB.applySchema(ctx)
|
|
// Re-initialize serial counter
|
|
_ = testDB.initSerialCounter()
|
|
}
|
|
|
|
// 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
|
|
}
|