Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 262 additions & 1 deletion packages/calling/src/CallingClient/calling/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import * as Utils from '../../common/Utils';
import {CALL_EVENT_KEYS, CallEvent, RoapEvent, RoapMessage} from '../../Events/types';
import {DEFAULT_SESSION_TIMER, ICE_CANDIDATES_TIMEOUT} from '../constants';
import {CallDirection, CallType, ServiceIndicator, WebexRequestPayload} from '../../common/types';
import {METRIC_EVENT, TRANSFER_ACTION, METRIC_TYPE} from '../../Metrics/types';
import {
METRIC_EVENT,
TRANSFER_ACTION,
METRIC_TYPE,
MEDIA_CONNECTION_ACTION,
} from '../../Metrics/types';
import {Call, createCall} from './call';
import {
MobiusCallState,
Expand Down Expand Up @@ -452,6 +457,262 @@ describe('Call Tests', () => {
);
});

it('registers and unregisters media connection listeners with stable handlers', () => {
const mockStream = {
outputStream: {
getAudioTracks: jest.fn().mockReturnValue([mockTrack]),
},
on: jest.fn(),
getEffectByKind: jest.fn().mockReturnValue(undefined),
};
const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream;
const call = createCall(
activeUrl,
webex,
CallDirection.OUTBOUND,
deviceId,
mockLineId,
deleteCallFromCollection,
defaultServiceIndicator,
dest
);

call.dial(localAudioStream);

const mediaOnMock = call['mediaConnection'].on as jest.Mock;
const mediaOffSpy = jest.spyOn(call['mediaConnection'], 'off');

expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.REMOTE_TRACK_ADDED,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOnMock).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR,
expect.any(Function)
);

call['unregisterMediaConnectionListeners']();

expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.REMOTE_TRACK_ADDED,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED,
expect.any(Function)
);
expect(mediaOffSpy).toBeCalledWith(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR,
expect.any(Function)
);
});

it('handles ICE listener payloads and submits metrics with event names', () => {
const mockStream = {
outputStream: {
getAudioTracks: jest.fn().mockReturnValue([mockTrack]),
},
on: jest.fn(),
getEffectByKind: jest.fn().mockReturnValue(undefined),
};
const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream;
const call = createCall(
activeUrl,
webex,
CallDirection.OUTBOUND,
deviceId,
mockLineId,
deleteCallFromCollection,
defaultServiceIndicator,
dest
);

call.dial(localAudioStream);

const metricSpy = jest.spyOn(call['metricManager'], 'submitMediaMetric');
const warnSpy = jest.spyOn(log, 'warn');

const getHandlerForEvent = (eventName: string) =>
(call['mediaConnection'].on as jest.Mock).mock.calls.find(
([name]) => name === eventName
)?.[1];

const iceGatheringHandler = getHandlerForEvent(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_GATHERING_STATE_CHANGED
);
const peerConnectionHandler = getHandlerForEvent(
InternalMediaCoreModule.MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED
);
const iceConnectionHandler = getHandlerForEvent(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED
);
const iceCandidateErrorHandler = getHandlerForEvent(
InternalMediaCoreModule.MediaConnectionEventNames.ICE_CANDIDATE_ERROR
);

iceGatheringHandler({iceGatheringState: 'gathering'});
peerConnectionHandler({connectionState: 'connected'});
iceConnectionHandler({iceConnectionState: 'completed'});
iceCandidateErrorHandler({
errorCode: 701,
errorText: 'STUN host lookup failed',
url: 'stun:example.org:3478',
});

expect(metricSpy).toHaveBeenNthCalledWith(
1,
METRIC_EVENT.MEDIA,
MEDIA_CONNECTION_ACTION.ICE_GATHERING_STATE_CHANGED,
METRIC_TYPE.BEHAVIORAL,
call.getCallId(),
call.getCorrelationId(),
undefined,
undefined,
'gathering'
);
expect(metricSpy).toHaveBeenNthCalledWith(
2,
METRIC_EVENT.MEDIA,
MEDIA_CONNECTION_ACTION.PEER_CONNECTION_STATE_CHANGED,
METRIC_TYPE.BEHAVIORAL,
call.getCallId(),
call.getCorrelationId(),
undefined,
undefined,
'connected'
);
expect(metricSpy).toHaveBeenNthCalledWith(
3,
METRIC_EVENT.MEDIA,
MEDIA_CONNECTION_ACTION.ICE_CONNECTION_STATE_CHANGED,
METRIC_TYPE.BEHAVIORAL,
call.getCallId(),
call.getCorrelationId(),
undefined,
undefined,
'completed'
);
expect(metricSpy).toHaveBeenNthCalledWith(
4,
METRIC_EVENT.MEDIA_ERROR,
MEDIA_CONNECTION_ACTION.ICE_CANDIDATE_ERROR,
METRIC_TYPE.BEHAVIORAL,
call.getCallId(),
call.getCorrelationId(),
undefined,
undefined,
undefined,
expect.any(CallError)
);
const mediaErrorCall = metricSpy.mock.calls[3];

expect((mediaErrorCall[mediaErrorCall.length - 1] as CallError).getCallError().message).toBe(
'ICE candidate error occurred: {"address":null,"errorCode":701,"errorText":"STUN host lookup failed","port":null,"url":"stun:example.org:3478"}'
);

expect(warnSpy).toHaveBeenCalledWith(
'ICE candidate error occurred: {"address":null,"errorCode":701,"errorText":"STUN host lookup failed","port":null,"url":"stun:example.org:3478"}',
{file: 'call', method: 'mediaIceEventsListener'}
);
});

