package spider import ( "context" "testing" "time" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/kind" "git.mleku.dev/mleku/nostr/encoders/tag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestExtractRelaysFromEvents(t *testing.T) { ds := &DirectorySpider{} tests := []struct { name string events []*event.E expected []string }{ { name: "empty events", events: []*event.E{}, expected: []string{}, }, { name: "single event with relays", events: []*event.E{ { Kind: kind.RelayListMetadata.K, Tags: &tag.S{ tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")), tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay2.example.com")), }, }, }, expected: []string{"wss://relay1.example.com", "wss://relay2.example.com"}, }, { name: "multiple events with duplicate relays", events: []*event.E{ { Kind: kind.RelayListMetadata.K, Tags: &tag.S{ tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")), }, }, { Kind: kind.RelayListMetadata.K, Tags: &tag.S{ tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay1.example.com")), tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay3.example.com")), }, }, }, expected: []string{"wss://relay1.example.com", "wss://relay3.example.com"}, }, { name: "event with empty r tags", events: []*event.E{ { Kind: kind.RelayListMetadata.K, Tags: &tag.S{ tag.NewFromBytesSlice([]byte("r")), // empty value tag.NewFromBytesSlice([]byte("r"), []byte("wss://valid.relay.com")), }, }, }, expected: []string{"wss://valid.relay.com"}, }, { name: "normalizes relay URLs", events: []*event.E{ { Kind: kind.RelayListMetadata.K, Tags: &tag.S{ tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay.example.com")), tag.NewFromBytesSlice([]byte("r"), []byte("wss://relay.example.com/")), // duplicate with trailing slash }, }, }, expected: []string{"wss://relay.example.com"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ds.extractRelaysFromEvents(tt.events) // For empty case, check length if len(tt.expected) == 0 { assert.Empty(t, result) return } // Check that all expected relays are present (order may vary) assert.Len(t, result, len(tt.expected)) for _, expected := range tt.expected { assert.Contains(t, result, expected) } }) } } func TestDirectorySpiderLifecycle(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create spider without database (will return error) _, err := NewDirectorySpider(ctx, nil, nil, 0, 0) require.Error(t, err) assert.Contains(t, err.Error(), "database cannot be nil") } func TestDirectorySpiderDefaults(t *testing.T) { // Test that defaults are applied correctly assert.Equal(t, 24*time.Hour, DirectorySpiderDefaultInterval) assert.Equal(t, 3, DirectorySpiderDefaultMaxHops) assert.Equal(t, 30*time.Second, DirectorySpiderRelayTimeout) assert.Equal(t, 60*time.Second, DirectorySpiderQueryTimeout) assert.Equal(t, 500*time.Millisecond, DirectorySpiderRelayDelay) assert.Equal(t, 5000, DirectorySpiderMaxEventsPerQuery) } func TestTriggerNow(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() ds := &DirectorySpider{ ctx: ctx, triggerChan: make(chan struct{}, 1), } // First trigger should succeed ds.TriggerNow() // Verify trigger was sent select { case <-ds.triggerChan: // Expected default: t.Error("trigger was not sent") } // Second trigger while channel is empty should also succeed ds.TriggerNow() // But if we trigger again without draining, it should not block ds.TriggerNow() // Should not block due to select default case } func TestLastRun(t *testing.T) { ds := &DirectorySpider{} // Initially should be zero assert.True(t, ds.LastRun().IsZero()) // Set a time now := time.Now() ds.lastRun = now // Should return the set time assert.Equal(t, now, ds.LastRun()) }