@webex/plugin-meetings 3.3.1-next.13 → 3.3.1-next.15

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.
@@ -1,12 +1,16 @@
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
7
  ReachabilityResults,
6
8
  ReachabilityResultsForBackend,
7
9
  } from '@webex/plugin-meetings/src/reachability/';
10
+ import { ClusterNode } from '../../../../src/reachability/request';
8
11
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
9
12
  import * as ClusterReachabilityModule from '@webex/plugin-meetings/src/reachability/clusterReachability';
13
+ import Metrics from '@webex/plugin-meetings/src/metrics';
10
14
 
11
15
  import {IP_VERSION} from '@webex/plugin-meetings/src/constants';
12
16
 
@@ -406,13 +410,71 @@ describe('isWebexMediaBackendUnreachable', () => {
406
410
  });
407
411
  });
408
412
 
413
+ /**
414
+ * helper class to mock ClusterReachability and allow to easily
415
+ * simulate 'resultReady' events from it
416
+ */
417
+ class MockClusterReachability extends EventEmitter {
418
+ mockResult = {
419
+ udp: {
420
+ result: 'untested',
421
+ },
422
+ tcp: {
423
+ result: 'untested',
424
+ },
425
+ xtls: {
426
+ result: 'untested',
427
+ },
428
+ };
429
+
430
+ isVideoMesh: boolean;
431
+ name: string;
432
+
433
+ constructor(name: string, clusterInfo: ClusterNode) {
434
+ super();
435
+ this.name = name;
436
+ this.isVideoMesh = clusterInfo.isVideoMesh;
437
+ }
438
+
439
+ abort = sinon.stub();
440
+ start = sinon.stub();
441
+
442
+ getResult() {
443
+ return this.mockResult;
444
+ }
445
+
446
+ /**
447
+ * Emits a fake 'resultReady' event and makes sure that the same result
448
+ * is returned when getResult() is called.
449
+ *
450
+ * @param protocol
451
+ * @param result
452
+ */
453
+ public emitFakeResult(protocol, result) {
454
+ this.mockResult[protocol] = result;
455
+ this.emit(ClusterReachabilityModule.Events.resultReady, {protocol, ...result});
456
+ }
457
+
458
+ public emitFakeClientMediaIpUpdate(protocol, newIp) {
459
+ this.mockResult[protocol].clientMediaIPs.push(newIp);
460
+ this.emit(ClusterReachabilityModule.Events.clientMediaIpsUpdated, {
461
+ protocol,
462
+ clientMediaIPs: this.mockResult[protocol].clientMediaIPs,
463
+ });
464
+ }
465
+ }
409
466
 
