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