package ws import ( "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // === SSE Registry Tests === func TestNewSSERegistry(t *testing.T) { reg := NewSSERegistry() assert.NotNil(t, reg) assert.NotNil(t, reg.channels) assert.NotNil(t, reg.accounts) assert.Equal(t, 0, reg.TotalChannelCount()) } func TestSSERegistry_Subscribe(t *testing.T) { reg := NewSSERegistry() ch := reg.Subscribe("ch1", 1, 100) require.NotNil(t, ch) assert.Equal(t, "ch1", ch.ID) assert.Equal(t, uint(1), ch.AccountID) assert.Equal(t, uint(100), ch.UserID) assert.False(t, ch.Closed) assert.NotNil(t, ch.Events) assert.Equal(t, 1, reg.TotalChannelCount()) assert.Equal(t, 1, reg.ChannelCount(1)) } func TestSSERegistry_SubscribeMultipleAccounts(t *testing.T) { reg := NewSSERegistry() reg.Subscribe("ch1", 1, 100) reg.Subscribe("ch2", 2, 200) reg.Subscribe("ch3", 1, 300) assert.Equal(t, 3, reg.TotalChannelCount()) assert.Equal(t, 2, reg.ChannelCount(1)) // ch1 and ch3 assert.Equal(t, 1, reg.ChannelCount(2)) // ch2 } func TestSSERegistry_Unsubscribe(t *testing.T) { reg := NewSSERegistry() ch := reg.Subscribe("ch1", 1, 100) require.NotNil(t, ch) reg.Unsubscribe("ch1") assert.True(t, ch.Closed) assert.Equal(t, 0, reg.TotalChannelCount()) assert.Equal(t, 0, reg.ChannelCount(1)) } func TestSSERegistry_UnsubscribeUnknownChannel(t *testing.T) { reg := NewSSERegistry() // Unsubscribing unknown channel should not panic reg.Unsubscribe("nonexistent") assert.Equal(t, 0, reg.TotalChannelCount()) } func TestSSERegistry_SubscribeConversation(t *testing.T) { reg := NewSSERegistry() ch := reg.Subscribe("ch1", 1, 100) require.NotNil(t, ch) reg.SubscribeConversation("ch1", 42) assert.True(t, ch.ConversationIDs[42]) reg.SubscribeConversation("ch1", 99) assert.True(t, ch.ConversationIDs[99]) } func TestSSERegistry_UnsubscribeConversation(t *testing.T) { reg := NewSSERegistry() ch := reg.Subscribe("ch1", 1, 100) require.NotNil(t, ch) reg.SubscribeConversation("ch1", 42) assert.True(t, ch.ConversationIDs[42]) reg.UnsubscribeConversation("ch1", 42) assert.False(t, ch.ConversationIDs[42]) } func TestSSERegistry_SendToAccount(t *testing.T) { reg := NewSSERegistry() ch1 := reg.Subscribe("ch1", 1, 100) ch2 := reg.Subscribe("ch2", 1, 200) ch3 := reg.Subscribe("ch3", 2, 300) // different account require.NotNil(t, ch1) require.NotNil(t, ch2) require.NotNil(t, ch3) event := SSEEvent{ Type: EventMessageCreated, Payload: map[string]interface{}{"id": 1, "content": "hello"}, } reg.SendToAccount(1, event) // ch1 and ch2 should receive the event (account 1) assertSSEEventReceived(t, ch1, event) assertSSEEventReceived(t, ch2, event) assertNoSSEEvent(t, ch3) } func TestSSERegistry_SendToAccount_NoSubscribers(t *testing.T) { reg := NewSSERegistry() event := SSEEvent{ Type: EventMessageCreated, Payload: map[string]interface{}{"id": 1}, } // Send to account with no subscribers — should not panic reg.SendToAccount(999, event) } func TestSSERegistry_SendToConversation_AccountWide(t *testing.T) { reg := NewSSERegistry() // ch1 has no conversation filters (subscribes to all events for account) ch1 := reg.Subscribe("ch1", 1, 100) // ch2 has a conversation filter ch2 := reg.Subscribe("ch2", 1, 200) reg.SubscribeConversation("ch2", 42) require.NotNil(t, ch1) require.NotNil(t, ch2) event := SSEEvent{ Type: EventMessageCreated, Payload: map[string]interface{}{"id": 1}, } // Send to conversation 42 in account 1 reg.SendToConversation(1, 42, event) // ch1 (no conv filter) should receive assertSSEEventReceived(t, ch1, event) // ch2 (subscribed to conv 42) should receive assertSSEEventReceived(t, ch2, event) } func TestSSERegistry_SendToConversation_Filtered(t *testing.T) { reg := NewSSERegistry() // ch1 subscribes to conversation 42 only ch1 := reg.Subscribe("ch1", 1, 100) reg.SubscribeConversation("ch1", 42) // ch2 subscribes to conversation 99 only ch2 := reg.Subscribe("ch2", 1, 200) reg.SubscribeConversation("ch2", 99) require.NotNil(t, ch1) require.NotNil(t, ch2) event := SSEEvent{ Type: EventConversationTypingOn, Payload: map[string]interface{}{"conversation_id": 42}, } // Send to conversation 42 — only ch1 should receive reg.SendToConversation(1, 42, event) assertSSEEventReceived(t, ch1, event) assertNoSSEEvent(t, ch2) } func TestSSERegistry_SendToAccount_SlowClientDrop(t *testing.T) { reg := NewSSERegistry() ch := reg.Subscribe("ch1", 1, 100) require.NotNil(t, ch) // Fill the buffer (64 capacity) for i := 0; i < 64; i++ { reg.SendToAccount(1, SSEEvent{ Type: EventMessageCreated, Payload: map[string]interface{}{"seq": i}, }) } // This event should be dropped because the buffer is full reg.SendToAccount(1, SSEEvent{ Type: EventMessageCreated, Payload: map[string]interface{}{"overflow": true}, }) // Drain events — should get exactly 64 drainCount := 0 timeout := time.After(2 * time.Second) for { select { case <-ch.Events: drainCount++ case <-timeout: goto done default: if drainCount >= 64 { goto done } } } done: assert.Equal(t, 64, drainCount) } func TestFormatSSE(t *testing.T) { event := SSEEvent{ Type: "message.created", Payload: map[string]interface{}{"id": 1, "content": "hello"}, } formatted, err := FormatSSE(event) require.NoError(t, err) assert.True(t, strings.HasPrefix(formatted, "event: message.created\n")) assert.True(t, strings.Contains(formatted, "data: ")) assert.True(t, strings.HasSuffix(formatted, "\n\n")) } func TestFormatSSE_ComplexPayload(t *testing.T) { event := SSEEvent{ Type: "conversation.updated", Payload: map[string]interface{}{ "id": 42, "status": "resolved", }, } formatted, err := FormatSSE(event) require.NoError(t, err) assert.True(t, strings.Contains(formatted, "event: conversation.updated\n")) assert.True(t, strings.Contains(formatted, "\"id\":42")) assert.True(t, strings.Contains(formatted, "\"status\":\"resolved\"")) } func TestFormatSSE_AllEventTypes(t *testing.T) { eventTypes := []string{ EventMessageCreated, EventMessageUpdated, EventMessageDeleted, EventConversationCreated, EventConversationUpdated, EventConversationStatusChanged, EventAssigneeChanged, EventTeamChanged, EventContactCreated, EventContactUpdated, EventContactDeleted, EventInboxCreated, EventInboxUpdated, EventInboxDeleted, EventNotificationCreated, EventNotificationUpdated, } for _, eventType := range eventTypes { event := SSEEvent{ Type: eventType, Payload: map[string]interface{}{"test": true}, } formatted, err := FormatSSE(event) require.NoError(t, err, "FormatSSE failed for event type: %s", eventType) assert.True(t, strings.Contains(formatted, "event: "+eventType+"\n"), "expected event line for type: %s", eventType) } } // === Helper functions for SSE tests === func assertSSEEventReceived(t *testing.T, ch *SSEChannel, expected SSEEvent) { t.Helper() select { case event := <-ch.Events: assert.Equal(t, expected.Type, event.Type) case <-time.After(200 * time.Millisecond): t.Fatalf("expected SSE event on channel %s but none received", ch.ID) } } func assertNoSSEEvent(t *testing.T, ch *SSEChannel) { t.Helper() select { case <-ch.Events: t.Fatalf("unexpected SSE event received on channel %s", ch.ID) case <-time.After(50 * time.Millisecond): // Expected: no event received within the timeout } }