304 lines
7.4 KiB
Go
304 lines
7.4 KiB
Go
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
|
|
}
|
|
} |