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