@webex/plugin-meetings 3.3.1 → 3.4.0-next.10

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 (138) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/constants.js +11 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/selfUtils.js +0 -5
  9. package/dist/locus-info/selfUtils.js.map +1 -1
  10. package/dist/media/MediaConnectionAwaiter.js +70 -15
  11. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  12. package/dist/media/index.js +18 -9
  13. package/dist/media/index.js.map +1 -1
  14. package/dist/meeting/connectionStateHandler.js +67 -0
  15. package/dist/meeting/connectionStateHandler.js.map +1 -0
  16. package/dist/meeting/index.js +576 -374
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/locusMediaRequest.js +7 -0
  19. package/dist/meeting/locusMediaRequest.js.map +1 -1
  20. package/dist/meeting/muteState.js +6 -1
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meeting-info/index.js +4 -4
  25. package/dist/meeting-info/index.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +2 -2
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meeting-info/util.js +17 -17
  29. package/dist/meeting-info/util.js.map +1 -1
  30. package/dist/meeting-info/utilv2.js +16 -16
  31. package/dist/meeting-info/utilv2.js.map +1 -1
  32. package/dist/meetings/collection.js +1 -1
  33. package/dist/meetings/collection.js.map +1 -1
  34. package/dist/meetings/index.js +41 -35
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/meetings.types.js +8 -0
  37. package/dist/meetings/meetings.types.js.map +1 -1
  38. package/dist/meetings/util.js +3 -2
  39. package/dist/meetings/util.js.map +1 -1
  40. package/dist/metrics/constants.js +2 -1
  41. package/dist/metrics/constants.js.map +1 -1
  42. package/dist/metrics/index.js +57 -0
  43. package/dist/metrics/index.js.map +1 -1
  44. package/dist/personal-meeting-room/index.js +1 -1
  45. package/dist/personal-meeting-room/index.js.map +1 -1
  46. package/dist/reachability/clusterReachability.js +108 -53
  47. package/dist/reachability/clusterReachability.js.map +1 -1
  48. package/dist/reachability/index.js +546 -115
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/reconnection-manager/index.js +1 -1
  51. package/dist/reconnection-manager/index.js.map +1 -1
  52. package/dist/rtcMetrics/index.js +26 -6
  53. package/dist/rtcMetrics/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +11 -3
  55. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  56. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  57. package/dist/types/meeting/index.d.ts +28 -8
  58. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  59. package/dist/types/meeting-info/index.d.ts +3 -2
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  61. package/dist/types/meeting-info/util.d.ts +5 -4
  62. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  63. package/dist/types/meetings/collection.d.ts +3 -2
  64. package/dist/types/meetings/index.d.ts +6 -4
  65. package/dist/types/meetings/meetings.types.d.ts +9 -0
  66. package/dist/types/metrics/constants.d.ts +1 -0
  67. package/dist/types/metrics/index.d.ts +15 -0
  68. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  69. package/dist/types/reachability/index.d.ts +107 -4
  70. package/dist/types/rtcMetrics/index.d.ts +11 -1
  71. package/dist/webinar/index.js +1 -1
  72. package/package.json +23 -23
  73. package/src/breakouts/index.ts +7 -1
  74. package/src/constants.ts +13 -17
  75. package/src/locus-info/selfUtils.ts +0 -5
  76. package/src/media/MediaConnectionAwaiter.ts +89 -14
  77. package/src/media/index.ts +18 -9
  78. package/src/meeting/connectionStateHandler.ts +65 -0
  79. package/src/meeting/index.ts +541 -298
  80. package/src/meeting/locusMediaRequest.ts +5 -0
  81. package/src/meeting/muteState.ts +6 -1
  82. package/src/meeting/util.ts +1 -0
  83. package/src/meeting-info/index.ts +9 -6
  84. package/src/meeting-info/meeting-info-v2.ts +4 -4
  85. package/src/meeting-info/util.ts +23 -28
  86. package/src/meeting-info/utilv2.ts +18 -24
  87. package/src/meetings/collection.ts +3 -3
  88. package/src/meetings/index.ts +43 -43
  89. package/src/meetings/meetings.types.ts +11 -0
  90. package/src/meetings/util.ts +5 -4
  91. package/src/metrics/constants.ts +1 -0
  92. package/src/metrics/index.ts +44 -0
  93. package/src/personal-meeting-room/index.ts +2 -2
  94. package/src/reachability/clusterReachability.ts +86 -25
  95. package/src/reachability/index.ts +364 -30
  96. package/src/reconnection-manager/index.ts +1 -1
  97. package/src/rtcMetrics/index.ts +25 -5
  98. package/test/unit/spec/breakouts/index.ts +51 -32
  99. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  100. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  101. package/test/unit/spec/media/index.ts +75 -34
  102. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  103. package/test/unit/spec/meeting/index.js +807 -185
  104. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  105. package/test/unit/spec/meeting/muteState.js +24 -0
  106. package/test/unit/spec/meeting-info/index.js +4 -4
  107. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  108. package/test/unit/spec/meeting-info/request.js +2 -2
  109. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  110. package/test/unit/spec/meetings/index.js +44 -3
  111. package/test/unit/spec/metrics/index.js +126 -0
  112. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  113. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  114. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  115. package/test/unit/spec/reachability/index.ts +1398 -131
  116. package/test/unit/spec/rtcMetrics/index.ts +32 -0
  117. package/dist/mediaQualityMetrics/config.js +0 -321
  118. package/dist/mediaQualityMetrics/config.js.map +0 -1
  119. package/dist/networkQualityMonitor/index.js +0 -227
  120. package/dist/networkQualityMonitor/index.js.map +0 -1
  121. package/dist/statsAnalyzer/global.js +0 -44
  122. package/dist/statsAnalyzer/global.js.map +0 -1
  123. package/dist/statsAnalyzer/index.js +0 -1072
  124. package/dist/statsAnalyzer/index.js.map +0 -1
  125. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  126. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  127. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  128. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  129. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  130. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  131. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  132. package/src/mediaQualityMetrics/config.ts +0 -255
  133. package/src/networkQualityMonitor/index.ts +0 -211
  134. package/src/statsAnalyzer/global.ts +0 -37
  135. package/src/statsAnalyzer/index.ts +0 -1318
  136. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  137. package/test/unit/spec/networkQualityMonitor/index.js +0 -99
  138. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -1,12 +1,15 @@
1
1
  import {assert} from '@webex/test-helper-chai';
2
2
  import MockWebex from '@webex/test-helper-mock-webex';
3
3
  import sinon from 'sinon';
4
+ import EventEmitter from 'events';
5
+ import testUtils from '../../../utils/testUtils';
4
6
  import Reachability, {
5
- ReachabilityResults,
6
7
  ReachabilityResultsForBackend,
7
8
  } from '@webex/plugin-meetings/src/reachability/';
9
+ import {ClusterNode} from '../../../../src/reachability/request';
8
10
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
9
11
  import * as ClusterReachabilityModule from '@webex/plugin-meetings/src/reachability/clusterReachability';
12
+ import Metrics from '@webex/plugin-meetings/src/metrics';
10
13
 
11
14
  import {IP_VERSION} from '@webex/plugin-meetings/src/constants';
12
15
 
@@ -141,7 +144,6 @@ describe('isAnyPublicClusterReachable', () => {
141
144
  });
142
145
  });
143
146
 
