You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
739 lines
19 KiB
739 lines
19 KiB
//go:build integration |
|
// +build integration |
|
|
|
package neo4j |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"testing" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/encoders/timestamp" |
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
|
) |
|
|
|
// TestSocialEventProcessor tests the social event processor with kinds 0, 3, 1984, 10000 |
|
// Uses the shared testDB instance from testmain_test.go to avoid auth rate limiting |
|
func TestSocialEventProcessor(t *testing.T) { |
|
if testDB == nil { |
|
t.Skip("Neo4j not available") |
|
} |
|
|
|
ctx := context.Background() |
|
|
|
// Clean database for this test |
|
cleanTestDatabase() |
|
|
|
// Generate test keypairs |
|
alice := generateTestKeypair(t, "alice") |
|
bob := generateTestKeypair(t, "bob") |
|
charlie := generateTestKeypair(t, "charlie") |
|
dave := generateTestKeypair(t, "dave") |
|
eve := generateTestKeypair(t, "eve") |
|
|
|
// Use explicit timestamps to avoid same-second timing issues |
|
// (Nostr timestamps are in seconds) |
|
baseTimestamp := timestamp.Now().V |
|
|
|
t.Run("Kind0_ProfileMetadata", func(t *testing.T) { |
|
testProfileMetadata(t, ctx, testDB, alice, baseTimestamp) |
|
}) |
|
|
|
t.Run("Kind3_ContactList_Initial", func(t *testing.T) { |
|
testContactListInitial(t, ctx, testDB, alice, bob, charlie, baseTimestamp+1) |
|
}) |
|
|
|
t.Run("Kind3_ContactList_Update_AddFollow", func(t *testing.T) { |
|
testContactListUpdate(t, ctx, testDB, alice, bob, charlie, dave, baseTimestamp+2) |
|
}) |
|
|
|
t.Run("Kind3_ContactList_Update_RemoveFollow", func(t *testing.T) { |
|
testContactListRemove(t, ctx, testDB, alice, bob, charlie, dave, baseTimestamp+3) |
|
}) |
|
|
|
t.Run("Kind3_ContactList_OlderEventRejected", func(t *testing.T) { |
|
// Use timestamp BEFORE the initial contact list to test rejection |
|
testContactListOlderRejected(t, ctx, testDB, alice, bob, baseTimestamp) |
|
}) |
|
|
|
t.Run("Kind10000_MuteList", func(t *testing.T) { |
|
testMuteList(t, ctx, testDB, alice, eve) |
|
}) |
|
|
|
t.Run("Kind1984_Reports", func(t *testing.T) { |
|
testReports(t, ctx, testDB, alice, bob, eve) |
|
}) |
|
|
|
t.Run("VerifyGraphState", func(t *testing.T) { |
|
verifyFinalGraphState(t, ctx, testDB, alice, bob, charlie, dave, eve) |
|
}) |
|
} |
|
|
|
// testProfileMetadata tests kind 0 profile metadata processing |
|
func testProfileMetadata(t *testing.T, ctx context.Context, db *N, user testKeypair, ts int64) { |
|
// Create profile metadata event |
|
ev := event.New() |
|
ev.Pubkey = user.pubkey |
|
ev.CreatedAt = ts |
|
ev.Kind = 0 |
|
ev.Content = []byte(`{"name":"Alice","about":"Test user","picture":"https://example.com/alice.jpg"}`) |
|
|
|
// Sign event |
|
if err := ev.Sign(user.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
// Save event (which triggers social processing) |
|
exists, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save profile event: %v", err) |
|
} |
|
if exists { |
|
t.Fatal("Event should not exist yet") |
|
} |
|
|
|
// Verify NostrUser node was created with profile data |
|
cypher := ` |
|
MATCH (u:NostrUser {pubkey: $pubkey}) |
|
RETURN u.name AS name, u.about AS about, u.picture AS picture |
|
` |
|
params := map[string]any{"pubkey": hex.Enc(user.pubkey[:])} |
|
|
|
result, err := db.ExecuteRead(ctx, cypher, params) |
|
if err != nil { |
|
t.Fatalf("Failed to query NostrUser: %v", err) |
|
} |
|
|
|
if !result.Next(ctx) { |
|
t.Fatal("NostrUser node not found") |
|
} |
|
|
|
record := result.Record() |
|
name := record.Values[0].(string) |
|
about := record.Values[1].(string) |
|
picture := record.Values[2].(string) |
|
|
|
if name != "Alice" { |
|
t.Errorf("Expected name 'Alice', got '%s'", name) |
|
} |
|
if about != "Test user" { |
|
t.Errorf("Expected about 'Test user', got '%s'", about) |
|
} |
|
if picture != "https://example.com/alice.jpg" { |
|
t.Errorf("Expected picture URL, got '%s'", picture) |
|
} |
|
|
|
t.Logf("✓ Profile metadata processed: name=%s", name) |
|
} |
|
|
|
// testContactListInitial tests initial contact list creation |
|
func testContactListInitial(t *testing.T, ctx context.Context, db *N, alice, bob, charlie testKeypair, ts int64) { |
|
// Alice follows Bob and Charlie |
|
ev := event.New() |
|
ev.Pubkey = alice.pubkey |
|
ev.CreatedAt = ts |
|
ev.Kind = 3 |
|
ev.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(bob.pubkey[:])), |
|
tag.NewFromAny("p", hex.Enc(charlie.pubkey[:])), |
|
) |
|
|
|
if err := ev.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
exists, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save contact list: %v", err) |
|
} |
|
if exists { |
|
t.Fatal("Event should not exist yet") |
|
} |
|
|
|
// Verify FOLLOWS relationships were created |
|
follows := queryFollows(t, ctx, db, alice.pubkey) |
|
if len(follows) != 2 { |
|
t.Fatalf("Expected 2 follows, got %d", len(follows)) |
|
} |
|
|
|
expectedFollows := map[string]bool{ |
|
hex.Enc(bob.pubkey[:]): true, |
|
hex.Enc(charlie.pubkey[:]): true, |
|
} |
|
|
|
for _, follow := range follows { |
|
if !expectedFollows[follow] { |
|
t.Errorf("Unexpected follow: %s", follow) |
|
} |
|
delete(expectedFollows, follow) |
|
} |
|
|
|
if len(expectedFollows) > 0 { |
|
t.Errorf("Missing follows: %v", expectedFollows) |
|
} |
|
|
|
t.Logf("✓ Initial contact list created: Alice follows [Bob, Charlie]") |
|
} |
|
|
|
// testContactListUpdate tests adding a follow to existing contact list |
|
func testContactListUpdate(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave testKeypair, ts int64) { |
|
// Alice now follows Bob, Charlie, and Dave |
|
ev := event.New() |
|
ev.Pubkey = alice.pubkey |
|
ev.CreatedAt = ts |
|
ev.Kind = 3 |
|
ev.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(bob.pubkey[:])), |
|
tag.NewFromAny("p", hex.Enc(charlie.pubkey[:])), |
|
tag.NewFromAny("p", hex.Enc(dave.pubkey[:])), |
|
) |
|
|
|
if err := ev.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
exists, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save contact list: %v", err) |
|
} |
|
if exists { |
|
t.Fatal("Event should not exist yet") |
|
} |
|
|
|
// Verify updated FOLLOWS relationships |
|
follows := queryFollows(t, ctx, db, alice.pubkey) |
|
if len(follows) != 3 { |
|
t.Fatalf("Expected 3 follows, got %d", len(follows)) |
|
} |
|
|
|
expectedFollows := map[string]bool{ |
|
hex.Enc(bob.pubkey[:]): true, |
|
hex.Enc(charlie.pubkey[:]): true, |
|
hex.Enc(dave.pubkey[:]): true, |
|
} |
|
|
|
for _, follow := range follows { |
|
if !expectedFollows[follow] { |
|
t.Errorf("Unexpected follow: %s", follow) |
|
} |
|
delete(expectedFollows, follow) |
|
} |
|
|
|
if len(expectedFollows) > 0 { |
|
t.Errorf("Missing follows: %v", expectedFollows) |
|
} |
|
|
|
t.Logf("✓ Contact list updated: Alice follows [Bob, Charlie, Dave]") |
|
} |
|
|
|
// testContactListRemove tests removing a follow from contact list |
|
func testContactListRemove(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave testKeypair, ts int64) { |
|
// Alice unfollows Charlie, keeps Bob and Dave |
|
ev := event.New() |
|
ev.Pubkey = alice.pubkey |
|
ev.CreatedAt = ts |
|
ev.Kind = 3 |
|
ev.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(bob.pubkey[:])), |
|
tag.NewFromAny("p", hex.Enc(dave.pubkey[:])), |
|
) |
|
|
|
if err := ev.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
exists, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save contact list: %v", err) |
|
} |
|
if exists { |
|
t.Fatal("Event should not exist yet") |
|
} |
|
|
|
// Verify Charlie was removed |
|
follows := queryFollows(t, ctx, db, alice.pubkey) |
|
if len(follows) != 2 { |
|
t.Fatalf("Expected 2 follows after removal, got %d", len(follows)) |
|
} |
|
|
|
expectedFollows := map[string]bool{ |
|
hex.Enc(bob.pubkey[:]): true, |
|
hex.Enc(dave.pubkey[:]): true, |
|
} |
|
|
|
for _, follow := range follows { |
|
if !expectedFollows[follow] { |
|
t.Errorf("Unexpected follow: %s", follow) |
|
} |
|
if follow == hex.Enc(charlie.pubkey[:]) { |
|
t.Error("Charlie should have been unfollowed") |
|
} |
|
delete(expectedFollows, follow) |
|
} |
|
|
|
t.Logf("✓ Contact list updated: Alice unfollowed Charlie") |
|
} |
|
|
|
// testContactListOlderRejected tests that older events are rejected |
|
func testContactListOlderRejected(t *testing.T, ctx context.Context, db *N, alice, bob testKeypair, ts int64) { |
|
// Try to save an old contact list (timestamp is older than the existing one) |
|
ev := event.New() |
|
ev.Pubkey = alice.pubkey |
|
ev.CreatedAt = ts // This is baseTimestamp, which is older than the current contact list |
|
ev.Kind = 3 |
|
ev.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(bob.pubkey[:])), |
|
) |
|
|
|
if err := ev.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
// Save should succeed (base event stored), but social processing should skip it |
|
_, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save event: %v", err) |
|
} |
|
|
|
// Verify follows list unchanged (should still be Bob and Dave from previous test) |
|
follows := queryFollows(t, ctx, db, alice.pubkey) |
|
if len(follows) != 2 { |
|
t.Fatalf("Expected follows list unchanged, got %d follows", len(follows)) |
|
} |
|
|
|
t.Logf("✓ Older contact list event rejected (follows unchanged)") |
|
} |
|
|
|
// testMuteList tests kind 10000 mute list processing |
|
func testMuteList(t *testing.T, ctx context.Context, db *N, alice, eve testKeypair) { |
|
// Alice mutes Eve |
|
ev := event.New() |
|
ev.Pubkey = alice.pubkey |
|
ev.CreatedAt = timestamp.Now().V |
|
ev.Kind = 10000 |
|
ev.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(eve.pubkey[:])), |
|
) |
|
|
|
if err := ev.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
exists, err := db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("Failed to save mute list: %v", err) |
|
} |
|
if exists { |
|
t.Fatal("Event should not exist yet") |
|
} |
|
|
|
// Verify MUTES relationship was created |
|
mutes := queryMutes(t, ctx, db, alice.pubkey) |
|
if len(mutes) != 1 { |
|
t.Fatalf("Expected 1 mute, got %d", len(mutes)) |
|
} |
|
|
|
if mutes[0] != hex.Enc(eve.pubkey[:]) { |
|
t.Errorf("Expected to mute Eve, got %s", mutes[0]) |
|
} |
|
|
|
t.Logf("✓ Mute list processed: Alice mutes Eve") |
|
} |
|
|
|
// testReports tests kind 1984 report processing |
|
func testReports(t *testing.T, ctx context.Context, db *N, alice, bob, eve testKeypair) { |
|
// Alice reports Eve for spam |
|
ev1 := event.New() |
|
ev1.Pubkey = alice.pubkey |
|
ev1.CreatedAt = timestamp.Now().V |
|
ev1.Kind = 1984 |
|
ev1.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(eve.pubkey[:]), "spam"), |
|
) |
|
ev1.Content = []byte("Spamming the relay") |
|
|
|
if err := ev1.Sign(alice.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
if _, err := db.SaveEvent(ctx, ev1); err != nil { |
|
t.Fatalf("Failed to save report: %v", err) |
|
} |
|
|
|
// Bob also reports Eve for illegal content |
|
ev2 := event.New() |
|
ev2.Pubkey = bob.pubkey |
|
ev2.CreatedAt = timestamp.Now().V |
|
ev2.Kind = 1984 |
|
ev2.Tags = tag.NewS( |
|
tag.NewFromAny("p", hex.Enc(eve.pubkey[:]), "illegal"), |
|
) |
|
|
|
if err := ev2.Sign(bob.signer); err != nil { |
|
t.Fatalf("Failed to sign event: %v", err) |
|
} |
|
|
|
if _, err := db.SaveEvent(ctx, ev2); err != nil { |
|
t.Fatalf("Failed to save report: %v", err) |
|
} |
|
|
|
// Verify REPORTS relationships were created |
|
reports := queryReports(t, ctx, db, eve.pubkey) |
|
if len(reports) != 2 { |
|
t.Fatalf("Expected 2 reports against Eve, got %d", len(reports)) |
|
} |
|
|
|
// Check report types |
|
reportTypes := make(map[string]int) |
|
for _, report := range reports { |
|
reportTypes[report.ReportType]++ |
|
} |
|
|
|
if reportTypes["spam"] != 1 { |
|
t.Errorf("Expected 1 spam report, got %d", reportTypes["spam"]) |
|
} |
|
if reportTypes["illegal"] != 1 { |
|
t.Errorf("Expected 1 illegal report, got %d", reportTypes["illegal"]) |
|
} |
|
|
|
t.Logf("✓ Reports processed: Eve reported by Alice (spam) and Bob (illegal)") |
|
} |
|
|
|
// verifyFinalGraphState verifies the complete graph state |
|
func verifyFinalGraphState(t *testing.T, ctx context.Context, db *N, alice, bob, charlie, dave, eve testKeypair) { |
|
t.Log("Verifying final graph state...") |
|
|
|
// Verify Alice's follows: Bob and Dave (Charlie removed) |
|
follows := queryFollows(t, ctx, db, alice.pubkey) |
|
if len(follows) != 2 { |
|
t.Errorf("Expected Alice to follow 2 users, got %d", len(follows)) |
|
} |
|
|
|
// Verify Alice's mutes: Eve |
|
mutes := queryMutes(t, ctx, db, alice.pubkey) |
|
if len(mutes) != 1 { |
|
t.Errorf("Expected Alice to mute 1 user, got %d", len(mutes)) |
|
} |
|
|
|
// Verify reports against Eve |
|
reports := queryReports(t, ctx, db, eve.pubkey) |
|
if len(reports) != 2 { |
|
t.Errorf("Expected 2 reports against Eve, got %d", len(reports)) |
|
} |
|
|
|
// Verify event traceability - all relationships should have created_by_event |
|
cypher := ` |
|
MATCH ()-[r:FOLLOWS|MUTES|REPORTS]->() |
|
WHERE r.created_by_event IS NULL |
|
RETURN count(r) AS count |
|
` |
|
result, err := db.ExecuteRead(ctx, cypher, nil) |
|
if err != nil { |
|
t.Fatalf("Failed to check traceability: %v", err) |
|
} |
|
|
|
if result.Next(ctx) { |
|
count := result.Record().Values[0].(int64) |
|
if count > 0 { |
|
t.Errorf("Found %d relationships without created_by_event", count) |
|
} |
|
} |
|
|
|
t.Log("✓ Final graph state verified") |
|
t.Logf(" - Alice follows: %v", follows) |
|
t.Logf(" - Alice mutes: %v", mutes) |
|
t.Logf(" - Reports against Eve: %d", len(reports)) |
|
} |
|
|
|
// Helper types and functions |
|
|
|
type testKeypair struct { |
|
pubkey []byte |
|
signer *p8k.Signer |
|
} |
|
|
|
type reportInfo struct { |
|
Reporter string |
|
ReportType string |
|
} |
|
|
|
func generateTestKeypair(t *testing.T, name string) testKeypair { |
|
t.Helper() |
|
|
|
signer, err := p8k.New() |
|
if err != nil { |
|
t.Fatalf("Failed to create signer for %s: %v", name, err) |
|
} |
|
|
|
if err := signer.Generate(); err != nil { |
|
t.Fatalf("Failed to generate keypair for %s: %v", name, err) |
|
} |
|
|
|
return testKeypair{ |
|
pubkey: signer.Pub(), |
|
signer: signer, |
|
} |
|
} |
|
|
|
func queryFollows(t *testing.T, ctx context.Context, db *N, pubkey []byte) []string { |
|
t.Helper() |
|
|
|
cypher := ` |
|
MATCH (user:NostrUser {pubkey: $pubkey})-[f:FOLLOWS]->(followed:NostrUser) |
|
WHERE NOT EXISTS { |
|
MATCH (old:ProcessedSocialEvent {event_id: f.created_by_event}) |
|
WHERE old.superseded_by IS NOT NULL |
|
} |
|
RETURN followed.pubkey AS pubkey |
|
` |
|
params := map[string]any{"pubkey": hex.Enc(pubkey)} |
|
|
|
result, err := db.ExecuteRead(ctx, cypher, params) |
|
if err != nil { |
|
t.Fatalf("Failed to query follows: %v", err) |
|
} |
|
|
|
var follows []string |
|
for result.Next(ctx) { |
|
follows = append(follows, result.Record().Values[0].(string)) |
|
} |
|
|
|
return follows |
|
} |
|
|
|
func queryMutes(t *testing.T, ctx context.Context, db *N, pubkey []byte) []string { |
|
t.Helper() |
|
|
|
cypher := ` |
|
MATCH (user:NostrUser {pubkey: $pubkey})-[m:MUTES]->(muted:NostrUser) |
|
WHERE NOT EXISTS { |
|
MATCH (old:ProcessedSocialEvent {event_id: m.created_by_event}) |
|
WHERE old.superseded_by IS NOT NULL |
|
} |
|
RETURN muted.pubkey AS pubkey |
|
` |
|
params := map[string]any{"pubkey": hex.Enc(pubkey)} |
|
|
|
result, err := db.ExecuteRead(ctx, cypher, params) |
|
if err != nil { |
|
t.Fatalf("Failed to query mutes: %v", err) |
|
} |
|
|
|
var mutes []string |
|
for result.Next(ctx) { |
|
mutes = append(mutes, result.Record().Values[0].(string)) |
|
} |
|
|
|
return mutes |
|
} |
|
|
|
func queryReports(t *testing.T, ctx context.Context, db *N, pubkey []byte) []reportInfo { |
|
t.Helper() |
|
|
|
cypher := ` |
|
MATCH (reporter:NostrUser)-[r:REPORTS]->(reported:NostrUser {pubkey: $pubkey}) |
|
RETURN reporter.pubkey AS reporter, r.report_type AS report_type |
|
` |
|
params := map[string]any{"pubkey": hex.Enc(pubkey)} |
|
|
|
result, err := db.ExecuteRead(ctx, cypher, params) |
|
if err != nil { |
|
t.Fatalf("Failed to query reports: %v", err) |
|
} |
|
|
|
var reports []reportInfo |
|
for result.Next(ctx) { |
|
record := result.Record() |
|
reports = append(reports, reportInfo{ |
|
Reporter: record.Values[0].(string), |
|
ReportType: record.Values[1].(string), |
|
}) |
|
} |
|
|
|
return reports |
|
} |
|
|
|
// TestDiffComputation tests the diff computation helper function |
|
func TestDiffComputation(t *testing.T) { |
|
tests := []struct { |
|
name string |
|
old []string |
|
new []string |
|
expectAdded []string |
|
expectRemoved []string |
|
}{ |
|
{ |
|
name: "Empty to non-empty", |
|
old: []string{}, |
|
new: []string{"a", "b", "c"}, |
|
expectAdded: []string{"a", "b", "c"}, |
|
expectRemoved: []string{}, |
|
}, |
|
{ |
|
name: "Non-empty to empty", |
|
old: []string{"a", "b", "c"}, |
|
new: []string{}, |
|
expectAdded: []string{}, |
|
expectRemoved: []string{"a", "b", "c"}, |
|
}, |
|
{ |
|
name: "No changes", |
|
old: []string{"a", "b", "c"}, |
|
new: []string{"a", "b", "c"}, |
|
expectAdded: []string{}, |
|
expectRemoved: []string{}, |
|
}, |
|
{ |
|
name: "Add some, remove some", |
|
old: []string{"a", "b", "c"}, |
|
new: []string{"b", "c", "d", "e"}, |
|
expectAdded: []string{"d", "e"}, |
|
expectRemoved: []string{"a"}, |
|
}, |
|
{ |
|
name: "All different", |
|
old: []string{"a", "b", "c"}, |
|
new: []string{"d", "e", "f"}, |
|
expectAdded: []string{"d", "e", "f"}, |
|
expectRemoved: []string{"a", "b", "c"}, |
|
}, |
|
} |
|
|
|
for _, tt := range tests { |
|
t.Run(tt.name, func(t *testing.T) { |
|
added, removed := diffStringSlices(tt.old, tt.new) |
|
|
|
if !slicesEqual(added, tt.expectAdded) { |
|
t.Errorf("Added mismatch:\n got: %v\n expected: %v", added, tt.expectAdded) |
|
} |
|
|
|
if !slicesEqual(removed, tt.expectRemoved) { |
|
t.Errorf("Removed mismatch:\n got: %v\n expected: %v", removed, tt.expectRemoved) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
// slicesEqual checks if two string slices contain the same elements (order doesn't matter) |
|
func slicesEqual(a, b []string) bool { |
|
if len(a) != len(b) { |
|
return false |
|
} |
|
|
|
aMap := make(map[string]int) |
|
for _, s := range a { |
|
aMap[s]++ |
|
} |
|
|
|
bMap := make(map[string]int) |
|
for _, s := range b { |
|
bMap[s]++ |
|
} |
|
|
|
for k, v := range aMap { |
|
if bMap[k] != v { |
|
return false |
|
} |
|
} |
|
|
|
return true |
|
} |
|
|
|
// TestExtractPTags tests the p-tag extraction helper function |
|
func TestExtractPTags(t *testing.T) { |
|
// Valid 64-character hex pubkeys for testing |
|
pk1 := "0000000000000000000000000000000000000000000000000000000000000001" |
|
pk2 := "0000000000000000000000000000000000000000000000000000000000000002" |
|
pk3 := "0000000000000000000000000000000000000000000000000000000000000003" |
|
|
|
tests := []struct { |
|
name string |
|
tags *tag.S |
|
expected []string |
|
}{ |
|
{ |
|
name: "No tags", |
|
tags: &tag.S{}, |
|
expected: []string{}, |
|
}, |
|
{ |
|
name: "Only p-tags", |
|
tags: tag.NewS( |
|
tag.NewFromAny("p", pk1), |
|
tag.NewFromAny("p", pk2), |
|
tag.NewFromAny("p", pk3), |
|
), |
|
expected: []string{pk1, pk2, pk3}, |
|
}, |
|
{ |
|
name: "Mixed tags", |
|
tags: tag.NewS( |
|
tag.NewFromAny("p", pk1), |
|
tag.NewFromAny("e", "event1"), |
|
tag.NewFromAny("p", pk2), |
|
tag.NewFromAny("t", "hashtag"), |
|
), |
|
expected: []string{pk1, pk2}, |
|
}, |
|
{ |
|
name: "Duplicate p-tags", |
|
tags: tag.NewS( |
|
tag.NewFromAny("p", pk1), |
|
tag.NewFromAny("p", pk1), |
|
tag.NewFromAny("p", pk2), |
|
), |
|
expected: []string{pk1, pk2}, |
|
}, |
|
{ |
|
name: "Invalid p-tags (too short)", |
|
tags: tag.NewS( |
|
tag.NewFromAny("p"), |
|
tag.NewFromAny("p", "tooshort"), |
|
), |
|
expected: []string{}, |
|
}, |
|
} |
|
|
|
for _, tt := range tests { |
|
t.Run(tt.name, func(t *testing.T) { |
|
ev := event.New() |
|
ev.Tags = tt.tags |
|
|
|
result := extractPTags(ev) |
|
|
|
if !slicesEqual(result, tt.expected) { |
|
t.Errorf("Extracted p-tags mismatch:\n got: %v\n expected: %v", result, tt.expected) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
// Benchmark tests |
|
func BenchmarkDiffComputation(b *testing.B) { |
|
old := make([]string, 1000) |
|
new := make([]string, 1000) |
|
|
|
for i := 0; i < 800; i++ { |
|
old[i] = fmt.Sprintf("pubkey%d", i) |
|
new[i] = fmt.Sprintf("pubkey%d", i) |
|
} |
|
|
|
// 200 removed from old |
|
for i := 800; i < 1000; i++ { |
|
old[i] = fmt.Sprintf("oldpubkey%d", i) |
|
} |
|
|
|
// 200 added to new |
|
for i := 800; i < 1000; i++ { |
|
new[i] = fmt.Sprintf("newpubkey%d", i) |
|
} |
|
|
|
b.ResetTimer() |
|
|
|
for i := 0; i < b.N; i++ { |
|
_, _ = diffStringSlices(old, new) |
|
} |
|
}
|
|
|