540 lines
15 KiB
Go
540 lines
15 KiB
Go
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)
|
|
}
|