144
-
145
147
  describe('isWebexMediaBackendUnreachable', () => {
146
148
  let webex;
147
149
 
@@ -381,156 +383,1066 @@ describe('isWebexMediaBackendUnreachable', () => {
381
383
  tcp: {result: 'unreachable'},
382
384
  xtls: {result: 'reachable'},
383
385
  },
384
- c: {
385
- udp: {result: 'unreachable'},
386
+ c: {
387
+ udp: {result: 'unreachable'},
388
+ tcp: {result: 'unreachable'},
389
+ xtls: {result: 'unreachable'},
390
+ },
391
+ },
392
+ expectedResult: false,
393
+ },
394
+ {
395
+ title: 'multiple clusters with some missing results',
396
+ mockStorage: {
397
+ a: {udp: {result: 'unreachable'}},
398
+ b: {tcp: {result: 'unreachable'}},
399
+ c: {xtls: {result: 'unreachable'}},
400
+ d: {},
401
+ },
402
+ expectedResult: true,
403
+ },
404
+ ].forEach(({mockStorage, expectedResult, title}) => {
405
+ it(`returns ${expectedResult} when ${title}`, async () => {
406
+ await runCheck(mockStorage, expectedResult);
407
+ });
408
+ });
409
+ });
410
+
411
+ /**
412
+ * helper class to mock ClusterReachability and allow to easily
413
+ * simulate 'resultReady' events from it
414
+ */
415
+ class MockClusterReachability extends EventEmitter {
416
+ mockResult = {
417
+ udp: {
418
+ result: 'untested',
419
+ },
420
+ tcp: {
421
+ result: 'untested',
422
+ },
423
+ xtls: {
424
+ result: 'untested',
425
+ },
426
+ };
427
+
428
+ isVideoMesh: boolean;
429
+ name: string;
430
+
431
+ constructor(name: string, clusterInfo: ClusterNode) {
432
+ super();
433
+ this.name = name;
434
+ this.isVideoMesh = clusterInfo.isVideoMesh;
435
+ }
436
+
437
+ abort = sinon.stub();
438
+ start = sinon.stub();
439
+
440
+ getResult() {
441
+ return this.mockResult;
442
+ }
443
+
444
+ /**
445
+ * Emits a fake 'resultReady' event and makes sure that the same result
446
+ * is returned when getResult() is called.
447
+ *
448
+ * @param protocol
449
+ * @param result
450
+ */
451
+ public emitFakeResult(protocol, result) {
452
+ this.mockResult[protocol] = result;
453
+ this.emit(ClusterReachabilityModule.Events.resultReady, {protocol, ...result});
454
+ }
455
+
456
+ public emitFakeClientMediaIpUpdate(protocol, newIp) {
457
+ this.mockResult[protocol].clientMediaIPs.push(newIp);
458
+ this.emit(ClusterReachabilityModule.Events.clientMediaIpsUpdated, {
459
+ protocol,
460
+ clientMediaIPs: this.mockResult[protocol].clientMediaIPs,
461
+ });
462
+ }
463
+ }
464
+
465
+ describe('gatherReachability', () => {
466
+ let webex;
467
+ let clock;
468
+ let clusterReachabilityCtorStub;
469
+ let mockClusterReachabilityInstances: Record<string, MockClusterReachability>;
470
+
471
+ beforeEach(async () => {
472
+ webex = new MockWebex();
473
+
474
+ sinon.stub(Metrics, 'sendBehavioralMetric');
475
+
476
+ await webex.boundedStorage.put(
477
+ 'Reachability',
478
+ 'reachability.result',
479
+ JSON.stringify({old: 'results'})
480
+ );
481
+ await webex.boundedStorage.put(
482
+ 'Reachability',
483
+ 'reachability.joinCookie',
484
+ JSON.stringify({old: 'joinCookie'})
485
+ );
486
+
487
+ webex.internal.device.ipNetworkDetector = {
488
+ supportsIpV4: false,
489
+ supportsIpV6: false,
490
+ firstIpV4: -1,
491
+ firstIpV6: -1,
492
+ firstMdns: -1,
493
+ totalTime: -1,
494
+ detect: sinon.stub().resolves(),
495
+ };
496
+
497
+ clock = sinon.useFakeTimers();
498
+
499
+ mockClusterReachabilityInstances = {};
500
+
501
+ clusterReachabilityCtorStub = sinon
502
+ .stub(ClusterReachabilityModule, 'ClusterReachability')
503
+ .callsFake((id, cluster) => {
504
+ const mockInstance = new MockClusterReachability(id, cluster);
505
+
506
+ mockClusterReachabilityInstances[id] = mockInstance;
507
+ return mockInstance;
508
+ });
509
+
510
+ webex.config.meetings.experimental = {
511
+ enableTcpReachability: false,
512
+ enableTlsReachability: false,
513
+ };
514
+ });
515
+
516
+ afterEach(() => {
517
+ sinon.restore();
518
+ clock.restore();
519
+ });
520
+
521
+ // simulates time progression so that Reachability times out
522
+ const simulateTimeout = async () => {
523
+ await testUtils.flushPromises();
524
+ clock.tick(3000);
525
+ };
526
+
527
+ const checkResults = async (expectedResults, expectedJoinCookie) => {
528
+ const storedResultForReachabilityResult = await webex.boundedStorage.get(
529
+ 'Reachability',
530
+ 'reachability.result'
531
+ );
532
+ const storedResultForJoinCookie = await webex.boundedStorage.get(
533
+ 'Reachability',
534
+ 'reachability.joinCookie'
535
+ );
536
+
537
+ assert.equal(storedResultForReachabilityResult, JSON.stringify(expectedResults));
538
+ assert.equal(storedResultForJoinCookie, JSON.stringify(expectedJoinCookie));
539
+ };
540
+
541
+ [
542
+ // ========================================================================
543
+ {
544
+ title: '1 cluster with events triggered for each protocol',
545
+ waitShortTimeout: false,
546
+ waitLongTimeout: false,
547
+ mockClusters: {
548
+ cluster1: {
549
+ udp: ['udp-url1'],
550
+ tcp: ['tcp-url1'],
551
+ xtls: ['xtls-url1'],
552
+ isVideoMesh: false,
553
+ },
554
+ },
555
+ mockResultReadyEvents: [
556
+ {
557
+ clusterId: 'cluster1',
558
+ protocol: 'tcp',
559
+ result: {
560
+ result: 'reachable',
561
+ latencyInMilliseconds: 11,
562
+ },
563
+ },
564
+ {
565
+ clusterId: 'cluster1',
566
+ protocol: 'udp',
567
+ result: {
568
+ result: 'reachable',
569
+ clientMediaIPs: ['1.2.3.4'],
570
+ latencyInMilliseconds: 22,
571
+ },
572
+ },
573
+ {
574
+ clusterId: 'cluster1',
575
+ protocol: 'xtls',
576
+ result: {
577
+ result: 'reachable',
578
+ latencyInMilliseconds: 33,
579
+ },
580
+ },
581
+ ],
582
+ expectedResults: {
583
+ cluster1: {
584
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 22},
585
+ tcp: {result: 'reachable', latencyInMilliseconds: 11},
586
+ xtls: {result: 'reachable', latencyInMilliseconds: 33},
587
+ isVideoMesh: false,
588
+ },
589
+ },
590
+ expectedMetrics: {
591
+ vmn_udp_min: -1,
592
+ vmn_udp_max: -1,
593
+ vmn_udp_average: -1,
594
+ public_udp_min: 22,
595
+ public_udp_max: 22,
596
+ public_udp_average: 22,
597
+ public_tcp_min: 11,
598
+ public_tcp_max: 11,
599
+ public_tcp_average: 11,
600
+ public_xtls_min: 33,
601
+ public_xtls_max: 33,
602
+ public_xtls_average: 33,
603
+ },
604
+ },
605
+ // ========================================================================
606
+ {
607
+ title:
608
+ '3 clusters: one with an event for each protocol, one with no events, one with no urls for tcp and xtls',
609
+ waitShortTimeout: 'public',
610
+ waitLongTimeout: true,
611
+ mockClusters: {
612
+ cluster1: {
613
+ udp: ['udp-url1.1', 'udp-url1.2'],
614
+ tcp: ['tcp-url1.1', 'tcp-url1.2'],
615
+ xtls: ['xtls-url1.1', 'xtls-url1.2'],
616
+ isVideoMesh: false,
617
+ },
618
+ cluster2: {
619
+ udp: ['udp-url2.1'],
620
+ tcp: ['tcp-url2.1'],
621
+ xtls: ['xtls-url2.1'],
622
+ isVideoMesh: false,
623
+ },
624
+ cluster3: {
625
+ udp: ['udp-url1'],
626
+ tcp: [],
627
+ xtls: [],
628
+ isVideoMesh: true,
629
+ },
630
+ },
631
+ mockResultReadyEvents: [
632
+ {
633
+ clusterId: 'cluster1',
634
+ protocol: 'udp',
635
+ result: {
636
+ result: 'reachable',
637
+ clientMediaIPs: ['1.2.3.4'],
638
+ latencyInMilliseconds: 13,
639
+ },
640
+ },
641
+ {
642
+ clusterId: 'cluster1',
643
+ protocol: 'tcp',
644
+ result: {
645
+ result: 'reachable',
646
+ latencyInMilliseconds: 53,
647
+ },
648
+ },
649
+ {
650
+ clusterId: 'cluster1',
651
+ protocol: 'xtls',
652
+ result: {
653
+ result: 'reachable',
654
+ latencyInMilliseconds: 113,
655
+ },
656
+ },
657
+ ],
658
+ expectedResults: {
659
+ cluster1: {
660
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 13},
661
+ tcp: {result: 'reachable', latencyInMilliseconds: 53},
662
+ xtls: {result: 'reachable', latencyInMilliseconds: 113},
663
+ isVideoMesh: false,
664
+ },
665
+ cluster2: {
666
+ udp: {result: 'unreachable'},
667
+ tcp: {result: 'unreachable'},
668
+ xtls: {result: 'unreachable'},
669
+ isVideoMesh: false,
670
+ },
671
+ cluster3: {
672
+ udp: {result: 'unreachable'},
673
+ tcp: {result: 'untested'},
674
+ xtls: {result: 'untested'},
675
+ isVideoMesh: true,
676
+ },
677
+ },
678
+ expectedMetrics: {
679
+ vmn_udp_min: -1,
680
+ vmn_udp_max: -1,
681
+ vmn_udp_average: -1,
682
+ public_udp_min: 13,
683
+ public_udp_max: 13,
684
+ public_udp_average: 13,
685
+ public_tcp_min: 53,
686
+ public_tcp_max: 53,
687
+ public_tcp_average: 53,
688
+ public_xtls_min: 113,
689
+ public_xtls_max: 113,
690
+ public_xtls_average: 113,
691
+ },
692
+ },
693
+ // ========================================================================
694
+ {
695
+ title: '3 clusters: all with all results ready in time for all protocols',
696
+ waitShortTimeout: false,
697
+ waitLongTimeout: false,
698
+ mockClusters: {
699
+ cluster1: {
700
+ udp: ['udp-url1'],
701
+ tcp: ['tcp-url1'],
702
+ xtls: ['xtls-url1'],
703
+ isVideoMesh: false,
704
+ },
705
+ cluster2: {
706
+ udp: ['udp-url2'],
707
+ tcp: ['tcp-url2'],
708
+ xtls: ['xtls-url2'],
709
+ isVideoMesh: false,
710
+ },
711
+ cluster3: {
712
+ udp: ['udp-url3'],
713
+ tcp: ['tcp-url3'],
714
+ xtls: ['xtls-url3'],
715
+ isVideoMesh: false,
716
+ },
717
+ },
718
+ mockResultReadyEvents: [
719
+ {
720
+ clusterId: 'cluster1',
721
+ protocol: 'udp',
722
+ result: {
723
+ result: 'reachable',
724
+ clientMediaIPs: ['1.2.3.4'],
725
+ latencyInMilliseconds: 10,
726
+ },
727
+ },
728
+ {
729
+ clusterId: 'cluster1',
730
+ protocol: 'tcp',
731
+ result: {
732
+ result: 'reachable',
733
+ latencyInMilliseconds: 100,
734
+ },
735
+ },
736
+ {
737
+ clusterId: 'cluster1',
738
+ protocol: 'xtls',
739
+ result: {
740
+ result: 'reachable',
741
+ latencyInMilliseconds: 200,
742
+ },
743
+ },
744
+ {
745
+ clusterId: 'cluster2',
746
+ protocol: 'udp',
747
+ result: {
748
+ result: 'reachable',
749
+ clientMediaIPs: ['1.2.3.4'],
750
+ latencyInMilliseconds: 20,
751
+ },
752
+ },
753
+ {
754
+ clusterId: 'cluster2',
755
+ protocol: 'tcp',
756
+ result: {
757
+ result: 'reachable',
758
+ latencyInMilliseconds: 110,
759
+ },
760
+ },
761
+ {
762
+ clusterId: 'cluster2',
763
+ protocol: 'xtls',
764
+ result: {
765
+ result: 'reachable',
766
+ latencyInMilliseconds: 220,
767
+ },
768
+ },
769
+ {
770
+ clusterId: 'cluster3',
771
+ protocol: 'udp',
772
+ result: {
773
+ result: 'reachable',
774
+ clientMediaIPs: ['1.2.3.4'],
775
+ latencyInMilliseconds: 30,
776
+ },
777
+ },
778
+ {
779
+ clusterId: 'cluster3',
780
+ protocol: 'tcp',
781
+ result: {
782
+ result: 'reachable',
783
+ latencyInMilliseconds: 120,
784
+ },
785
+ },
786
+ {
787
+ clusterId: 'cluster3',
788
+ protocol: 'xtls',
789
+ result: {
790
+ result: 'reachable',
791
+ latencyInMilliseconds: 240,
792
+ },
793
+ },
794
+ ],
795
+ expectedResults: {
796
+ cluster1: {
797
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 10},
798
+ tcp: {result: 'reachable', latencyInMilliseconds: 100},
799
+ xtls: {result: 'reachable', latencyInMilliseconds: 200},
800
+ isVideoMesh: false,
801
+ },
802
+ cluster2: {
803
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 20},
804
+ tcp: {result: 'reachable', latencyInMilliseconds: 110},
805
+ xtls: {result: 'reachable', latencyInMilliseconds: 220},
806
+ isVideoMesh: false,
807
+ },
808
+ cluster3: {
809
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 30},
810
+ tcp: {result: 'reachable', latencyInMilliseconds: 120},
811
+ xtls: {result: 'reachable', latencyInMilliseconds: 240},
812
+ isVideoMesh: false,
813
+ },
814
+ },
815
+ expectedMetrics: {
816
+ vmn_udp_min: -1,
817
+ vmn_udp_max: -1,
818
+ vmn_udp_average: -1,
819
+ public_udp_min: 10,
820
+ public_udp_max: 30,
821
+ public_udp_average: 20,
822
+ public_tcp_min: 100,
823
+ public_tcp_max: 120,
824
+ public_tcp_average: 110,
825
+ public_xtls_min: 200,
826
+ public_xtls_max: 240,
827
+ public_xtls_average: 220,
828
+ },
829
+ },
830
+ // ========================================================================
831
+ {
832
+ title: '2 clusters: both with no results at all',
833
+ waitShortTimeout: 'public',
834
+ waitLongTimeout: true,
835
+ mockClusters: {
836
+ cluster1: {
837
+ udp: ['udp-url1'],
838
+ tcp: ['tcp-url1'],
839
+ xtls: ['xtls-url1'],
840
+ isVideoMesh: false,
841
+ },
842
+ cluster2: {
843
+ udp: ['udp-url2'],
844
+ tcp: ['tcp-url2'],
845
+ xtls: ['xtls-url2'],
846
+ isVideoMesh: false,
847
+ },
848
+ },
849
+ mockResultReadyEvents: [],
850
+ expectedResults: {
851
+ cluster1: {
852
+ udp: {result: 'unreachable'},
853
+ tcp: {result: 'unreachable'},
854
+ xtls: {result: 'unreachable'},
855
+ isVideoMesh: false,
856
+ },
857
+ cluster2: {
858
+ udp: {result: 'unreachable'},
859
+ tcp: {result: 'unreachable'},
860
+ xtls: {result: 'unreachable'},
861
+ isVideoMesh: false,
862
+ },
863
+ },
864
+ expectedMetrics: {
865
+ vmn_udp_min: -1,
866
+ vmn_udp_max: -1,
867
+ vmn_udp_average: -1,
868
+ public_udp_min: -1,
869
+ public_udp_max: -1,
870
+ public_udp_average: -1,
871
+ public_tcp_min: -1,
872
+ public_tcp_max: -1,
873
+ public_tcp_average: -1,
874
+ public_xtls_min: -1,
875
+ public_xtls_max: -1,
876
+ public_xtls_average: -1,
877
+ },
878
+ },
879
+ // ========================================================================
880
+ {
881
+ title:
882
+ '3 clusters: 2 VMN clusters missing results, but the public one has all results within 1s',
883
+ waitShortTimeout: 'vmn',
884
+ waitLongTimeout: true,
885
+ mockClusters: {
886
+ vmnCluster1: {
887
+ udp: ['udp-url1'],
888
+ tcp: ['tcp-url1'],
889
+ xtls: ['xtls-url1'],
890
+ isVideoMesh: true,
891
+ },
892
+ publicCluster: {
893
+ udp: ['udp-url2'],
894
+ tcp: ['tcp-url2'],
895
+ xtls: ['xtls-url2'],
896
+ isVideoMesh: false,
897
+ },
898
+ vmnCluster2: {
899
+ udp: ['udp-url3'],
900
+ tcp: ['tcp-url3'],
901
+ xtls: ['xtls-url3'],
902
+ isVideoMesh: true,
903
+ },
904
+ },
905
+ mockResultReadyEvents: [
906
+ {
907
+ clusterId: 'publicCluster',
908
+ protocol: 'udp',
909
+ result: {
910
+ result: 'reachable',
911
+ clientMediaIPs: ['1.2.3.4'],
912
+ latencyInMilliseconds: 10,
913
+ },
914
+ },
915
+ {
916
+ clusterId: 'publicCluster',
917
+ protocol: 'tcp',
918
+ result: {
919
+ result: 'reachable',
920
+ latencyInMilliseconds: 100,
921
+ },
922
+ },
923
+ {
924
+ clusterId: 'publicCluster',
925
+ protocol: 'xtls',
926
+ result: {
927
+ result: 'reachable',
928
+ latencyInMilliseconds: 200,
929
+ },
930
+ },
931
+ ],
932
+ expectedResults: {
933
+ vmnCluster1: {
934
+ udp: {result: 'unreachable'},
935
+ tcp: {result: 'untested'},
936
+ xtls: {result: 'untested'},
937
+ isVideoMesh: true,
938
+ },
939
+ publicCluster: {
940
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 10},
941
+ tcp: {result: 'reachable', latencyInMilliseconds: 100},
942
+ xtls: {result: 'reachable', latencyInMilliseconds: 200},
943
+ isVideoMesh: false,
944
+ },
945
+ vmnCluster2: {
946
+ udp: {result: 'unreachable'},
947
+ tcp: {result: 'untested'},
948
+ xtls: {result: 'untested'},
949
+ isVideoMesh: true,
950
+ },
951
+ },
952
+ expectedMetrics: {
953
+ vmn_udp_min: -1,
954
+ vmn_udp_max: -1,
955
+ vmn_udp_average: -1,
956
+ public_udp_min: 10,
957
+ public_udp_max: 10,
958
+ public_udp_average: 10,
959
+ public_tcp_min: 100,
960
+ public_tcp_max: 100,
961
+ public_tcp_average: 100,
962
+ public_xtls_min: 200,
963
+ public_xtls_max: 200,
964
+ public_xtls_average: 200,
965
+ },
966
+ },
967
+ // ========================================================================
968
+ {
969
+ title: '2 VMN clusters with all results',
970
+ waitShortTimeout: false,
971
+ waitLongTimeout: false,
972
+ mockClusters: {
973
+ vmnCluster1: {
974
+ udp: ['udp-url1'],
975
+ tcp: [],
976
+ xtls: [],
977
+ isVideoMesh: true,
978
+ },
979
+ vmnCluster2: {
980
+ udp: ['udp-url3'],
981
+ tcp: [],
982
+ xtls: [],
983
+ isVideoMesh: true,
984
+ },
985
+ },
986
+ mockResultReadyEvents: [
987
+ {
988
+ clusterId: 'vmnCluster1',
989
+ protocol: 'udp',
990
+ result: {
991
+ result: 'reachable',
992
+ clientMediaIPs: ['192.168.10.1'],
993
+ latencyInMilliseconds: 100,
994
+ },
995
+ },
996
+ {
997
+ clusterId: 'vmnCluster2',
998
+ protocol: 'udp',
999
+ result: {
1000
+ result: 'reachable',
1001
+ clientMediaIPs: ['192.168.0.1'],
1002
+ latencyInMilliseconds: 300,
1003
+ },
1004
+ },
1005
+ ],
1006
+ expectedResults: {
1007
+ vmnCluster1: {
1008
+ udp: {result: 'reachable', clientMediaIPs: ['192.168.10.1'], latencyInMilliseconds: 100},
1009
+ tcp: {result: 'untested'},
1010
+ xtls: {result: 'untested'},
1011
+ isVideoMesh: true,
1012
+ },
1013
+ vmnCluster2: {
1014
+ udp: {result: 'reachable', clientMediaIPs: ['192.168.0.1'], latencyInMilliseconds: 300},
1015
+ tcp: {result: 'untested'},
1016
+ xtls: {result: 'untested'},
1017
+ isVideoMesh: true,
1018
+ },
1019
+ },
1020
+ expectedMetrics: {
1021
+ vmn_udp_min: 100,
1022
+ vmn_udp_max: 300,
1023
+ vmn_udp_average: 200,
1024
+ public_udp_min: -1,
1025
+ public_udp_max: -1,
1026
+ public_udp_average: -1,
1027
+ public_tcp_min: -1,
1028
+ public_tcp_max: -1,
1029
+ public_tcp_average: -1,
1030
+ public_xtls_min: -1,
1031
+ public_xtls_max: -1,
1032
+ public_xtls_average: -1,
1033
+ },
1034
+ },
1035
+ ].forEach(
1036
+ ({
1037
+ title,
1038
+ waitShortTimeout,
1039
+ waitLongTimeout,
1040
+ mockClusters,
1041
+ mockResultReadyEvents,
1042
+ expectedResults,
1043
+ expectedMetrics,
1044
+ }) =>
1045
+ it(`works correctly for the case: ${title}`, async () => {
1046
+ webex.config.meetings.experimental = {
1047
+ enableTcpReachability: true,
1048
+ enableTlsReachability: true,
1049
+ };
1050
+
1051
+ // the metrics related to ipver and trigger are not tested in these tests and are all the same, so setting them up here
1052
+ const expectedMetricsFull = {
1053
+ ...expectedMetrics,
1054
+ ipver_firstIpV4: -1,
1055
+ ipver_firstIpV6: -1,
1056
+ ipver_firstMdns: -1,
1057
+ ipver_totalTime: -1,
1058
+ trigger: 'test',
1059
+ };
1060
+
1061
+ const receivedEvents = {
1062
+ done: 0,
1063
+ firstResultAvailable: {
1064
+ udp: 0,
1065
+ tcp: 0,
1066
+ xtls: 0,
1067
+ },
1068
+ };
1069
+
1070
+ const reachability = new Reachability(webex);
1071
+
1072
+ reachability.on('reachability:done', () => {
1073
+ receivedEvents.done += 1;
1074
+ });
1075
+ reachability.on('reachability:firstResultAvailable', ({protocol}) => {
1076
+ receivedEvents.firstResultAvailable[protocol] += 1;
1077
+ });
1078
+
1079
+ const mockGetClustersResult = {
1080
+ clusters: {},
1081
+ joinCookie: {id: 'id'},
1082
+ };
1083
+
1084
+ Object.entries(mockClusters).forEach(([id, mockCluster]) => {
1085
+ mockGetClustersResult.clusters[id] = mockCluster;
1086
+ });
1087
+
1088
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
1089
+
1090
+ const resultPromise = reachability.gatherReachability('test');
1091
+
1092
+ await testUtils.flushPromises();
1093
+
1094
+ // check that ClusterReachability instance was created for each cluster
1095
+ Object.entries(mockClusters).forEach(([id, mockCluster]) => {
1096
+ assert.calledWith(clusterReachabilityCtorStub, id, mockCluster);
1097
+ });
1098
+
1099
+ // trigger mock result events from ClusterReachability instances
1100
+ mockResultReadyEvents.forEach((mockEvent) => {
1101
+ mockClusterReachabilityInstances[mockEvent.clusterId].emitFakeResult(
1102
+ mockEvent.protocol,
1103
+ mockEvent.result
1104
+ );
1105
+ });
1106
+
1107
+ if (waitShortTimeout === 'public') {
1108
+ clock.tick(3000);
1109
+ }
1110
+ if (waitShortTimeout === 'vmn') {
1111
+ clock.tick(1000);
1112
+ }
1113
+
1114
+ await resultPromise;
1115
+
1116
+ await checkResults(expectedResults, mockGetClustersResult.joinCookie);
1117
+
1118
+ if (waitLongTimeout) {
1119
+ // we need to wait either 14 or 12 seconds to get to the 15s timeout (depending on how much we waited earlier)
1120
+ clock.tick(waitShortTimeout === 'vmn' ? 14000 : 12000);
1121
+
1122
+ // we check the results again after the long timeout - they should be the same
1123
+ await checkResults(expectedResults, mockGetClustersResult.joinCookie);
1124
+ }
1125
+
1126
+ // now check events emitted by Reachability class
1127
+ assert.equal(receivedEvents['done'], 1);
1128
+
1129
+ // if we've mocked at least one event for any protocol, check that we received
1130
+ // firstResultAvailable event for that protocol
1131
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'udp').length > 0) {
1132
+ assert.equal(receivedEvents['firstResultAvailable']['udp'], 1);
1133
+ }
1134
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'tcp').length > 0) {
1135
+ assert.equal(receivedEvents['firstResultAvailable']['tcp'], 1);
1136
+ }
1137
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'xtls').length > 0) {
1138
+ assert.equal(receivedEvents['firstResultAvailable']['xtls'], 1);
1139
+ }
1140
+
1141
+ // finally, check the metrics
1142
+ assert.calledWith(
1143
+ Metrics.sendBehavioralMetric,
1144
+ 'js_sdk_reachability_completed',
1145
+ expectedMetricsFull
1146
+ );
1147
+ })
1148
+ );
1149
+
1150
+ it('sends the trigger parameter in the metrics', async () => {
1151
+ const reachability = new TestReachability(webex);
1152
+
1153
+ const mockGetClustersResult = {
1154
+ clusters: {
1155
+ clusterA: {
1156
+ udp: ['udp-url'],
1157
+ tcp: [],
1158
+ xtls: [],
1159
+ isVideoMesh: false,
1160
+ },
1161
+ },
1162
+ joinCookie: {id: 'id'},
1163
+ };
1164
+
1165
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
1166
+
1167
+ const resultPromise = reachability.gatherReachability('some trigger');
1168
+
1169
+ // let it time out
1170
+ await testUtils.flushPromises();
1171
+ clock.tick(15000);
1172
+ await resultPromise;
1173
+
1174
+ // check the metric contains the right trigger value
1175
+ assert.calledWith(
1176
+ Metrics.sendBehavioralMetric,
1177
+ 'js_sdk_reachability_completed',
1178
+ sinon.match({trigger: 'some trigger'})
1179
+ );
1180
+ });
1181
+
1182
+ it(`starts ip network version detection and includes the results in the metrics`, async () => {
1183
+ webex.config.meetings.experimental = {
1184
+ enableTcpReachability: true,
1185
+ enableTlsReachability: true,
1186
+ };
1187
+ webex.internal.device.ipNetworkDetector = {
1188
+ supportsIpV4: true,
1189
+ supportsIpV6: true,
1190
+ firstIpV4: 10,
1191
+ firstIpV6: 20,
1192
+ firstMdns: 30,
1193
+ totalTime: 40,
1194
+ detect: sinon.stub().resolves(),
1195
+ };
1196
+
1197
+ const receivedEvents = {
1198
+ done: 0,
1199
+ };
1200
+
1201
+ const reachability = new Reachability(webex);
1202
+
1203
+ reachability.on('reachability:done', () => {
1204
+ receivedEvents.done += 1;
1205
+ });
1206
+
1207
+ // simulate having just 1 cluster, we don't need more for this test
1208
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns({
1209
+ clusters: {
1210
+ publicCluster: {
1211
+ udp: ['udp-url'],
1212
+ tcp: [],
1213
+ xtls: [],
1214
+ isVideoMesh: false,
1215
+ },
1216
+ },
1217
+ joinCookie: {id: 'id'},
1218
+ });
1219
+
1220
+ const resultPromise = reachability.gatherReachability('test');
1221
+
1222
+ await testUtils.flushPromises();
1223
+
1224
+ // trigger mock result events from ClusterReachability instance
1225
+ mockClusterReachabilityInstances['publicCluster'].emitFakeResult('udp', {
1226
+ result: 'reachable',
1227
+ clientMediaIPs: ['1.2.3.4'],
1228
+ latencyInMilliseconds: 100,
1229
+ });
1230
+
1231
+ await resultPromise;
1232
+
1233
+ // check events emitted by Reachability class
1234
+ assert.equal(receivedEvents['done'], 1);
1235
+
1236
+ // and that ip network detection was started
1237
+ assert.calledOnceWithExactly(webex.internal.device.ipNetworkDetector.detect);
1238
+
1239
+ // finally, check the metrics - they should contain values from ipNetworkDetector
1240
+ assert.calledWith(Metrics.sendBehavioralMetric, 'js_sdk_reachability_completed', {
1241
+ vmn_udp_min: -1,
1242
+ vmn_udp_max: -1,
1243
+ vmn_udp_average: -1,
1244
+ public_udp_min: 100,
1245
+ public_udp_max: 100,
1246
+ public_udp_average: 100,
1247
+ public_tcp_min: -1,
1248
+ public_tcp_max: -1,
1249
+ public_tcp_average: -1,
1250
+ public_xtls_min: -1,
1251
+ public_xtls_max: -1,
1252
+ public_xtls_average: -1,
1253
+ ipver_firstIpV4: webex.internal.device.ipNetworkDetector.firstIpV4,
1254
+ ipver_firstIpV6: webex.internal.device.ipNetworkDetector.firstIpV6,
1255
+ ipver_firstMdns: webex.internal.device.ipNetworkDetector.firstMdns,
1256
+ ipver_totalTime: webex.internal.device.ipNetworkDetector.totalTime,
1257
+ trigger: 'test',
1258
+ });
1259
+ });
1260
+
1261
+ it('keeps updating reachability results after the 3s public cloud timeout expires', async () => {
1262
+ webex.config.meetings.experimental = {
1263
+ enableTcpReachability: true,
1264
+ enableTlsReachability: true,
1265
+ };
1266
+
1267
+ const reachability = new Reachability(webex);
1268
+
1269
+ const mockGetClustersResult = {
1270
+ clusters: {
1271
+ clusterA: {
1272
+ udp: ['udp-urlA'],
1273
+ tcp: ['tcp-urlA'],
1274
+ xtls: ['xtls-urlA'],
1275
+ isVideoMesh: false,
1276
+ },
1277
+ clusterB: {
1278
+ udp: ['udp-urlB'],
1279
+ tcp: ['tcp-urlB'],
1280
+ xtls: ['xtls-urlB'],
1281
+ isVideoMesh: false,
1282
+ },
1283
+ },
1284
+ joinCookie: {id: 'id'},
1285
+ };
1286
+
1287
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
1288
+
1289
+ const resultPromise = reachability.gatherReachability('test');
1290
+
1291
+ await testUtils.flushPromises();
1292
+
1293
+ // trigger some mock result events from ClusterReachability instances
1294
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('udp', {
1295
+ result: 'reachable',
1296
+ clientMediaIPs: ['1.2.3.4'],
1297
+ latencyInMilliseconds: 11,
1298
+ });
1299
+ mockClusterReachabilityInstances['clusterB'].emitFakeResult('udp', {
1300
+ result: 'reachable',
1301
+ clientMediaIPs: ['10.20.30.40'],
1302
+ latencyInMilliseconds: 22,
1303
+ });
1304
+
1305
+ clock.tick(3000);
1306
+ await resultPromise;
1307
+
1308
+ // check that the reachability results contain the 2 results from above
1309
+ await checkResults(
1310
+ {
1311
+ clusterA: {
1312
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 11},
1313
+ tcp: {result: 'unreachable'},
1314
+ xtls: {result: 'unreachable'},
1315
+ isVideoMesh: false,
1316
+ },
1317
+ clusterB: {
1318
+ udp: {result: 'reachable', clientMediaIPs: ['10.20.30.40'], latencyInMilliseconds: 22},
386
1319
  tcp: {result: 'unreachable'},
387
1320
  xtls: {result: 'unreachable'},
1321
+ isVideoMesh: false,
388
1322
  },
389
1323
  },
390
- expectedResult: false,
391
- },
392
- {
393
- title: 'multiple clusters with some missing results',
394
- mockStorage: {
395
- a: {udp: {result: 'unreachable'}},
396
- b: {tcp: {result: 'unreachable'}},
397
- c: {xtls: {result: 'unreachable'}},
398
- d: {},
399
- },
400
- expectedResult: true,
401
- },
402
- ].forEach(({mockStorage, expectedResult, title}) => {
403
- it(`returns ${expectedResult} when ${title}`, async () => {
404
- await runCheck(mockStorage, expectedResult);
405
- });
406
- });
407
- });
408
-
1324
+ mockGetClustersResult.joinCookie
1325
+ );
409
1326
 
