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