it('handles ROAP failure listener and submits media error metric', () => {
const mockStream = {
outputStream: {
getAudioTracks: jest.fn().mockReturnValue([mockTrack]),
},
on: jest.fn(),
getEffectByKind: jest.fn().mockReturnValue(undefined),
};
const localAudioStream = mockStream as unknown as InternalMediaCoreModule.LocalMicrophoneStream;
const call = createCall(
activeUrl,
webex,
CallDirection.OUTBOUND,
deviceId,
mockLineId,
deleteCallFromCollection,
defaultServiceIndicator,
dest
);

call.dial(localAudioStream);

const metricSpy = jest.spyOn(call['metricManager'], 'submitMediaMetric');
const warnSpy = jest.spyOn(log, 'warn');
const roapFailureHandler = (call['mediaConnection'].on as jest.Mock).mock.calls.find(
([name]) => name === InternalMediaCoreModule.MediaConnectionEventNames.ROAP_FAILURE
)?.[1];

const roapFailure = new Error('Failed to process remote SDP');

roapFailureHandler(roapFailure);

expect(metricSpy).toHaveBeenCalledWith(
METRIC_EVENT.MEDIA_ERROR,
MEDIA_CONNECTION_ACTION.ROAP_FAILURE,
METRIC_TYPE.BEHAVIORAL,
call.getCallId(),
call.getCorrelationId(),
undefined,
undefined,
undefined,
expect.any(CallError)
);

const roapFailureMetricCall = metricSpy.mock.calls.find(
([name, metricAction]) =>
name === METRIC_EVENT.MEDIA_ERROR && metricAction === MEDIA_CONNECTION_ACTION.ROAP_FAILURE
);

expect(roapFailureMetricCall).toBeDefined();

expect(
(roapFailureMetricCall?.[roapFailureMetricCall.length - 1] as CallError).getCallError()
.message
).toBe('ROAP failure occurred: Failed to process remote SDP');
expect(warnSpy).toHaveBeenCalledWith('ROAP failure occurred: Failed to process remote SDP', {
file: 'call',
method: 'mediaRoapEventsListener',
});
});

it('sends connect before ROAP answer when inbound offer is delayed', async () => {
const mockStream = {
outputStream: {
Expand Down
Loading
Loading