diff --git a/backend/internal/handler/ws/ws_test.go b/backend/internal/handler/ws/ws_test.go index c89ce51f..43aeb98f 100644 --- a/backend/internal/handler/ws/ws_test.go +++ b/backend/internal/handler/ws/ws_test.go @@ -8,6 +8,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "sync" "testing" @@ -416,7 +418,7 @@ func TestHub_SendToAccountSanitizesVisitorIdentity(t *testing.T) { hub.subscribeClient(visitor.ID, accountRoomName(10)) visitor.SubscribedRooms[accountRoomName(10)] = true - hub.SendToAccount(10, []byte(`{"event":"message.created","data":{"content":"same reply","sender_type":"AgentBot","sender_id":7,"ai_takeover_active":true,"additional_attributes":{"agent_name":"Captain"}}}`)) + hub.SendToAccount(10, []byte(`{"event":"message.created","data":{"content":"same reply","message_type":"outgoing","sender":{"available_name":"Captain","name":"Captain","agent_name":"Captain"},"sender_type":"AgentBot","sender_id":7,"ai_takeover_active":true,"additional_attributes":{"agent_name":"Captain","sender_name":"Captain"}}}`)) var agentMessage, visitorMessage []byte select { @@ -438,6 +440,31 @@ func TestHub_SendToAccountSanitizesVisitorIdentity(t *testing.T) { assert.NotContains(t, string(visitorMessage), "Captain") } +func TestEventDataForClient_VisitorMessageKeepsPublicFieldsOnly(t *testing.T) { + hub := NewHubSimple() + visitor := NewClient(2, 10, nil, hub) + visitor.IsContact = true + + data := eventDataForClient(visitor, []byte(`{"event":"message.created","data":{"id":12,"content":"Dashboard reply","message_type":1,"conversation_id":42,"sender":{"available_name":"Captain","name":"Captain","agent_name":"Captain"},"sender_type":"AgentBot","sender_id":7,"ai_takeover_active":true,"additional_attributes":{"sender_name":"Captain","agent_name":"Captain"}}}`)) + fixture, err := os.ReadFile(filepath.Join("..", "..", "..", "..", "frontend", "testdata", "visitor_ws_payload.json")) + require.NoError(t, err) + assert.JSONEq(t, string(fixture), string(data)) + + var payload map[string]any + require.NoError(t, json.Unmarshal(data, &payload)) + assert.Equal(t, "message.created", payload["event"]) + message, ok := payload["data"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "Dashboard reply", message["content"]) + assert.Equal(t, float64(1), message["message_type"]) + assert.Equal(t, float64(42), message["conversation_id"]) + assert.NotContains(t, message, "sender") + assert.NotContains(t, message, "sender_type") + assert.NotContains(t, message, "sender_id") + assert.NotContains(t, message, "ai_takeover_active") + assert.Equal(t, map[string]any{}, message["additional_attributes"]) +} + func TestHub_VisitorRoomAndClientDeliverySanitizeIdentity(t *testing.T) { hub := NewHubSimple() visitor := NewClient(2, 10, nil, hub) @@ -809,7 +836,7 @@ func TestServeCable_WidgetReceivesTokenRoomEvent(t *testing.T) { require.NoError(t, json.Unmarshal(confirmation, &confirm)) assert.Equal(t, ServerConfirmSubscribe, confirm.Type) - hub.SendToRoom(pubsubTokenRoomName("visitor-token"), []byte(`{"event":"message.created","data":{"id":12,"content":"Dashboard reply","message_type":1,"conversation_id":42}}`)) + hub.SendToRoom(pubsubTokenRoomName("visitor-token"), []byte(`{"event":"message.created","data":{"id":12,"content":"Dashboard reply","message_type":1,"conversation_id":42,"sender":{"available_name":"Captain","name":"Captain"},"sender_type":"AgentBot","sender_id":7,"ai_takeover_active":true,"additional_attributes":{"sender_name":"Captain","agent_name":"Captain"}}}`)) _, message, err := conn.ReadMessage() require.NoError(t, err) var delivered struct { @@ -825,6 +852,11 @@ func TestServeCable_WidgetReceivesTokenRoomEvent(t *testing.T) { assert.Equal(t, "Dashboard reply", payload["content"]) assert.Equal(t, float64(1), payload["message_type"]) assert.Equal(t, float64(42), payload["conversation_id"]) + assert.NotContains(t, payload, "sender") + assert.NotContains(t, payload, "sender_type") + assert.NotContains(t, payload, "sender_id") + assert.NotContains(t, payload, "ai_takeover_active") + assert.Equal(t, map[string]interface{}{}, payload["additional_attributes"]) } func TestDashboardOutgoingReachesDashboardAndReconnectedWidget(t *testing.T) { diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 51e12f8e..3b3688b5 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -43,6 +43,14 @@ services: - "8025:8025" # Web UI # Reference: Chatwoot docker-compose includes mailhog for email testing + web-channel-test: + image: nginx:alpine + container_name: gochat-web-channel-test + ports: + - "8081:80" + volumes: + - ./web-channel-test/index.html:/usr/share/nginx/html/index.html:ro + gochat: build: context: ../.. diff --git a/deploy/docker/web-channel-test/index.html b/deploy/docker/web-channel-test/index.html new file mode 100644 index 00000000..6e3909fc --- /dev/null +++ b/deploy/docker/web-channel-test/index.html @@ -0,0 +1,27 @@ + + + + + + GoChat WEB CHANNEL 测试 + + +

GoChat WEB CHANNEL 测试

+ + + diff --git a/frontend/app/javascript/widget/components/specs/agentName.spec.js b/frontend/app/javascript/widget/components/specs/agentName.spec.js index c4f3a714..04947bb7 100644 --- a/frontend/app/javascript/widget/components/specs/agentName.spec.js +++ b/frontend/app/javascript/widget/components/specs/agentName.spec.js @@ -1,26 +1,113 @@ +import { mount } from '@vue/test-utils'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; +import visitorPayload from '../../../../../testdata/visitor_ws_payload.json'; import AgentMessage from '../AgentMessage.vue'; import UnreadMessage from '../UnreadMessage.vue'; const context = { - channelConfig: { websiteName: 'GoChat' }, + channelConfig: { + websiteName: 'GoChat', + enabledFeatures: [], + avatarUrl: '', + }, useInboxAvatarForBot: false, $t: key => key, }; -describe('widget agent name', () => { - it('uses the inbox name when a conversation message has no public sender', () => { - expect( - AgentMessage.computed.agentName.call({ ...context, message: {} }) - ).toBe('GoChat'); +window.chatwootWebChannel = context.channelConfig; + +const mountAgentMessage = message => + mount(AgentMessage, { + props: { + message: { ...message, showAvatar: true }, + }, + global: { + plugins: [VueDOMPurifyHTML], + stubs: { + AgentMessageBubble: { template: '
' }, + Avatar: { template: '
' }, + FileBubble: { template: '
' }, + ImageBubble: { template: '
' }, + MessageReplyButton: { template: '
' }, + ReplyToChip: { template: '
' }, + UserMessage: { template: '
' }, + VideoBubble: { template: '
' }, + }, + }, }); - it('uses the inbox name when an unread message has no public sender', () => { +describe('widget agent name', () => { + it.each([ + [ + 'available_name', + { + sender: { available_name: 'Available', name: 'Name' }, + additional_attributes: { sender_name: 'Visitor service' }, + }, + 'Available', + ], + [ + 'name', + { + sender: { name: 'Name' }, + additional_attributes: { sender_name: 'Visitor service' }, + }, + 'Name', + ], + [ + 'message sender_name', + { additional_attributes: { sender_name: 'Visitor service' } }, + 'Visitor service', + ], + ['inbox name', {}, 'GoChat'], + ])('conversation message prefers %s', (_source, message, expected) => { + expect(AgentMessage.computed.agentName.call({ ...context, message })).toBe( + expected + ); + }); + + it('returns an empty name when the sanitized inbox name is unavailable', () => { + expect( + AgentMessage.computed.agentName.call({ + ...context, + channelConfig: { ...context.channelConfig, websiteName: '' }, + message: {}, + }) + ).toBe(''); + }); + + it.each([ + [ + 'available_name', + { available_name: 'Available', name: 'Name' }, + 'Available', + ], + ['name', { name: 'Name' }, 'Name'], + ['inbox name', {}, 'GoChat'], + ])('unread message prefers %s', (_source, sender, expected) => { expect( UnreadMessage.computed.agentName.call({ ...context, + sender, + isSenderExist: UnreadMessage.methods.isSenderExist, + }) + ).toBe(expected); + }); + + it('returns an empty unread name when the inbox name is unavailable', () => { + expect( + UnreadMessage.computed.agentName.call({ + ...context, + channelConfig: { ...context.channelConfig, websiteName: '' }, sender: {}, isSenderExist: UnreadMessage.methods.isSenderExist, }) - ).toBe('GoChat'); + ).toBe(''); + }); + + it('mounts the visitor WS output with the inbox name', () => { + const wrapper = mountAgentMessage(visitorPayload.data); + + expect(wrapper.get('.agent-name').text()).toBe('GoChat'); }); }); diff --git a/frontend/testdata/visitor_ws_payload.json b/frontend/testdata/visitor_ws_payload.json new file mode 100644 index 00000000..84204281 --- /dev/null +++ b/frontend/testdata/visitor_ws_payload.json @@ -0,0 +1,10 @@ +{ + "event": "message.created", + "data": { + "id": 12, + "content": "Dashboard reply", + "message_type": 1, + "conversation_id": 42, + "additional_attributes": {} + } +}