@webex/plugin-meetings 1.153.1 → 1.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/common/errors/webex-errors.js +27 -3
  2. package/dist/common/errors/webex-errors.js.map +1 -1
  3. package/dist/constants.js +20 -13
  4. package/dist/constants.js.map +1 -1
  5. package/dist/meeting/index.js +54 -41
  6. package/dist/meeting/index.js.map +1 -1
  7. package/dist/meeting/request.js +24 -20
  8. package/dist/meeting/request.js.map +1 -1
  9. package/dist/meeting/state.js +8 -15
  10. package/dist/meeting/state.js.map +1 -1
  11. package/dist/meeting/util.js +2 -1
  12. package/dist/meeting/util.js.map +1 -1
  13. package/dist/meetings/index.js +8 -1
  14. package/dist/meetings/index.js.map +1 -1
  15. package/dist/metrics/config.js +3 -1
  16. package/dist/metrics/config.js.map +1 -1
  17. package/dist/metrics/index.js +23 -9
  18. package/dist/metrics/index.js.map +1 -1
  19. package/dist/peer-connection-manager/index.js +63 -56
  20. package/dist/peer-connection-manager/index.js.map +1 -1
  21. package/dist/reconnection-manager/index.js +1 -1
  22. package/dist/reconnection-manager/index.js.map +1 -1
  23. package/dist/roap/handler.js +4 -4
  24. package/dist/roap/handler.js.map +1 -1
  25. package/package.json +5 -5
  26. package/src/common/errors/webex-errors.js +21 -2
  27. package/src/constants.js +16 -11
  28. package/src/meeting/index.js +85 -68
  29. package/src/meeting/request.js +10 -5
  30. package/src/meeting/state.js +8 -17
  31. package/src/meeting/util.js +2 -1
  32. package/src/meetings/index.js +14 -3
  33. package/src/metrics/config.js +1 -0
  34. package/src/metrics/index.js +23 -9
  35. package/src/peer-connection-manager/index.js +61 -60
  36. package/src/reconnection-manager/index.js +3 -3
  37. package/src/roap/handler.js +5 -5
  38. package/test/unit/spec/meeting/index.js +39 -7
  39. package/test/unit/spec/meeting/request.js +53 -1
  40. package/test/unit/spec/meeting/utils.js +41 -0
  41. package/test/unit/spec/meetings/index.js +3 -1
  42. package/test/unit/spec/metrics/index.js +6 -6
  43. package/test/unit/spec/peerconnection-manager/index.js +69 -0
@@ -14,7 +14,7 @@ import BrowserDetection from '../common/browser-detection';
14
14
 
15
15
  import {
16
16
  error, eventType, errorCodes as ERROR_CODE, OS_NAME, UNKNOWN, CLIENT_NAME,
17
- mediaType
17
+ mediaType, PLATFORM
18
18
  } from './config';
19
19
 
