tag_model_test.go raw
1 package neo4j
2
3 import (
4 "context"
5 "fmt"
6 "testing"
7
8 "next.orly.dev/pkg/nostr/encoders/event"
9 "next.orly.dev/pkg/nostr/encoders/filter"
10 "next.orly.dev/pkg/nostr/encoders/hex"
11 "next.orly.dev/pkg/nostr/encoders/tag"
12 "next.orly.dev/pkg/nostr/encoders/timestamp"
13 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
14 )
15
16 // =============================================================================
17 // Tag-Based E/P Model Tests
18 // =============================================================================
19
20 // TestTagBasedModel_ETagCreatesTagNode verifies that e-tags create Tag nodes
21 // with type='e' and TAGGED_WITH relationships from the event.
22 func TestTagBasedModel_ETagCreatesTagNode(t *testing.T) {
23 if testDB == nil {
24 t.Skip("Neo4j not available")
25 }
26
27 ctx := context.Background()
28 cleanTestDatabase()
29
30 signer, err := p8k.New()
31 if err != nil {
32 t.Fatalf("Failed to create signer: %v", err)
33 }
34 if err := signer.Generate(); err != nil {
35 t.Fatalf("Failed to generate keypair: %v", err)
36 }
37
38 // Create a target event first
39 targetEvent := event.New()
40 targetEvent.Pubkey = signer.Pub()
41 targetEvent.CreatedAt = timestamp.Now().V
42 targetEvent.Kind = 1
43 targetEvent.Content = []byte("Target event")
44 if err := targetEvent.Sign(signer); err != nil {
45 t.Fatalf("Failed to sign target event: %v", err)
46 }
47
48 if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
49 t.Fatalf("Failed to save target event: %v", err)
50 }
51
52 targetID := hex.Enc(targetEvent.ID[:])
53
54 // Create event with e-tag referencing the target
55 ev := event.New()
56 ev.Pubkey = signer.Pub()
57 ev.CreatedAt = timestamp.Now().V + 1
58 ev.Kind = 1
59 ev.Content = []byte("Event with e-tag")
60 ev.Tags = tag.NewS(
61 tag.NewFromAny("e", targetID, "", "reply"),
62 )
63 if err := ev.Sign(signer); err != nil {
64 t.Fatalf("Failed to sign event: %v", err)
65 }
66
67 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
68 t.Fatalf("Failed to save event: %v", err)
69 }
70
71 eventID := hex.Enc(ev.ID[:])
72
73 // Verify Tag node was created
74 tagCypher := `
75 MATCH (t:Tag {type: 'e', value: $targetId})
76 RETURN t.type AS type, t.value AS value
77 `
78 tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{"targetId": targetID})
79 if err != nil {
80 t.Fatalf("Failed to query Tag node: %v", err)
81 }
82
83 if !tagResult.Next(ctx) {
84 t.Fatal("Expected Tag node with type='e' to be created")
85 }
86
87 record := tagResult.Record()
88 tagType := record.Values[0].(string)
89 tagValue := record.Values[1].(string)
90
91 if tagType != "e" {
92 t.Errorf("Expected tag type 'e', got %q", tagType)
93 }
94 if tagValue != targetID {
95 t.Errorf("Expected tag value %q, got %q", targetID, tagValue)
96 }
97
98 // Verify TAGGED_WITH relationship exists
99 taggedWithCypher := `
100 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $targetId})
101 RETURN count(t) AS count
102 `
103 twResult, err := testDB.ExecuteRead(ctx, taggedWithCypher, map[string]any{
104 "eventId": eventID,
105 "targetId": targetID,
106 })
107 if err != nil {
108 t.Fatalf("Failed to query TAGGED_WITH: %v", err)
109 }
110
111 if twResult.Next(ctx) {
112 count := twResult.Record().Values[0].(int64)
113 if count != 1 {
114 t.Errorf("Expected 1 TAGGED_WITH relationship, got %d", count)
115 }
116 } else {
117 t.Fatal("Expected TAGGED_WITH relationship to exist")
118 }
119
120 // Verify REFERENCES relationship from Tag to Event
121 refCypher := `
122 MATCH (t:Tag {type: 'e', value: $targetId})-[:REFERENCES]->(target:Event {id: $targetId})
123 RETURN count(target) AS count
124 `
125 refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"targetId": targetID})
126 if err != nil {
127 t.Fatalf("Failed to query REFERENCES: %v", err)
128 }
129
130 if refResult.Next(ctx) {
131 count := refResult.Record().Values[0].(int64)
132 if count != 1 {
133 t.Errorf("Expected 1 REFERENCES relationship from Tag to Event, got %d", count)
134 }
135 } else {
136 t.Fatal("Expected REFERENCES relationship from Tag to Event")
137 }
138
139 t.Logf("Tag-based e-tag model verified: Event -> Tag{e} -> Event")
140 }
141
142 // TestTagBasedModel_PTagCreatesTagNode verifies that p-tags create Tag nodes
143 // with type='p' and REFERENCES relationships to NostrUser nodes.
144 func TestTagBasedModel_PTagCreatesTagNode(t *testing.T) {
145 if testDB == nil {
146 t.Skip("Neo4j not available")
147 }
148
149 ctx := context.Background()
150 cleanTestDatabase()
151
152 // Create two signers: author and mentioned user
153 author, err := p8k.New()
154 if err != nil {
155 t.Fatalf("Failed to create author signer: %v", err)
156 }
157 if err := author.Generate(); err != nil {
158 t.Fatalf("Failed to generate author keypair: %v", err)
159 }
160
161 mentioned, err := p8k.New()
162 if err != nil {
163 t.Fatalf("Failed to create mentioned signer: %v", err)
164 }
165 if err := mentioned.Generate(); err != nil {
166 t.Fatalf("Failed to generate mentioned keypair: %v", err)
167 }
168
169 mentionedPubkey := hex.Enc(mentioned.Pub())
170
171 // Create event with p-tag
172 ev := event.New()
173 ev.Pubkey = author.Pub()
174 ev.CreatedAt = timestamp.Now().V
175 ev.Kind = 1
176 ev.Content = []byte("Event mentioning someone")
177 ev.Tags = tag.NewS(
178 tag.NewFromAny("p", mentionedPubkey),
179 )
180 if err := ev.Sign(author); err != nil {
181 t.Fatalf("Failed to sign event: %v", err)
182 }
183
184 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
185 t.Fatalf("Failed to save event: %v", err)
186 }
187
188 eventID := hex.Enc(ev.ID[:])
189
190 // Verify Tag node was created
191 tagCypher := `
192 MATCH (t:Tag {type: 'p', value: $pubkey})
193 RETURN t.type AS type, t.value AS value
194 `
195 tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{"pubkey": mentionedPubkey})
196 if err != nil {
197 t.Fatalf("Failed to query Tag node: %v", err)
198 }
199
200 if !tagResult.Next(ctx) {
201 t.Fatal("Expected Tag node with type='p' to be created")
202 }
203
204 // Verify TAGGED_WITH relationship exists
205 taggedWithCypher := `
206 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $pubkey})
207 RETURN count(t) AS count
208 `
209 twResult, err := testDB.ExecuteRead(ctx, taggedWithCypher, map[string]any{
210 "eventId": eventID,
211 "pubkey": mentionedPubkey,
212 })
213 if err != nil {
214 t.Fatalf("Failed to query TAGGED_WITH: %v", err)
215 }
216
217 if twResult.Next(ctx) {
218 count := twResult.Record().Values[0].(int64)
219 if count != 1 {
220 t.Errorf("Expected 1 TAGGED_WITH relationship, got %d", count)
221 }
222 }
223
224 // Verify REFERENCES relationship from Tag to NostrUser
225 refCypher := `
226 MATCH (t:Tag {type: 'p', value: $pubkey})-[:REFERENCES]->(u:NostrUser {pubkey: $pubkey})
227 RETURN count(u) AS count
228 `
229 refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"pubkey": mentionedPubkey})
230 if err != nil {
231 t.Fatalf("Failed to query REFERENCES: %v", err)
232 }
233
234 if refResult.Next(ctx) {
235 count := refResult.Record().Values[0].(int64)
236 if count != 1 {
237 t.Errorf("Expected 1 REFERENCES relationship from Tag to NostrUser, got %d", count)
238 }
239 } else {
240 t.Fatal("Expected REFERENCES relationship from Tag to NostrUser")
241 }
242
243 // Verify NostrUser was created for the mentioned pubkey
244 userCypher := `
245 MATCH (u:NostrUser {pubkey: $pubkey})
246 RETURN u.pubkey AS pubkey
247 `
248 userResult, err := testDB.ExecuteRead(ctx, userCypher, map[string]any{"pubkey": mentionedPubkey})
249 if err != nil {
250 t.Fatalf("Failed to query NostrUser: %v", err)
251 }
252
253 if !userResult.Next(ctx) {
254 t.Fatal("Expected NostrUser to be created for mentioned pubkey")
255 }
256
257 t.Logf("Tag-based p-tag model verified: Event -> Tag{p} -> NostrUser")
258 }
259
260 // TestTagBasedModel_ETagWithoutTargetEvent verifies that e-tags create Tag nodes
261 // even when the referenced event doesn't exist, but don't create REFERENCES.
262 func TestTagBasedModel_ETagWithoutTargetEvent(t *testing.T) {
263 if testDB == nil {
264 t.Skip("Neo4j not available")
265 }
266
267 ctx := context.Background()
268 cleanTestDatabase()
269
270 signer, err := p8k.New()
271 if err != nil {
272 t.Fatalf("Failed to create signer: %v", err)
273 }
274 if err := signer.Generate(); err != nil {
275 t.Fatalf("Failed to generate keypair: %v", err)
276 }
277
278 // Non-existent event ID
279 nonExistentID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
280
281 // Create event with e-tag referencing non-existent event
282 ev := event.New()
283 ev.Pubkey = signer.Pub()
284 ev.CreatedAt = timestamp.Now().V
285 ev.Kind = 1
286 ev.Content = []byte("Reply to ghost event")
287 ev.Tags = tag.NewS(
288 tag.NewFromAny("e", nonExistentID, "", "reply"),
289 )
290 if err := ev.Sign(signer); err != nil {
291 t.Fatalf("Failed to sign event: %v", err)
292 }
293
294 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
295 t.Fatalf("Failed to save event: %v", err)
296 }
297
298 eventID := hex.Enc(ev.ID[:])
299
300 // Verify Tag node WAS created (for query purposes)
301 tagCypher := `
302 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $targetId})
303 RETURN t.value AS value
304 `
305 tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{
306 "eventId": eventID,
307 "targetId": nonExistentID,
308 })
309 if err != nil {
310 t.Fatalf("Failed to query Tag node: %v", err)
311 }
312
313 if !tagResult.Next(ctx) {
314 t.Fatal("Expected Tag node to be created even for non-existent target")
315 }
316
317 // Verify REFERENCES was NOT created (target doesn't exist)
318 refCypher := `
319 MATCH (t:Tag {type: 'e', value: $targetId})-[:REFERENCES]->(target:Event)
320 RETURN count(target) AS count
321 `
322 refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"targetId": nonExistentID})
323 if err != nil {
324 t.Fatalf("Failed to query REFERENCES: %v", err)
325 }
326
327 if refResult.Next(ctx) {
328 count := refResult.Record().Values[0].(int64)
329 if count != 0 {
330 t.Errorf("Expected 0 REFERENCES for non-existent event, got %d", count)
331 }
332 }
333
334 t.Logf("Correctly handled e-tag to non-existent event: Tag created, no REFERENCES")
335 }
336
337 // =============================================================================
338 // Tag Filter Query Tests (#e and #p filters)
339 // =============================================================================
340
341 // TestTagFilter_ETagQuery tests that #e filters work with the Tag-based model.
342 func TestTagFilter_ETagQuery(t *testing.T) {
343 if testDB == nil {
344 t.Skip("Neo4j not available")
345 }
346
347 ctx := context.Background()
348 cleanTestDatabase()
349
350 signer, err := p8k.New()
351 if err != nil {
352 t.Fatalf("Failed to create signer: %v", err)
353 }
354 if err := signer.Generate(); err != nil {
355 t.Fatalf("Failed to generate keypair: %v", err)
356 }
357
358 // Create root event
359 rootEvent := event.New()
360 rootEvent.Pubkey = signer.Pub()
361 rootEvent.CreatedAt = timestamp.Now().V
362 rootEvent.Kind = 1
363 rootEvent.Content = []byte("Root event")
364 if err := rootEvent.Sign(signer); err != nil {
365 t.Fatalf("Failed to sign root event: %v", err)
366 }
367
368 if _, err := testDB.SaveEvent(ctx, rootEvent); err != nil {
369 t.Fatalf("Failed to save root event: %v", err)
370 }
371
372 rootID := hex.Enc(rootEvent.ID[:])
373
374 // Create reply event with e-tag
375 replyEvent := event.New()
376 replyEvent.Pubkey = signer.Pub()
377 replyEvent.CreatedAt = timestamp.Now().V + 1
378 replyEvent.Kind = 1
379 replyEvent.Content = []byte("Reply to root")
380 replyEvent.Tags = tag.NewS(
381 tag.NewFromAny("e", rootID, "", "root"),
382 )
383 if err := replyEvent.Sign(signer); err != nil {
384 t.Fatalf("Failed to sign reply event: %v", err)
385 }
386
387 if _, err := testDB.SaveEvent(ctx, replyEvent); err != nil {
388 t.Fatalf("Failed to save reply event: %v", err)
389 }
390
391 // Create unrelated event (no e-tag)
392 unrelatedEvent := event.New()
393 unrelatedEvent.Pubkey = signer.Pub()
394 unrelatedEvent.CreatedAt = timestamp.Now().V + 2
395 unrelatedEvent.Kind = 1
396 unrelatedEvent.Content = []byte("Unrelated event")
397 if err := unrelatedEvent.Sign(signer); err != nil {
398 t.Fatalf("Failed to sign unrelated event: %v", err)
399 }
400
401 if _, err := testDB.SaveEvent(ctx, unrelatedEvent); err != nil {
402 t.Fatalf("Failed to save unrelated event: %v", err)
403 }
404
405 // Query events with #e filter
406 f := &filter.F{
407 Tags: tag.NewS(tag.NewFromAny("e", rootID)),
408 }
409
410 events, err := testDB.QueryEvents(ctx, f)
411 if err != nil {
412 t.Fatalf("Failed to query with #e filter: %v", err)
413 }
414
415 if len(events) != 1 {
416 t.Errorf("Expected 1 event with #e filter, got %d", len(events))
417 }
418
419 if len(events) > 0 {
420 foundID := hex.Enc(events[0].ID[:])
421 expectedID := hex.Enc(replyEvent.ID[:])
422 if foundID != expectedID {
423 t.Errorf("Expected to find reply event, got event %s", foundID[:8])
424 }
425 }
426
427 t.Logf("#e filter query working correctly with Tag-based model")
428 }
429
430 // TestTagFilter_PTagQuery tests that #p filters work with the Tag-based model.
431 func TestTagFilter_PTagQuery(t *testing.T) {
432 if testDB == nil {
433 t.Skip("Neo4j not available")
434 }
435
436 ctx := context.Background()
437 cleanTestDatabase()
438
439 // Create two signers
440 author, err := p8k.New()
441 if err != nil {
442 t.Fatalf("Failed to create author signer: %v", err)
443 }
444 if err := author.Generate(); err != nil {
445 t.Fatalf("Failed to generate author keypair: %v", err)
446 }
447
448 mentioned, err := p8k.New()
449 if err != nil {
450 t.Fatalf("Failed to create mentioned signer: %v", err)
451 }
452 if err := mentioned.Generate(); err != nil {
453 t.Fatalf("Failed to generate mentioned keypair: %v", err)
454 }
455
456 mentionedPubkey := hex.Enc(mentioned.Pub())
457
458 // Create event that mentions someone
459 mentionEvent := event.New()
460 mentionEvent.Pubkey = author.Pub()
461 mentionEvent.CreatedAt = timestamp.Now().V
462 mentionEvent.Kind = 1
463 mentionEvent.Content = []byte("Hey @someone")
464 mentionEvent.Tags = tag.NewS(
465 tag.NewFromAny("p", mentionedPubkey),
466 )
467 if err := mentionEvent.Sign(author); err != nil {
468 t.Fatalf("Failed to sign mention event: %v", err)
469 }
470
471 if _, err := testDB.SaveEvent(ctx, mentionEvent); err != nil {
472 t.Fatalf("Failed to save mention event: %v", err)
473 }
474
475 // Create event without p-tag
476 regularEvent := event.New()
477 regularEvent.Pubkey = author.Pub()
478 regularEvent.CreatedAt = timestamp.Now().V + 1
479 regularEvent.Kind = 1
480 regularEvent.Content = []byte("Regular post")
481 if err := regularEvent.Sign(author); err != nil {
482 t.Fatalf("Failed to sign regular event: %v", err)
483 }
484
485 if _, err := testDB.SaveEvent(ctx, regularEvent); err != nil {
486 t.Fatalf("Failed to save regular event: %v", err)
487 }
488
489 // Query events with #p filter
490 f := &filter.F{
491 Tags: tag.NewS(tag.NewFromAny("p", mentionedPubkey)),
492 }
493
494 events, err := testDB.QueryEvents(ctx, f)
495 if err != nil {
496 t.Fatalf("Failed to query with #p filter: %v", err)
497 }
498
499 if len(events) != 1 {
500 t.Errorf("Expected 1 event with #p filter, got %d", len(events))
501 }
502
503 if len(events) > 0 {
504 foundID := hex.Enc(events[0].ID[:])
505 expectedID := hex.Enc(mentionEvent.ID[:])
506 if foundID != expectedID {
507 t.Errorf("Expected to find mention event, got event %s", foundID[:8])
508 }
509 }
510
511 t.Logf("#p filter query working correctly with Tag-based model")
512 }
513
514 // TestTagFilter_MultiplePTags tests events with multiple p-tags.
515 func TestTagFilter_MultiplePTags(t *testing.T) {
516 if testDB == nil {
517 t.Skip("Neo4j not available")
518 }
519
520 ctx := context.Background()
521 cleanTestDatabase()
522
523 author, err := p8k.New()
524 if err != nil {
525 t.Fatalf("Failed to create author signer: %v", err)
526 }
527 if err := author.Generate(); err != nil {
528 t.Fatalf("Failed to generate author keypair: %v", err)
529 }
530
531 // Generate 5 pubkeys to mention
532 var mentionedPubkeys []string
533 for i := 0; i < 5; i++ {
534 mentionedPubkeys = append(mentionedPubkeys, fmt.Sprintf("%064x", i+1))
535 }
536
537 // Create event mentioning all 5
538 ev := event.New()
539 ev.Pubkey = author.Pub()
540 ev.CreatedAt = timestamp.Now().V
541 ev.Kind = 1
542 ev.Content = []byte("Group mention")
543 tags := tag.NewS()
544 for _, pk := range mentionedPubkeys {
545 tags.Append(tag.NewFromAny("p", pk))
546 }
547 ev.Tags = tags
548 if err := ev.Sign(author); err != nil {
549 t.Fatalf("Failed to sign event: %v", err)
550 }
551
552 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
553 t.Fatalf("Failed to save event: %v", err)
554 }
555
556 // Verify all Tag nodes were created
557 countCypher := `
558 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})
559 RETURN count(t) AS count
560 `
561 result, err := testDB.ExecuteRead(ctx, countCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
562 if err != nil {
563 t.Fatalf("Failed to count p-tag Tags: %v", err)
564 }
565
566 if result.Next(ctx) {
567 count := result.Record().Values[0].(int64)
568 if count != int64(len(mentionedPubkeys)) {
569 t.Errorf("Expected %d p-tag Tag nodes, got %d", len(mentionedPubkeys), count)
570 }
571 }
572
573 // Query for events mentioning any of the pubkeys
574 f := &filter.F{
575 Tags: tag.NewS(tag.NewFromAny("p", mentionedPubkeys[2])), // Query for the 3rd pubkey
576 }
577
578 events, err := testDB.QueryEvents(ctx, f)
579 if err != nil {
580 t.Fatalf("Failed to query with #p filter: %v", err)
581 }
582
583 if len(events) != 1 {
584 t.Errorf("Expected 1 event mentioning pubkey, got %d", len(events))
585 }
586
587 t.Logf("Multiple p-tags correctly stored and queryable")
588 }
589
590 // =============================================================================
591 // CheckForDeleted with Tag Traversal Tests
592 // =============================================================================
593
594 // TestCheckForDeleted_WithTagModel tests that CheckForDeleted works with
595 // the new Tag-based model for e-tag references.
596 func TestCheckForDeleted_WithTagModel(t *testing.T) {
597 if testDB == nil {
598 t.Skip("Neo4j not available")
599 }
600
601 ctx := context.Background()
602 cleanTestDatabase()
603
604 signer, err := p8k.New()
605 if err != nil {
606 t.Fatalf("Failed to create signer: %v", err)
607 }
608 if err := signer.Generate(); err != nil {
609 t.Fatalf("Failed to generate keypair: %v", err)
610 }
611
612 // Create target event
613 targetEvent := event.New()
614 targetEvent.Pubkey = signer.Pub()
615 targetEvent.CreatedAt = timestamp.Now().V
616 targetEvent.Kind = 1
617 targetEvent.Content = []byte("Event to be deleted")
618 if err := targetEvent.Sign(signer); err != nil {
619 t.Fatalf("Failed to sign target event: %v", err)
620 }
621
622 if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
623 t.Fatalf("Failed to save target event: %v", err)
624 }
625
626 targetID := hex.Enc(targetEvent.ID[:])
627
628 // Create kind 5 deletion event with e-tag
629 deleteEvent := event.New()
630 deleteEvent.Pubkey = signer.Pub()
631 deleteEvent.CreatedAt = timestamp.Now().V + 1
632 deleteEvent.Kind = 5
633 deleteEvent.Content = []byte("Deleting my event")
634 deleteEvent.Tags = tag.NewS(
635 tag.NewFromAny("e", targetID),
636 )
637 if err := deleteEvent.Sign(signer); err != nil {
638 t.Fatalf("Failed to sign delete event: %v", err)
639 }
640
641 if _, err := testDB.SaveEvent(ctx, deleteEvent); err != nil {
642 t.Fatalf("Failed to save delete event: %v", err)
643 }
644
645 // Verify the Tag-based traversal exists:
646 // DeleteEvent-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->TargetEvent
647 traversalCypher := `
648 MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target:Event {id: $targetId})
649 RETURN delete.id AS deleteId
650 `
651 result, err := testDB.ExecuteRead(ctx, traversalCypher, map[string]any{"targetId": targetID})
652 if err != nil {
653 t.Fatalf("Failed to query traversal: %v", err)
654 }
655
656 if !result.Next(ctx) {
657 t.Fatal("Expected Tag-based traversal from delete event to target")
658 }
659
660 // Test CheckForDeleted
661 admins := [][]byte{} // No admins, author can delete own events
662 err = testDB.CheckForDeleted(targetEvent, admins)
663
664 if err == nil {
665 t.Error("Expected CheckForDeleted to return error for deleted event")
666 } else if err.Error() != "event has been deleted" {
667 t.Errorf("Unexpected error message: %v", err)
668 }
669
670 t.Logf("CheckForDeleted correctly detects deletion via Tag-based traversal")
671 }
672
673 // TestCheckForDeleted_NotDeleted verifies CheckForDeleted returns nil for
674 // events that haven't been deleted.
675 func TestCheckForDeleted_NotDeleted(t *testing.T) {
676 if testDB == nil {
677 t.Skip("Neo4j not available")
678 }
679
680 ctx := context.Background()
681 cleanTestDatabase()
682
683 signer, err := p8k.New()
684 if err != nil {
685 t.Fatalf("Failed to create signer: %v", err)
686 }
687 if err := signer.Generate(); err != nil {
688 t.Fatalf("Failed to generate keypair: %v", err)
689 }
690
691 // Create event that won't be deleted
692 ev := event.New()
693 ev.Pubkey = signer.Pub()
694 ev.CreatedAt = timestamp.Now().V
695 ev.Kind = 1
696 ev.Content = []byte("Regular event")
697 if err := ev.Sign(signer); err != nil {
698 t.Fatalf("Failed to sign event: %v", err)
699 }
700
701 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
702 t.Fatalf("Failed to save event: %v", err)
703 }
704
705 // CheckForDeleted should return nil
706 admins := [][]byte{}
707 err = testDB.CheckForDeleted(ev, admins)
708
709 if err != nil {
710 t.Errorf("Expected nil for non-deleted event, got: %v", err)
711 }
712
713 t.Logf("CheckForDeleted correctly returns nil for non-deleted event")
714 }
715
716 // =============================================================================
717 // Migration v3 Tests
718 // =============================================================================
719
720 // TestMigrationV3_ConvertDirectReferences tests that the v3 migration
721 // correctly converts direct REFERENCES relationships to Tag-based model.
722 func TestMigrationV3_ConvertDirectReferences(t *testing.T) {
723 if testDB == nil {
724 t.Skip("Neo4j not available")
725 }
726
727 ctx := context.Background()
728 cleanTestDatabase()
729
730 // Manually create old-style direct REFERENCES relationship
731 // (simulating pre-migration data)
732 setupCypher := `
733 // Create two events
734 CREATE (source:Event {
735 id: '1111111111111111111111111111111111111111111111111111111111111111',
736 pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
737 kind: 1,
738 created_at: 1700000000,
739 content: 'Source event',
740 sig: '0000000000000000000000000000000000000000000000000000000000000000' +
741 '0000000000000000000000000000000000000000000000000000000000000000',
742 tags: '[]',
743 serial: 1,
744 expiration: 0
745 })
746 CREATE (target:Event {
747 id: '2222222222222222222222222222222222222222222222222222222222222222',
748 pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
749 kind: 1,
750 created_at: 1699999999,
751 content: 'Target event',
752 sig: '0000000000000000000000000000000000000000000000000000000000000000' +
753 '0000000000000000000000000000000000000000000000000000000000000000',
754 tags: '[]',
755 serial: 2,
756 expiration: 0
757 })
758 // Create old-style direct REFERENCES (pre-migration)
759 CREATE (source)-[:REFERENCES]->(target)
760 `
761
762 if _, err := testDB.ExecuteWrite(ctx, setupCypher, nil); err != nil {
763 t.Fatalf("Failed to setup pre-migration data: %v", err)
764 }
765
766 // Verify old-style relationship exists
767 checkOldCypher := `
768 MATCH (s:Event)-[r:REFERENCES]->(t:Event)
769 WHERE NOT (s)-[:TAGGED_WITH]->(:Tag)
770 RETURN count(r) AS count
771 `
772 result, err := testDB.ExecuteRead(ctx, checkOldCypher, nil)
773 if err != nil {
774 t.Fatalf("Failed to check old relationship: %v", err)
775 }
776
777 var oldCount int64
778 if result.Next(ctx) {
779 oldCount = result.Record().Values[0].(int64)
780 }
781
782 if oldCount == 0 {
783 t.Skip("No old-style REFERENCES to migrate")
784 }
785
786 t.Logf("Found %d old-style REFERENCES to migrate", oldCount)
787
788 // Run migration
789 err = migrateToTagBasedReferences(ctx, testDB)
790 if err != nil {
791 t.Fatalf("Migration failed: %v", err)
792 }
793
794 // Verify old-style relationship was removed
795 result, err = testDB.ExecuteRead(ctx, checkOldCypher, nil)
796 if err != nil {
797 t.Fatalf("Failed to check post-migration: %v", err)
798 }
799
800 if result.Next(ctx) {
801 count := result.Record().Values[0].(int64)
802 if count != 0 {
803 t.Errorf("Expected 0 old-style REFERENCES after migration, got %d", count)
804 }
805 }
806
807 // Verify new Tag-based structure exists
808 checkNewCypher := `
809 MATCH (s:Event {id: '1111111111111111111111111111111111111111111111111111111111111111'})
810 -[:TAGGED_WITH]->(t:Tag {type: 'e'})
811 -[:REFERENCES]->(target:Event {id: '2222222222222222222222222222222222222222222222222222222222222222'})
812 RETURN t.value AS tagValue
813 `
814 result, err = testDB.ExecuteRead(ctx, checkNewCypher, nil)
815 if err != nil {
816 t.Fatalf("Failed to check new structure: %v", err)
817 }
818
819 if !result.Next(ctx) {
820 t.Error("Expected Tag-based structure after migration")
821 } else {
822 tagValue := result.Record().Values[0].(string)
823 expectedValue := "2222222222222222222222222222222222222222222222222222222222222222"
824 if tagValue != expectedValue {
825 t.Errorf("Expected tag value %s, got %s", expectedValue, tagValue)
826 }
827 }
828
829 t.Logf("Migration v3 correctly converted REFERENCES to Tag-based model")
830 }
831
832 // TestMigrationV3_ConvertDirectMentions tests that the v3 migration
833 // correctly converts direct MENTIONS relationships to Tag-based model.
834 func TestMigrationV3_ConvertDirectMentions(t *testing.T) {
835 if testDB == nil {
836 t.Skip("Neo4j not available")
837 }
838
839 ctx := context.Background()
840 cleanTestDatabase()
841
842 // Manually create old-style direct MENTIONS relationship
843 setupCypher := `
844 // Create event and user
845 CREATE (source:Event {
846 id: '3333333333333333333333333333333333333333333333333333333333333333',
847 pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
848 kind: 1,
849 created_at: 1700000000,
850 content: 'Event mentioning user',
851 sig: '0000000000000000000000000000000000000000000000000000000000000000' +
852 '0000000000000000000000000000000000000000000000000000000000000000',
853 tags: '[]',
854 serial: 3,
855 expiration: 0
856 })
857 MERGE (user:NostrUser {pubkey: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'})
858 // Create old-style direct MENTIONS (pre-migration)
859 CREATE (source)-[:MENTIONS]->(user)
860 `
861
862 if _, err := testDB.ExecuteWrite(ctx, setupCypher, nil); err != nil {
863 t.Fatalf("Failed to setup pre-migration data: %v", err)
864 }
865
866 // Verify old-style relationship exists
867 checkOldCypher := `
868 MATCH (e:Event)-[r:MENTIONS]->(u:NostrUser)
869 RETURN count(r) AS count
870 `
871 result, err := testDB.ExecuteRead(ctx, checkOldCypher, nil)
872 if err != nil {
873 t.Fatalf("Failed to check old relationship: %v", err)
874 }
875
876 var oldCount int64
877 if result.Next(ctx) {
878 oldCount = result.Record().Values[0].(int64)
879 }
880
881 if oldCount == 0 {
882 t.Skip("No old-style MENTIONS to migrate")
883 }
884
885 t.Logf("Found %d old-style MENTIONS to migrate", oldCount)
886
887 // Run migration
888 err = migrateToTagBasedReferences(ctx, testDB)
889 if err != nil {
890 t.Fatalf("Migration failed: %v", err)
891 }
892
893 // Verify old-style relationship was removed
894 result, err = testDB.ExecuteRead(ctx, checkOldCypher, nil)
895 if err != nil {
896 t.Fatalf("Failed to check post-migration: %v", err)
897 }
898
899 if result.Next(ctx) {
900 count := result.Record().Values[0].(int64)
901 if count != 0 {
902 t.Errorf("Expected 0 old-style MENTIONS after migration, got %d", count)
903 }
904 }
905
906 // Verify new Tag-based structure exists
907 checkNewCypher := `
908 MATCH (e:Event {id: '3333333333333333333333333333333333333333333333333333333333333333'})
909 -[:TAGGED_WITH]->(t:Tag {type: 'p'})
910 -[:REFERENCES]->(u:NostrUser {pubkey: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'})
911 RETURN t.value AS tagValue
912 `
913 result, err = testDB.ExecuteRead(ctx, checkNewCypher, nil)
914 if err != nil {
915 t.Fatalf("Failed to check new structure: %v", err)
916 }
917
918 if !result.Next(ctx) {
919 t.Error("Expected Tag-based structure after migration")
920 } else {
921 tagValue := result.Record().Values[0].(string)
922 expectedValue := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
923 if tagValue != expectedValue {
924 t.Errorf("Expected tag value %s, got %s", expectedValue, tagValue)
925 }
926 }
927
928 t.Logf("Migration v3 correctly converted MENTIONS to Tag-based model")
929 }
930
931 // TestMigrationV3_Idempotent tests that the v3 migration is idempotent
932 // (safe to run multiple times).
933 func TestMigrationV3_Idempotent(t *testing.T) {
934 if testDB == nil {
935 t.Skip("Neo4j not available")
936 }
937
938 ctx := context.Background()
939 cleanTestDatabase()
940
941 // Create proper Tag-based structure (as if migration already ran)
942 signer, err := p8k.New()
943 if err != nil {
944 t.Fatalf("Failed to create signer: %v", err)
945 }
946 if err := signer.Generate(); err != nil {
947 t.Fatalf("Failed to generate keypair: %v", err)
948 }
949
950 // Create event with e-tag using new model
951 targetEvent := event.New()
952 targetEvent.Pubkey = signer.Pub()
953 targetEvent.CreatedAt = timestamp.Now().V
954 targetEvent.Kind = 1
955 targetEvent.Content = []byte("Target")
956 if err := targetEvent.Sign(signer); err != nil {
957 t.Fatalf("Failed to sign target event: %v", err)
958 }
959
960 if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
961 t.Fatalf("Failed to save target event: %v", err)
962 }
963
964 replyEvent := event.New()
965 replyEvent.Pubkey = signer.Pub()
966 replyEvent.CreatedAt = timestamp.Now().V + 1
967 replyEvent.Kind = 1
968 replyEvent.Content = []byte("Reply")
969 replyEvent.Tags = tag.NewS(
970 tag.NewFromAny("e", hex.Enc(targetEvent.ID[:])),
971 )
972 if err := replyEvent.Sign(signer); err != nil {
973 t.Fatalf("Failed to sign reply event: %v", err)
974 }
975
976 if _, err := testDB.SaveEvent(ctx, replyEvent); err != nil {
977 t.Fatalf("Failed to save reply event: %v", err)
978 }
979
980 // Count Tag nodes before running migration
981 countBefore := countNodes(t, "Tag")
982
983 // Run migration (should be no-op since data is already correct)
984 err = migrateToTagBasedReferences(ctx, testDB)
985 if err != nil {
986 t.Fatalf("Migration failed: %v", err)
987 }
988
989 // Count Tag nodes after - should be unchanged
990 countAfter := countNodes(t, "Tag")
991
992 if countBefore != countAfter {
993 t.Errorf("Migration changed Tag count: before=%d, after=%d", countBefore, countAfter)
994 }
995
996 // Run migration again - should still be idempotent
997 err = migrateToTagBasedReferences(ctx, testDB)
998 if err != nil {
999 t.Fatalf("Second migration run failed: %v", err)
1000 }
1001
1002 countAfterSecond := countNodes(t, "Tag")
1003 if countAfter != countAfterSecond {
1004 t.Errorf("Second migration run changed Tag count: %d -> %d", countAfter, countAfterSecond)
1005 }
1006
1007 t.Logf("Migration v3 is idempotent (safe to run multiple times)")
1008 }
1009
1010 // =============================================================================
1011 // Large Dataset Tests
1012 // =============================================================================
1013
1014 // TestLargeETagBatch tests events with many e-tags are handled correctly.
1015 func TestLargeETagBatch(t *testing.T) {
1016 if testDB == nil {
1017 t.Skip("Neo4j not available")
1018 }
1019
1020 ctx := context.Background()
1021 cleanTestDatabase()
1022
1023 signer, err := p8k.New()
1024 if err != nil {
1025 t.Fatalf("Failed to create signer: %v", err)
1026 }
1027 if err := signer.Generate(); err != nil {
1028 t.Fatalf("Failed to generate keypair: %v", err)
1029 }
1030
1031 // Create 100 target events
1032 numTargets := 100
1033 var targetIDs []string
1034 for i := 0; i < numTargets; i++ {
1035 targetEvent := event.New()
1036 targetEvent.Pubkey = signer.Pub()
1037 targetEvent.CreatedAt = timestamp.Now().V + int64(i)
1038 targetEvent.Kind = 1
1039 targetEvent.Content = []byte(fmt.Sprintf("Target %d", i))
1040 if err := targetEvent.Sign(signer); err != nil {
1041 t.Fatalf("Failed to sign target event %d: %v", i, err)
1042 }
1043
1044 if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
1045 t.Fatalf("Failed to save target event %d: %v", i, err)
1046 }
1047
1048 targetIDs = append(targetIDs, hex.Enc(targetEvent.ID[:]))
1049 }
1050
1051 // Create event referencing all 100 targets
1052 ev := event.New()
1053 ev.Pubkey = signer.Pub()
1054 ev.CreatedAt = timestamp.Now().V + int64(numTargets+1)
1055 ev.Kind = 1
1056 ev.Content = []byte("Event with many e-tags")
1057 tags := tag.NewS()
1058 for _, id := range targetIDs {
1059 tags.Append(tag.NewFromAny("e", id))
1060 }
1061 ev.Tags = tags
1062 if err := ev.Sign(signer); err != nil {
1063 t.Fatalf("Failed to sign event: %v", err)
1064 }
1065
1066 if _, err := testDB.SaveEvent(ctx, ev); err != nil {
1067 t.Fatalf("Failed to save event with %d e-tags: %v", numTargets, err)
1068 }
1069
1070 // Verify all Tag nodes were created
1071 countCypher := `
1072 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})
1073 RETURN count(t) AS count
1074 `
1075 result, err := testDB.ExecuteRead(ctx, countCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
1076 if err != nil {
1077 t.Fatalf("Failed to count e-tag Tags: %v", err)
1078 }
1079
1080 if result.Next(ctx) {
1081 count := result.Record().Values[0].(int64)
1082 if count != int64(numTargets) {
1083 t.Errorf("Expected %d e-tag Tag nodes, got %d", numTargets, count)
1084 }
1085 }
1086
1087 // Verify all REFERENCES were created (since all targets exist)
1088 refCountCypher := `
1089 MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target:Event)
1090 RETURN count(target) AS count
1091 `
1092 refResult, err := testDB.ExecuteRead(ctx, refCountCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
1093 if err != nil {
1094 t.Fatalf("Failed to count REFERENCES: %v", err)
1095 }
1096
1097 if refResult.Next(ctx) {
1098 count := refResult.Record().Values[0].(int64)
1099 if count != int64(numTargets) {
1100 t.Errorf("Expected %d REFERENCES, got %d", numTargets, count)
1101 }
1102 }
1103
1104 t.Logf("Large e-tag batch (%d tags) handled correctly", numTargets)
1105 }
1106