testmain_test.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"os"
   6  	"testing"
   7  	"time"
   8  
   9  	"next.orly.dev/pkg/database"
  10  )
  11  
  12  // testDB is the shared database instance for tests
  13  var testDB *N
  14  
  15  // TestMain sets up and tears down the test database
  16  func TestMain(m *testing.M) {
  17  	// Skip integration tests if NEO4J_TEST_URI is not set
  18  	neo4jURI := os.Getenv("NEO4J_TEST_URI")
  19  	if neo4jURI == "" {
  20  		neo4jURI = "bolt://localhost:7687"
  21  	}
  22  	neo4jUser := os.Getenv("NEO4J_TEST_USER")
  23  	if neo4jUser == "" {
  24  		neo4jUser = "neo4j"
  25  	}
  26  	neo4jPassword := os.Getenv("NEO4J_TEST_PASSWORD")
  27  	if neo4jPassword == "" {
  28  		neo4jPassword = "testpassword"
  29  	}
  30  
  31  	// Try to connect to Neo4j
  32  	ctx, cancel := context.WithCancel(context.Background())
  33  	cfg := &database.DatabaseConfig{
  34  		DataDir:       os.TempDir(),
  35  		Neo4jURI:      neo4jURI,
  36  		Neo4jUser:     neo4jUser,
  37  		Neo4jPassword: neo4jPassword,
  38  	}
  39  
  40  	var err error
  41  	testDB, err = NewWithConfig(ctx, cancel, cfg)
  42  	if err != nil {
  43  		// If Neo4j is not available, skip integration tests
  44  		os.Stderr.WriteString("Neo4j not available, skipping integration tests: " + err.Error() + "\n")
  45  		os.Stderr.WriteString("Start Neo4j with: docker compose -f pkg/neo4j/docker-compose.yaml up -d\n")
  46  		os.Exit(0)
  47  	}
  48  
  49  	// Wait for database to be ready
  50  	select {
  51  	case <-testDB.Ready():
  52  		// Database is ready
  53  	case <-time.After(30 * time.Second):
  54  		os.Stderr.WriteString("Timeout waiting for Neo4j to be ready\n")
  55  		os.Exit(1)
  56  	}
  57  
  58  	// Clean database before running tests
  59  	cleanTestDatabase()
  60  
  61  	// Run tests
  62  	code := m.Run()
  63  
  64  	// Clean up
  65  	cleanTestDatabase()
  66  	testDB.Close()
  67  	cancel()
  68  
  69  	os.Exit(code)
  70  }
  71  
  72  // cleanTestDatabase removes all nodes and relationships, then re-initializes
  73  func cleanTestDatabase() {
  74  	ctx := context.Background()
  75  	// Delete all nodes and relationships
  76  	_, _ = testDB.ExecuteWrite(ctx, "MATCH (n) DETACH DELETE n", nil)
  77  	// Re-apply schema (constraints and indexes)
  78  	_ = testDB.applySchema(ctx)
  79  	// Re-initialize serial counter
  80  	_ = testDB.initSerialCounter()
  81  }
  82  
  83  // setupTestEvent creates a test event directly in Neo4j for testing queries
  84  func setupTestEvent(t *testing.T, eventID, pubkey string, kind int64, tags string) {
  85  	t.Helper()
  86  	ctx := context.Background()
  87  
  88  	cypher := `
  89  		MERGE (a:NostrUser {pubkey: $pubkey})
  90  		CREATE (e:Event {
  91  			id: $eventId,
  92  			serial: $serial,
  93  			kind: $kind,
  94  			created_at: $createdAt,
  95  			content: $content,
  96  			sig: $sig,
  97  			pubkey: $pubkey,
  98  			tags: $tags,
  99  			expiration: 0
 100  		})
 101  		CREATE (e)-[:AUTHORED_BY]->(a)
 102  	`
 103  
 104  	params := map[string]any{
 105  		"eventId":   eventID,
 106  		"serial":    time.Now().UnixNano(),
 107  		"kind":      kind,
 108  		"createdAt": time.Now().Unix(),
 109  		"content":   "test content",
 110  		"sig": "0000000000000000000000000000000000000000000000000000000000000000" +
 111  			"0000000000000000000000000000000000000000000000000000000000000000",
 112  		"pubkey": pubkey,
 113  		"tags":   tags,
 114  	}
 115  
 116  	_, err := testDB.ExecuteWrite(ctx, cypher, params)
 117  	if err != nil {
 118  		t.Fatalf("Failed to setup test event: %v", err)
 119  	}
 120  }
 121  
 122  // setupInvalidNostrUser creates a NostrUser with an invalid (binary) pubkey for testing migrations
 123  func setupInvalidNostrUser(t *testing.T, invalidPubkey string) {
 124  	t.Helper()
 125  	ctx := context.Background()
 126  
 127  	cypher := `CREATE (u:NostrUser {pubkey: $pubkey, created_at: timestamp()})`
 128  	params := map[string]any{"pubkey": invalidPubkey}
 129  
 130  	_, err := testDB.ExecuteWrite(ctx, cypher, params)
 131  	if err != nil {
 132  		t.Fatalf("Failed to setup invalid NostrUser: %v", err)
 133  	}
 134  }
 135  
 136  // setupInvalidEvent creates an Event with an invalid pubkey/ID for testing migrations
 137  func setupInvalidEvent(t *testing.T, invalidID, invalidPubkey string) {
 138  	t.Helper()
 139  	ctx := context.Background()
 140  
 141  	cypher := `
 142  		CREATE (e:Event {
 143  			id: $id,
 144  			pubkey: $pubkey,
 145  			kind: 1,
 146  			created_at: timestamp(),
 147  			content: 'test',
 148  			sig: 'invalid',
 149  			tags: '[]',
 150  			serial: $serial,
 151  			expiration: 0
 152  		})
 153  	`
 154  	params := map[string]any{
 155  		"id":     invalidID,
 156  		"pubkey": invalidPubkey,
 157  		"serial": time.Now().UnixNano(),
 158  	}
 159  
 160  	_, err := testDB.ExecuteWrite(ctx, cypher, params)
 161  	if err != nil {
 162  		t.Fatalf("Failed to setup invalid Event: %v", err)
 163  	}
 164  }
 165  
 166  // setupInvalidTag creates a Tag node with invalid value for testing migrations
 167  func setupInvalidTag(t *testing.T, tagType string, invalidValue string) {
 168  	t.Helper()
 169  	ctx := context.Background()
 170  
 171  	cypher := `CREATE (tag:Tag {type: $type, value: $value})`
 172  	params := map[string]any{
 173  		"type":  tagType,
 174  		"value": invalidValue,
 175  	}
 176  
 177  	_, err := testDB.ExecuteWrite(ctx, cypher, params)
 178  	if err != nil {
 179  		t.Fatalf("Failed to setup invalid Tag: %v", err)
 180  	}
 181  }
 182  
 183  // countNodes counts nodes with a given label
 184  func countNodes(t *testing.T, label string) int64 {
 185  	t.Helper()
 186  	ctx := context.Background()
 187  
 188  	cypher := "MATCH (n:" + label + ") RETURN count(n) AS count"
 189  	result, err := testDB.ExecuteRead(ctx, cypher, nil)
 190  	if err != nil {
 191  		t.Fatalf("Failed to count nodes: %v", err)
 192  	}
 193  
 194  	if result.Next(ctx) {
 195  		if count, ok := result.Record().Values[0].(int64); ok {
 196  			return count
 197  		}
 198  	}
 199  	return 0
 200  }
 201  
 202  // countInvalidNostrUsers counts NostrUser nodes with invalid pubkeys
 203  func countInvalidNostrUsers(t *testing.T) int64 {
 204  	t.Helper()
 205  	ctx := context.Background()
 206  
 207  	cypher := `
 208  		MATCH (u:NostrUser)
 209  		WHERE size(u.pubkey) <> 64
 210  		   OR NOT u.pubkey =~ '^[0-9a-f]{64}$'
 211  		RETURN count(u) AS count
 212  	`
 213  	result, err := testDB.ExecuteRead(ctx, cypher, nil)
 214  	if err != nil {
 215  		t.Fatalf("Failed to count invalid NostrUsers: %v", err)
 216  	}
 217  
 218  	if result.Next(ctx) {
 219  		if count, ok := result.Record().Values[0].(int64); ok {
 220  			return count
 221  		}
 222  	}
 223  	return 0
 224  }
 225  
 226  // countInvalidTags counts Tag nodes (e/p type) with invalid values
 227  func countInvalidTags(t *testing.T) int64 {
 228  	t.Helper()
 229  	ctx := context.Background()
 230  
 231  	cypher := `
 232  		MATCH (t:Tag)
 233  		WHERE t.type IN ['e', 'p']
 234  		  AND (size(t.value) <> 64 OR NOT t.value =~ '^[0-9a-f]{64}$')
 235  		RETURN count(t) AS count
 236  	`
 237  	result, err := testDB.ExecuteRead(ctx, cypher, nil)
 238  	if err != nil {
 239  		t.Fatalf("Failed to count invalid Tags: %v", err)
 240  	}
 241  
 242  	if result.Next(ctx) {
 243  		if count, ok := result.Record().Values[0].(int64); ok {
 244  			return count
 245  		}
 246  	}
 247  	return 0
 248  }
 249