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

286 lines
8.0 KiB
Go

package ws
import (
"encoding/json"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// === EventPublisher Tests ===
// Uses mockHandler (from broadcast_test.go) as the MessageHandler implementation,
// plus SSERegistry for SSE delivery verification.
func TestEventPublisher_LocalDelivery(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
// Subscribe SSE client to account 1
ch := sse.Subscribe("sse1", 1, 100)
require.NotNil(t, ch)
payload := map[string]interface{}{"id": 1, "content": "hello"}
publisher.PublishEvent(1, EventMessageCreated, payload)
// Verify WebSocket Hub received the event
data := hub.getAccountData(1)
require.NotNil(t, data, "Hub should have received event for account 1")
var wsMsg WSMessage
err := json.Unmarshal(data, &wsMsg)
require.NoError(t, err)
assert.Equal(t, EventMessageCreated, wsMsg.Event)
assert.Equal(t, uint(1), wsMsg.AccountID)
// Verify SSE channel received the event
select {
case event := <-ch.Events:
assert.Equal(t, EventMessageCreated, event.Type)
default:
t.Fatal("SSE channel should have received event")
}
}
func TestEventPublisher_AccountRouting(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
// Subscribe SSE clients to different accounts
ch1 := sse.Subscribe("sse1", 1, 100)
ch2 := sse.Subscribe("sse2", 2, 200)
require.NotNil(t, ch1)
require.NotNil(t, ch2)
payload := map[string]interface{}{"id": 1}
publisher.PublishEvent(1, EventConversationCreated, payload)
// Account 1 SSE client should receive
select {
case event := <-ch1.Events:
assert.Equal(t, EventConversationCreated, event.Type)
default:
t.Fatal("account 1 SSE client should receive event")
}
// Account 2 SSE client should NOT receive
assertNoSSEEvent(t, ch2)
}
func TestEventPublisher_ConversationEvent(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
// Subscribe SSE clients
chBroad := sse.Subscribe("sse_broad", 1, 100) // no conv filter
chFiltered := sse.Subscribe("sse_filtered", 1, 200)
sse.SubscribeConversation("sse_filtered", 42)
require.NotNil(t, chBroad)
require.NotNil(t, chFiltered)
payload := map[string]interface{}{"message": "typing..."}
publisher.PublishConversationEvent(1, 42, EventConversationTypingOn, payload)
// Broad SSE client should receive (no conversation filter)
assertSSEEventReceived(t, chBroad, SSEEvent{Type: EventConversationTypingOn})
// Filtered SSE client should receive (subscribed to conv 42)
assertSSEEventReceived(t, chFiltered, SSEEvent{Type: EventConversationTypingOn})
}
func TestEventPublisher_ConversationEvent_FilteredOut(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
// Subscribe SSE client to conversation 99 only
ch := sse.Subscribe("sse1", 1, 100)
sse.SubscribeConversation("sse1", 99)
require.NotNil(t, ch)
payload := map[string]interface{}{"message": "typing..."}
publisher.PublishConversationEvent(1, 42, EventConversationTypingOn, payload)
// Client subscribed to conv 99 should NOT receive conv 42 event
assertNoSSEEvent(t, ch)
}
func TestEventPublisher_AllEventTypes(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
ch := sse.Subscribe("sse1", 1, 100)
require.NotNil(t, ch)
eventTypes := []string{
EventMessageCreated,
EventMessageUpdated,
EventMessageDeleted,
EventConversationCreated,
EventConversationUpdated,
EventConversationStatusChanged,
EventAssigneeChanged,
EventTeamChanged,
EventContactCreated,
EventContactUpdated,
EventContactDeleted,
EventInboxCreated,
EventInboxUpdated,
EventInboxDeleted,
EventNotificationCreated,
EventNotificationUpdated,
}
for _, eventType := range eventTypes {
payload := map[string]interface{}{"test": eventType}
publisher.PublishEvent(1, eventType, payload)
// Verify SSE delivery
select {
case event := <-ch.Events:
assert.Equal(t, eventType, event.Type)
default:
t.Fatalf("SSE channel should receive event type: %s", eventType)
}
}
}
func TestEventPublisher_NoHub(t *testing.T) {
sse := NewSSERegistry()
// Create publisher with no hub — SSE should still work
publisher := NewEventPublisherLocal(nil, sse)
ch := sse.Subscribe("sse1", 1, 100)
require.NotNil(t, ch)
payload := map[string]interface{}{"id": 1}
publisher.PublishEvent(1, EventMessageCreated, payload)
// SSE should still receive the event
select {
case event := <-ch.Events:
assert.Equal(t, EventMessageCreated, event.Type)
default:
t.Fatal("SSE channel should receive event even without hub")
}
}
func TestEventPublisher_NoSSE(t *testing.T) {
hub := newMockHandler()
// Create publisher with no SSE — hub should still work
publisher := NewEventPublisherLocal(hub, nil)
payload := map[string]interface{}{"id": 1}
publisher.PublishEvent(1, EventMessageCreated, payload)
// Hub should still receive the event
data := hub.getAccountData(1)
require.NotNil(t, data, "Hub should receive event even without SSE")
}
func TestEventPublisher_NilTargets(t *testing.T) {
// Both hub and SSE are nil — should not panic
publisher := NewEventPublisherLocal(nil, nil)
payload := map[string]interface{}{"id": 1}
publisher.PublishEvent(1, EventMessageCreated, payload)
// No crash = success
}
func TestEventPublisher_WSMessageFormat(t *testing.T) {
hub := newMockHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
payload := map[string]interface{}{
"id": 42,
"content": "test message",
"status": "open",
}
publisher.PublishEvent(1, EventConversationCreated, payload)
data := hub.getAccountData(1)
require.NotNil(t, data)
var wsMsg WSMessage
err := json.Unmarshal(data, &wsMsg)
require.NoError(t, err)
assert.Equal(t, EventConversationCreated, wsMsg.Event)
assert.Equal(t, uint(1), wsMsg.AccountID)
// Data should contain the payload fields
dataMap, ok := wsMsg.Data.(map[string]interface{})
require.True(t, ok, "Data should be a map")
assert.Equal(t, float64(42), dataMap["id"])
assert.Equal(t, "test message", dataMap["content"])
}
// === Mock MessageHandler reuse ===
// The mockHandler type is defined in broadcast_test.go in the same package.
// We reuse it here for EventPublisher tests. If needed, here's a duplicate
// for reference (Go test files in the same package share types):
//
// Note: mockHandler is already defined in broadcast_test.go and accessible
// from this test file since they're in the same Go package (ws).
// Additional mock for tracking room-based sends
type mockRoomHandler struct {
mu sync.Mutex
accounts map[uint][]byte
rooms map[string][]byte
}
func newMockRoomHandler() *mockRoomHandler {
return &mockRoomHandler{
accounts: make(map[uint][]byte),
rooms: make(map[string][]byte),
}
}
func (m *mockRoomHandler) SendToAccount(accountID uint, data []byte) {
m.mu.Lock()
defer m.mu.Unlock()
m.accounts[accountID] = data
}
func (m *mockRoomHandler) SendToRoom(room string, data []byte) {
m.mu.Lock()
defer m.mu.Unlock()
m.rooms[room] = data
}
func (m *mockRoomHandler) getAccountData(accountID uint) []byte {
m.mu.Lock()
defer m.mu.Unlock()
return m.accounts[accountID]
}
func (m *mockRoomHandler) getRoomData(room string) []byte {
m.mu.Lock()
defer m.mu.Unlock()
return m.rooms[room]
}
func TestEventPublisher_ConversationEvent_RoomDelivery(t *testing.T) {
hub := newMockRoomHandler()
sse := NewSSERegistry()
publisher := NewEventPublisherLocal(hub, sse)
payload := map[string]interface{}{"message": "typing..."}
publisher.PublishConversationEvent(1, 42, EventConversationTypingOn, payload)
// Verify account room received
data := hub.getAccountData(1)
require.NotNil(t, data, "Hub account room should receive event")
// Verify conversation room received
convRoom := conversationRoomNameHelper(1, 42)
roomData := hub.getRoomData(convRoom)
require.NotNil(t, roomData, "Hub conversation room should receive event")
}