410
- describe('gatherReachability', () => {
411
- let webex;
1327
+ // now simulate some more "late" results
1328
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('tcp', {
1329
+ result: 'reachable',
1330
+ latencyInMilliseconds: 101,
1331
+ });
1332
+ mockClusterReachabilityInstances['clusterB'].emitFakeResult('xtls', {
1333
+ result: 'reachable',
1334
+ latencyInMilliseconds: 102,
1335
+ });
412
1336
 
413
- beforeEach(async () => {
414
- webex = new MockWebex();
1337
+ // and wait for the final overall timeout
1338
+ clock.tick(12000);
415
1339
 
416
- await webex.boundedStorage.put(
417
- 'Reachability',
418
- 'reachability.result',
419
- JSON.stringify({old: 'results'})
420
- );
421
- await webex.boundedStorage.put(
422
- 'Reachability',
423
- 'reachability.joinCookie',
424
- JSON.stringify({old: 'joinCookie'})
1340
+ // the reachability results should include all results from above (including the late ones)
1341
+ await checkResults(
1342
+ {
1343
+ clusterA: {
1344
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 11},
1345
+ tcp: {result: 'reachable', latencyInMilliseconds: 101},
1346
+ xtls: {result: 'unreachable'},
1347
+ isVideoMesh: false,
1348
+ },
1349
+ clusterB: {
1350
+ udp: {result: 'reachable', clientMediaIPs: ['10.20.30.40'], latencyInMilliseconds: 22},
1351
+ tcp: {result: 'unreachable'},
1352
+ xtls: {result: 'reachable', latencyInMilliseconds: 102},
1353
+ isVideoMesh: false,
1354
+ },
1355
+ },
1356
+ mockGetClustersResult.joinCookie
425
1357
  );
426
1358
  });
427
1359
 
428
- afterEach(() => {
429
- sinon.restore();
430
- });
1360
+ it('handles clientMediaIpsUpdated event by updating clientMediaIps in results', async () => {
1361
+ webex.config.meetings.experimental = {
1362
+ enableTcpReachability: true,
1363
+ enableTlsReachability: true,
1364
+ };
431
1365
 
432
- it('stores the reachability', async () => {
433
1366
  const reachability = new Reachability(webex);
434
1367
 
435
- const reachabilityResults = {
1368
+ const mockGetClustersResult = {
436
1369
  clusters: {
437
- clusterId: {
438
- udp: 'testUDP',
1370
+ clusterA: {
1371
+ udp: ['udp-urlA'],
1372
+ tcp: ['tcp-urlA'],
1373
+ xtls: ['xtls-urlA'],
1374
+ isVideoMesh: false,
439
1375
  },
440
1376
  },
441
- };
442
- const getClustersResult = {
443
- clusters: {clusterId: 'cluster'},
444
1377
  joinCookie: {id: 'id'},
445
1378
  };
446
1379
 
447
- reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
448
- (reachability as any).performReachabilityChecks = sinon.stub().returns(reachabilityResults);
1380
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
449
1381
 
450
- const result = await reachability.gatherReachability();
1382
+ const resultPromise = reachability.gatherReachability('test');
451
1383
 
452
- assert.equal(result, reachabilityResults);
1384
+ await testUtils.flushPromises();
453
1385
 
454
- const storedResultForReachabilityResult = await webex.boundedStorage.get(
455
- 'Reachability',
456
- 'reachability.result'
457
- );
458
- const storedResultForJoinCookie = await webex.boundedStorage.get(
459
- 'Reachability',
460
- 'reachability.joinCookie'
461
- );
1386
+ // trigger a mock result event
1387
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('udp', {
1388
+ result: 'reachable',
1389
+ clientMediaIPs: ['64.103.40.20'],
1390
+ latencyInMilliseconds: 11,
1391
+ });
1392
+ // followed by some updates to client media IPs
1393
+ mockClusterReachabilityInstances['clusterA'].emitFakeClientMediaIpUpdate('udp', '64.103.40.21');
1394
+ mockClusterReachabilityInstances['clusterA'].emitFakeClientMediaIpUpdate('udp', '64.103.40.22');
1395
+
1396
+ // wait for the final overall timeout
1397
+ clock.tick(15000);
1398
+ await resultPromise;
462
1399
 
463
- assert.equal(JSON.stringify(result), storedResultForReachabilityResult);
464
- assert.equal(JSON.stringify(getClustersResult.joinCookie), storedResultForJoinCookie);
1400
+ // check that the reachability results contain all the client media ips
1401
+ await checkResults(
1402
+ {
1403
+ clusterA: {
1404
+ udp: {
1405
+ result: 'reachable',
1406
+ clientMediaIPs: ['64.103.40.20', '64.103.40.21', '64.103.40.22'],
1407
+ latencyInMilliseconds: 11,
1408
+ },
1409
+ tcp: {result: 'unreachable'},
1410
+ xtls: {result: 'unreachable'},
1411
+ isVideoMesh: false,
1412
+ },
1413
+ },
1414
+ mockGetClustersResult.joinCookie
1415
+ );
465
1416
  });
466
1417
 
467
1418
  it('keeps the stored reachability from previous call to gatherReachability if getClusters fails', async () => {
468
1419
  const reachability = new Reachability(webex);
469
1420
 
470
- const reachabilityResults = {
471
- clusters: {
472
- clusterId: {
473
- udp: 'testUDP',
474
- },
475
- },
476
- };
477
- const getClustersResult = {
478
- clusters: {clusterId: 'cluster'},
479
- joinCookie: {id: 'id'},
480
- };
481
-
482
1421
  reachability.reachabilityRequest.getClusters = sinon.stub().throws();
483
1422
 
484
- const result = await reachability.gatherReachability();
1423
+ const result = await reachability.gatherReachability('test');
485
1424
 
486
1425
  assert.empty(result);
487
1426
 
488
- const storedResultForReachabilityResult = await webex.boundedStorage.get(
489
- 'Reachability',
490
- 'reachability.result'
491
- );
492
- const storedResultForJoinCookie = await webex.boundedStorage.get(
493
- 'Reachability',
494
- 'reachability.joinCookie'
495
- );
496
-
497
- assert.equal(JSON.stringify({old: 'results'}), storedResultForReachabilityResult);
498
- assert.equal(JSON.stringify({old: 'joinCookie'}), storedResultForJoinCookie);
1427
+ await checkResults({old: 'results'}, {old: 'joinCookie'});
499
1428
  });
500
1429
 
501
1430
  it('keeps the stored reachability from previous call to gatherReachability if performReachabilityChecks fails', async () => {
502
1431
  const reachability = new Reachability(webex);
503
1432
 
504
- const reachabilityResults = {
505
- clusters: {
506
- clusterId: {
507
- udp: 'testUDP',
508
- },
509
- },
510
- };
511
1433
  const getClustersResult = {
512
1434
  clusters: {clusterId: 'cluster'},
513
- joinCookie: {id: 'id'},
1435
+ joinCookie: {id: 'cookie id'},
514
1436
  };
515
1437
 
516
1438
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
517
1439
  (reachability as any).performReachabilityChecks = sinon.stub().throws();
518
1440
 
519
- const result = await reachability.gatherReachability();
1441
+ const result = await reachability.gatherReachability('test');
520
1442
 
521
1443
  assert.empty(result);
522
1444
 
523
- const storedResultForReachabilityResult = await webex.boundedStorage.get(
524
- 'Reachability',
525
- 'reachability.result'
526
- );
527
- const storedResultForJoinCookie = await webex.boundedStorage.get(
528
- 'Reachability',
529
- 'reachability.joinCookie'
530
- );
531
-
532
- assert.equal(JSON.stringify({old: 'results'}), storedResultForReachabilityResult);
533
- assert.equal(JSON.stringify({old: 'joinCookie'}), storedResultForJoinCookie);
1445
+ await checkResults({old: 'results'}, {id: 'cookie id'});
534
1446
  });
535
1447
 
536
1448
  it('starts ClusterReachability on each media cluster', async () => {
@@ -561,14 +1473,10 @@ describe('gatherReachability', () => {
561
1473
 
562
1474
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
563
1475
 
564
- const startStub = sinon.stub().resolves({});
565
- const clusterReachabilityCtorStub = sinon
566
- .stub(ClusterReachabilityModule, 'ClusterReachability')
567
- .callsFake(() => ({
568
- start: startStub,
569
- }));
1476
+ const promise = reachability.gatherReachability('test');
570
1477
 
571
- await reachability.gatherReachability();
1478
+ await simulateTimeout();
1479
+ await promise;
572
1480
 
573
1481
  assert.calledTwice(clusterReachabilityCtorStub);
574
1482
  assert.calledWith(clusterReachabilityCtorStub, 'cluster 1', {
@@ -585,7 +1493,8 @@ describe('gatherReachability', () => {
585
1493
  isVideoMesh: true,
586
1494
  });
587
1495
 
588
- assert.calledTwice(startStub);
1496
+ assert.calledOnce(mockClusterReachabilityInstances['cluster 1'].start);
1497
+ assert.calledOnce(mockClusterReachabilityInstances['cluster 2'].start);
589
1498
  });
590
1499
 
591
1500
  it('does not do TCP reachability if it is disabled in config', async () => {
@@ -610,13 +1519,9 @@ describe('gatherReachability', () => {
610
1519
 
611
1520
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
612
1521
 
613
- const clusterReachabilityCtorStub = sinon
614
- .stub(ClusterReachabilityModule, 'ClusterReachability')
615
- .callsFake(() => ({
616
- start: sinon.stub().resolves({}),
617
- }));
618
-
619
- await reachability.gatherReachability();
1522
+ const promise = reachability.gatherReachability('test');
1523
+ await simulateTimeout();
1524
+ await promise;
620
1525
 
621
1526
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
622
1527
  isVideoMesh: false,
@@ -648,13 +1553,10 @@ describe('gatherReachability', () => {
648
1553
 
649
1554
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
650
1555
 
651
- const clusterReachabilityCtorStub = sinon
652
- .stub(ClusterReachabilityModule, 'ClusterReachability')
653
- .callsFake(() => ({
654
- start: sinon.stub().resolves({}),
655
- }));
1556
+ const promise = reachability.gatherReachability('test');
656
1557
 
657
- await reachability.gatherReachability();
1558
+ await simulateTimeout();
1559
+ await promise;
658
1560
 
659
1561
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
660
1562
  isVideoMesh: false,
@@ -686,13 +1588,10 @@ describe('gatherReachability', () => {
686
1588
 
687
1589
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
688
1590
 
689
- const clusterReachabilityCtorStub = sinon
690
- .stub(ClusterReachabilityModule, 'ClusterReachability')
691
- .callsFake(() => ({
692
- start: sinon.stub().resolves({}),
693
- }));
1591
+ const promise = reachability.gatherReachability('test');
694
1592
 
695
- await reachability.gatherReachability();
1593
+ await simulateTimeout();
1594
+ await promise;
696
1595
 
697
1596
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
698
1597
  isVideoMesh: false,
@@ -701,6 +1600,70 @@ describe('gatherReachability', () => {
701
1600
  xtls: [], // empty list because TLS is disabled in config
702
1601
  });
703
1602
  });
1603
+
1604
+ it('retry of getClusters is succesfull', async () => {
1605
+ webex.config.meetings.experimental = {
1606
+ enableTcpReachability: true,
1607
+ enableTlsReachability: false,
1608
+ };
1609
+
1610
+ const getClustersResult = {
1611
+ clusters: {
1612
+ 'cluster name': {
1613
+ udp: ['testUDP1', 'testUDP2'],
1614
+ tcp: ['testTCP1', 'testTCP2'],
1615
+ xtls: ['testXTLS1', 'testXTLS2'],
1616
+ isVideoMesh: false,
1617
+ },
1618
+ },
1619
+ joinCookie: {id: 'id'},
1620
+ };
1621
+
1622
+ const reachability = new Reachability(webex);
1623
+
1624
+ let getClustersCallCount = 0;
1625
+
1626
+ reachability.reachabilityRequest.getClusters = sinon.stub().callsFake(() => {
1627
+ getClustersCallCount++;
1628
+
1629
+ if (getClustersCallCount == 1) {
1630
+ throw new Error('fake error');
1631
+ }
1632
+
1633
+ return getClustersResult;
1634
+ });
1635
+
1636
+ const promise = reachability.gatherReachability('test');
1637
+
1638
+ await simulateTimeout();
1639
+ await promise;
1640
+
1641
+ assert.equal(getClustersCallCount, 2);
1642
+
1643
+ assert.calledOnce(clusterReachabilityCtorStub);
1644
+ });
1645
+
1646
+ it('two failed calls to getClusters', async () => {
1647
+ const reachability = new Reachability(webex);
1648
+
1649
+ let getClustersCallCount = 0;
1650
+
1651
+ reachability.reachabilityRequest.getClusters = sinon.stub().callsFake(() => {
1652
+ getClustersCallCount++;
1653
+
1654
+ throw new Error('fake error');
1655
+ });
1656
+
1657
+ const promise = reachability.gatherReachability('test');
1658
+
1659
+ await simulateTimeout();
1660
+
1661
+ await promise;
1662
+
1663
+ assert.equal(getClustersCallCount, 2);
1664
+
1665
+ assert.neverCalledWith(clusterReachabilityCtorStub);
1666
+ });
704
1667
  });
705
1668
 
706
1669
  describe('getReachabilityResults', () => {
@@ -1033,3 +1996,307 @@ describe('getReachabilityMetrics', () => {
1033
1996
  );
1034
1997
  });
1035
1998
  });
1999
+
2000
+ class TestReachability extends Reachability {
2001
+ constructor(webex: object) {
2002
+ super(webex);
2003
+ }
2004
+
2005
+ public testGetStatistics(
2006
+ results: Array<ClusterReachabilityModule.ClusterReachabilityResult & {isVideoMesh: boolean}>,
2007
+ protocol: 'udp' | 'tcp' | 'xtls',
2008
+ isVideoMesh: boolean
2009
+ ) {
2010
+ return this.getStatistics(results, protocol, isVideoMesh);
2011
+ }
2012
+
2013
+ public testSendMetric() {
2014
+ return this.sendMetric();
2015
+ }
2016
+
2017
+ public setFakeClusterReachability(fakeClusterReachability) {
2018
+ this.clusterReachability = fakeClusterReachability;
2019
+ }
2020
+ }
2021
+
2022
+ describe('getStatistics', () => {
2023
+ let webex;
2024
+ let reachability;
2025
+
2026
+ beforeEach(() => {
2027
+ webex = new MockWebex();
2028
+ reachability = new TestReachability(webex);
2029
+ });
2030
+
2031
+ it('takes values from the correct protocol', () => {
2032
+ const results = [
2033
+ {
2034
+ udp: {
2035
+ result: 'reachable',
2036
+ latencyInMilliseconds: 10,
2037
+ },
2038
+ tcp: {
2039
+ result: 'reachable',
2040
+ latencyInMilliseconds: 1010,
2041
+ },
2042
+ xtls: {
2043
+ result: 'reachable',
2044
+ latencyInMilliseconds: 2010,
2045
+ },
2046
+ isVideoMesh: false,
2047
+ },
2048
+ {
2049
+ udp: {
2050
+ result: 'reachable',
2051
+ latencyInMilliseconds: 20,
2052
+ },
2053
+ tcp: {
2054
+ result: 'reachable',
2055
+ latencyInMilliseconds: 1020,
2056
+ },
2057
+ xtls: {
2058
+ result: 'reachable',
2059
+ latencyInMilliseconds: 2020,
2060
+ },
2061
+ isVideoMesh: false,
2062
+ },
2063
+ {
2064
+ udp: {
2065
+ result: 'reachable',
2066
+ latencyInMilliseconds: 30,
2067
+ },
2068
+ tcp: {
2069
+ result: 'reachable',
2070
+ latencyInMilliseconds: 1030,
2071
+ },
2072
+ xtls: {
2073
+ result: 'reachable',
2074
+ latencyInMilliseconds: 2030,
2075
+ },
2076
+ isVideoMesh: false,
2077
+ },
2078
+ ];
2079
+
2080
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
2081
+ min: 10,
2082
+ max: 30,
2083
+ average: 20,
2084
+ });
2085
+ assert.deepEqual(reachability.testGetStatistics(results, 'tcp', false), {
2086
+ min: 1010,
2087
+ max: 1030,
2088
+ average: 1020,
2089
+ });
2090
+ assert.deepEqual(reachability.testGetStatistics(results, 'xtls', false), {
2091
+ min: 2010,
2092
+ max: 2030,
2093
+ average: 2020,
2094
+ });
2095
+ });
2096
+
2097
+ it('filters based on isVideoMesh value', () => {
2098
+ const results = [
2099
+ {
2100
+ udp: {
2101
+ result: 'reachable',
2102
+ latencyInMilliseconds: 10,
2103
+ },
2104
+ isVideoMesh: true,
2105
+ },
2106
+ {
2107
+ udp: {
2108
+ result: 'reachable',
2109
+ latencyInMilliseconds: 20,
2110
+ },
2111
+ isVideoMesh: true,
2112
+ },
2113
+ {
2114
+ udp: {
2115
+ result: 'reachable',
2116
+ latencyInMilliseconds: 30,
2117
+ },
2118
+ isVideoMesh: true,
2119
+ },
2120
+ {
2121
+ udp: {
2122
+ result: 'reachable',
2123
+ latencyInMilliseconds: 100,
2124
+ },
2125
+ isVideoMesh: false,
2126
+ },
2127
+ {
2128
+ udp: {
2129
+ result: 'reachable',
2130
+ latencyInMilliseconds: 200,
2131
+ },
2132
+ isVideoMesh: false,
2133
+ },
2134
+ ];
2135
+
2136
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', true), {
2137
+ min: 10,
2138
+ max: 30,
2139
+ average: 20,
2140
+ });
2141
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
2142
+ min: 100,
2143
+ max: 200,
2144
+ average: 150,
2145
+ });
2146
+ });
2147
+
2148
+ it('only takes into account "reachable" results', () => {
2149
+ const results = [
2150
+ {
2151
+ udp: {
2152
+ result: 'reachable',
2153
+ latencyInMilliseconds: 10,
2154
+ },
2155
+ isVideoMesh: false,
2156
+ },
2157
+ {
2158
+ udp: {
2159
+ result: 'unreachable',
2160
+ latencyInMilliseconds: 100, // value put in here just for testing, in practice we wouldn't have any value here if it was unreachable
2161
+ },
2162
+ isVideoMesh: false,
2163
+ },
2164
+ {
2165
+ udp: {
2166
+ result: 'reachable',
2167
+ latencyInMilliseconds: 20,
2168
+ },
2169
+ isVideoMesh: false,
2170
+ },
2171
+ {
2172
+ udp: {
2173
+ result: 'untested',
2174
+ latencyInMilliseconds: 200, // value put in here just for testing, in practice we wouldn't have any value here if it was untested
2175
+ },
2176
+ isVideoMesh: false,
2177
+ },
2178
+ ];
2179
+
2180
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
2181
+ min: 10,
2182
+ max: 20,
2183
+ average: 15,
2184
+ });
2185
+ });
2186
+
2187
+ it('handles the case when results are empty', () => {
2188
+ assert.deepEqual(reachability.testGetStatistics([], 'udp', false), {
2189
+ min: -1,
2190
+ max: -1,
2191
+ average: -1,
2192
+ });
2193
+ });
2194
+
2195
+ it('handles the case when results are empty after filtering', () => {
2196
+ const fakeResults = [
2197
+ {
2198
+ udp: {
2199
+ result: 'untested', // it will get filtered out because of this value
2200
+ latencyInMilliseconds: 10,
2201
+ },
2202
+ tcp: {
2203
+ result: 'reachable',
2204
+ latencyInMilliseconds: 10, // it will get filtered out because of the tcp protocol
2205
+ },
2206
+ isVideoMesh: false,
2207
+ },
2208
+ {
2209
+ udp: {
2210
+ result: 'reachable',
2211
+ latencyInMilliseconds: 10,
2212
+ },
2213
+ isVideoMesh: true, // it will get filtered out because of this value
2214
+ },
2215
+ ];
2216
+
2217
+ assert.deepEqual(reachability.testGetStatistics(fakeResults, 'udp', false), {
2218
+ min: -1,
2219
+ max: -1,
2220
+ average: -1,
2221
+ });
2222
+ });
2223
+ });
2224
+
2225
+ describe('sendMetric', () => {
2226
+ let webex;
2227
+ let reachability;
2228
+
2229
+ beforeEach(() => {
2230
+ webex = new MockWebex();
2231
+ reachability = new TestReachability(webex);
2232
+
2233
+ sinon.stub(Metrics, 'sendBehavioralMetric');
2234
+ });
2235
+
2236
+ it('works as expected', async () => {
2237
+ // setup stub for getStatistics to return values that show what parameters it was called with,
2238
+ // this way we can verify that the correct results of calls to getStatistics are placed
2239
+ // in correct data fields when sendBehavioralMetric() is called
2240
+ const getStatisticsStub = sinon
2241
+ .stub(reachability, 'getStatistics')
2242
+ .callsFake((results, protocol, isVideoMesh) => {
2243
+ return {result: 'fake', protocol, isVideoMesh};
2244
+ });
2245
+
2246
+ // setup fake clusterReachability results
2247
+ reachability.setFakeClusterReachability({
2248
+ cluster1: {
2249
+ getResult: sinon.stub().returns({result: 'result 1'}),
2250
+ isVideoMesh: true,
2251
+ },
2252
+ cluster2: {
2253
+ getResult: sinon.stub().returns({result: 'result 2'}),
2254
+ isVideoMesh: false,
2255
+ },
2256
+ cluster3: {
2257
+ getResult: sinon.stub().returns({result: 'result 3'}),
2258
+ isVideoMesh: false,
2259
+ },
2260
+ });
2261
+
2262
+ await reachability.sendMetric();
2263
+
2264
+ // each call to getStatistics should be made with all the results from all fake clusterReachability:
2265
+ const expectedResults = [
2266
+ {
2267
+ result: 'result 1',
2268
+ isVideoMesh: true,
2269
+ },
2270
+ {
2271
+ result: 'result 2',
2272
+ isVideoMesh: false,
2273
+ },
2274
+ {
2275
+ result: 'result 3',
2276
+ isVideoMesh: false,
2277
+ },
2278
+ ];
2279
+
2280
+ // check that getStatistics is called 4 times and each time with all the results
2281
+ assert.callCount(getStatisticsStub, 4);
2282
+ assert.alwaysCalledWith(getStatisticsStub, expectedResults, sinon.match.any, sinon.match.any);
2283
+
2284
+ assert.calledWith(Metrics.sendBehavioralMetric, 'js_sdk_reachability_completed', {
2285
+ vmn_udp_result: 'fake',
2286
+ vmn_udp_protocol: 'udp',
2287
+ vmn_udp_isVideoMesh: true,
2288
+
2289
+ public_udp_result: 'fake',
2290
+ public_udp_protocol: 'udp',
2291
+ public_udp_isVideoMesh: false,
2292
+
2293
+ public_tcp_result: 'fake',
2294
+ public_tcp_protocol: 'tcp',
2295
+ public_tcp_isVideoMesh: false,
2296
+
2297
+ public_xtls_result: 'fake',
2298
+ public_xtls_protocol: 'xtls',
2299
+ public_xtls_isVideoMesh: false,
2300
+ });
2301
+ });
2302
+ });