410
467
  describe('gatherReachability', () => {
411
468
  let webex;
469
+ let clock;
470
+ let clusterReachabilityCtorStub;
471
+ let mockClusterReachabilityInstances: Record<string, MockClusterReachability>;
412
472
 
413
473
  beforeEach(async () => {
414
474
  webex = new MockWebex();
415
475
 
476
+ sinon.stub(Metrics, 'sendBehavioralMetric');
477
+
416
478
  await webex.boundedStorage.put(
417
479
  'Reachability',
418
480
  'reachability.result',
@@ -423,94 +485,784 @@ describe('gatherReachability', () => {
423
485
  'reachability.joinCookie',
424
486
  JSON.stringify({old: 'joinCookie'})
425
487
  );
488
+
489
+ clock = sinon.useFakeTimers();
490
+
491
+ mockClusterReachabilityInstances = {};
492
+
493
+ clusterReachabilityCtorStub = sinon
494
+ .stub(ClusterReachabilityModule, 'ClusterReachability')
495
+ .callsFake((id, cluster) => {
496
+ const mockInstance = new MockClusterReachability(id, cluster);
497
+
498
+ mockClusterReachabilityInstances[id] = mockInstance;
499
+ return mockInstance;
500
+ });
426
501
  });
427
502
 
428
503
  afterEach(() => {
429
504
  sinon.restore();
505
+ clock.restore();
430
506
  });
431
507
 
432
- it('stores the reachability', async () => {
508
+ // simulates time progression so that Reachability times out
509
+ const simulateTimeout = async () => {
510
+ await testUtils.flushPromises();
511
+ clock.tick(3000);
512
+ };
513
+
514
+ const checkResults = async (expectedResults, expectedJoinCookie) => {
515
+ const storedResultForReachabilityResult = await webex.boundedStorage.get(
516
+ 'Reachability',
517
+ 'reachability.result'
518
+ );
519
+ const storedResultForJoinCookie = await webex.boundedStorage.get(
520
+ 'Reachability',
521
+ 'reachability.joinCookie'
522
+ );
523
+
524
+ assert.equal(storedResultForReachabilityResult, JSON.stringify(expectedResults));
525
+ assert.equal(storedResultForJoinCookie, JSON.stringify(expectedJoinCookie));
526
+ };
527
+
528
+ [
529
+ // ========================================================================
530
+ {
531
+ title: '1 cluster with events triggered for each protocol',
532
+ waitShortTimeout: false,
533
+ waitLongTimeout: false,
534
+ mockClusters: {
535
+ cluster1: {
536
+ udp: ['udp-url1'],
537
+ tcp: ['tcp-url1'],
538
+ xtls: ['xtls-url1'],
539
+ isVideoMesh: false,
540
+ },
541
+ },
542
+ mockResultReadyEvents: [
543
+ {
544
+ clusterId: 'cluster1',
545
+ protocol: 'tcp',
546
+ result: {
547
+ result: 'reachable',
548
+ latencyInMilliseconds: 11,
549
+ },
550
+ },
551
+ {
552
+ clusterId: 'cluster1',
553
+ protocol: 'udp',
554
+ result: {
555
+ result: 'reachable',
556
+ clientMediaIPs: ['1.2.3.4'],
557
+ latencyInMilliseconds: 22,
558
+ },
559
+ },
560
+ {
561
+ clusterId: 'cluster1',
562
+ protocol: 'xtls',
563
+ result: {
564
+ result: 'reachable',
565
+ latencyInMilliseconds: 33,
566
+ },
567
+ },
568
+ ],
569
+ expectedResults: {
570
+ cluster1: {
571
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 22},
572
+ tcp: {result: 'reachable', latencyInMilliseconds: 11},
573
+ xtls: {result: 'reachable', latencyInMilliseconds: 33},
574
+ isVideoMesh: false,
575
+ },
576
+ },
577
+ expectedMetrics: {
578
+ vmn: {udp: {min: -1, max: -1, average: -1}},
579
+ public: {
580
+ udp: {min: 22, max: 22, average: 22},
581
+ tcp: {min: 11, max: 11, average: 11},
582
+ xtls: {min: 33, max: 33, average: 33},
583
+ },
584
+ },
585
+ },
586
+ // ========================================================================
587
+ {
588
+ title:
589
+ '3 clusters: one with an event for each protocol, one with no events, one with no urls for tcp and xtls',
590
+ waitShortTimeout: 'public',
591
+ waitLongTimeout: true,
592
+ mockClusters: {
593
+ cluster1: {
594
+ udp: ['udp-url1.1', 'udp-url1.2'],
595
+ tcp: ['tcp-url1.1', 'tcp-url1.2'],
596
+ xtls: ['xtls-url1.1', 'xtls-url1.2'],
597
+ isVideoMesh: false,
598
+ },
599
+ cluster2: {
600
+ udp: ['udp-url2.1'],
601
+ tcp: ['tcp-url2.1'],
602
+ xtls: ['xtls-url2.1'],
603
+ isVideoMesh: false,
604
+ },
605
+ cluster3: {
606
+ udp: ['udp-url1'],
607
+ tcp: [],
608
+ xtls: [],
609
+ isVideoMesh: true,
610
+ },
611
+ },
612
+ mockResultReadyEvents: [
613
+ {
614
+ clusterId: 'cluster1',
615
+ protocol: 'udp',
616
+ result: {
617
+ result: 'reachable',
618
+ clientMediaIPs: ['1.2.3.4'],
619
+ latencyInMilliseconds: 13,
620
+ },
621
+ },
622
+ {
623
+ clusterId: 'cluster1',
624
+ protocol: 'tcp',
625
+ result: {
626
+ result: 'reachable',
627
+ latencyInMilliseconds: 53,
628
+ },
629
+ },
630
+ {
631
+ clusterId: 'cluster1',
632
+ protocol: 'xtls',
633
+ result: {
634
+ result: 'reachable',
635
+ latencyInMilliseconds: 113,
636
+ },
637
+ },
638
+ ],
639
+ expectedResults: {
640
+ cluster1: {
641
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 13},
642
+ tcp: {result: 'reachable', latencyInMilliseconds: 53},
643
+ xtls: {result: 'reachable', latencyInMilliseconds: 113},
644
+ isVideoMesh: false,
645
+ },
646
+ cluster2: {
647
+ udp: {result: 'unreachable'},
648
+ tcp: {result: 'unreachable'},
649
+ xtls: {result: 'unreachable'},
650
+ isVideoMesh: false,
651
+ },
652
+ cluster3: {
653
+ udp: {result: 'unreachable'},
654
+ tcp: {result: 'untested'},
655
+ xtls: {result: 'untested'},
656
+ isVideoMesh: true,
657
+ },
658
+ },
659
+ expectedMetrics: {
660
+ vmn: {udp: {min: -1, max: -1, average: -1}},
661
+ public: {
662
+ udp: {min: 13, max: 13, average: 13},
663
+ tcp: {min: 53, max: 53, average: 53},
664
+ xtls: {min: 113, max: 113, average: 113},
665
+ },
666
+ },
667
+ },
668
+ // ========================================================================
669
+ {
670
+ title: '3 clusters: all with all results ready in time for all protocols',
671
+ waitShortTimeout: false,
672
+ waitLongTimeout: false,
673
+ mockClusters: {
674
+ cluster1: {
675
+ udp: ['udp-url1'],
676
+ tcp: ['tcp-url1'],
677
+ xtls: ['xtls-url1'],
678
+ isVideoMesh: false,
679
+ },
680
+ cluster2: {
681
+ udp: ['udp-url2'],
682
+ tcp: ['tcp-url2'],
683
+ xtls: ['xtls-url2'],
684
+ isVideoMesh: false,
685
+ },
686
+ cluster3: {
687
+ udp: ['udp-url3'],
688
+ tcp: ['tcp-url3'],
689
+ xtls: ['xtls-url3'],
690
+ isVideoMesh: false,
691
+ },
692
+ },
693
+ mockResultReadyEvents: [
694
+ {
695
+ clusterId: 'cluster1',
696
+ protocol: 'udp',
697
+ result: {
698
+ result: 'reachable',
699
+ clientMediaIPs: ['1.2.3.4'],
700
+ latencyInMilliseconds: 10,
701
+ },
702
+ },
703
+ {
704
+ clusterId: 'cluster1',
705
+ protocol: 'tcp',
706
+ result: {
707
+ result: 'reachable',
708
+ latencyInMilliseconds: 100,
709
+ },
710
+ },
711
+ {
712
+ clusterId: 'cluster1',
713
+ protocol: 'xtls',
714
+ result: {
715
+ result: 'reachable',
716
+ latencyInMilliseconds: 200,
717
+ },
718
+ },
719
+ {
720
+ clusterId: 'cluster2',
721
+ protocol: 'udp',
722
+ result: {
723
+ result: 'reachable',
724
+ clientMediaIPs: ['1.2.3.4'],
725
+ latencyInMilliseconds: 20,
726
+ },
727
+ },
728
+ {
729
+ clusterId: 'cluster2',
730
+ protocol: 'tcp',
731
+ result: {
732
+ result: 'reachable',
733
+ latencyInMilliseconds: 110,
734
+ },
735
+ },
736
+ {
737
+ clusterId: 'cluster2',
738
+ protocol: 'xtls',
739
+ result: {
740
+ result: 'reachable',
741
+ latencyInMilliseconds: 220,
742
+ },
743
+ },
744
+ {
745
+ clusterId: 'cluster3',
746
+ protocol: 'udp',
747
+ result: {
748
+ result: 'reachable',
749
+ clientMediaIPs: ['1.2.3.4'],
750
+ latencyInMilliseconds: 30,
751
+ },
752
+ },
753
+ {
754
+ clusterId: 'cluster3',
755
+ protocol: 'tcp',
756
+ result: {
757
+ result: 'reachable',
758
+ latencyInMilliseconds: 120,
759
+ },
760
+ },
761
+ {
762
+ clusterId: 'cluster3',
763
+ protocol: 'xtls',
764
+ result: {
765
+ result: 'reachable',
766
+ latencyInMilliseconds: 240,
767
+ },
768
+ },
769
+ ],
770
+ expectedResults: {
771
+ cluster1: {
772
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 10},
773
+ tcp: {result: 'reachable', latencyInMilliseconds: 100},
774
+ xtls: {result: 'reachable', latencyInMilliseconds: 200},
775
+ isVideoMesh: false,
776
+ },
777
+ cluster2: {
778
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 20},
779
+ tcp: {result: 'reachable', latencyInMilliseconds: 110},
780
+ xtls: {result: 'reachable', latencyInMilliseconds: 220},
781
+ isVideoMesh: false,
782
+ },
783
+ cluster3: {
784
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 30},
785
+ tcp: {result: 'reachable', latencyInMilliseconds: 120},
786
+ xtls: {result: 'reachable', latencyInMilliseconds: 240},
787
+ isVideoMesh: false,
788
+ },
789
+ },
790
+ expectedMetrics: {
791
+ vmn: {udp: {min: -1, max: -1, average: -1}},
792
+ public: {
793
+ udp: {min: 10, max: 30, average: 20},
794
+ tcp: {min: 100, max: 120, average: 110},
795
+ xtls: {min: 200, max: 240, average: 220},
796
+ },
797
+ },
798
+ },
799
+ // ========================================================================
800
+ {
801
+ title: '2 clusters: both with no results at all',
802
+ waitShortTimeout: 'public',
803
+ waitLongTimeout: true,
804
+ mockClusters: {
805
+ cluster1: {
806
+ udp: ['udp-url1'],
807
+ tcp: ['tcp-url1'],
808
+ xtls: ['xtls-url1'],
809
+ isVideoMesh: false,
810
+ },
811
+ cluster2: {
812
+ udp: ['udp-url2'],
813
+ tcp: ['tcp-url2'],
814
+ xtls: ['xtls-url2'],
815
+ isVideoMesh: false,
816
+ },
817
+ },
818
+ mockResultReadyEvents: [],
819
+ expectedResults: {
820
+ cluster1: {
821
+ udp: {result: 'unreachable'},
822
+ tcp: {result: 'unreachable'},
823
+ xtls: {result: 'unreachable'},
824
+ isVideoMesh: false,
825
+ },
826
+ cluster2: {
827
+ udp: {result: 'unreachable'},
828
+ tcp: {result: 'unreachable'},
829
+ xtls: {result: 'unreachable'},
830
+ isVideoMesh: false,
831
+ },
832
+ },
833
+ expectedMetrics: {
834
+ vmn: {udp: {min: -1, max: -1, average: -1}},
835
+ public: {
836
+ udp: {min: -1, max: -1, average: -1},
837
+ tcp: {min: -1, max: -1, average: -1},
838
+ xtls: {min: -1, max: -1, average: -1},
839
+ },
840
+ },
841
+ },
842
+ // ========================================================================
843
+ {
844
+ title:
845
+ '3 clusters: 2 VMN clusters missing results, but the public one has all results within 1s',
846
+ waitShortTimeout: 'vmn',
847
+ waitLongTimeout: true,
848
+ mockClusters: {
849
+ vmnCluster1: {
850
+ udp: ['udp-url1'],
851
+ tcp: ['tcp-url1'],
852
+ xtls: ['xtls-url1'],
853
+ isVideoMesh: true,
854
+ },
855
+ publicCluster: {
856
+ udp: ['udp-url2'],
857
+ tcp: ['tcp-url2'],
858
+ xtls: ['xtls-url2'],
859
+ isVideoMesh: false,
860
+ },
861
+ vmnCluster2: {
862
+ udp: ['udp-url3'],
863
+ tcp: ['tcp-url3'],
864
+ xtls: ['xtls-url3'],
865
+ isVideoMesh: true,
866
+ },
867
+ },
868
+ mockResultReadyEvents: [
869
+ {
870
+ clusterId: 'publicCluster',
871
+ protocol: 'udp',
872
+ result: {
873
+ result: 'reachable',
874
+ clientMediaIPs: ['1.2.3.4'],
875
+ latencyInMilliseconds: 10,
876
+ },
877
+ },
878
+ {
879
+ clusterId: 'publicCluster',
880
+ protocol: 'tcp',
881
+ result: {
882
+ result: 'reachable',
883
+ latencyInMilliseconds: 100,
884
+ },
885
+ },
886
+ {
887
+ clusterId: 'publicCluster',
888
+ protocol: 'xtls',
889
+ result: {
890
+ result: 'reachable',
891
+ latencyInMilliseconds: 200,
892
+ },
893
+ },
894
+ ],
895
+ expectedResults: {
896
+ vmnCluster1: {
897
+ udp: {result: 'unreachable'},
898
+ tcp: {result: 'untested'},
899
+ xtls: {result: 'untested'},
900
+ isVideoMesh: true,
901
+ },
902
+ publicCluster: {
903
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 10},
904
+ tcp: {result: 'reachable', latencyInMilliseconds: 100},
905
+ xtls: {result: 'reachable', latencyInMilliseconds: 200},
906
+ isVideoMesh: false,
907
+ },
908
+ vmnCluster2: {
909
+ udp: {result: 'unreachable'},
910
+ tcp: {result: 'untested'},
911
+ xtls: {result: 'untested'},
912
+ isVideoMesh: true,
913
+ },
914
+ },
915
+ expectedMetrics: {
916
+ vmn: {udp: {min: -1, max: -1, average: -1}},
917
+ public: {
918
+ udp: {min: 10, max: 10, average: 10},
919
+ tcp: {min: 100, max: 100, average: 100},
920
+ xtls: {min: 200, max: 200, average: 200},
921
+ },
922
+ },
923
+ },
924
+ // ========================================================================
925
+ {
926
+ title: '2 VMN clusters with all results',
927
+ waitShortTimeout: false,
928
+ waitLongTimeout: false,
929
+ mockClusters: {
930
+ vmnCluster1: {
931
+ udp: ['udp-url1'],
932
+ tcp: [],
933
+ xtls: [],
934
+ isVideoMesh: true,
935
+ },
936
+ vmnCluster2: {
937
+ udp: ['udp-url3'],
938
+ tcp: [],
939
+ xtls: [],
940
+ isVideoMesh: true,
941
+ },
942
+ },
943
+ mockResultReadyEvents: [
944
+ {
945
+ clusterId: 'vmnCluster1',
946
+ protocol: 'udp',
947
+ result: {
948
+ result: 'reachable',
949
+ clientMediaIPs: ['192.168.10.1'],
950
+ latencyInMilliseconds: 100,
951
+ },
952
+ },
953
+ {
954
+ clusterId: 'vmnCluster2',
955
+ protocol: 'udp',
956
+ result: {
957
+ result: 'reachable',
958
+ clientMediaIPs: ['192.168.0.1'],
959
+ latencyInMilliseconds: 300,
960
+ },
961
+ },
962
+ ],
963
+ expectedResults: {
964
+ vmnCluster1: {
965
+ udp: {result: 'reachable', clientMediaIPs: ['192.168.10.1'], latencyInMilliseconds: 100},
966
+ tcp: {result: 'untested'},
967
+ xtls: {result: 'untested'},
968
+ isVideoMesh: true,
969
+ },
970
+ vmnCluster2: {
971
+ udp: {result: 'reachable', clientMediaIPs: ['192.168.0.1'], latencyInMilliseconds: 300},
972
+ tcp: {result: 'untested'},
973
+ xtls: {result: 'untested'},
974
+ isVideoMesh: true,
975
+ },
976
+ },
977
+ expectedMetrics: {
978
+ vmn: {udp: {min: 100, max: 300, average: 200}},
979
+ public: {
980
+ udp: {min: -1, max: -1, average: -1},
981
+ tcp: {min: -1, max: -1, average: -1},
982
+ xtls: {min: -1, max: -1, average: -1},
983
+ },
984
+ },
985
+ },
986
+ ].forEach(
987
+ ({
988
+ title,
989
+ waitShortTimeout,
990
+ waitLongTimeout,
991
+ mockClusters,
992
+ mockResultReadyEvents,
993
+ expectedResults,
994
+ expectedMetrics,
995
+ }) =>
996
+ it(`works correctly for the case: ${title}`, async () => {
997
+ webex.config.meetings.experimental = {
998
+ enableTcpReachability: true,
999
+ enableTlsReachability: true,
1000
+ };
1001
+
1002
+ const receivedEvents = {
1003
+ done: 0,
1004
+ firstResultAvailable: {
1005
+ udp: 0,
1006
+ tcp: 0,
1007
+ xtls: 0,
1008
+ },
1009
+ };
1010
+
1011
+ const reachability = new Reachability(webex);
1012
+
1013
+ reachability.on('reachability:done', () => {
1014
+ receivedEvents.done += 1;
1015
+ });
1016
+ reachability.on('reachability:firstResultAvailable', ({protocol}) => {
1017
+ receivedEvents.firstResultAvailable[protocol] += 1;
1018
+ });
1019
+
1020
+ const mockGetClustersResult = {
1021
+ clusters: {},
1022
+ joinCookie: {id: 'id'},
1023
+ };
1024
+
1025
+ Object.entries(mockClusters).forEach(([id, mockCluster]) => {
1026
+ mockGetClustersResult.clusters[id] = mockCluster;
1027
+ });
1028
+
1029
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
1030
+
1031
+ const resultPromise = reachability.gatherReachability();
1032
+
1033
+ await testUtils.flushPromises();
1034
+
1035
+ // check that ClusterReachability instance was created for each cluster
1036
+ Object.entries(mockClusters).forEach(([id, mockCluster]) => {
1037
+ assert.calledWith(clusterReachabilityCtorStub, id, mockCluster);
1038
+ });
1039
+
1040
+ // trigger mock result events from ClusterReachability instances
1041
+ mockResultReadyEvents.forEach((mockEvent) => {
1042
+ mockClusterReachabilityInstances[mockEvent.clusterId].emitFakeResult(
1043
+ mockEvent.protocol,
1044
+ mockEvent.result
1045
+ );
1046
+ });
1047
+
1048
+ if (waitShortTimeout === 'public') {
1049
+ clock.tick(3000);
1050
+ }
1051
+ if (waitShortTimeout === 'vmn') {
1052
+ clock.tick(1000);
1053
+ }
1054
+
1055
+ await resultPromise;
1056
+
1057
+ await checkResults(expectedResults, mockGetClustersResult.joinCookie);
1058
+
1059
+ if (waitLongTimeout) {
1060
+ // we need to wait either 14 or 12 seconds to get to the 15s timeout (depending on how much we waited earlier)
1061
+ clock.tick(waitShortTimeout === 'vmn' ? 14000 : 12000);
1062
+
1063
+ // we check the results again after the long timeout - they should be the same
1064
+ await checkResults(expectedResults, mockGetClustersResult.joinCookie);
1065
+ }
1066
+
1067
+ // now check events emitted by Reachability class
1068
+ assert.equal(receivedEvents['done'], 1);
1069
+
1070
+ // if we've mocked at least one event for any protocol, check that we received
1071
+ // firstResultAvailable event for that protocol
1072
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'udp').length > 0) {
1073
+ assert.equal(receivedEvents['firstResultAvailable']['udp'], 1);
1074
+ }
1075
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'tcp').length > 0) {
1076
+ assert.equal(receivedEvents['firstResultAvailable']['tcp'], 1);
1077
+ }
1078
+ if (mockResultReadyEvents.filter((event) => event.protocol === 'xtls').length > 0) {
1079
+ assert.equal(receivedEvents['firstResultAvailable']['xtls'], 1);
1080
+ }
1081
+
1082
+ // finally, check the metrics
1083
+ assert.calledWith(
1084
+ Metrics.sendBehavioralMetric,
1085
+ 'js_sdk_reachability_completed',
1086
+ expectedMetrics
1087
+ );
1088
+ })
1089
+ );
1090
+
1091
+ it('keeps updating reachability results after the 3s public cloud timeout expires', async () => {
1092
+ webex.config.meetings.experimental = {
1093
+ enableTcpReachability: true,
1094
+ enableTlsReachability: true,
1095
+ };
1096
+
433
1097
  const reachability = new Reachability(webex);
434
1098
 
435
- const reachabilityResults = {
1099
+ const mockGetClustersResult = {
436
1100
  clusters: {
437
- clusterId: {
438
- udp: 'testUDP',
1101
+ clusterA: {
1102
+ udp: ['udp-urlA'],
1103
+ tcp: ['tcp-urlA'],
1104
+ xtls: ['xtls-urlA'],
1105
+ isVideoMesh: false,
1106
+ },
1107
+ clusterB: {
1108
+ udp: ['udp-urlB'],
1109
+ tcp: ['tcp-urlB'],
1110
+ xtls: ['xtls-urlB'],
1111
+ isVideoMesh: false,
439
1112
  },
440
1113
  },
441
- };
442
- const getClustersResult = {
443
- clusters: {clusterId: 'cluster'},
444
1114
  joinCookie: {id: 'id'},
445
1115
  };
446
1116
 
447
- reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
448
- (reachability as any).performReachabilityChecks = sinon.stub().returns(reachabilityResults);
1117
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
449
1118
 
450
- const result = await reachability.gatherReachability();
1119
+ const resultPromise = reachability.gatherReachability();
451
1120
 
452
- assert.equal(result, reachabilityResults);
1121
+ await testUtils.flushPromises();
453
1122
 
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'
1123
+ // trigger some mock result events from ClusterReachability instances
1124
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('udp', {
1125
+ result: 'reachable',
1126
+ clientMediaIPs: ['1.2.3.4'],
1127
+ latencyInMilliseconds: 11,
1128
+ });
1129
+ mockClusterReachabilityInstances['clusterB'].emitFakeResult('udp', {
1130
+ result: 'reachable',
1131
+ clientMediaIPs: ['10.20.30.40'],
1132
+ latencyInMilliseconds: 22,
1133
+ });
1134
+
1135
+ clock.tick(3000);
1136
+ await resultPromise;
1137
+
1138
+ // check that the reachability results contain the 2 results from above
1139
+ await checkResults(
1140
+ {
1141
+ clusterA: {
1142
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 11},
1143
+ tcp: {result: 'unreachable'},
1144
+ xtls: {result: 'unreachable'},
1145
+ isVideoMesh: false,
1146
+ },
1147
+ clusterB: {
1148
+ udp: {result: 'reachable', clientMediaIPs: ['10.20.30.40'], latencyInMilliseconds: 22},
1149
+ tcp: {result: 'unreachable'},
1150
+ xtls: {result: 'unreachable'},
1151
+ isVideoMesh: false,
1152
+ },
1153
+ },
1154
+ mockGetClustersResult.joinCookie
461
1155
  );
462
1156
 
463
- assert.equal(JSON.stringify(result), storedResultForReachabilityResult);
464
- assert.equal(JSON.stringify(getClustersResult.joinCookie), storedResultForJoinCookie);
1157
+ // now simulate some more "late" results
1158
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('tcp', {
1159
+ result: 'reachable',
1160
+ latencyInMilliseconds: 101,
1161
+ });
1162
+ mockClusterReachabilityInstances['clusterB'].emitFakeResult('xtls', {
1163
+ result: 'reachable',
1164
+ latencyInMilliseconds: 102,
1165
+ });
1166
+
1167
+ // and wait for the final overall timeout
1168
+ clock.tick(12000);
1169
+
1170
+ // the reachability results should include all results from above (including the late ones)
1171
+ await checkResults(
1172
+ {
1173
+ clusterA: {
1174
+ udp: {result: 'reachable', clientMediaIPs: ['1.2.3.4'], latencyInMilliseconds: 11},
1175
+ tcp: {result: 'reachable', latencyInMilliseconds: 101},
1176
+ xtls: {result: 'unreachable'},
1177
+ isVideoMesh: false,
1178
+ },
1179
+ clusterB: {
1180
+ udp: {result: 'reachable', clientMediaIPs: ['10.20.30.40'], latencyInMilliseconds: 22},
1181
+ tcp: {result: 'unreachable'},
1182
+ xtls: {result: 'reachable', latencyInMilliseconds: 102},
1183
+ isVideoMesh: false,
1184
+ },
1185
+ },
1186
+ mockGetClustersResult.joinCookie
1187
+ );
465
1188
  });
466
1189
 
467
- it('keeps the stored reachability from previous call to gatherReachability if getClusters fails', async () => {
1190
+ it('handles clientMediaIpsUpdated event by updating clientMediaIps in results', async () => {
1191
+ webex.config.meetings.experimental = {
1192
+ enableTcpReachability: true,
1193
+ enableTlsReachability: true,
1194
+ };
1195
+
468
1196
  const reachability = new Reachability(webex);
469
1197
 
470
- const reachabilityResults = {
1198
+ const mockGetClustersResult = {
471
1199
  clusters: {
472
- clusterId: {
473
- udp: 'testUDP',
1200
+ clusterA: {
1201
+ udp: ['udp-urlA'],
1202
+ tcp: ['tcp-urlA'],
1203
+ xtls: ['xtls-urlA'],
1204
+ isVideoMesh: false,
474
1205
  },
475
1206
  },
476
- };
477
- const getClustersResult = {
478
- clusters: {clusterId: 'cluster'},
479
1207
  joinCookie: {id: 'id'},
480
1208
  };
481
1209
 
1210
+ reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
1211
+
1212
+ const resultPromise = reachability.gatherReachability();
1213
+
1214
+ await testUtils.flushPromises();
1215
+
1216
+ // trigger a mock result event
1217
+ mockClusterReachabilityInstances['clusterA'].emitFakeResult('udp', {
1218
+ result: 'reachable',
1219
+ clientMediaIPs: ['64.103.40.20'],
1220
+ latencyInMilliseconds: 11,
1221
+ });
1222
+ // followed by some updates to client media IPs
1223
+ mockClusterReachabilityInstances['clusterA'].emitFakeClientMediaIpUpdate('udp', '64.103.40.21');
1224
+ mockClusterReachabilityInstances['clusterA'].emitFakeClientMediaIpUpdate('udp', '64.103.40.22');
1225
+
1226
+ // wait for the final overall timeout
1227
+ clock.tick(15000);
1228
+ await resultPromise;
1229
+
1230
+ // check that the reachability results contain all the client media ips
1231
+ await checkResults(
1232
+ {
1233
+ clusterA: {
1234
+ udp: {
1235
+ result: 'reachable',
1236
+ clientMediaIPs: ['64.103.40.20', '64.103.40.21', '64.103.40.22'],
1237
+ latencyInMilliseconds: 11,
1238
+ },
1239
+ tcp: {result: 'unreachable'},
1240
+ xtls: {result: 'unreachable'},
1241
+ isVideoMesh: false,
1242
+ },
1243
+ },
1244
+ mockGetClustersResult.joinCookie
1245
+ );
1246
+ });
1247
+
1248
+ it('keeps the stored reachability from previous call to gatherReachability if getClusters fails', async () => {
1249
+ const reachability = new Reachability(webex);
1250
+
482
1251
  reachability.reachabilityRequest.getClusters = sinon.stub().throws();
483
1252
 
484
1253
  const result = await reachability.gatherReachability();
485
1254
 
486
1255
  assert.empty(result);
487
1256
 
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);
1257
+ await checkResults({old: 'results'}, {old: 'joinCookie'});
499
1258
  });
500
1259
 
501
1260
  it('keeps the stored reachability from previous call to gatherReachability if performReachabilityChecks fails', async () => {
502
1261
  const reachability = new Reachability(webex);
503
1262
 
504
- const reachabilityResults = {
505
- clusters: {
506
- clusterId: {
507
- udp: 'testUDP',
508
- },
509
- },
510
- };
511
1263
  const getClustersResult = {
512
1264
  clusters: {clusterId: 'cluster'},
513
- joinCookie: {id: 'id'},
1265
+ joinCookie: {id: 'cookie id'},
514
1266
  };
515
1267
 
516
1268
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
@@ -520,17 +1272,7 @@ describe('gatherReachability', () => {
520
1272
 
521
1273
  assert.empty(result);
522
1274
 
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);
1275
+ await checkResults({old: 'results'}, {id: 'cookie id'});
534
1276
  });
535
1277
 
536
1278
  it('starts ClusterReachability on each media cluster', async () => {
@@ -561,14 +1303,10 @@ describe('gatherReachability', () => {
561
1303
 
562
1304
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
563
1305
 
564
- const startStub = sinon.stub().resolves({});
565
- const clusterReachabilityCtorStub = sinon
566
- .stub(ClusterReachabilityModule, 'ClusterReachability')
567
- .callsFake(() => ({
568
- start: startStub,
569
- }));
1306
+ const promise = reachability.gatherReachability();
570
1307
 
571
- await reachability.gatherReachability();
1308
+ await simulateTimeout();
1309
+ await promise;
572
1310
 
573
1311
  assert.calledTwice(clusterReachabilityCtorStub);
574
1312
  assert.calledWith(clusterReachabilityCtorStub, 'cluster 1', {
@@ -585,7 +1323,8 @@ describe('gatherReachability', () => {
585
1323
  isVideoMesh: true,
586
1324
  });
587
1325
 
588
- assert.calledTwice(startStub);
1326
+ assert.calledOnce(mockClusterReachabilityInstances['cluster 1'].start);
1327
+ assert.calledOnce(mockClusterReachabilityInstances['cluster 2'].start);
589
1328
  });
590
1329
 
591
1330
  it('does not do TCP reachability if it is disabled in config', async () => {
@@ -610,13 +1349,9 @@ describe('gatherReachability', () => {
610
1349
 
611
1350
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
612
1351
 
613
- const clusterReachabilityCtorStub = sinon
614
- .stub(ClusterReachabilityModule, 'ClusterReachability')
615
- .callsFake(() => ({
616
- start: sinon.stub().resolves({}),
617
- }));
618
-
619
- await reachability.gatherReachability();
1352
+ const promise = reachability.gatherReachability();
1353
+ await simulateTimeout();
1354
+ await promise;
620
1355
 
621
1356
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
622
1357
  isVideoMesh: false,
@@ -648,13 +1383,10 @@ describe('gatherReachability', () => {
648
1383
 
649
1384
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
650
1385
 
651
- const clusterReachabilityCtorStub = sinon
652
- .stub(ClusterReachabilityModule, 'ClusterReachability')
653
- .callsFake(() => ({
654
- start: sinon.stub().resolves({}),
655
- }));
1386
+ const promise = reachability.gatherReachability();
656
1387
 
657
- await reachability.gatherReachability();
1388
+ await simulateTimeout();
1389
+ await promise;
658
1390
 
659
1391
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
660
1392
  isVideoMesh: false,
@@ -686,13 +1418,10 @@ describe('gatherReachability', () => {
686
1418
 
687
1419
  reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult);
688
1420
 
689
- const clusterReachabilityCtorStub = sinon
690
- .stub(ClusterReachabilityModule, 'ClusterReachability')
691
- .callsFake(() => ({
692
- start: sinon.stub().resolves({}),
693
- }));
1421
+ const promise = reachability.gatherReachability();
694
1422
 
695
- await reachability.gatherReachability();
1423
+ await simulateTimeout();
1424
+ await promise;
696
1425
 
697
1426
  assert.calledOnceWithExactly(clusterReachabilityCtorStub, 'cluster name', {
698
1427
  isVideoMesh: false,
@@ -1033,3 +1762,310 @@ describe('getReachabilityMetrics', () => {
1033
1762
  );
1034
1763
  });
1035
1764
  });
1765
+
1766
+ class TestReachability extends Reachability {
1767
+ constructor(webex: object) {
1768
+ super(webex);
1769
+ }
1770
+
1771
+ public testGetStatistics(
1772
+ results: Array<ClusterReachabilityModule.ClusterReachabilityResult & {isVideoMesh: boolean}>,
1773
+ protocol: 'udp' | 'tcp' | 'xtls',
1774
+ isVideoMesh: boolean
1775
+ ) {
1776
+ return this.getStatistics(results, protocol, isVideoMesh);
1777
+ }
1778
+
1779
+ public testSendMetric() {
1780
+ return this.sendMetric();
1781
+ }
1782
+
1783
+ public setFakeClusterReachability(fakeClusterReachability) {
1784
+ this.clusterReachability = fakeClusterReachability;
1785
+ }
1786
+ }
1787
+
1788
+ describe('getStatistics', () => {
1789
+ let webex;
1790
+ let reachability;
1791
+
1792
+ beforeEach(() => {
1793
+ webex = new MockWebex();
1794
+ reachability = new TestReachability(webex);
1795
+ });
1796
+
1797
+ it('takes values from the correct protocol', () => {
1798
+ const results = [
1799
+ {
1800
+ udp: {
1801
+ result: 'reachable',
1802
+ latencyInMilliseconds: 10,
1803
+ },
1804
+ tcp: {
1805
+ result: 'reachable',
1806
+ latencyInMilliseconds: 1010,
1807
+ },
1808
+ xtls: {
1809
+ result: 'reachable',
1810
+ latencyInMilliseconds: 2010,
1811
+ },
1812
+ isVideoMesh: false,
1813
+ },
1814
+ {
1815
+ udp: {
1816
+ result: 'reachable',
1817
+ latencyInMilliseconds: 20,
1818
+ },
1819
+ tcp: {
1820
+ result: 'reachable',
1821
+ latencyInMilliseconds: 1020,
1822
+ },
1823
+ xtls: {
1824
+ result: 'reachable',
1825
+ latencyInMilliseconds: 2020,
1826
+ },
1827
+ isVideoMesh: false,
1828
+ },
1829
+ {
1830
+ udp: {
1831
+ result: 'reachable',
1832
+ latencyInMilliseconds: 30,
1833
+ },
1834
+ tcp: {
1835
+ result: 'reachable',
1836
+ latencyInMilliseconds: 1030,
1837
+ },
1838
+ xtls: {
1839
+ result: 'reachable',
1840
+ latencyInMilliseconds: 2030,
1841
+ },
1842
+ isVideoMesh: false,
1843
+ },
1844
+ ];
1845
+
1846
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
1847
+ min: 10,
1848
+ max: 30,
1849
+ average: 20,
1850
+ });
1851
+ assert.deepEqual(reachability.testGetStatistics(results, 'tcp', false), {
1852
+ min: 1010,
1853
+ max: 1030,
1854
+ average: 1020,
1855
+ });
1856
+ assert.deepEqual(reachability.testGetStatistics(results, 'xtls', false), {
1857
+ min: 2010,
1858
+ max: 2030,
1859
+ average: 2020,
1860
+ });
1861
+ });
1862
+
1863
+ it('filters based on isVideoMesh value', () => {
1864
+ const results = [
1865
+ {
1866
+ udp: {
1867
+ result: 'reachable',
1868
+ latencyInMilliseconds: 10,
1869
+ },
1870
+ isVideoMesh: true,
1871
+ },
1872
+ {
1873
+ udp: {
1874
+ result: 'reachable',
1875
+ latencyInMilliseconds: 20,
1876
+ },
1877
+ isVideoMesh: true,
1878
+ },
1879
+ {
1880
+ udp: {
1881
+ result: 'reachable',
1882
+ latencyInMilliseconds: 30,
1883
+ },
1884
+ isVideoMesh: true,
1885
+ },
1886
+ {
1887
+ udp: {
1888
+ result: 'reachable',
1889
+ latencyInMilliseconds: 100,
1890
+ },
1891
+ isVideoMesh: false,
1892
+ },
1893
+ {
1894
+ udp: {
1895
+ result: 'reachable',
1896
+ latencyInMilliseconds: 200,
1897
+ },
1898
+ isVideoMesh: false,
1899
+ },
1900
+ ];
1901
+
1902
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', true), {
1903
+ min: 10,
1904
+ max: 30,
1905
+ average: 20,
1906
+ });
1907
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
1908
+ min: 100,
1909
+ max: 200,
1910
+ average: 150,
1911
+ });
1912
+ });
1913
+
1914
+ it('only takes into account "reachable" results', () => {
1915
+ const results = [
1916
+ {
1917
+ udp: {
1918
+ result: 'reachable',
1919
+ latencyInMilliseconds: 10,
1920
+ },
1921
+ isVideoMesh: false,
1922
+ },
1923
+ {
1924
+ udp: {
1925
+ result: 'unreachable',
1926
+ latencyInMilliseconds: 100, // value put in here just for testing, in practice we wouldn't have any value here if it was unreachable
1927
+ },
1928
+ isVideoMesh: false,
1929
+ },
1930
+ {
1931
+ udp: {
1932
+ result: 'reachable',
1933
+ latencyInMilliseconds: 20,
1934
+ },
1935
+ isVideoMesh: false,
1936
+ },
1937
+ {
1938
+ udp: {
1939
+ result: 'untested',
1940
+ latencyInMilliseconds: 200, // value put in here just for testing, in practice we wouldn't have any value here if it was untested
1941
+ },
1942
+ isVideoMesh: false,
1943
+ },
1944
+ ];
1945
+
1946
+ assert.deepEqual(reachability.testGetStatistics(results, 'udp', false), {
1947
+ min: 10,
1948
+ max: 20,
1949
+ average: 15,
1950
+ });
1951
+ });
1952
+
1953
+ it('handles the case when results are empty', () => {
1954
+ assert.deepEqual(reachability.testGetStatistics([], 'udp', false), {
1955
+ min: -1,
1956
+ max: -1,
1957
+ average: -1,
1958
+ });
1959
+ });
1960
+
1961
+ it('handles the case when results are empty after filtering', () => {
1962
+ const fakeResults = [
1963
+ {
1964
+ udp: {
1965
+ result: 'untested', // it will get filtered out because of this value
1966
+ latencyInMilliseconds: 10,
1967
+ },
1968
+ tcp: {
1969
+ result: 'reachable',
1970
+ latencyInMilliseconds: 10, // it will get filtered out because of the tcp protocol
1971
+ },
1972
+ isVideoMesh: false,
1973
+ },
1974
+ {
1975
+ udp: {
1976
+ result: 'reachable',
1977
+ latencyInMilliseconds: 10,
1978
+ },
1979
+ isVideoMesh: true, // it will get filtered out because of this value
1980
+ },
1981
+ ];
1982
+
1983
+ assert.deepEqual(reachability.testGetStatistics(fakeResults, 'udp', false), {
1984
+ min: -1,
1985
+ max: -1,
1986
+ average: -1,
1987
+ });
1988
+ });
1989
+ });
1990
+
1991
+ describe('sendMetric', () => {
1992
+ let webex;
1993
+ let reachability;
1994
+
1995
+ beforeEach(() => {
1996
+ webex = new MockWebex();
1997
+ reachability = new TestReachability(webex);
1998
+
1999
+ sinon.stub(Metrics, 'sendBehavioralMetric');
2000
+ });
2001
+
2002
+ it('works as expected', async () => {
2003
+ // setup stub for getStatistics to return values that show what parameters it was called with,
2004
+ // this way we can verify that the correct results of calls to getStatistics are placed
2005
+ // in correct data fields when sendBehavioralMetric() is called
2006
+ sinon.stub(reachability, 'getStatistics').callsFake((results, protocol, isVideoMesh) => {
2007
+ return {results, protocol, isVideoMesh};
2008
+ });
2009
+
2010
+ // setup fake clusterReachability results
2011
+ reachability.setFakeClusterReachability({
2012
+ cluster1: {
2013
+ getResult: sinon.stub().returns({result: 'result 1'}),
2014
+ isVideoMesh: true,
2015
+ },
2016
+ cluster2: {
2017
+ getResult: sinon.stub().returns({result: 'result 2'}),
2018
+ isVideoMesh: false,
2019
+ },
2020
+ cluster3: {
2021
+ getResult: sinon.stub().returns({result: 'result 3'}),
2022
+ isVideoMesh: false,
2023
+ },
2024
+ });
2025
+
2026
+ await reachability.sendMetric();
2027
+
2028
+ // each call to getStatistics should be made with all the results from all fake clusterReachability:
2029
+ const expectedResults = [
2030
+ {
2031
+ result: 'result 1',
2032
+ isVideoMesh: true,
2033
+ },
2034
+ {
2035
+ result: 'result 2',
2036
+ isVideoMesh: false,
2037
+ },
2038
+ {
2039
+ result: 'result 3',
2040
+ isVideoMesh: false,
2041
+ },
2042
+ ];
2043
+
2044
+ assert.calledWith(Metrics.sendBehavioralMetric, 'js_sdk_reachability_completed', {
2045
+ vmn: {
2046
+ udp: {
2047
+ results: expectedResults,
2048
+ protocol: 'udp',
2049
+ isVideoMesh: true,
2050
+ },
2051
+ },
2052
+ public: {
2053
+ udp: {
2054
+ results: expectedResults,
2055
+ protocol: 'udp',
2056
+ isVideoMesh: false,
2057
+ },
2058
+ tcp: {
2059
+ results: expectedResults,
2060
+ protocol: 'tcp',
2061
+ isVideoMesh: false,
2062
+ },
2063
+ xtls: {
2064
+ results: expectedResults,
2065
+ protocol: 'xtls',
2066
+ isVideoMesh: false,
2067
+ },
2068
+ },
2069
+ });
2070
+ });
2071
+ });