20
20
  const OSMap = {
@@ -489,11 +489,11 @@ class Metrics {
489
489
  }
490
490
 
491
491
  /**
492
- * Uploads given metric to the Metrics service as an operational metric.
492
+ * Uploads given metric to the Metrics service as an Behavioral metric.
493
493
  * Metadata about the environment such as browser, OS, SDK and their versions
494
494
  * are automatically added when the metric is sent.
495
495
  *
496
- * The Metrics service will send an operational metric to InfluxDB for
496
+ * The Metrics service will send an Behavioral metric to InfluxDB for
497
497
  * aggregation.
498
498
  * See https://confluence-eng-gpk2.cisco.com/conf/display/WBXT/Getting+started+with+Metrics+Service.
499
499
  *
@@ -503,12 +503,24 @@ class Metrics {
503
503
  *
504
504
  * @returns {void}
505
505
  */
506
- sendOperationalMetric(metricName, metricFields = {}, metricTags = {}) {
506
+ sendBehavioralMetric(metricName, metricFields = {}, metricTags = {}) {
507
507
  const fields = {
508
508
  ...metricFields,
509
509
  browser_version: getBrowserVersion(),
510
510
  os_version: getOSVersion(),
511
- sdk_version: this.webex.version
511
+ sdk_version: this.webex.version,
512
+ platform: PLATFORM
513
+ };
514
+
515
+ const context = {
516
+ app: {
517
+ version: this.webex.version
518
+ },
519
+ locale: 'en-US',
520
+ os: {
521
+ name: getOSName(),
522
+ version: getOSVersion()
523
+ }
512
524
  };
513
525
 
514
526
  const tags = {
@@ -517,17 +529,19 @@ class Metrics {
517
529
  org_id: this.webex.credentials.getOrgId(),
518
530
  os: getOSName(),
519
531
  domain: window.location.hostname,
520
- client_id: this.webex.credentials.config.client_id
532
+ client_id: this.webex.credentials.config.client_id,
533
+ user_id: this.webex.internal.device.userId
521
534
  };
522
535
 
523
536
  if (!metricName) {
524
- throw Error('Missing operational metric name. Please provide one');
537
+ throw Error('Missing behavioral metric name. Please provide one');
525
538
  }
526
539
 
527
540
  this.webex.internal.metrics.submitClientMetrics(metricName, {
528
- type: ['operational'],
541
+ type: ['behavioral', 'operational'],
529
542
  fields,
530
- tags
543
+ tags,
544
+ context
531
545
  });
532
546
  }
533
547
  }
@@ -10,9 +10,8 @@ import Metrics from '../metrics';
10
10
  import LoggerProxy from '../common/logs/logger-proxy';
11
11
  import StaticConfig from '../common/config';
12
12
  import {
13
- RETRY_TIMEOUT,
14
- ICE_TIMEOUT,
15
- HOST,
13
+ COMPLETE,
14
+ GATHERING,
16
15
  AUDIO,
17
16
  SDP,
18
17
  ICE_STATE,
@@ -22,13 +21,12 @@ import {
22
21
  OFFER,
23
22
  QUALITY_LEVELS,
24
23
  MAX_FRAMESIZES,
25
- METRICS_OPERATIONAL_MEASURES,
26
- IPV4_REGEX
24
+ BEHAVIORAL_METRICS
27
25
  } from '../constants';
28
26
  import {error, eventType} from '../metrics/config';
29
27
  import MediaError from '../common/errors/media';
30
28
  import ParameterError from '../common/errors/parameter';
31
- import {InvalidSdpError} from '../common/errors/webex-errors';
29
+ import {InvalidSdpError, IceGatheringFailed} from '../common/errors/webex-errors';
32
30
  import BrowserDetection from '../common/browser-detection';
33
31
 
34
32
  import PeerConnectionUtils from './util';
@@ -122,18 +120,10 @@ const isSdpInvalid = (sdp) => {
122
120
  const parsedSdp = sdpTransform.parse(sdp);
123
121
 
124
122
  for (const mediaLine of parsedSdp.media) {
125
- if (mediaLine.candidates && mediaLine.candidates.length === 0) {
123
+ if (!mediaLine.candidates || mediaLine.candidates?.length === 0) {
126
124
  LoggerProxy.logger.error('PeerConnectionManager:index#isSdpInvalid --> iceCandidate: Ice candadate never completed');
127
125
 
128
- return 'iceCandidate: Ice candadate never completed';
129
- }
130
- // Sometimes the candidates might be there but only IPV6 we need to makes sure we have IPV4
131
- const hostCandidate = mediaLine.candidates.filter((candidate) => !!(candidate.type === HOST && candidate.ip.match(IPV4_REGEX)));
132
-
133
- if (hostCandidate.length === 0) {
134
- LoggerProxy.logger.error('PeerConnectionManager:index#isSdpInvalid --> iceCandidate: no IPV4 candidate present');
135
-
136
- return 'iceCandidate: no IPV4 candidate present';
126
+ return 'iceCandidate: Ice gathering never completed';
137
127
  }
138
128
 
139
129
  if (SDP.BAD_MEDIA_PORTS.includes(mediaLine.port)) {
@@ -193,45 +183,52 @@ pc.setContentSlides = (screenPc) => {
193
183
  */
194
184
  pc.iceCandidate = (peerConnection, {remoteQualityLevel}) =>
195
185
  new Promise((resolve, reject) => {
196
- // TODO: we dont need timeout as we can check the api state and validate.
197
- const timeout = setTimeout(() => {
186
+ const now = Date.now();
187
+ const doneGatheringIceCandidate = () => {
188
+ const miliseconds = parseInt(Math.abs(Date.now() - now), 4);
189
+
198
190
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
199
191
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
200
192
  peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
201
193
 
202
- if (isSdpInvalid(peerConnection.sdp)) {
203
- setTimeout(() => {
204
- // peerconnection does gather ice candidate IP but in some cases due to firewall
205
- // or proxy the ice candidate does not get gathered so we need to wait and then retry
206
- // if still not valid then throw an error saying missing ice candidate
207
- // if ice candidate still not present after retry
208
- const invalidSdpPresent = isSdpInvalid(peerConnection.sdp);
194
+ const invalidSdpPresent = isSdpInvalid(peerConnection.sdp);
209
195
 
210
- if (!invalidSdpPresent) {
211
- resolve(peerConnection);
212
- }
213
- else {
214
- LoggerProxy.logger.error('PeerConnectionManager:index#iceCandidate --> SDP not valid after waiting.');
215
- reject(new InvalidSdpError(invalidSdpPresent));
216
- }
217
- }, RETRY_TIMEOUT);
196
+ if (invalidSdpPresent) {
197
+ LoggerProxy.logger.error('PeerConnectionManager:index#iceCandidate --> SDP not valid after waiting.');
198
+ reject(new InvalidSdpError(invalidSdpPresent));
218
199
  }
219
- else {
220
- resolve(peerConnection);
200
+ LoggerProxy.logger.log(`PeerConnectionManager:index#iceCandidate --> Time to gather ice candidate ${miliseconds} miliseconds`);
201
+
202
+
203
+ resolve();
204
+ };
205
+
206
+ // If ice has already been gathered
207
+ if (peerConnection.iceGatheringState === COMPLETE) {
208
+ doneGatheringIceCandidate();
209
+ }
210
+
211
+ peerConnection.onIceGatheringStateChange = () => {
212
+ if (peerConnection.iceGatheringState === COMPLETE) {
213
+ doneGatheringIceCandidate(peerConnection);
221
214
  }
222
- }, ICE_TIMEOUT);
215
+ if (peerConnection.iceGatheringState === GATHERING) {
216
+ LoggerProxy.logger.log('PeerConnectionManager:index#onIceGatheringStateChange --> Ice state changed to gathering');
217
+ }
218
+ };
223
219
 
224
220
  peerConnection.onicecandidate = (evt) => {
225
- if (!evt.candidate && !peerConnection.sdp) {
226
- peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
227
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
228
- peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
229
-
230
- if (evt.candidate === null && !isSdpInvalid(peerConnection.sdp)) {
231
- clearTimeout(timeout);
232
- resolve(peerConnection);
233
- }
221
+ if (evt.candidate === null) {
222
+ doneGatheringIceCandidate(peerConnection);
234
223
  }
224
+ else {
225
+ LoggerProxy.logger.log(`PeerConnectionManager:index#onicecandidate --> Candidate ${evt.candidate?.type} ${evt.candidate?.protocol} ${evt.candidate?.address}:${evt.candidate?.port}`);
226
+ }
227
+ };
228
+
229
+ peerConnection.onicecandidateerror = (event) => {
230
+ LoggerProxy.logger.error('PeerConnectionManager:index#onicecandidateerror --> Failed to gather ice candidate.', event);
231
+ reject(new IceGatheringFailed());
235
232
  };
236
233
  });
237
234
 
@@ -345,7 +342,7 @@ pc.setRemoteSessionDetails = (
345
342
  LoggerProxy.logger.error(`Peer-connection-manager:index#setRemoteDescription --> ${error} missing remotesdp`);
346
343
 
347
344
 
348
- const metricName = METRICS_OPERATIONAL_MEASURES.PEERCONNECTION_FAILURE;
345
+ const metricName = BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE;
349
346
  const data = {
350
347
  correlation_id: meetingId,
351
348
  reason: error.message,
@@ -355,7 +352,7 @@ pc.setRemoteSessionDetails = (
355
352
  type: error.name
356
353
  };
357
354
 
358
- Metrics.sendOperationalMetric(metricName, data, metadata);
355
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
359
356
 
360
357
  return Metrics.postEvent({
361
358
  event: eventType.REMOTE_SDP_RECEIVED,
@@ -406,9 +403,6 @@ pc.createOffer = (peerConnection, {
406
403
  })
407
404
  .then(() => pc.iceCandidate(peerConnection, {remoteQualityLevel}))
408
405
  .then(() => {
409
- peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
410
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
411
- peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
412
406
  if (!checkH264Support(peerConnection.sdp)) {
413
407
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
414
408
  }
@@ -429,8 +423,8 @@ pc.createOffer = (peerConnection, {
429
423
  .catch((error) => {
430
424
  LoggerProxy.logger.error(`Peer-connection-manager:index#createOffer --> ${error}`);
431
425
  if (error instanceof InvalidSdpError) {
432
- Metrics.sendOperationalMetric(
433
- METRICS_OPERATIONAL_MEASURES.INVALID_ICE_CANDIDATE,
426
+ Metrics.sendBehavioralMetric(
427
+ BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
434
428
  {
435
429
  correlation_id: meetingId,
436
430
  code: error.code,
@@ -439,7 +433,7 @@ pc.createOffer = (peerConnection, {
439
433
  );
440
434
  }
441
435
  else {
442
- const metricName = METRICS_OPERATIONAL_MEASURES.PEERCONNECTION_FAILURE;
436
+ const metricName = BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE;
443
437
  const data = {
444
438
  correlation_id: meetingId,
445
439
  reason: error.message,
@@ -449,7 +443,7 @@ pc.createOffer = (peerConnection, {
449
443
  type: error.name
450
444
  };
451
445
 
452
- Metrics.sendOperationalMetric(metricName, data, metadata);
446
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
453
447
  }
454
448
 
455
449
  Metrics.postEvent({
@@ -544,15 +538,15 @@ pc.createAnswer = (params, {meetingId, remoteQualityLevel}) => {
544
538
  })
545
539
  .catch((error) => {
546
540
  if (error instanceof InvalidSdpError) {
547
- Metrics.sendOperationalMetric(
548
- METRICS_OPERATIONAL_MEASURES.INVALID_ICE_CANDIDATE,
541
+ Metrics.sendBehavioralMetric(
542
+ BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
549
543
  {
550
544
  correlation_id: meetingId
551
545
  }
552
546
  );
553
547
  }
554
548
  else {
555
- const metricName = METRICS_OPERATIONAL_MEASURES.PEERCONNECTION_FAILURE;
549
+ const metricName = BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE;
556
550
  const data = {
557
551
  correlation_id: meetingId,
558
552
  reason: error.message,
@@ -562,7 +556,7 @@ pc.createAnswer = (params, {meetingId, remoteQualityLevel}) => {
562
556
  type: error.name
563
557
  };
564
558
 
565
- Metrics.sendOperationalMetric(metricName, data, metadata);
559
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
566
560
  }
567
561
 
568
562
  LoggerProxy.logger.error(`PeerConnectionManager:index#setRemoteSessionDetails --> Error creating remote session, error: ${error}`);
@@ -625,8 +619,8 @@ pc.setPeerConnectionEvents = (meeting) => {
625
619
  function: 'connectionFailed'
626
620
  });
627
621
 
628
- Metrics.sendOperationalMetric(
629
- METRICS_OPERATIONAL_MEASURES.CONNECTION_FAILURE,
622
+ Metrics.sendBehavioralMetric(
623
+ BEHAVIORAL_METRICS.CONNECTION_FAILURE,
630
624
  {
631
625
  correlation_id: meeting.correlationId,
632
626
  locus_id: meeting.locusId
@@ -648,6 +642,13 @@ pc.setPeerConnectionEvents = (meeting) => {
648
642
  // Ice connection state goes to connected when both client and server sends STUN packets and
649
643
  // Established connected between them. Firefox does not trigger COMPLETED and only trigger CONNECTED
650
644
  Metrics.postEvent({event: eventType.ICE_END, meeting});
645
+ Metrics.sendBehavioralMetric(
646
+ BEHAVIORAL_METRICS.CONNECTION_SUCCESS,
647
+ {
648
+ correlation_id: meeting.correlationId,
649
+ locus_id: meeting.locusId
650
+ }
651
+ );
651
652
  meeting.setNetworkStatus(NETWORK_STATUS.CONNECTED);
652
653
  meeting.reconnectionManager.iceReconnected();
653
654
  LoggerProxy.logger.info('PeerConnectionManager:index#setPeerConnectionEvents --> ICE STATE CONNECTED.');
@@ -8,7 +8,7 @@ import LoggerProxy from '../common/logs/logger-proxy';
8
8
  import Trigger from '../common/events/trigger-proxy';
9
9
  import {
10
10
  EVENT_TRIGGERS,
11
- METRICS_OPERATIONAL_MEASURES,
11
+ BEHAVIORAL_METRICS,
12
12
  RECONNECTION,
13
13
  SHARE_STATUS,
14
14
  SHARE_STOPPED_REASON,
@@ -424,8 +424,8 @@ export default class ReconnectionManager {
424
424
  }
425
425
  else {
426
426
  LoggerProxy.logger.error('ReconnectionManager:index#rejoinMeeting --> Unable to rejoin meeting after max attempts.', joinError);
427
- Metrics.sendOperationalMetric(
428
- METRICS_OPERATIONAL_MEASURES.MEETING_MAX_REJOIN_FAILURE,
427
+ Metrics.sendBehavioralMetric(
428
+ BEHAVIORAL_METRICS.MEETING_MAX_REJOIN_FAILURE,
429
429
  {
430
430
  locus_id: this.meeting.locusUrl.split('/').pop(),
431
431
  reason: joinError.message,
@@ -2,7 +2,7 @@
2
2
  import {StatelessWebexPlugin} from '@webex/webex-core';
3
3
 
4
4
  import LoggerProxy from '../common/logs/logger-proxy';
5
- import {ROAP, _OFFER_, METRICS_OPERATIONAL_MEASURES} from '../constants';
5
+ import {ROAP, _OFFER_, BEHAVIORAL_METRICS} from '../constants';
6
6
  import Metrics from '../metrics';
7
7
 
8
8
  import RoapUtil from './util';
@@ -44,14 +44,14 @@ const handleSessionStep = ({
44
44
  if (session.OFFER && messageType === _OFFER_) {
45
45
  session.GLARE_OFFER = roap.msg;
46
46
  session.GLARE_OFFER.remote = !!roap.remote;
47
- const metricName = METRICS_OPERATIONAL_MEASURES.ROAP_GLARE_CONDITION;
47
+ const metricName = BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION;
48
48
  const data = {
49
49
  correlation_id: correlationId,
50
50
  locus_id: locusUrl.split('/').pop(),
51
51
  sequence: sequenceId
52
52
  };
53
53
 
54
- Metrics.sendOperationalMetric(metricName, data);
54
+ Metrics.sendBehavioralMetric(metricName, data);
55
55
 
56
56
  LoggerProxy.logger.warn(`Roap:handler#handleSessionStep --> Glare condition occurred with new mercury event, sequenceId: ${sequenceId}`);
57
57
  }
@@ -112,7 +112,7 @@ export default class RoapHandler extends StatelessWebexPlugin {
112
112
  });
113
113
  })
114
114
  .catch((error) => {
115
- const metricName = METRICS_OPERATIONAL_MEASURES.ROAP_ANSWER_FAILURE;
115
+ const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
116
116
  const data = {
117
117
  correlation_id: meeting.correlationId,
118
118
  locus_id: meeting.locusUrl.split('/').pop(),
@@ -123,7 +123,7 @@ export default class RoapHandler extends StatelessWebexPlugin {
123
123
  type: error.name
124
124
  };
125
125
 
126
- Metrics.sendOperationalMetric(metricName, data, metadata);
126
+ Metrics.sendBehavioralMetric(metricName, data, metadata);
127
127
  LoggerProxy.logger.error(`Roap:handler#perform --> Error occured during wait receive answer, continuing, ${error}`);
128
128
  });
129
129
  }
@@ -30,7 +30,7 @@ import Metrics from '@webex/plugin-meetings/src/metrics';
30
30
  import {
31
31
  FLOOR_ACTION,
32
32
  SHARE_STATUS,
33
- METRICS_OPERATIONAL_MEASURES,
33
+ BEHAVIORAL_METRICS,
34
34
  MEETING_INFO_FAILURE_REASON,
35
35
  PASSWORD_STATUS,
36
36
  EVENTS,
@@ -48,6 +48,7 @@ import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-er
48
48
  import ParameterError from '../../../../src/common/errors/parameter';
49
49
  import PasswordError from '../../../../src/common/errors/password-error';
50
50
  import CaptchaError from '../../../../src/common/errors/captcha-error';
51
+ import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
51
52
  import DefaultSDKConfig from '../../../../src/config';
52
53
  import testUtils from '../../../utils/testUtils';
53
54
  import {MeetingInfoV2CaptchaError, MeetingInfoV2PasswordError} from '../../../../src/meeting-info/meeting-info-v2';
@@ -609,6 +610,17 @@ describe('plugin-meetings', () => {
609
610
  });
610
611
  });
611
612
  describe('#join', () => {
613
+ let sandbox = null;
614
+
615
+ beforeEach(() => {
616
+ sandbox = sinon.createSandbox();
617
+ });
618
+
619
+ afterEach(() => {
620
+ sandbox.restore();
621
+ sandbox = null;
622
+ });
623
+
612
624
  it('should have #join', () => {
613
625
  assert.exists(meeting.join);
614
626
  });
@@ -618,8 +630,9 @@ describe('plugin-meetings', () => {
618
630
  });
619
631
  describe('successful', () => {
620
632
  beforeEach(() => {
621
- MeetingUtil.joinMeeting = sinon.stub().returns(Promise.resolve());
633
+ sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve());
622
634
  });
635
+
623
636
  it('should join the meeting and return promise', async () => {
624
637
  const join = meeting.join();
625
638
 
@@ -649,9 +662,10 @@ describe('plugin-meetings', () => {
649
662
  });
650
663
  describe('failure', () => {
651
664
  beforeEach(() => {
652
- MeetingUtil.joinMeeting = sinon.stub().returns(Promise.reject());
665
+ sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.reject());
653
666
  meeting.logger.log = sinon.stub().returns(true);
654
667
  });
668
+
655
669
  describe('guest join', () => {
656
670
  beforeEach(() => {
657
671
  MeetingUtil.isPinOrGuest = sinon.stub().returns(true);
@@ -662,6 +676,24 @@ describe('plugin-meetings', () => {
662
676
  assert.calledOnce(MeetingUtil.joinMeeting);
663
677
  });
664
678
  });
679
+ it('should succeed when called again after IntentToJoinError error', async () => {
680
+ let joinSucceeded = false;
681
+
682
+ try {
683
+ await meeting.join();
684
+ joinSucceeded = true;
685
+ }
686
+ catch (e) {
687
+ assert.instanceOf(e, IntentToJoinError);
688
+ }
689
+ assert.isFalse(joinSucceeded);
690
+
691
+ // IntentToJoinError means that client should call join() again
692
+ // with moderator and pin explicitly set
693
+ MeetingUtil.joinMeeting = sinon.stub().returns(Promise.resolve());
694
+ await meeting.join({pin: '1234', moderator: false});
695
+ assert.calledWith(MeetingUtil.joinMeeting, meeting, {moderator: false, pin: '1234'});
696
+ });
665
697
  });
666
698
  it('should fail if password is required', async () => {
667
699
  meeting.passwordStatus = PASSWORD_STATUS.REQUIRED;
@@ -2401,12 +2433,12 @@ describe('plugin-meetings', () => {
2401
2433
  });
2402
2434
 
2403
2435
  it('should send metrics on reconnect failure', async () => {
2404
- sandbox.stub(Metrics, 'sendOperationalMetric');
2436
+ sandbox.stub(Metrics, 'sendBehavioralMetric');
2405
2437
  await assert.isRejected(meeting.reconnect());
2406
- assert(Metrics.sendOperationalMetric.calledOnce);
2438
+ assert(Metrics.sendBehavioralMetric.calledOnce);
2407
2439
  assert.calledWith(
2408
- Metrics.sendOperationalMetric,
2409
- METRICS_OPERATIONAL_MEASURES.MEETING_RECONNECT_FAILURE,
2440
+ Metrics.sendBehavioralMetric,
2441
+ BEHAVIORAL_METRICS.MEETING_RECONNECT_FAILURE,
2410
2442
  {
2411
2443
  correlation_id: meeting.correlationId,
2412
2444
  locus_id: meeting.locusUrl.split('/').pop(),
@@ -19,10 +19,18 @@ describe('plugin-meetings', () => {
19
19
  regionCode: 'WEST-COAST'
20
20
  };
21
21
 
22
+ webex.internal = {
23
+ services: {
24
+ get: sinon.mock().returns('locusUrl'),
25
+ waitForCatalog: sinon.mock().returns(Promise.resolve({}))
26
+ }
27
+ };
28
+
22
29
  meetingsRequest = new MeetingRequest({}, {
23
30
  parent: webex
24
31
  });
25
32
 
33
+
26
34
  meetingsRequest.request = sinon.mock().returns(Promise.resolve({}));
27
35
  });
28
36
 
@@ -112,12 +120,15 @@ describe('plugin-meetings', () => {
112
120
  const deviceUrl = 'deviceUrl';
113
121
  const correlationId = 'random-uuid';
114
122
  const roapMessage = 'roap-message';
123
+ const permissionToken = 'permission-token';
115
124
 
116
125
  await meetingsRequest.joinMeeting({
117
126
  locusUrl,
118
127
  deviceUrl,
119
128
  correlationId,
120
- roapMessage
129
+ roapMessage,
130
+ permissionToken
131
+
121
132
  });
122
133
  const requestParams = meetingsRequest.request.getCall(0).args[0];
123
134
 
@@ -125,8 +136,49 @@ describe('plugin-meetings', () => {
125
136
  assert.equal(requestParams.uri, `${locusUrl}/participant?alternateRedirect=true`);
126
137
  assert.equal(requestParams.body.device.url, deviceUrl);
127
138
  assert.equal(requestParams.body.device.countryCode, 'US');
139
+ assert.equal(requestParams.body.permissionToken, 'permission-token');
128
140
  assert.equal(requestParams.body.device.regionCode, 'WEST-COAST');
129
141
  });
142
+
143
+ it('sends /call with meetingNumber if inviteeAddress does not exist', async () => {
144
+ const deviceUrl = 'deviceUrl';
145
+ const correlationId = 'random-uuid';
146
+ const roapMessage = 'roap-message';
147
+ const meetingNumber = 'meetingNumber';
148
+
149
+ await meetingsRequest.joinMeeting({
150
+ deviceUrl,
151
+ correlationId,
152
+ roapMessage,
153
+ meetingNumber
154
+ });
155
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
156
+
157
+ assert.equal(requestParams.method, 'POST');
158
+ assert.equal(requestParams.uri, 'locusUrl/loci/call?alternateRedirect=true');
159
+ assert.equal(requestParams.body.invitee.address, 'wbxmn:meetingNumber');
160
+ });
161
+
162
+ it('sends /call with inviteeAddress over meetingNumber as preference', async () => {
163
+ const deviceUrl = 'deviceUrl';
164
+ const correlationId = 'random-uuid';
165
+ const roapMessage = 'roap-message';
166
+ const meetingNumber = 'meetingNumber';
167
+ const inviteeAddress = 'sipUrl';
168
+
169
+ await meetingsRequest.joinMeeting({
170
+ deviceUrl,
171
+ correlationId,
172
+ roapMessage,
173
+ meetingNumber,
174
+ inviteeAddress
175
+ });
176
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
177
+
178
+ assert.equal(requestParams.method, 'POST');
179
+ assert.equal(requestParams.uri, 'locusUrl/loci/call?alternateRedirect=true');
180
+ assert.equal(requestParams.body.invitee.address, 'sipUrl');
181
+ });
130
182
  });
131
183
 
132
184
  describe('#pstn', () => {
@@ -4,6 +4,7 @@ import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
4
4
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
5
5
  import LoggerConfig
6
6
  from '@webex/plugin-meetings/src/common/logs/logger-config';
7
+ import Metrics from '@webex/plugin-meetings/src/metrics/index';
7
8
 
8
9
  describe('plugin-meetings', () => {
9
10
  describe('Meeting utils function', () => {
@@ -11,6 +12,7 @@ describe('plugin-meetings', () => {
11
12
  const meeting = {};
12
13
 
13
14
  beforeEach(() => {
15
+ Metrics.postEvent = sinon.stub();
14
16
  const logger = {
15
17
  info: sandbox.stub(),
16
18
  log: sandbox.stub(),
@@ -132,6 +134,45 @@ describe('plugin-meetings', () => {
132
134
  });
133
135
  });
134
136
  });
137
+
138
+ describe('joinMeeting', () => {
139
+ it('#Should call `meetingRequest.joinMeeting', async () => {
140
+ const meeting = {meetingJoinUrl: 'meetingJoinUrl', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
141
+
142
+ MeetingUtil.parseLocusJoin = sinon.stub();
143
+ await MeetingUtil.joinMeeting(meeting, {});
144
+
145
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
146
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
147
+
148
+ assert.equal(parameter.inviteeAddress, 'meetingJoinUrl');
149
+ });
150
+
151
+ it('#Should fallback sipUrl if meetingJoinUrl does not exists', async () => {
152
+ const meeting = {sipUri: 'sipUri', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
153
+
154
+ MeetingUtil.parseLocusJoin = sinon.stub();
155
+ await MeetingUtil.joinMeeting(meeting, {});
156
+
157
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
158
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
159
+
160
+ assert.equal(parameter.inviteeAddress, 'sipUri');
161
+ });
162
+
163
+ it('#Should fallback to meetingNumber if meetingJoinUrl/sipUrl does not exists', async () => {
164
+ const meeting = {meetingNumber: 'meetingNumber', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
165
+
166
+ MeetingUtil.parseLocusJoin = sinon.stub();
167
+ await MeetingUtil.joinMeeting(meeting, {});
168
+
169
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
170
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
171
+
172
+ assert.isUndefined(parameter.inviteeAddress);
173
+ assert.equal(parameter.meetingNumber, 'meetingNumber');
174
+ });
175
+ });
135
176
  });
136
177
  });
137
178
 
@@ -468,7 +468,7 @@ skipInBrowser(describe)('plugin-meetings', () => {
468
468
  });
469
469
  describe('successful MeetingInfo.#fetchMeetingInfo', () => {
470
470
  beforeEach(() => {
471
- webex.meetings.meetingInfo.fetchMeetingInfo = sinon.stub().returns(Promise.resolve(true));
471
+ webex.meetings.meetingInfo.fetchMeetingInfo = sinon.stub().returns(Promise.resolve({body: {permissionToken: 'PT', meetingJoinUrl: 'meetingJoinUrl'}}));
472
472
  });
473
473
  it('creates the meeting from a successful meeting info fetch promise testing', async () => {
474
474
  const meeting = webex.meetings.createMeeting('test', 'test');
@@ -482,6 +482,8 @@ skipInBrowser(describe)('plugin-meetings', () => {
482
482
  assert.calledWith(webex.meetings.meetingInfo.fetchMeetingInfo, 'test');
483
483
  assert.calledWith(MeetingsUtil.extractDestination, 'test', 'test');
484
484
  assert.calledWith(MeetingsUtil.getMeetingAddedType, 'test');
485
+ assert.equal(meeting.permissionToken, 'PT');
486
+ assert.equal(meeting.meetingJoinUrl, 'meetingJoinUrl');
485
487
  });
486
488
 
487
489
  it('creates the meeting from a successful meeting info fetch meeting resolve testing', async () => {
@@ -90,9 +90,9 @@ browserOnly(describe)('Meeting metrics', () => {
90
90
  });
91
91
  });
92
92
 
93
- describe('#sendOperationalMetric', () => {
93
+ describe('#sendBehavioralMetric', () => {
94
94
  it('sends client metric via Metrics plugin', () => {
95
- metrics.sendOperationalMetric('myMetric');
95
+ metrics.sendBehavioralMetric('myMetric');
96
96
 
97
97
  assert.calledOnce(mockSubmitMetric);
98
98
  });
@@ -101,13 +101,13 @@ browserOnly(describe)('Meeting metrics', () => {
101
101
  const data = {value: 567};
102
102
  const metadata = {test: true};
103
103
 
104
- metrics.sendOperationalMetric('myMetric', data, metadata);
104
+ metrics.sendBehavioralMetric('myMetric', data, metadata);
105
105
 
106
106
  assert.calledWithMatch(
107
107
  mockSubmitMetric,
108
108
  'myMetric',
109
109
  {
110
- type: ['operational'],
110
+ type: ['behavioral'],
111
111
  fields: {
112
112
  browser_version: getBrowserVersion(),
113
113
  os_version: getOSVersion(),
@@ -127,8 +127,8 @@ browserOnly(describe)('Meeting metrics', () => {
127
127
 
128
128
  it('throws error if no metric name is given', () => {
129
129
  assert.throws(
130
- () => metrics.sendOperationalMetric(),
131
- 'Missing operational metric name. Please provide one'
130
+ () => metrics.sendBehavioralMetric(),
131
+ 'Missing behavioral metric name. Please provide one'
132
132
  );
133
133
  });
134
134
  });