diff --git a/packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts b/packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts index 8e801ee39ae..bb42a3b6c18 100644 --- a/packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts +++ b/packages/@webex/plugin-cc/src/services/core/WebSocket/WebSocketManager.ts @@ -48,12 +48,6 @@ export class WebSocketManager extends EventTarget { }); } - async reconnect() { - await this.connect().catch(() => { - LoggerProxy.logger.error(`[WebSocketStatus] | Error in connecting Websocket`); - }); - } - close(shouldReconnect: boolean, reason = 'Unknown') { if (!this.isSocketClosed && this.shouldReconnect) { this.shouldReconnect = shouldReconnect; diff --git a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts index 9830eff0223..762d51589e5 100644 --- a/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts +++ b/packages/@webex/plugin-cc/src/services/core/aqm-reqs.ts @@ -205,13 +205,13 @@ export default class AqmReqs { private readonly onMessage = (msg: any) => { const event = JSON.parse(msg.detail); if (event.type === 'Welcome') { - LoggerProxy.logger.info(`Welcome message from Notifs Websocket${event}`); + LoggerProxy.logger.info(`Welcome message from Notifs Websocket`); return; } if (event.keepalive === 'true') { - LoggerProxy.logger.info(`Keepalive from web socket ${event}`); + LoggerProxy.logger.info(`Keepalive from web socket`); return; } @@ -241,7 +241,7 @@ export default class AqmReqs { if (!isHandled) { LoggerProxy.logger.info( - `event=missingEventHandler | [AqmReqs] missing routing message handler${event}` + `event=missingEventHandler | [AqmReqs] missing routing message handler` ); } }; diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/core/WebSocket/WebSocketManager.ts b/packages/@webex/plugin-cc/test/unit/spec/services/core/WebSocket/WebSocketManager.ts index ec2e2be6394..bb571acf38a 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/core/WebSocket/WebSocketManager.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/core/WebSocket/WebSocketManager.ts @@ -2,6 +2,7 @@ import { WebSocketManager } from '../../../../../../src/services/core/WebSocket/WebSocketManager'; import { WebexSDK, SubscribeRequest } from '../../../../../../src/types'; import { SUBSCRIBE_API, WCC_API_GATEWAY } from '../../../../../../src/services/constants'; +import LoggerProxy from '../../../../../../src/logger-proxy'; jest.mock('../../../../../../src/services/core/HttpRequest'); jest.mock('../../../../../../src/logger-proxy', () => ({ @@ -10,6 +11,7 @@ jest.mock('../../../../../../src/logger-proxy', () => ({ logger: { log: jest.fn(), error: jest.fn(), + info: jest.fn(), }, initialize: jest.fn(), }, @@ -49,6 +51,13 @@ describe('WebSocketManager', () => { let mockWebex: WebexSDK; let mockWorker: any; + const fakeSubscribeRequest: SubscribeRequest = { + force: true, + isKeepAliveEnabled: false, + clientType: 'WebexCCSDK', + allowMultiLogin: true, + }; + beforeEach(() => { jest.clearAllMocks(); @@ -77,7 +86,6 @@ describe('WebSocketManager', () => { setTimeout(() => { MockWebSocket.inst.onopen(); MockWebSocket.inst.onmessage({ data: JSON.stringify({ type: "Welcome" }) }); - webSocketManager.close(false); }, 1); console.log = jest.fn(); @@ -97,17 +105,13 @@ describe('WebSocketManager', () => { (mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse); - const subscribeRequest: SubscribeRequest = { - routingId: 'test-routing-id', - }; - - await webSocketManager.initWebSocket({ body: subscribeRequest }); + await webSocketManager.initWebSocket({ body: fakeSubscribeRequest }); expect(mockWebex.request).toHaveBeenCalledWith({ service: WCC_API_GATEWAY, resource: SUBSCRIBE_API, method: 'POST', - body: subscribeRequest, + body: fakeSubscribeRequest, }); }); @@ -120,11 +124,7 @@ describe('WebSocketManager', () => { (mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse); - const subscribeRequest: SubscribeRequest = { - routingId: 'test-routing-id', - }; - - await webSocketManager.initWebSocket({ body: subscribeRequest }); + await webSocketManager.initWebSocket({ body: fakeSubscribeRequest }); webSocketManager.close(true, 'Test reason'); @@ -141,11 +141,7 @@ describe('WebSocketManager', () => { (mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse); - const subscribeRequest: SubscribeRequest = { - routingId: 'test-routing-id', - }; - - await webSocketManager.initWebSocket({ body: subscribeRequest }); + await webSocketManager.initWebSocket({ body: fakeSubscribeRequest }); setTimeout(() => { MockWebSocket.inst.onopen(); @@ -160,7 +156,7 @@ describe('WebSocketManager', () => { expect(MockWebSocket.inst.send).toHaveBeenCalledWith(JSON.stringify({ keepalive: 'true' })); }); - it('should handle WebSocket close due to network issue', async () => { + it('should handle web socket close and webSocketOnCloseHandler', async () => { const subscribeResponse = { body: { webSocketUrl: 'wss://fake-url', @@ -169,21 +165,34 @@ describe('WebSocketManager', () => { (mockWebex.request as jest.Mock).mockResolvedValueOnce(subscribeResponse); - const subscribeRequest: SubscribeRequest = { - routingId: 'test-routing-id', - }; - - await webSocketManager.initWebSocket({ body: subscribeRequest }); + await webSocketManager.initWebSocket({ body: fakeSubscribeRequest }); + webSocketManager.shouldReconnect = true; + // Mock navigator.onLine to simulate network issue + Object.defineProperty(global, 'navigator', { + value: { + onLine: false, + }, + configurable: true, + }); setTimeout(() => { - MockWebSocket.inst.onopen(); - mockWorker.onmessage({ - data: { - type: 'closeSocket' - } + MockWebSocket.inst.onclose({ + wasClean: false, + code: 1006, + reason: 'network issue', + target: MockWebSocket.inst, }); }, 1); - expect(MockWebSocket.inst.close).toHaveBeenCalled(); + // Wait for the close event to be handled + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(mockWorker.postMessage).toHaveBeenCalledWith({ type: 'terminate' }); + expect(LoggerProxy.logger.info).toHaveBeenCalledWith( + '[WebSocketStatus] | desktop online status is false' + ); + expect(LoggerProxy.logger.error).toHaveBeenCalledWith( + '[WebSocketStatus] | event=webSocketClose | WebSocket connection closed REASON: network issue' + ); }); }); \ No newline at end of file diff --git a/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts b/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts index 619438e8a0a..d601a67c772 100644 --- a/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts +++ b/packages/@webex/plugin-cc/test/unit/spec/services/core/aqm-reqs.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import AqmReqs from '../../../../../src/services/core/aqm-reqs'; import HttpRequest from '../../../../../src/services/core/HttpRequest'; -import { WebSocketManager } from '../../../../../src/services/core/WebSocket/WebSocketManager'; +import {WebSocketManager} from '../../../../../src/services/core/WebSocket/WebSocketManager'; +import LoggerProxy from '../../../../../src/logger-proxy'; +import {IHttpResponse} from '../../../../../src/types'; jest.mock('../../../../../src/services/core/HttpRequest'); jest.mock('../../../../../src/logger-proxy', () => ({ @@ -21,7 +23,7 @@ jest.mock('../../../../../src/services/core/WebSocket/WebSocketManager'); class MockCustomEvent extends Event { detail: T; - constructor(event: string, params: { detail: T }) { + constructor(event: string, params: {detail: T}) { super(event); this.detail = params.detail; } @@ -29,51 +31,62 @@ class MockCustomEvent extends Event { global.CustomEvent = MockCustomEvent as any; +global.window = { + setTimeout: global.setTimeout, +} as any; + const mockHttpRequest = HttpRequest as jest.MockedClass; const mockWebSocketManager = WebSocketManager as jest.MockedClass; describe('AqmReqs', () => { let httpRequestInstance: jest.Mocked; let webSocketManagerInstance: jest.Mocked; + const mockHttpRequestResolvedValue: IHttpResponse = { + status: 202, + data: {webSocketUrl: 'fake-url'}, + statusText: 'OK', + headers: {}, + config: {}, + }; + let aqm: AqmReqs; beforeEach(() => { jest.clearAllMocks(); httpRequestInstance = new HttpRequest() as jest.Mocked; mockHttpRequest.getInstance = jest.fn().mockReturnValue(httpRequestInstance); - webSocketManagerInstance = new WebSocketManager({ webex: {} as any }) as jest.Mocked; + webSocketManagerInstance = new WebSocketManager({ + webex: {} as any, + }) as jest.Mocked; - // Mock the addEventListener and dispatchEvent methods - webSocketManagerInstance.addEventListener = jest.fn(); - webSocketManagerInstance.dispatchEvent = jest.fn(); + // Mock the addEventListener method + webSocketManagerInstance.addEventListener = jest.fn((event, callback) => { + if (event === 'message') { + webSocketManagerInstance.dispatchEvent = callback; + } + }); + aqm = new AqmReqs(webSocketManagerInstance); mockWebSocketManager.mockImplementation(() => webSocketManagerInstance); }); it('AqmReqs should be defined', async () => { - httpRequestInstance.request.mockResolvedValueOnce({ - status: 202, - data: { webSocketUrl: 'fake-url' }, - statusText: 'OK', - headers: {}, - config: {}, - }); + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); - const aqm = new AqmReqs(webSocketManagerInstance); const req = aqm.req(() => ({ url: '/url', timeout: 2000, notifSuccess: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultConferenced', interactionId: 'intrid' }, + data: {type: 'AgentConsultConferenced', interactionId: 'intrid'}, }, msg: {}, }, notifFail: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultConferenceFailed' }, + data: {type: 'AgentConsultConferenceFailed'}, }, errId: 'Service.aqm.contact.consult', }, @@ -87,15 +100,8 @@ describe('AqmReqs', () => { }); it('AqmReqs notifcancel', async () => { - httpRequestInstance.request.mockResolvedValueOnce({ - status: 202, - data: { webSocketUrl: 'fake-url' }, - statusText: 'OK', - headers: {}, - config: {}, - }); + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); - const aqm = new AqmReqs(webSocketManagerInstance); const req = aqm.req(() => ({ url: '/url', timeout: 4000, @@ -112,7 +118,7 @@ describe('AqmReqs', () => { notifFail: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultFailed' }, + data: {type: 'AgentConsultFailed'}, }, errId: 'Service.aqm.contact.consult', }, @@ -133,15 +139,17 @@ describe('AqmReqs', () => { req({}), new Promise((resolve) => { setTimeout(() => { - webSocketManagerInstance.dispatchEvent(new CustomEvent('message', { - detail: JSON.stringify({ - type: 'RoutingMessage', - data: { - type: 'AgentCtqCancelled', - interactionId: '6920dda3-337a-48b1-b82d-2333392f9905', - }, - }), - })); + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'RoutingMessage', + data: { + type: 'AgentCtqCancelled', + interactionId: '6920dda3-337a-48b1-b82d-2333392f9905', + }, + }), + }) + ); resolve(); }, 1000); }), @@ -151,15 +159,8 @@ describe('AqmReqs', () => { }); it('AqmReqs notif success', async () => { - httpRequestInstance.request.mockResolvedValueOnce({ - status: 202, - data: { webSocketUrl: 'fake-url' }, - statusText: 'OK', - headers: {}, - config: {}, - }); + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); - const aqm = new AqmReqs(webSocketManagerInstance); const req = aqm.req(() => ({ url: '/url', timeout: 4000, @@ -176,7 +177,7 @@ describe('AqmReqs', () => { notifFail: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultFailed' }, + data: {type: 'AgentConsultFailed'}, }, errId: 'Service.aqm.contact.consult', }, @@ -197,15 +198,17 @@ describe('AqmReqs', () => { req({}), new Promise((resolve) => { setTimeout(() => { - webSocketManagerInstance.dispatchEvent(new CustomEvent('message', { - detail: JSON.stringify({ - type: 'RoutingMessage', - data: { - type: 'AgentConsultCreated', - interactionId: '6920dda3-337a-48b1-b82d-2333392f9906', - }, - }), - })); + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'RoutingMessage', + data: { + type: 'AgentConsultCreated', + interactionId: '6920dda3-337a-48b1-b82d-2333392f9906', + }, + }), + }) + ); resolve(); }, 1000); }), @@ -217,7 +220,6 @@ describe('AqmReqs', () => { it('AqmReqs notif success with async error', async () => { httpRequestInstance.request.mockRejectedValueOnce(new Error('Async error')); - const aqm = new AqmReqs(webSocketManagerInstance); const req = aqm.req(() => ({ url: '/url', timeout: 4000, @@ -234,7 +236,7 @@ describe('AqmReqs', () => { notifFail: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultFailed' }, + data: {type: 'AgentConsultFailed'}, }, errId: 'Service.aqm.contact.consult', }, @@ -258,15 +260,8 @@ describe('AqmReqs', () => { }); it('AqmReqs notif fail', async () => { - httpRequestInstance.request.mockResolvedValueOnce({ - status: 202, - data: { webSocketUrl: 'fake-url' }, - statusText: 'OK', - headers: {}, - config: {}, - }); + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); - const aqm = new AqmReqs(webSocketManagerInstance); const req = aqm.req(() => ({ url: '/url', timeout: 4000, @@ -283,7 +278,7 @@ describe('AqmReqs', () => { notifFail: { bind: { type: 'RoutingMessage', - data: { type: 'AgentConsultFailed' }, + data: {type: 'AgentConsultFailed'}, }, errId: 'Service.aqm.contact.consult', }, @@ -304,15 +299,17 @@ describe('AqmReqs', () => { req({}), new Promise((resolve) => { setTimeout(() => { - webSocketManagerInstance.dispatchEvent(new CustomEvent('message', { - detail: JSON.stringify({ - type: 'RoutingMessage', - data: { - type: 'AgentConsultFailed', - interactionId: '6920dda3-337a-48b1-b82d-2333392f9907', - }, - }), - })); + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'RoutingMessage', + data: { + type: 'AgentConsultFailed', + interactionId: '6920dda3-337a-48b1-b82d-2333392f9907', + }, + }), + }) + ); resolve(); }, 1000); }), @@ -320,4 +317,225 @@ describe('AqmReqs', () => { expect(p).toBeDefined(); } catch (e) {} }); -}); \ No newline at end of file + + describe('Event tests', () => { + it('should handle onMessage events', async () => { + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); + + const req = aqm.req(() => ({ + url: '/url', + timeout: 2000, + notifSuccess: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultConferenced', interactionId: 'intrid'}, + }, + msg: {}, + }, + notifFail: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultConferenceFailed'}, + }, + errId: 'Service.aqm.contact.consult', + }, + })); + + try { + await req({}); + } catch (e) { + expect(e).toBeDefined(); + } + + // Welcome event + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'Welcome', + data: {type: 'WelcomeEvent'}, + }), + }) + ); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith('Welcome message from Notifs Websocket'); + + // Keep-alive events + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + keepalive: 'true', + data: {type: 'KeepaliveEvent'}, + }), + }) + ); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith('Keepalive from web socket'); + + // Unhandled event + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'UnhandledMessage', + data: {type: 'UnhandledEvent'}, + }), + }) + ); + + expect(LoggerProxy.logger.info).toHaveBeenCalledWith( + 'event=missingEventHandler | [AqmReqs] missing routing message handler' + ); + }); + + it('should correctly print bind object', () => { + const bind = { + type: 'RoutingMessage', + data: { + type: 'AgentConsultCreated', + interactionId: 'intrid', + }, + }; + const result = aqm['bindPrint'](bind); + expect(result).toBe( + 'type=RoutingMessage,data=(type=AgentConsultCreated,interactionId=intrid)' + ); + }); + + it('should correctly check bind object', () => { + const bind = { + type: 'RoutingMessage', + data: { + type: 'AgentConsultCreated', + interactionId: 'intrid', + }, + }; + const msg = { + type: 'RoutingMessage', + data: { + type: 'AgentConsultCreated', + interactionId: 'intrid', + }, + }; + const result = aqm['bindCheck'](bind, msg); + expect(result).toBe(true); + }); + + it('should handle reqEmpty', async () => { + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); + + const reqEmpty = aqm.reqEmpty(() => ({ + url: '/url', + timeout: 2000, + notifSuccess: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultConferenced', interactionId: 'intrid'}, + }, + msg: {}, + }, + notifFail: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultConferenceFailed'}, + }, + errId: 'Service.aqm.contact.consult', + }, + })); + + try { + await reqEmpty(); + } catch (e) { + expect(e).toBeDefined(); + } + }); + + it('should handle failed request with err function', async () => { + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); + + const conf = { + host: 'fake-host', + url: '/url', + method: 'POST', + data: {}, + notifSuccess: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultCreated', interactionId: 'intrid'}, + }, + }, + notifFail: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultFailed'}, + }, + err: (msg: any) => new Error('Custom error'), + }, + }; + + const promise = aqm['createPromise'](conf); + global.setTimeout(() => { + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify({ + type: 'RoutingMessage', + data: { + type: 'AgentConsultFailed', + interactionId: 'intrid', + }, + }), + }) + ); + }, 0); + + await expect(promise).rejects.toThrow('Custom error'); + }); + + it('should handle request with notifCancel', async () => { + httpRequestInstance.request.mockResolvedValueOnce(mockHttpRequestResolvedValue); + + const conf = { + host: 'fake-host', + url: '/url', + method: 'POST', + data: {}, + notifSuccess: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultCreated', interactionId: 'intrid'}, + }, + }, + notifFail: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentConsultFailed'}, + }, + errId: 'Service.aqm.contact.consult', + }, + notifCancel: { + bind: { + type: 'RoutingMessage', + data: {type: 'AgentCtqCancelled', interactionId: 'intrid'}, + }, + }, + }; + + const promise = aqm['createPromise'](conf); + const eventData = { + type: 'RoutingMessage', + data: { + type: 'AgentCtqCancelled', + interactionId: 'intrid', + }, + }; + global.setTimeout(() => { + webSocketManagerInstance.dispatchEvent( + new CustomEvent('message', { + detail: JSON.stringify(eventData), + }) + ); + }, 0); + + const result = await promise; + expect(result).toEqual(eventData); + }); + }); +});