social-event-processor_test.go raw
1 //go:build integration
2 // +build integration
3
4 package neo4j
5
6 import (
7 "context"
8 "fmt"
9 "testing"
10
11 "next.orly.dev/pkg/nostr/encoders/event"
12 "next.orly.dev/pkg/nostr/encoders/hex"
13 "next.orly.dev/pkg/nostr/encoders/tag"
14 "next.orly.dev/pkg/nostr/encoders/timestamp"
15 "next.orly.dev/pkg/nostr/interfaces/signer/p8k"
16 )
17
18 // TestSocialEventProcessor tests the social event processor with kinds 0, 3, 1984, 10000
19 // Uses the shared testDB instance from testmain_test.go to avoid auth rate limiting
20 func TestSocialEventProcessor(t *testing.T) {
21 if testDB == nil {
22 t.Skip("Neo4j not available")
23 }
24
25 ctx := context.Background()
26
27 // Clean database for this test
28 cleanTestDatabase()
29
30 // Generate test keypairs
31 alice := generateTestKeypair(t, "alice")
32 bob := generateTestKeypair(t, "bob")
33 charlie := generateTestKeypair(t, "charlie")
34 dave := generateTestKeypair(t, "dave")
35 eve := generateTestKeypair(t, "eve")
36
37 // Use explicit timestamps to avoid same-second timing issues
38 // (Nostr timestamps are in seconds)
39 baseTimestamp := timestamp.Now().V
40
41 t.Run("Kind0_ProfileMetadata", func(t *testing.T) {
42 testProfileMetadata(t, ctx, testDB, alice, baseTimestamp)
43 })
44
45 t.Run("Kind3_ContactList_Initial", func(t *testing.T) {
46 testContactListInitial(t, ctx, testDB, alice, bob, charlie, baseTimestamp+1)
47 })
48
49 t.Run("Kind3_ContactList_Update_AddFollow", func(t *testing.T) {
50 testContactListUpdate(t, ctx, testDB, alice, bob, charlie, dave, baseTimestamp+2)
51 })
52
53 t.Run("Kind3_ContactList_Update_RemoveFollow", func(t *testing.T) {
54 testContactListRemove(t, ctx, testDB, alice, bob, charlie, dave, baseTimestamp+3)
55 })
56
57 t.Run("Kind3_ContactList_OlderEventRejected", func(t *testing.T) {
58 // Use timestamp BEFORE the initial contact list to test rejection
59 testContactListOlderRejected(t, ctx, testDB, alice, bob, baseTimestamp)
60 })
61
62 t.Run("Kind10000_MuteList", func(t *testing.T) {
63 testMuteList(t, ctx, testDB, alice, eve)
64 })
65
66 t.Run("Kind1984_Reports", func(t *testing.T) {
67 testReports(t, ctx, testDB, alice, bob, eve)
68 })
69
70 t.Run("VerifyGraphState", func(t *testing.T) {
71 verifyFinalGraphState(t, ctx, testDB, alice, bob, charlie, dave, eve)
72 })
73 }
74
75 // testProfileMetadata tests kind 0 profile metadata processing
76 func testProfileMetadata(t *testing.T, ctx context.Context, db *N, user testKeypair, ts int64) {
77 // Create profile metadata event
78 ev := event.New()
79 ev.Pubkey = user.pubkey
80 ev.CreatedAt = ts
81 ev.Kind = 0
82 ev.Content = []byte(`{"name":"Alice","about":"Test user","picture":"https://example.com/alice.jpg"}`)
83
84 // Sign event
85 if err := ev.Sign(user.signer); err != nil {
86 t.Fatalf("Failed to sign event: %v", err)
87 }
88
89 // Save event (which triggers social processing)
90 exists, err := db.SaveEvent(ctx, ev)
91 if err != nil {
92 t.Fatalf("Failed to save profile event: %v", err)
93 }
94 if exists {
95 t.Fatal("Event should not exist yet")
96 }
97
98 // Verify NostrUser node was created with profile data
99 cypher := `
100 MATCH (u:NostrUser {pubkey: $pubkey})
101 RETURN u.name AS name, u.about AS about, u.picture AS picture
102 `
103 params := map[string]any{"pubkey": hex.Enc(user.pubkey[:])}
104
105 result, err := db.ExecuteRead(ctx, cypher, params)
106 if err != nil {
107 t.Fatalf("Failed to query NostrUser: %v", err)
108 }
109
110 if !result.Next(ctx) {
111 t.Fatal("NostrUser node not found")
112 }
113
114 record := result.Record()
115 name := record.Values[0].(string)
116 about := record.Values[1].(string)
117 picture := record.Values[2].(string)
118
119 if name != "Alice" {
120 t.Errorf("Expected name 'Alice', got '%s'", name)
121 }
122 if about != "Test user" {
123 t.Errorf("Expected about 'Test user', got '%s'", about)
124 }
125 if picture != "https://example.com/alice.jpg" {
126 t.Errorf("Expected picture URL, got '%s'", picture)
127 }
128
129 t.Logf("✓ Profile metadata processed: name=%s", name)
130 }
131
132 // testContactListInitial tests initial contact list creation
133 func testContactListInitial(t *testing.T, ctx context.Context, db *N, alice, bob, charlie testKeypair, ts int64) {
134 // Alice follows Bob and Charlie
135 ev := event.New()
136 ev.Pubkey = alice.pubkey
137 ev.CreatedAt = ts
138 ev.Kind = 3
139 ev.Tags = tag.NewS(
140 tag.NewFromAny("p", hex.Enc(bob.pubkey[:])),
141 tag.NewFromAny("p", hex.Enc(charlie.pubkey[:])),
142 )
143
144 if err := ev.Sign(alice.signer); err != nil {
145 t.Fatalf("Failed to sign event: %v", err)
146 }
147
148 exists, err := db.SaveEvent(ctx, ev)
149 if err != nil {
150 t.Fatalf("Failed to save contact list: %v", err)
151 }
152 if exists {
153 t.Fatal("Event should not exist yet")
154 }
155
156 // Verify FOLLOWS relationships were created
157 follows := queryFollows(t, ctx, db, alice.pubkey)
158 if len(follows) != 2 {
159 t.Fatalf("Expected 2 follows, got %d", len(follows))
160 }
161
162 expectedFollows := map[string]bool{
163 hex.Enc(bob.pubkey[:]): true,
164 hex.Enc(charlie.pubkey[:]): true,
165 }
166
167 for _, follow := range follows {
168 if !expectedFollows[follow] {
169 t.Errorf("Unexpected follow: %s", follow)
170 }
171 delete(expectedFollows, follow)
172 }
173
174 if len(expectedFollows) > 0 {
175 t.Errorf("Missing follows: %v", expectedFollows)
176 }
177
178 t.Logf("✓ Initial contact list created: Alice follows [Bob, Charlie]")
179 }
180
181 // testContactListUpdate tests adding a follow to existing contact list
182 func testContactListUpdate(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave testKeypair, ts int64) {
183 // Alice now follows Bob, Charlie, and Dave
184 ev := event.New()
185 ev.Pubkey = alice.pubkey
186 ev.CreatedAt = ts
187 ev.Kind = 3
188 ev.Tags = tag.NewS(
189 tag.NewFromAny("p", hex.Enc(bob.pubkey[:])),
190 tag.NewFromAny("p", hex.Enc(charlie.pubkey[:])),
191 tag.NewFromAny("p", hex.Enc(dave.pubkey[:])),
192 )
193
194 if err := ev.Sign(alice.signer); err != nil {
195 t.Fatalf("Failed to sign event: %v", err)
196 }
197
198 exists, err := db.SaveEvent(ctx, ev)
199 if err != nil {
200 t.Fatalf("Failed to save contact list: %v", err)
201 }
202 if exists {
203 t.Fatal("Event should not exist yet")
204 }
205
206 // Verify updated FOLLOWS relationships
207 follows := queryFollows(t, ctx, db, alice.pubkey)
208 if len(follows) != 3 {
209 t.Fatalf("Expected 3 follows, got %d", len(follows))
210 }
211
212 expectedFollows := map[string]bool{
213 hex.Enc(bob.pubkey[:]): true,
214 hex.Enc(charlie.pubkey[:]): true,
215 hex.Enc(dave.pubkey[:]): true,
216 }
217
218 for _, follow := range follows {
219 if !expectedFollows[follow] {
220 t.Errorf("Unexpected follow: %s", follow)
221 }
222 delete(expectedFollows, follow)
223 }
224
225 if len(expectedFollows) > 0 {
226 t.Errorf("Missing follows: %v", expectedFollows)
227 }
228
229 t.Logf("✓ Contact list updated: Alice follows [Bob, Charlie, Dave]")
230 }
231
232 // testContactListRemove tests removing a follow from contact list
233 func testContactListRemove(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave testKeypair, ts int64) {
234 // Alice unfollows Charlie, keeps Bob and Dave
235 ev := event.New()
236 ev.Pubkey = alice.pubkey
237 ev.CreatedAt = ts
238 ev.Kind = 3
239 ev.Tags = tag.NewS(
240 tag.NewFromAny("p", hex.Enc(bob.pubkey[:])),
241 tag.NewFromAny("p", hex.Enc(dave.pubkey[:])),
242 )
243
244 if err := ev.Sign(alice.signer); err != nil {
245 t.Fatalf("Failed to sign event: %v", err)
246 }
247
248 exists, err := db.SaveEvent(ctx, ev)
249 if err != nil {
250 t.Fatalf("Failed to save contact list: %v", err)
251 }
252 if exists {
253 t.Fatal("Event should not exist yet")
254 }
255
256 // Verify Charlie was removed
257 follows := queryFollows(t, ctx, db, alice.pubkey)
258 if len(follows) != 2 {
259 t.Fatalf("Expected 2 follows after removal, got %d", len(follows))
260 }
261
262 expectedFollows := map[string]bool{
263 hex.Enc(bob.pubkey[:]): true,
264 hex.Enc(dave.pubkey[:]): true,
265 }
266
267 for _, follow := range follows {
268 if !expectedFollows[follow] {
269 t.Errorf("Unexpected follow: %s", follow)
270 }
271 if follow == hex.Enc(charlie.pubkey[:]) {
272 t.Error("Charlie should have been unfollowed")
273 }
274 delete(expectedFollows, follow)
275 }
276
277 t.Logf("✓ Contact list updated: Alice unfollowed Charlie")
278 }
279
280 // testContactListOlderRejected tests that older events are rejected
281 func testContactListOlderRejected(t *testing.T, ctx context.Context, db *N, alice, bob testKeypair, ts int64) {
282 // Try to save an old contact list (timestamp is older than the existing one)
283 ev := event.New()
284 ev.Pubkey = alice.pubkey
285 ev.CreatedAt = ts // This is baseTimestamp, which is older than the current contact list
286 ev.Kind = 3
287 ev.Tags = tag.NewS(
288 tag.NewFromAny("p", hex.Enc(bob.pubkey[:])),
289 )
290
291 if err := ev.Sign(alice.signer); err != nil {
292 t.Fatalf("Failed to sign event: %v", err)
293 }
294
295 // Save should succeed (base event stored), but social processing should skip it
296 _, err := db.SaveEvent(ctx, ev)
297 if err != nil {
298 t.Fatalf("Failed to save event: %v", err)
299 }
300
301 // Verify follows list unchanged (should still be Bob and Dave from previous test)
302 follows := queryFollows(t, ctx, db, alice.pubkey)
303 if len(follows) != 2 {
304 t.Fatalf("Expected follows list unchanged, got %d follows", len(follows))
305 }
306
307 t.Logf("✓ Older contact list event rejected (follows unchanged)")
308 }
309
310 // testMuteList tests kind 10000 mute list processing
311 func testMuteList(t *testing.T, ctx context.Context, db *N, alice, eve testKeypair) {
312 // Alice mutes Eve
313 ev := event.New()
314 ev.Pubkey = alice.pubkey
315 ev.CreatedAt = timestamp.Now().V
316 ev.Kind = 10000
317 ev.Tags = tag.NewS(
318 tag.NewFromAny("p", hex.Enc(eve.pubkey[:])),
319 )
320
321 if err := ev.Sign(alice.signer); err != nil {
322 t.Fatalf("Failed to sign event: %v", err)
323 }
324
325 exists, err := db.SaveEvent(ctx, ev)
326 if err != nil {
327 t.Fatalf("Failed to save mute list: %v", err)
328 }
329 if exists {
330 t.Fatal("Event should not exist yet")
331 }
332
333 // Verify MUTES relationship was created
334 mutes := queryMutes(t, ctx, db, alice.pubkey)
335 if len(mutes) != 1 {
336 t.Fatalf("Expected 1 mute, got %d", len(mutes))
337 }
338
339 if mutes[0] != hex.Enc(eve.pubkey[:]) {
340 t.Errorf("Expected to mute Eve, got %s", mutes[0])
341 }
342
343 t.Logf("✓ Mute list processed: Alice mutes Eve")
344 }
345
346 // testReports tests kind 1984 report processing
347 func testReports(t *testing.T, ctx context.Context, db *N, alice, bob, eve testKeypair) {
348 // Alice reports Eve for spam
349 ev1 := event.New()
350 ev1.Pubkey = alice.pubkey
351 ev1.CreatedAt = timestamp.Now().V
352 ev1.Kind = 1984
353 ev1.Tags = tag.NewS(
354 tag.NewFromAny("p", hex.Enc(eve.pubkey[:]), "spam"),
355 )
356 ev1.Content = []byte("Spamming the relay")
357
358 if err := ev1.Sign(alice.signer); err != nil {
359 t.Fatalf("Failed to sign event: %v", err)
360 }
361
362 if _, err := db.SaveEvent(ctx, ev1); err != nil {
363 t.Fatalf("Failed to save report: %v", err)
364 }
365
366 // Bob also reports Eve for illegal content
367 ev2 := event.New()
368 ev2.Pubkey = bob.pubkey
369 ev2.CreatedAt = timestamp.Now().V
370 ev2.Kind = 1984
371 ev2.Tags = tag.NewS(
372 tag.NewFromAny("p", hex.Enc(eve.pubkey[:]), "illegal"),
373 )
374
375 if err := ev2.Sign(bob.signer); err != nil {
376 t.Fatalf("Failed to sign event: %v", err)
377 }
378
379 if _, err := db.SaveEvent(ctx, ev2); err != nil {
380 t.Fatalf("Failed to save report: %v", err)
381 }
382
383 // Verify REPORTS relationships were created
384 reports := queryReports(t, ctx, db, eve.pubkey)
385 if len(reports) != 2 {
386 t.Fatalf("Expected 2 reports against Eve, got %d", len(reports))
387 }
388
389 // Check report types
390 reportTypes := make(map[string]int)
391 for _, report := range reports {
392 reportTypes[report.ReportType]++
393 }
394
395 if reportTypes["spam"] != 1 {
396 t.Errorf("Expected 1 spam report, got %d", reportTypes["spam"])
397 }
398 if reportTypes["illegal"] != 1 {
399 t.Errorf("Expected 1 illegal report, got %d", reportTypes["illegal"])
400 }
401
402 t.Logf("✓ Reports processed: Eve reported by Alice (spam) and Bob (illegal)")
403 }
404
405 // verifyFinalGraphState verifies the complete graph state
406 func verifyFinalGraphState(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave, eve testKeypair) {
407 t.Log("Verifying final graph state...")
408
409 // Verify Alice's follows: Bob and Dave (Charlie removed)
410 follows := queryFollows(t, ctx, db, alice.pubkey)
411 if len(follows) != 2 {
412 t.Errorf("Expected Alice to follow 2 users, got %d", len(follows))
413 }
414
415 // Verify Alice's mutes: Eve
416 mutes := queryMutes(t, ctx, db, alice.pubkey)
417 if len(mutes) != 1 {
418 t.Errorf("Expected Alice to mute 1 user, got %d", len(mutes))
419 }
420
421 // Verify reports against Eve
422 reports := queryReports(t, ctx, db, eve.pubkey)
423 if len(reports) != 2 {
424 t.Errorf("Expected 2 reports against Eve, got %d", len(reports))
425 }
426
427 // Verify event traceability - all relationships should have created_by_event
428 cypher := `
429 MATCH ()-[r:FOLLOWS|MUTES|REPORTS]->()
430 WHERE r.created_by_event IS NULL
431 RETURN count(r) AS count
432 `
433 result, err := db.ExecuteRead(ctx, cypher, nil)
434 if err != nil {
435 t.Fatalf("Failed to check traceability: %v", err)
436 }
437
438 if result.Next(ctx) {
439 count := result.Record().Values[0].(int64)
440 if count > 0 {
441 t.Errorf("Found %d relationships without created_by_event", count)
442 }
443 }
444
445 t.Log("✓ Final graph state verified")
446 t.Logf(" - Alice follows: %v", follows)
447 t.Logf(" - Alice mutes: %v", mutes)
448 t.Logf(" - Reports against Eve: %d", len(reports))
449 }
450
451 // Helper types and functions
452
453 type testKeypair struct {
454 pubkey []byte
455 signer *p8k.Signer
456 }
457
458 type reportInfo struct {
459 Reporter string
460 ReportType string
461 }
462
463 func generateTestKeypair(t *testing.T, name string) testKeypair {
464 t.Helper()
465
466 signer, err := p8k.New()
467 if err != nil {
468 t.Fatalf("Failed to create signer for %s: %v", name, err)
469 }
470
471 if err := signer.Generate(); err != nil {
472 t.Fatalf("Failed to generate keypair for %s: %v", name, err)
473 }
474
475 return testKeypair{
476 pubkey: signer.Pub(),
477 signer: signer,
478 }
479 }
480
481 func queryFollows(t *testing.T, ctx context.Context, db *N, pubkey []byte) []string {
482 t.Helper()
483
484 cypher := `
485 MATCH (user:NostrUser {pubkey: $pubkey})-[f:FOLLOWS]->(followed:NostrUser)
486 WHERE NOT EXISTS {
487 MATCH (old:ProcessedSocialEvent {event_id: f.created_by_event})
488 WHERE old.superseded_by IS NOT NULL
489 }
490 RETURN followed.pubkey AS pubkey
491 `
492 params := map[string]any{"pubkey": hex.Enc(pubkey)}
493
494 result, err := db.ExecuteRead(ctx, cypher, params)
495 if err != nil {
496 t.Fatalf("Failed to query follows: %v", err)
497 }
498
499 var follows []string
500 for result.Next(ctx) {
501 follows = append(follows, result.Record().Values[0].(string))
502 }
503
504 return follows
505 }
506
507 func queryMutes(t *testing.T, ctx context.Context, db *N, pubkey []byte) []string {
508 t.Helper()
509
510 cypher := `
511 MATCH (user:NostrUser {pubkey: $pubkey})-[m:MUTES]->(muted:NostrUser)
512 WHERE NOT EXISTS {
513 MATCH (old:ProcessedSocialEvent {event_id: m.created_by_event})
514 WHERE old.superseded_by IS NOT NULL
515 }
516 RETURN muted.pubkey AS pubkey
517 `
518 params := map[string]any{"pubkey": hex.Enc(pubkey)}
519
520 result, err := db.ExecuteRead(ctx, cypher, params)
521 if err != nil {
522 t.Fatalf("Failed to query mutes: %v", err)
523 }
524
525 var mutes []string
526 for result.Next(ctx) {
527 mutes = append(mutes, result.Record().Values[0].(string))
528 }
529
530 return mutes
531 }
532
533 func queryReports(t *testing.T, ctx context.Context, db *N, pubkey []byte) []reportInfo {
534 t.Helper()
535
536 cypher := `
537 MATCH (reporter:NostrUser)-[r:REPORTS]->(reported:NostrUser {pubkey: $pubkey})
538 RETURN reporter.pubkey AS reporter, r.report_type AS report_type
539 `
540 params := map[string]any{"pubkey": hex.Enc(pubkey)}
541
542 result, err := db.ExecuteRead(ctx, cypher, params)
543 if err != nil {
544 t.Fatalf("Failed to query reports: %v", err)
545 }
546
547 var reports []reportInfo
548 for result.Next(ctx) {
549 record := result.Record()
550 reports = append(reports, reportInfo{
551 Reporter: record.Values[0].(string),
552 ReportType: record.Values[1].(string),
553 })
554 }
555
556 return reports
557 }
558
559 // TestDiffComputation tests the diff computation helper function
560 func TestDiffComputation(t *testing.T) {
561 tests := []struct {
562 name string
563 old []string
564 new []string
565 expectAdded []string
566 expectRemoved []string
567 }{
568 {
569 name: "Empty to non-empty",
570 old: []string{},
571 new: []string{"a", "b", "c"},
572 expectAdded: []string{"a", "b", "c"},
573 expectRemoved: []string{},
574 },
575 {
576 name: "Non-empty to empty",
577 old: []string{"a", "b", "c"},
578 new: []string{},
579 expectAdded: []string{},
580 expectRemoved: []string{"a", "b", "c"},
581 },
582 {
583 name: "No changes",
584 old: []string{"a", "b", "c"},
585 new: []string{"a", "b", "c"},
586 expectAdded: []string{},
587 expectRemoved: []string{},
588 },
589 {
590 name: "Add some, remove some",
591 old: []string{"a", "b", "c"},
592 new: []string{"b", "c", "d", "e"},
593 expectAdded: []string{"d", "e"},
594 expectRemoved: []string{"a"},
595 },
596 {
597 name: "All different",
598 old: []string{"a", "b", "c"},
599 new: []string{"d", "e", "f"},
600 expectAdded: []string{"d", "e", "f"},
601 expectRemoved: []string{"a", "b", "c"},
602 },
603 }
604
605 for _, tt := range tests {
606 t.Run(tt.name, func(t *testing.T) {
607 added, removed := diffStringSlices(tt.old, tt.new)
608
609 if !slicesEqual(added, tt.expectAdded) {
610 t.Errorf("Added mismatch:\n got: %v\n expected: %v", added, tt.expectAdded)
611 }
612
613 if !slicesEqual(removed, tt.expectRemoved) {
614 t.Errorf("Removed mismatch:\n got: %v\n expected: %v", removed, tt.expectRemoved)
615 }
616 })
617 }
618 }
619
620 // slicesEqual checks if two string slices contain the same elements (order doesn't matter)
621 func slicesEqual(a, b []string) bool {
622 if len(a) != len(b) {
623 return false
624 }
625
626 aMap := make(map[string]int)
627 for _, s := range a {
628 aMap[s]++
629 }
630
631 bMap := make(map[string]int)
632 for _, s := range b {
633 bMap[s]++
634 }
635
636 for k, v := range aMap {
637 if bMap[k] != v {
638 return false
639 }
640 }
641
642 return true
643 }
644
645 // TestExtractPTags tests the p-tag extraction helper function
646 func TestExtractPTags(t *testing.T) {
647 // Valid 64-character hex pubkeys for testing
648 pk1 := "0000000000000000000000000000000000000000000000000000000000000001"
649 pk2 := "0000000000000000000000000000000000000000000000000000000000000002"
650 pk3 := "0000000000000000000000000000000000000000000000000000000000000003"
651
652 tests := []struct {
653 name string
654 tags *tag.S
655 expected []string
656 }{
657 {
658 name: "No tags",
659 tags: &tag.S{},
660 expected: []string{},
661 },
662 {
663 name: "Only p-tags",
664 tags: tag.NewS(
665 tag.NewFromAny("p", pk1),
666 tag.NewFromAny("p", pk2),
667 tag.NewFromAny("p", pk3),
668 ),
669 expected: []string{pk1, pk2, pk3},
670 },
671 {
672 name: "Mixed tags",
673 tags: tag.NewS(
674 tag.NewFromAny("p", pk1),
675 tag.NewFromAny("e", "event1"),
676 tag.NewFromAny("p", pk2),
677 tag.NewFromAny("t", "hashtag"),
678 ),
679 expected: []string{pk1, pk2},
680 },
681 {
682 name: "Duplicate p-tags",
683 tags: tag.NewS(
684 tag.NewFromAny("p", pk1),
685 tag.NewFromAny("p", pk1),
686 tag.NewFromAny("p", pk2),
687 ),
688 expected: []string{pk1, pk2},
689 },
690 {
691 name: "Invalid p-tags (too short)",
692 tags: tag.NewS(
693 tag.NewFromAny("p"),
694 tag.NewFromAny("p", "tooshort"),
695 ),
696 expected: []string{},
697 },
698 }
699
700 for _, tt := range tests {
701 t.Run(tt.name, func(t *testing.T) {
702 ev := event.New()
703 ev.Tags = tt.tags
704
705 result := extractPTags(ev)
706
707 if !slicesEqual(result, tt.expected) {
708 t.Errorf("Extracted p-tags mismatch:\n got: %v\n expected: %v", result, tt.expected)
709 }
710 })
711 }
712 }
713
714 // Benchmark tests
715 func BenchmarkDiffComputation(b *testing.B) {
716 old := make([]string, 1000)
717 new := make([]string, 1000)
718
719 for i := 0; i < 800; i++ {
720 old[i] = fmt.Sprintf("pubkey%d", i)
721 new[i] = fmt.Sprintf("pubkey%d", i)
722 }
723
724 // 200 removed from old
725 for i := 800; i < 1000; i++ {
726 old[i] = fmt.Sprintf("oldpubkey%d", i)
727 }
728
729 // 200 added to new
730 for i := 800; i < 1000; i++ {
731 new[i] = fmt.Sprintf("newpubkey%d", i)
732 }
733
734 b.ResetTimer()
735
736 for i := 0; i < b.N; i++ {
737 _, _ = diffStringSlices(old, new)
738 }
739 }
740
741 // TestReportDeduplication tests that duplicate REPORTS are deduplicated
742 func TestReportDeduplication(t *testing.T) {
743 if testDB == nil {
744 t.Skip("Neo4j not available")
745 }
746
747 ctx := context.Background()
748
749 t.Run("DeduplicateSameType", func(t *testing.T) {
750 // Clean database for this subtest
751 cleanTestDatabase()
752
753 reporter := generateTestKeypair(t, "reporter")
754 reported := generateTestKeypair(t, "reported")
755
756 reporterPubkey := hex.Enc(reporter.pubkey[:])
757 reportedPubkey := hex.Enc(reported.pubkey[:])
758
759 // Create first report (older timestamp)
760 ev1 := event.New()
761 ev1.Pubkey = reporter.pubkey
762 ev1.CreatedAt = 1000
763 ev1.Kind = 1984
764 ev1.Tags = tag.NewS(
765 tag.NewFromAny("p", reportedPubkey, "impersonation"),
766 )
767 ev1.Content = []byte("First report")
768
769 if err := ev1.Sign(reporter.signer); err != nil {
770 t.Fatalf("Failed to sign first event: %v", err)
771 }
772
773 if _, err := testDB.SaveEvent(ctx, ev1); err != nil {
774 t.Fatalf("Failed to save first report: %v", err)
775 }
776
777 // Create second report (newer timestamp, same type)
778 ev2 := event.New()
779 ev2.Pubkey = reporter.pubkey
780 ev2.CreatedAt = 2000 // Newer timestamp
781 ev2.Kind = 1984
782 ev2.Tags = tag.NewS(
783 tag.NewFromAny("p", reportedPubkey, "impersonation"),
784 )
785 ev2.Content = []byte("Second report")
786
787 if err := ev2.Sign(reporter.signer); err != nil {
788 t.Fatalf("Failed to sign second event: %v", err)
789 }
790
791 if _, err := testDB.SaveEvent(ctx, ev2); err != nil {
792 t.Fatalf("Failed to save second report: %v", err)
793 }
794
795 // Verify only ONE REPORTS relationship exists
796 cypher := `
797 MATCH (r:NostrUser {pubkey: $reporter})-[rel:REPORTS]->(d:NostrUser {pubkey: $reported})
798 RETURN count(rel) AS count, rel.created_at AS created_at, rel.created_by_event AS event_id
799 `
800 params := map[string]any{
801 "reporter": reporterPubkey,
802 "reported": reportedPubkey,
803 }
804
805 result, err := testDB.ExecuteRead(ctx, cypher, params)
806 if err != nil {
807 t.Fatalf("Failed to query REPORTS: %v", err)
808 }
809
810 if !result.Next(ctx) {
811 t.Fatal("No REPORTS relationship found")
812 }
813
814 record := result.Record()
815 count := record.Values[0].(int64)
816 createdAt := record.Values[1].(int64)
817 eventID := record.Values[2].(string)
818
819 if count != 1 {
820 t.Errorf("Expected 1 REPORTS relationship, got %d", count)
821 }
822
823 // Verify the relationship has the newer event's data
824 if createdAt != 2000 {
825 t.Errorf("Expected created_at=2000 (newer), got %d", createdAt)
826 }
827
828 ev2ID := hex.Enc(ev2.ID[:])
829 if eventID != ev2ID {
830 t.Errorf("Expected event_id=%s, got %s", ev2ID, eventID)
831 }
832
833 t.Log("✓ Duplicate reports correctly deduplicated to single relationship with newest data")
834 })
835
836 t.Run("DifferentTypesAllowed", func(t *testing.T) {
837 // Clean database for this subtest
838 cleanTestDatabase()
839
840 reporter := generateTestKeypair(t, "reporter2")
841 reported := generateTestKeypair(t, "reported2")
842
843 reporterPubkey := hex.Enc(reporter.pubkey[:])
844 reportedPubkey := hex.Enc(reported.pubkey[:])
845
846 // Report for impersonation
847 ev1 := event.New()
848 ev1.Pubkey = reporter.pubkey
849 ev1.CreatedAt = 1000
850 ev1.Kind = 1984
851 ev1.Tags = tag.NewS(
852 tag.NewFromAny("p", reportedPubkey, "impersonation"),
853 )
854
855 if err := ev1.Sign(reporter.signer); err != nil {
856 t.Fatalf("Failed to sign event: %v", err)
857 }
858
859 if _, err := testDB.SaveEvent(ctx, ev1); err != nil {
860 t.Fatalf("Failed to save report: %v", err)
861 }
862
863 // Report for spam (different type)
864 ev2 := event.New()
865 ev2.Pubkey = reporter.pubkey
866 ev2.CreatedAt = 2000
867 ev2.Kind = 1984
868 ev2.Tags = tag.NewS(
869 tag.NewFromAny("p", reportedPubkey, "spam"),
870 )
871
872 if err := ev2.Sign(reporter.signer); err != nil {
873 t.Fatalf("Failed to sign event: %v", err)
874 }
875
876 if _, err := testDB.SaveEvent(ctx, ev2); err != nil {
877 t.Fatalf("Failed to save report: %v", err)
878 }
879
880 // Verify TWO REPORTS relationships exist (different types)
881 cypher := `
882 MATCH (r:NostrUser {pubkey: $reporter})-[rel:REPORTS]->(d:NostrUser {pubkey: $reported})
883 RETURN rel.report_type AS type ORDER BY type
884 `
885 params := map[string]any{
886 "reporter": reporterPubkey,
887 "reported": reportedPubkey,
888 }
889
890 result, err := testDB.ExecuteRead(ctx, cypher, params)
891 if err != nil {
892 t.Fatalf("Failed to query REPORTS: %v", err)
893 }
894
895 var types []string
896 for result.Next(ctx) {
897 types = append(types, result.Record().Values[0].(string))
898 }
899
900 if len(types) != 2 {
901 t.Errorf("Expected 2 REPORTS relationships, got %d", len(types))
902 }
903
904 if len(types) >= 2 && (types[0] != "impersonation" || types[1] != "spam") {
905 t.Errorf("Expected [impersonation, spam], got %v", types)
906 }
907
908 t.Log("✓ Different report types correctly create separate relationships")
909 })
910
911 t.Run("SupersededEventTracking", func(t *testing.T) {
912 // Clean database for this subtest
913 cleanTestDatabase()
914
915 reporter := generateTestKeypair(t, "reporter3")
916 reported := generateTestKeypair(t, "reported3")
917
918 reporterPubkey := hex.Enc(reporter.pubkey[:])
919 reportedPubkey := hex.Enc(reported.pubkey[:])
920
921 // Create first report
922 ev1 := event.New()
923 ev1.Pubkey = reporter.pubkey
924 ev1.CreatedAt = 1000
925 ev1.Kind = 1984
926 ev1.Tags = tag.NewS(
927 tag.NewFromAny("p", reportedPubkey, "spam"),
928 )
929
930 if err := ev1.Sign(reporter.signer); err != nil {
931 t.Fatalf("Failed to sign first event: %v", err)
932 }
933
934 if _, err := testDB.SaveEvent(ctx, ev1); err != nil {
935 t.Fatalf("Failed to save first report: %v", err)
936 }
937
938 ev1ID := hex.Enc(ev1.ID[:])
939
940 // Create second report (supersedes first)
941 ev2 := event.New()
942 ev2.Pubkey = reporter.pubkey
943 ev2.CreatedAt = 2000
944 ev2.Kind = 1984
945 ev2.Tags = tag.NewS(
946 tag.NewFromAny("p", reportedPubkey, "spam"),
947 )
948
949 if err := ev2.Sign(reporter.signer); err != nil {
950 t.Fatalf("Failed to sign second event: %v", err)
951 }
952
953 if _, err := testDB.SaveEvent(ctx, ev2); err != nil {
954 t.Fatalf("Failed to save second report: %v", err)
955 }
956
957 ev2ID := hex.Enc(ev2.ID[:])
958
959 // Verify first ProcessedSocialEvent is superseded
960 cypher := `
961 MATCH (evt:ProcessedSocialEvent {event_id: $event_id, event_kind: 1984})
962 RETURN evt.superseded_by AS superseded_by
963 `
964 params := map[string]any{"event_id": ev1ID}
965
966 result, err := testDB.ExecuteRead(ctx, cypher, params)
967 if err != nil {
968 t.Fatalf("Failed to query ProcessedSocialEvent: %v", err)
969 }
970
971 if !result.Next(ctx) {
972 t.Fatal("First ProcessedSocialEvent not found")
973 }
974
975 supersededBy := result.Record().Values[0]
976 if supersededBy == nil {
977 t.Error("Expected first event to be superseded, but superseded_by is null")
978 } else if supersededBy.(string) != ev2ID {
979 t.Errorf("Expected superseded_by=%s, got %v", ev2ID, supersededBy)
980 }
981
982 // Verify second ProcessedSocialEvent is NOT superseded
983 params = map[string]any{"event_id": ev2ID}
984 result, err = testDB.ExecuteRead(ctx, cypher, params)
985 if err != nil {
986 t.Fatalf("Failed to query second ProcessedSocialEvent: %v", err)
987 }
988
989 if !result.Next(ctx) {
990 t.Fatal("Second ProcessedSocialEvent not found")
991 }
992
993 supersededBy = result.Record().Values[0]
994 if supersededBy != nil {
995 t.Errorf("Expected second event not to be superseded, but superseded_by=%v", supersededBy)
996 }
997
998 t.Log("✓ ProcessedSocialEvent correctly tracks superseded events")
999 })
1000 }
1001