Files
gochat/internal/ws/sse_registry_test.go
T
2026-06-04 15:44:48 +08:00

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
}
}