expiration_test.go raw
1 //go:build integration
2 // +build integration
3
4 package neo4j
5
6 import (
7 "bytes"
8 "context"
9 "encoding/json"
10 "testing"
11 "time"
12
13 "next.orly.dev/pkg/nostr/encoders/event"
14 "next.orly.dev/pkg/nostr/encoders/filter"
15 "next.orly.dev/pkg/nostr/encoders/hex"
16 "next.orly.dev/pkg/nostr/encoders/kind"
17 "next.orly.dev/pkg/nostr/encoders/tag"
18 "next.orly.dev/pkg/nostr/encoders/timestamp"
19 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
20 )
21
22 // All tests in this file use the shared testDB instance from testmain_test.go
23 // to avoid Neo4j authentication rate limiting from too many connections.
24
25 func TestExpiration_SaveEventWithExpiration(t *testing.T) {
26 if testDB == nil {
27 t.Skip("Neo4j not available")
28 }
29
30 cleanTestDatabase()
31
32 ctx := context.Background()
33
34 signer, err := p8k.New()
35 if err != nil {
36 t.Fatalf("Failed to create signer: %v", err)
37 }
38 if err := signer.Generate(); err != nil {
39 t.Fatalf("Failed to generate keypair: %v", err)
40 }
41
42 // Create event with expiration tag (expires in 1 hour)
43 futureExpiration := time.Now().Add(1 * time.Hour).Unix()
44
45 ev := event.New()
46 ev.Pubkey = signer.Pub()
47 ev.CreatedAt = timestamp.Now().V
48 ev.Kind = 1
49 ev.Content = []byte("Event with expiration")
50 ev.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
51
52 if err := ev.Sign(signer); err != nil {
53 t.Fatalf("Failed to sign event: %v", err)
54 }
55
56 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
57 t.Fatalf("Failed to save event: %v", err)
58 }
59
60 // Query the event to verify it was saved
61 evs, err := testDB.QueryEvents(ctx, &filter.F{
62 Ids: tag.NewFromBytesSlice(ev.ID),
63 })
64 if err != nil {
65 t.Fatalf("Failed to query event: %v", err)
66 }
67
68 if len(evs) != 1 {
69 t.Fatalf("Expected 1 event, got %d", len(evs))
70 }
71
72 t.Logf("✓ Event with expiration tag saved successfully")
73 }
74
75 func TestExpiration_DeleteExpiredEvents(t *testing.T) {
76 if testDB == nil {
77 t.Skip("Neo4j not available")
78 }
79
80 cleanTestDatabase()
81
82 ctx := context.Background()
83
84 signer, err := p8k.New()
85 if err != nil {
86 t.Fatalf("Failed to create signer: %v", err)
87 }
88 if err := signer.Generate(); err != nil {
89 t.Fatalf("Failed to generate keypair: %v", err)
90 }
91
92 // Create an expired event (expired 1 hour ago)
93 pastExpiration := time.Now().Add(-1 * time.Hour).Unix()
94
95 expiredEv := event.New()
96 expiredEv.Pubkey = signer.Pub()
97 expiredEv.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
98 expiredEv.Kind = 1
99 expiredEv.Content = []byte("Expired event")
100 expiredEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(pastExpiration).String()))
101
102 if err := expiredEv.Sign(signer); err != nil {
103 t.Fatalf("Failed to sign expired event: %v", err)
104 }
105
106 if _, err := testDB.SaveEvent(ctx, expiredEv); err != nil {
107 t.Fatalf("Failed to save expired event: %v", err)
108 }
109
110 // Create a non-expired event (expires in 1 hour)
111 futureExpiration := time.Now().Add(1 * time.Hour).Unix()
112
113 validEv := event.New()
114 validEv.Pubkey = signer.Pub()
115 validEv.CreatedAt = timestamp.Now().V
116 validEv.Kind = 1
117 validEv.Content = []byte("Valid event")
118 validEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
119
120 if err := validEv.Sign(signer); err != nil {
121 t.Fatalf("Failed to sign valid event: %v", err)
122 }
123
124 if _, err := testDB.SaveEvent(ctx, validEv); err != nil {
125 t.Fatalf("Failed to save valid event: %v", err)
126 }
127
128 // Create an event without expiration
129 permanentEv := event.New()
130 permanentEv.Pubkey = signer.Pub()
131 permanentEv.CreatedAt = timestamp.Now().V + 1
132 permanentEv.Kind = 1
133 permanentEv.Content = []byte("Permanent event (no expiration)")
134
135 if err := permanentEv.Sign(signer); err != nil {
136 t.Fatalf("Failed to sign permanent event: %v", err)
137 }
138
139 if _, err := testDB.SaveEvent(ctx, permanentEv); err != nil {
140 t.Fatalf("Failed to save permanent event: %v", err)
141 }
142
143 // Verify all 3 events exist
144 evs, err := testDB.QueryEvents(ctx, &filter.F{
145 Authors: tag.NewFromBytesSlice(signer.Pub()),
146 })
147 if err != nil {
148 t.Fatalf("Failed to query events: %v", err)
149 }
150 if len(evs) != 3 {
151 t.Fatalf("Expected 3 events before deletion, got %d", len(evs))
152 }
153
154 // Run DeleteExpired
155 testDB.DeleteExpired()
156
157 // Verify only expired event was deleted
158 evs, err = testDB.QueryEvents(ctx, &filter.F{
159 Authors: tag.NewFromBytesSlice(signer.Pub()),
160 })
161 if err != nil {
162 t.Fatalf("Failed to query events after deletion: %v", err)
163 }
164
165 if len(evs) != 2 {
166 t.Fatalf("Expected 2 events after deletion (expired removed), got %d", len(evs))
167 }
168
169 // Verify the correct events remain
170 foundValid := false
171 foundPermanent := false
172 for _, ev := range evs {
173 if hex.Enc(ev.ID[:]) == hex.Enc(validEv.ID[:]) {
174 foundValid = true
175 }
176 if hex.Enc(ev.ID[:]) == hex.Enc(permanentEv.ID[:]) {
177 foundPermanent = true
178 }
179 }
180
181 if !foundValid {
182 t.Fatal("Valid event (with future expiration) was incorrectly deleted")
183 }
184 if !foundPermanent {
185 t.Fatal("Permanent event (no expiration) was incorrectly deleted")
186 }
187
188 t.Logf("✓ DeleteExpired correctly removed only expired events")
189 }
190
191 func TestExpiration_NoExpirationTag(t *testing.T) {
192 if testDB == nil {
193 t.Skip("Neo4j not available")
194 }
195
196 cleanTestDatabase()
197
198 ctx := context.Background()
199
200 signer, err := p8k.New()
201 if err != nil {
202 t.Fatalf("Failed to create signer: %v", err)
203 }
204 if err := signer.Generate(); err != nil {
205 t.Fatalf("Failed to generate keypair: %v", err)
206 }
207
208 // Create event without expiration tag
209 ev := event.New()
210 ev.Pubkey = signer.Pub()
211 ev.CreatedAt = timestamp.Now().V
212 ev.Kind = 1
213 ev.Content = []byte("Event without expiration")
214
215 if err := ev.Sign(signer); err != nil {
216 t.Fatalf("Failed to sign event: %v", err)
217 }
218
219 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
220 t.Fatalf("Failed to save event: %v", err)
221 }
222
223 // Run DeleteExpired - event should not be deleted
224 testDB.DeleteExpired()
225
226 // Verify event still exists
227 evs, err := testDB.QueryEvents(ctx, &filter.F{
228 Ids: tag.NewFromBytesSlice(ev.ID),
229 })
230 if err != nil {
231 t.Fatalf("Failed to query event: %v", err)
232 }
233
234 if len(evs) != 1 {
235 t.Fatalf("Expected 1 event (no expiration should not be deleted), got %d", len(evs))
236 }
237
238 t.Logf("✓ Events without expiration tag are not deleted")
239 }
240
241 func TestExport_AllEvents(t *testing.T) {
242 if testDB == nil {
243 t.Skip("Neo4j not available")
244 }
245
246 cleanTestDatabase()
247
248 ctx := context.Background()
249
250 signer, err := p8k.New()
251 if err != nil {
252 t.Fatalf("Failed to create signer: %v", err)
253 }
254 if err := signer.Generate(); err != nil {
255 t.Fatalf("Failed to generate keypair: %v", err)
256 }
257
258 // Create and save some events
259 for i := 0; i < 5; i++ {
260 ev := event.New()
261 ev.Pubkey = signer.Pub()
262 ev.CreatedAt = timestamp.Now().V + int64(i)
263 ev.Kind = 1
264 ev.Content = []byte("Test event for export")
265 ev.Tags = tag.NewS(tag.NewFromAny("t", "test"))
266
267 if err := ev.Sign(signer); err != nil {
268 t.Fatalf("Failed to sign event: %v", err)
269 }
270
271 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
272 t.Fatalf("Failed to save event: %v", err)
273 }
274 }
275
276 // Export all events
277 var buf bytes.Buffer
278 testDB.Export(ctx, &buf)
279
280 // Parse the exported JSONL
281 lines := bytes.Split(buf.Bytes(), []byte("\n"))
282 validLines := 0
283 for _, line := range lines {
284 if len(line) == 0 {
285 continue
286 }
287 var ev event.E
288 if err := json.Unmarshal(line, &ev); err != nil {
289 t.Fatalf("Failed to parse exported event: %v", err)
290 }
291 validLines++
292 }
293
294 if validLines != 5 {
295 t.Fatalf("Expected 5 exported events, got %d", validLines)
296 }
297
298 t.Logf("✓ Export all events returned %d events in JSONL format", validLines)
299 }
300
301 func TestExport_FilterByPubkey(t *testing.T) {
302 if testDB == nil {
303 t.Skip("Neo4j not available")
304 }
305
306 cleanTestDatabase()
307
308 ctx := context.Background()
309
310 // Create two signers
311 alice, _ := p8k.New()
312 alice.Generate()
313
314 bob, _ := p8k.New()
315 bob.Generate()
316
317 baseTs := timestamp.Now().V
318
319 // Create events from Alice
320 for i := 0; i < 3; i++ {
321 ev := event.New()
322 ev.Pubkey = alice.Pub()
323 ev.CreatedAt = baseTs + int64(i)
324 ev.Kind = 1
325 ev.Content = []byte("Alice's event")
326
327 if err := ev.Sign(alice); err != nil {
328 t.Fatalf("Failed to sign event: %v", err)
329 }
330
331 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
332 t.Fatalf("Failed to save event: %v", err)
333 }
334 }
335
336 // Create events from Bob
337 for i := 0; i < 2; i++ {
338 ev := event.New()
339 ev.Pubkey = bob.Pub()
340 ev.CreatedAt = baseTs + int64(i) + 10
341 ev.Kind = 1
342 ev.Content = []byte("Bob's event")
343
344 if err := ev.Sign(bob); err != nil {
345 t.Fatalf("Failed to sign event: %v", err)
346 }
347
348 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
349 t.Fatalf("Failed to save event: %v", err)
350 }
351 }
352
353 // Export only Alice's events
354 var buf bytes.Buffer
355 testDB.Export(ctx, &buf, alice.Pub())
356
357 // Parse the exported JSONL
358 lines := bytes.Split(buf.Bytes(), []byte("\n"))
359 validLines := 0
360 alicePubkey := hex.Enc(alice.Pub())
361 for _, line := range lines {
362 if len(line) == 0 {
363 continue
364 }
365 var ev event.E
366 if err := json.Unmarshal(line, &ev); err != nil {
367 t.Fatalf("Failed to parse exported event: %v", err)
368 }
369 if hex.Enc(ev.Pubkey[:]) != alicePubkey {
370 t.Fatalf("Exported event has wrong pubkey (expected Alice)")
371 }
372 validLines++
373 }
374
375 if validLines != 3 {
376 t.Fatalf("Expected 3 events from Alice, got %d", validLines)
377 }
378
379 t.Logf("✓ Export with pubkey filter returned %d events from Alice only", validLines)
380 }
381
382 func TestExport_Empty(t *testing.T) {
383 if testDB == nil {
384 t.Skip("Neo4j not available")
385 }
386
387 cleanTestDatabase()
388
389 ctx := context.Background()
390
391 // Export from empty database
392 var buf bytes.Buffer
393 testDB.Export(ctx, &buf)
394
395 // Should be empty or just whitespace
396 content := bytes.TrimSpace(buf.Bytes())
397 if len(content) != 0 {
398 t.Fatalf("Expected empty export, got: %s", string(content))
399 }
400
401 t.Logf("✓ Export from empty database returns empty result")
402 }
403
404 func TestImportExport_RoundTrip(t *testing.T) {
405 if testDB == nil {
406 t.Skip("Neo4j not available")
407 }
408
409 cleanTestDatabase()
410
411 ctx := context.Background()
412
413 signer, _ := p8k.New()
414 signer.Generate()
415
416 // Create original events
417 originalEvents := make([]*event.E, 3)
418 for i := 0; i < 3; i++ {
419 ev := event.New()
420 ev.Pubkey = signer.Pub()
421 ev.CreatedAt = timestamp.Now().V + int64(i)
422 ev.Kind = 1
423 ev.Content = []byte("Round trip test event")
424 ev.Tags = tag.NewS(tag.NewFromAny("t", "roundtrip"))
425
426 if err := ev.Sign(signer); err != nil {
427 t.Fatalf("Failed to sign event: %v", err)
428 }
429
430 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
431 t.Fatalf("Failed to save event: %v", err)
432 }
433 originalEvents[i] = ev
434 }
435
436 // Export events
437 var buf bytes.Buffer
438 testDB.Export(ctx, &buf)
439
440 // Wipe database
441 if err := testDB.Wipe(); err != nil {
442 t.Fatalf("Failed to wipe database: %v", err)
443 }
444
445 // Verify database is empty
446 evs, err := testDB.QueryEvents(ctx, &filter.F{
447 Kinds: kind.NewS(kind.New(1)),
448 })
449 if err != nil {
450 t.Fatalf("Failed to query events: %v", err)
451 }
452 if len(evs) != 0 {
453 t.Fatalf("Expected 0 events after wipe, got %d", len(evs))
454 }
455
456 // Import events
457 testDB.Import(bytes.NewReader(buf.Bytes()))
458
459 // Verify events were restored
460 evs, err = testDB.QueryEvents(ctx, &filter.F{
461 Authors: tag.NewFromBytesSlice(signer.Pub()),
462 })
463 if err != nil {
464 t.Fatalf("Failed to query imported events: %v", err)
465 }
466
467 if len(evs) != 3 {
468 t.Fatalf("Expected 3 imported events, got %d", len(evs))
469 }
470
471 // Verify event IDs match
472 importedIDs := make(map[string]bool)
473 for _, ev := range evs {
474 importedIDs[hex.Enc(ev.ID[:])] = true
475 }
476
477 for _, orig := range originalEvents {
478 if !importedIDs[hex.Enc(orig.ID[:])] {
479 t.Fatalf("Original event %s not found after import", hex.Enc(orig.ID[:]))
480 }
481 }
482
483 t.Logf("✓ Export/Import round trip preserved %d events correctly", len(evs))
484 }
485