package dispatch import ( "context" "encoding/json" "fmt" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" ) // --- mock listener for testing --- type mockListener_Cov3 struct { name string err error calls int32 events []*channel.ChannelEvent mu sync.Mutex } func (m *mockListener_Cov3) Name() string { return m.name } func (m *mockListener_Cov3) OnEvent(ctx context.Context, event *channel.ChannelEvent) error { atomic.AddInt32(&m.calls, 1) m.mu.Lock() m.events = append(m.events, event) m.mu.Unlock() return m.err } func newMockListener_Cov3(name string, err error) *mockListener_Cov3 { return &mockListener_Cov3{name: name, err: err} } // --- ListenerRegistry tests --- func TestListenerRegistry_SubscribeWildcard_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1") // wildcard subs := r.SubscribersForEvent("message.created") assert.Contains(t, subs, "listener1") } func TestListenerRegistry_SubscribeSpecific_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created") subs := r.SubscribersForEvent("message.created") assert.Contains(t, subs, "listener1") // Should NOT be returned for other events subs2 := r.SubscribersForEvent("conversation.created") assert.NotContains(t, subs2, "listener1") } func TestListenerRegistry_SubscribeMultipleEvents_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created", "conversation.created") subs1 := r.SubscribersForEvent("message.created") assert.Contains(t, subs1, "listener1") subs2 := r.SubscribersForEvent("conversation.created") assert.Contains(t, subs2, "listener1") } func TestListenerRegistry_Unsubscribe_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created") r.Unsubscribe("listener1") subs := r.SubscribersForEvent("message.created") assert.NotContains(t, subs, "listener1") } func TestListenerRegistry_UnsubscribeCleansEmpty_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created") r.Unsubscribe("listener1") // Event should be removed from subscriptions when no more listeners assert.Empty(t, r.subscriptions["message.created"]) } func TestListenerRegistry_AllSubscriptions_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created") r.Subscribe("listener2", "conversation.created") subs := r.AllSubscriptions() assert.Contains(t, subs, "message.created") assert.Contains(t, subs, "conversation.created") } func TestListenerRegistry_HasSubscribers_Wildcard_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1") // wildcard assert.True(t, r.HasSubscribers("any.event")) } func TestListenerRegistry_HasSubscribers_Specific_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1", "message.created") assert.True(t, r.HasSubscribers("message.created")) assert.False(t, r.HasSubscribers("conversation.created")) } func TestListenerRegistry_HasSubscribers_None_Cov3(t *testing.T) { r := NewListenerRegistry() assert.False(t, r.HasSubscribers("message.created")) } func TestListenerRegistry_SubscribersForEvent_NoMatch_Cov3(t *testing.T) { r := NewListenerRegistry() subs := r.SubscribersForEvent("nonexistent.event") assert.Empty(t, subs) } func TestListenerRegistry_SubscribersForEvent_Dedup_Cov3(t *testing.T) { r := NewListenerRegistry() r.Subscribe("listener1") // wildcard r.Subscribe("listener1", "message.created") // specific (same listener) subs := r.SubscribersForEvent("message.created") // Should be deduplicated — only one entry assert.Len(t, subs, 1) assert.Contains(t, subs, "listener1") } // --- Event helpers tests --- func TestExtractConversationID_Uint_Cov3(t *testing.T) { data := map[string]interface{}{"conversation_id": uint(42)} id, err := ExtractConversationID(data) require.NoError(t, err) assert.Equal(t, uint(42), id) } func TestExtractConversationID_Int_Cov3(t *testing.T) { data := map[string]interface{}{"conversation_id": int(42)} id, err := ExtractConversationID(data) require.NoError(t, err) assert.Equal(t, uint(42), id) } func TestExtractConversationID_Float64_Cov3(t *testing.T) { data := map[string]interface{}{"conversation_id": float64(42)} id, err := ExtractConversationID(data) require.NoError(t, err) assert.Equal(t, uint(42), id) } func TestExtractConversationID_Missing_Cov3(t *testing.T) { data := map[string]interface{}{} _, err := ExtractConversationID(data) assert.Error(t, err) } func TestExtractConversationID_WrongType_Cov3(t *testing.T) { data := map[string]interface{}{"conversation_id": "not a number"} _, err := ExtractConversationID(data) assert.Error(t, err) } func TestExtractAccountID_Uint_Cov3(t *testing.T) { data := map[string]interface{}{"account_id": uint(1)} id, err := ExtractAccountID(data) require.NoError(t, err) assert.Equal(t, uint(1), id) } func TestExtractAccountID_Int_Cov3(t *testing.T) { data := map[string]interface{}{"account_id": int(1)} id, err := ExtractAccountID(data) require.NoError(t, err) assert.Equal(t, uint(1), id) } func TestExtractAccountID_Float64_Cov3(t *testing.T) { data := map[string]interface{}{"account_id": float64(1)} id, err := ExtractAccountID(data) require.NoError(t, err) assert.Equal(t, uint(1), id) } func TestExtractAccountID_Missing_Cov3(t *testing.T) { data := map[string]interface{}{} _, err := ExtractAccountID(data) assert.Error(t, err) } func TestExtractAccountID_WrongType_Cov3(t *testing.T) { data := map[string]interface{}{"account_id": true} _, err := ExtractAccountID(data) assert.Error(t, err) } func TestExtractInboxID_Uint_Cov3(t *testing.T) { data := map[string]interface{}{"inbox_id": uint(5)} id, err := ExtractInboxID(data) require.NoError(t, err) assert.Equal(t, uint(5), id) } func TestExtractInboxID_Int_Cov3(t *testing.T) { data := map[string]interface{}{"inbox_id": int(5)} id, err := ExtractInboxID(data) require.NoError(t, err) assert.Equal(t, uint(5), id) } func TestExtractInboxID_Float64_Cov3(t *testing.T) { data := map[string]interface{}{"inbox_id": float64(5)} id, err := ExtractInboxID(data) require.NoError(t, err) assert.Equal(t, uint(5), id) } func TestExtractInboxID_Missing_Cov3(t *testing.T) { data := map[string]interface{}{} _, err := ExtractInboxID(data) assert.Error(t, err) } func TestExtractInboxID_WrongType_Cov3(t *testing.T) { data := map[string]interface{}{"inbox_id": "abc"} _, err := ExtractInboxID(data) assert.Error(t, err) } func TestExtractAssigneeID_Uint_Cov3(t *testing.T) { data := map[string]interface{}{"assignee_id": uint(3)} id, err := ExtractAssigneeID(data) require.NoError(t, err) assert.Equal(t, uint(3), id) } func TestExtractAssigneeID_Int_Cov3(t *testing.T) { data := map[string]interface{}{"assignee_id": int(3)} id, err := ExtractAssigneeID(data) require.NoError(t, err) assert.Equal(t, uint(3), id) } func TestExtractAssigneeID_Float64_Cov3(t *testing.T) { data := map[string]interface{}{"assignee_id": float64(3)} id, err := ExtractAssigneeID(data) require.NoError(t, err) assert.Equal(t, uint(3), id) } func TestExtractAssigneeID_Missing_Cov3(t *testing.T) { data := map[string]interface{}{} _, err := ExtractAssigneeID(data) assert.Error(t, err) } func TestExtractAssigneeID_WrongType_Cov3(t *testing.T) { data := map[string]interface{}{"assignee_id": []int{1}} _, err := ExtractAssigneeID(data) assert.Error(t, err) } // --- Event constructors --- func TestNewChannelEvent_Cov3(t *testing.T) { event := NewChannelEvent("message.created", map[string]interface{}{"foo": "bar"}) assert.Equal(t, "message.created", string(event.Type)) assert.Equal(t, "bar", event.Data["foo"]) assert.NotZero(t, event.Timestamp) } func TestNewChannelEvent_NilData_Cov3(t *testing.T) { event := NewChannelEvent("test.event", nil) assert.NotNil(t, event.Data) } func TestNewDispatchEvent_Cov3(t *testing.T) { event := NewDispatchEvent(DispatchEventCampaignStarted, map[string]interface{}{"id": 1}) assert.Equal(t, "campaign.started", string(event.Type)) } func TestEventDataWithConversation_Cov3(t *testing.T) { data := EventDataWithConversation(1, 2, 3) assert.Equal(t, uint(1), data["conversation_id"]) assert.Equal(t, uint(2), data["account_id"]) assert.Equal(t, uint(3), data["inbox_id"]) } func TestEventDataWithAssignment_Cov3(t *testing.T) { data := EventDataWithAssignment(1, 2, 3, 4) assert.Equal(t, uint(1), data["conversation_id"]) assert.Equal(t, uint(2), data["account_id"]) assert.Equal(t, uint(3), data["inbox_id"]) assert.Equal(t, uint(4), data["assignee_id"]) } // --- Dispatcher tests --- func TestEventDispatcher_Dispatch_Sync_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("sync_listener", nil) ed.RegisterSync(l1, "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) require.NoError(t, err) assert.Equal(t, int32(1), atomic.LoadInt32(&l1.calls)) } func TestEventDispatcher_Dispatch_SyncError_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("err_listener", fmt.Errorf("listener error")) ed.RegisterSync(l1, "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) assert.Error(t, err) assert.Contains(t, err.Error(), "listener error") } func TestEventDispatcher_Dispatch_Async_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("async_listener", nil) ed.RegisterAsync(l1, "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) require.NoError(t, err) // async errors are not returned // Wait for async goroutine to complete time.Sleep(100 * time.Millisecond) assert.Equal(t, int32(1), atomic.LoadInt32(&l1.calls)) } func TestEventDispatcher_Dispatch_Wildcard_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("wildcard_listener", nil) ed.RegisterSync(l1) // wildcard — all events event := &channel.ChannelEvent{ Type: "any.event", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) require.NoError(t, err) assert.Equal(t, int32(1), atomic.LoadInt32(&l1.calls)) } func TestEventDispatcher_Dispatch_NoSubscribers_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) require.NoError(t, err) } func TestEventDispatcher_DispatchAsync_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("sync_listener", nil) ed.RegisterSync(l1, "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } ed.DispatchAsync(context.Background(), event) time.Sleep(100 * time.Millisecond) assert.Equal(t, int32(1), atomic.LoadInt32(&l1.calls)) } func TestEventDispatcher_DispatchAsync_NoSubscribers_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } ed.DispatchAsync(context.Background(), event) // should not panic } func TestEventDispatcher_DispatchAsync_UnknownListener_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) // Manually add a subscriber name that doesn't exist in entries ed.registry.Subscribe("nonexistent_listener", "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } ed.DispatchAsync(context.Background(), event) // should not panic — just skips unknown listener } func TestEventDispatcher_Dispatch_UnknownListenerEntry_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) // Add subscriber that doesn't have an entry ed.registry.Subscribe("ghost", "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } err := ed.Dispatch(context.Background(), event) require.NoError(t, err) } func TestEventDispatcher_PerformListenerJob_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("job_listener", nil) ed.RegisterSync(l1, "message.created") event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } payload := listenerJobPayload{ListenerName: "job_listener", Event: event} payloadBytes, _ := json.Marshal(payload) job := &model.BackgroundJob{ Payload: payloadBytes, } err := ed.performListenerJob(context.Background(), job) require.NoError(t, err) assert.Equal(t, int32(1), atomic.LoadInt32(&l1.calls)) } func TestEventDispatcher_PerformListenerJob_BadPayload_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) job := &model.BackgroundJob{ Payload: json.RawMessage(`{"bad": "json"`), } err := ed.performListenerJob(context.Background(), job) assert.Error(t, err) } func TestEventDispatcher_PerformListenerJob_UnknownListener_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) event := &channel.ChannelEvent{ Type: "message.created", Data: map[string]interface{}{}, } payload := listenerJobPayload{ListenerName: "unknown", Event: event} payloadBytes, _ := json.Marshal(payload) job := &model.BackgroundJob{ Payload: payloadBytes, } err := ed.performListenerJob(context.Background(), job) assert.Error(t, err) assert.Contains(t, err.Error(), "not registered") } func TestEventDispatcher_PerformListenerJob_NilEvent_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l1 := newMockListener_Cov3("job_listener", nil) ed.RegisterSync(l1, "message.created") payload := listenerJobPayload{ListenerName: "job_listener", Event: nil} payloadBytes, _ := json.Marshal(payload) job := &model.BackgroundJob{ Payload: payloadBytes, } err := ed.performListenerJob(context.Background(), job) assert.Error(t, err) assert.Contains(t, err.Error(), "missing event") } func TestEventDispatcher_ChannelDispatcher_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) assert.NotNil(t, ed.ChannelDispatcher()) } func TestEventDispatcher_Registry_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) assert.NotNil(t, ed.Registry()) } func TestEventDispatcher_RegisterSync_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l := newMockListener_Cov3("test", nil) ed.RegisterSync(l, "event1") assert.Contains(t, ed.entries, "test") assert.Equal(t, SyncMode, ed.entries["test"].mode) } func TestEventDispatcher_RegisterAsync_Cov3(t *testing.T) { cd := channel.NewDispatcher() ed := NewEventDispatcher(cd) l := newMockListener_Cov3("test", nil) ed.RegisterAsync(l, "event1") assert.Contains(t, ed.entries, "test") assert.Equal(t, AsyncMode, ed.entries["test"].mode) }