@webex/plugin-meetings 3.0.0-beta.261 → 3.0.0-beta.263

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.
@@ -5817,12 +5817,16 @@ export default class Meeting extends StatelessWebexPlugin {
5817
5817
  return Promise.resolve();
5818
5818
  })
5819
5819
  .then(() => this.mediaProperties.getCurrentConnectionType())
5820
- .then((connectionType) => {
5820
+ .then(async (connectionType) => {
5821
+ // @ts-ignore
5822
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5823
+
5821
5824
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5822
5825
  correlation_id: this.correlationId,
5823
5826
  locus_id: this.locusUrl.split('/').pop(),
5824
5827
  connectionType,
5825
5828
  isMultistream: this.isMultistream,
5829
+ ...reachabilityStats,
5826
5830
  });
5827
5831
  // @ts-ignore
5828
5832
  this.webex.internal.newMetrics.submitClientEvent({
@@ -5838,9 +5842,12 @@ export default class Meeting extends StatelessWebexPlugin {
5838
5842
  // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
5839
5843
  this.remoteMediaManager?.logAllReceiveSlots();
5840
5844
  })
5841
- .catch((error) => {
5845
+ .catch(async (error) => {
5842
5846
  LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
5843
5847
 
5848
+ // @ts-ignore
5849
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5850
+
5844
5851
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5845
5852
  correlation_id: this.correlationId,
5846
5853
  locus_id: this.locusUrl.split('/').pop(),
@@ -5865,38 +5872,37 @@ export default class Meeting extends StatelessWebexPlugin {
5865
5872
  ?.iceConnectionState ||
5866
5873
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5867
5874
  'unknown',
5875
+ ...reachabilityMetrics,
5868
5876
  });
5869
5877
 
5870
5878
  // Clean up stats analyzer, peer connection, and turn off listeners
5871
- const stopStatsAnalyzer = this.statsAnalyzer
5872
- ? this.statsAnalyzer.stopAnalyzer()
5873
- : Promise.resolve();
5879
+ if (this.statsAnalyzer) {
5880
+ await this.statsAnalyzer.stopAnalyzer();
5881
+ }
5874
5882
 
5875
- return stopStatsAnalyzer.then(() => {
5876
- this.statsAnalyzer = null;
5883
+ this.statsAnalyzer = null;
5877
5884
 
5878
- if (this.mediaProperties.webrtcMediaConnection) {
5879
- this.closePeerConnections();
5880
- this.unsetPeerConnections();
5881
- }
5885
+ if (this.mediaProperties.webrtcMediaConnection) {
5886
+ this.closePeerConnections();
5887
+ this.unsetPeerConnections();
5888
+ }
5882
5889
 
5883
- // Upload logs on error while adding media
5884
- Trigger.trigger(
5885
- this,
5886
- {
5887
- file: 'meeting/index',
5888
- function: 'addMedia',
5889
- },
5890
- EVENTS.REQUEST_UPLOAD_LOGS,
5891
- this
5892
- );
5890
+ // Upload logs on error while adding media
5891
+ Trigger.trigger(
5892
+ this,
5893
+ {
5894
+ file: 'meeting/index',
5895
+ function: 'addMedia',
5896
+ },
5897
+ EVENTS.REQUEST_UPLOAD_LOGS,
5898
+ this
5899
+ );
5893
5900
 
5894
- if (error instanceof Errors.SdpError) {
5895
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5896
- }
5901
+ if (error instanceof Errors.SdpError) {
5902
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5903
+ }
5897
5904
 
5898
- throw error;
5899
- });
5905
+ throw error;
5900
5906
  });
5901
5907
  }
5902
5908
 
@@ -61,6 +61,7 @@ import MeetingsUtil from './util';
61
61
  import PermissionError from '../common/errors/permission';
62
62
  import {INoiseReductionEffect, IVirtualBackgroundEffect} from './meetings.types';
63
63
  import {SpaceIDDeprecatedError} from '../common/errors/webex-errors';
64
+ import NoMeetingInfoError from '../common/errors/no-meeting-info';
64
65
 
65
66
  let mediaLogger;
66
67
 
@@ -1043,6 +1044,7 @@ export default class Meetings extends WebexPlugin {
1043
1044
  * @param {Boolean} useRandomDelayForInfo - whether a random delay should be added to fetching meeting info
1044
1045
  * @param {Object} infoExtraParams extra parameters to be provided when fetching meeting info
1045
1046
  * @param {string} correlationId - the optional specified correlationId
1047
+ * @param {Boolean} failOnMissingMeetingInfo - whether to throw an error if meeting info fails to fetch (for calls that are not 1:1 or content share)
1046
1048
  * @returns {Promise<Meeting>} A new Meeting.
1047
1049
  * @public
1048
1050
  * @memberof Meetings
@@ -1052,7 +1054,8 @@ export default class Meetings extends WebexPlugin {
1052
1054
  type: string = null,
1053
1055
  useRandomDelayForInfo = false,
1054
1056
  infoExtraParams = {},
1055
- correlationId: string = undefined
1057
+ correlationId: string = undefined,
1058
+ failOnMissingMeetingInfo = false
1056
1059
  ) {
1057
1060
  // TODO: type should be from a dictionary
1058
1061
 
@@ -1106,7 +1109,8 @@ export default class Meetings extends WebexPlugin {
1106
1109
  type,
1107
1110
  useRandomDelayForInfo,
1108
1111
  infoExtraParams,
1109
- correlationId
1112
+ correlationId,
1113
+ failOnMissingMeetingInfo
1110
1114
  ).then((createdMeeting: any) => {
1111
1115
  // If the meeting was successfully created.
1112
1116
  if (createdMeeting && createdMeeting.on) {
@@ -1161,6 +1165,7 @@ export default class Meetings extends WebexPlugin {
1161
1165
  * @param {Boolean} useRandomDelayForInfo whether a random delay should be added to fetching meeting info
1162
1166
  * @param {Object} infoExtraParams extra parameters to be provided when fetching meeting info
1163
1167
  * @param {String} correlationId the optional specified correlationId
1168
+ * @param {Boolean} failOnMissingMeetingInfo - whether to throw an error if meeting info fails to fetch (for calls that are not 1:1 or content share)
1164
1169
  * @returns {Promise} a new meeting instance complete with meeting info and destination
1165
1170
  * @private
1166
1171
  * @memberof Meetings
@@ -1170,7 +1175,8 @@ export default class Meetings extends WebexPlugin {
1170
1175
  type: string = null,
1171
1176
  useRandomDelayForInfo = false,
1172
1177
  infoExtraParams = {},
1173
- correlationId: string = undefined
1178
+ correlationId: string = undefined,
1179
+ failOnMissingMeetingInfo = false
1174
1180
  ) {
1175
1181
  const meeting = new Meeting(
1176
1182
  {
@@ -1239,10 +1245,18 @@ export default class Meetings extends WebexPlugin {
1239
1245
  !(err instanceof PasswordError) &&
1240
1246
  !(err instanceof PermissionError)
1241
1247
  ) {
1242
- // if there is no meeting info we assume its a 1:1 call or wireless share
1243
1248
  LoggerProxy.logger.info(
1244
1249
  `Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`
1245
1250
  );
1251
+ if (failOnMissingMeetingInfo) {
1252
+ LoggerProxy.logger.info(
1253
+ `Meetings:index#createMeeting --> Destroying meeting due to missing meeting info.`
1254
+ );
1255
+ // @ts-ignore
1256
+ this.destroy(meeting, MEETING_REMOVED_REASON.MISSING_MEETING_INFO);
1257
+ throw new NoMeetingInfoError();
1258
+ }
1259
+ // if there is no meeting info and no error should be thrown then we assume its a 1:1 call or wireless share
1246
1260
  LoggerProxy.logger.info(
1247
1261
  'Meetings:index#createMeeting --> Info assuming this destination is a 1:1 or wireless share'
1248
1262
  );
@@ -16,9 +16,20 @@ import ReachabilityRequest from './request';
16
16
  const DEFAULT_TIMEOUT = 3000;
17
17
  const VIDEO_MESH_TIMEOUT = 1000;
18
18
 
19
+ export type ReachabilityMetrics = {
20
+ reachability_public_udp_success: number;
21
+ reachability_public_udp_failed: number;
22
+ reachability_public_tcp_success: number;
23
+ reachability_public_tcp_failed: number;
24
+ reachability_vmn_udp_success: number;
25
+ reachability_vmn_udp_failed: number;
26
+ reachability_vmn_tcp_success: number;
27
+ reachability_vmn_tcp_failed: number;
28
+ };
29
+
19
30
  // result for a specific transport protocol (like udp or tcp)
20
31
  export type TransportResult = {
21
- reachable: 'true' | 'false';
32
+ reachable?: 'true' | 'false';
22
33
  latencyInMilliseconds?: string;
23
34
  clientMediaIPs?: string[];
24
35
  untested?: 'true';
@@ -138,6 +149,58 @@ export default class Reachability {
138
149
  }
139
150
  }
140
151
 
152
+ /**
153
+ * Returns statistics about last reachability results. The returned value is an object
154
+ * with a flat list of properties so that it can be easily sent with metrics
155
+ *
156
+ * @returns {Promise} Promise with metrics values, it never rejects/throws.
157
+ */
158
+ async getReachabilityMetrics(): Promise<ReachabilityMetrics> {
159
+ const stats: ReachabilityMetrics = {
160
+ reachability_public_udp_success: 0,
161
+ reachability_public_udp_failed: 0,
162
+ reachability_public_tcp_success: 0,
163
+ reachability_public_tcp_failed: 0,
164
+ reachability_vmn_udp_success: 0,
165
+ reachability_vmn_udp_failed: 0,
166
+ reachability_vmn_tcp_success: 0,
167
+ reachability_vmn_tcp_failed: 0,
168
+ };
169
+
170
+ const updateStats = (clusterType: 'public' | 'vmn', result: ReachabilityResult) => {
171
+ if (result.udp?.reachable) {
172
+ const outcome = result.udp.reachable === 'true' ? 'success' : 'failed';
173
+ stats[`reachability_${clusterType}_udp_${outcome}`] += 1;
174
+ }
175
+ if (result.tcp?.reachable) {
176
+ const outcome = result.tcp.reachable === 'true' ? 'success' : 'failed';
177
+ stats[`reachability_${clusterType}_tcp_${outcome}`] += 1;
178
+ }
179
+ };
180
+
181
+ try {
182
+ // @ts-ignore
183
+ const resultsJson = await this.webex.boundedStorage.get(
184
+ REACHABILITY.namespace,
185
+ REACHABILITY.localStorageResult
186
+ );
187
+
188
+ const internalResults: InternalReachabilityResults = JSON.parse(resultsJson);
189
+
190
+ Object.values(internalResults).forEach((result) => {
191
+ updateStats(result.isVideoMesh ? 'vmn' : 'public', result);
192
+ });
193
+ } catch (e) {
194
+ // empty storage, that's ok
195
+ LoggerProxy.logger.warn(
196
+ 'Roap:request#getReachabilityMetrics --> Error parsing reachability data: ',
197
+ e
198
+ );
199
+ }
200
+
201
+ return stats;
202
+ }
203
+
141
204
  /**
142
205
  * Reachability results as an object in the format that backend expects
143
206
  *
@@ -44,14 +44,12 @@ import {
44
44
  StreamEventNames,
45
45
  } from '@webex/media-helpers';
46
46
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
47
- import * as MuteStateModule from '@webex/plugin-meetings/src/meeting/muteState';
48
47
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
49
48
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
50
49
  import Meeting from '@webex/plugin-meetings/src/meeting';
51
50
  import Members from '@webex/plugin-meetings/src/members';
52
51
  import * as MembersImport from '@webex/plugin-meetings/src/members';
53
52
  import Roap from '@webex/plugin-meetings/src/roap';
54
- import RoapRequest from '@webex/plugin-meetings/src/roap/request';
55
53
  import MeetingRequest from '@webex/plugin-meetings/src/meeting/request';
56
54
  import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/request';
57
55
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
@@ -81,7 +79,6 @@ import {
81
79
  UserNotJoinedError,
82
80
  MeetingNotActiveError,
83
81
  UserInLobbyError,
84
- NoMediaEstablishedYetError,
85
82
  } from '../../../../src/common/errors/webex-errors';
86
83
  import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-error';
87
84
  import ParameterError from '../../../../src/common/errors/parameter';
@@ -89,19 +86,14 @@ import PasswordError from '../../../../src/common/errors/password-error';
89
86
  import CaptchaError from '../../../../src/common/errors/captcha-error';
90
87
  import PermissionError from '../../../../src/common/errors/permission';
91
88
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
92
- import DefaultSDKConfig from '../../../../src/config';
93
89
  import testUtils from '../../../utils/testUtils';
94
90
  import {
95
91
  MeetingInfoV2CaptchaError,
96
92
  MeetingInfoV2PasswordError,
97
93
  MeetingInfoV2PolicyError,
98
94
  } from '../../../../src/meeting-info/meeting-info-v2';
99
- import {ANNOTATION_POLICY} from '../../../../src/annotation/constants';
100
95
 
101
96
 
102
- // Non-stubbed function
103
- const {getDisplayMedia} = Media;
104
-
105
97
  describe('plugin-meetings', () => {
106
98
  const logger = {
107
99
  info: () => {},
@@ -228,6 +220,11 @@ describe('plugin-meetings', () => {
228
220
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
229
221
  webex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
230
222
  webex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
223
+ webex.meetings.reachability = {
224
+ isAnyPublicClusterReachable: sinon.stub().resolves(true),
225
+ getReachabilityResults: sinon.stub().resolves(undefined),
226
+ getReachabilityMetrics: sinon.stub().resolves({}),
227
+ };
231
228
  webex.internal.llm.on = sinon.stub();
232
229
  membersSpy = sinon.spy(MembersImport, 'default');
233
230
  meetingRequestSpy = sinon.spy(MeetingRequestImport, 'default');
@@ -1170,8 +1167,15 @@ describe('plugin-meetings', () => {
1170
1167
  }
1171
1168
  });
1172
1169
 
1173
- it('should reset the statsAnalyzer to null if addMedia throws an error', async () => {
1170
+ it('should send metrics and reset the statsAnalyzer to null if addMedia throws an error', async () => {
1174
1171
  meeting.meetingState = 'ACTIVE';
1172
+ meeting.webex.meetings.reachability = {
1173
+ getReachabilityMetrics: sinon.stub().resolves({
1174
+ someReachabilityMetric1: 'some value1',
1175
+ someReachabilityMetric2: 'some value2'
1176
+ }),
1177
+ };
1178
+
1175
1179
  // setup the mock to return an incomplete object - this will cause addMedia to fail
1176
1180
  // because some methods (like on() or initiateOffer()) are missing
1177
1181
  Media.createMediaConnection = sinon.stub().returns({
@@ -1195,6 +1199,8 @@ describe('plugin-meetings', () => {
1195
1199
  signalingState: 'unknown',
1196
1200
  connectionState: 'unknown',
1197
1201
  iceConnectionState: 'unknown',
1202
+ someReachabilityMetric1: 'some value1',
1203
+ someReachabilityMetric2: 'some value2',
1198
1204
  });
1199
1205
  });
1200
1206
 
@@ -1546,6 +1552,12 @@ describe('plugin-meetings', () => {
1546
1552
 
1547
1553
  it('should send ADD_MEDIA_SUCCESS metrics', async () => {
1548
1554
  meeting.meetingState = 'ACTIVE';
1555
+ meeting.webex.meetings.reachability = {
1556
+ getReachabilityMetrics: sinon.stub().resolves({
1557
+ someReachabilityMetric1: 'some value1',
1558
+ someReachabilityMetric2: 'some value2'
1559
+ }),
1560
+ };
1549
1561
  await meeting.addMedia({
1550
1562
  mediaSettings: {},
1551
1563
  });
@@ -1556,6 +1568,8 @@ describe('plugin-meetings', () => {
1556
1568
  locus_id: meeting.locusUrl.split('/').pop(),
1557
1569
  connectionType: 'udp',
1558
1570
  isMultistream: false,
1571
+ someReachabilityMetric1: 'some value1',
1572
+ someReachabilityMetric2: 'some value2'
1559
1573
  });
1560
1574
 
1561
1575
  assert.called(webex.internal.newMetrics.submitClientEvent);
@@ -1792,10 +1806,6 @@ describe('plugin-meetings', () => {
1792
1806
  meeting.setMercuryListener = sinon.stub();
1793
1807
  meeting.locusInfo.onFullLocus = sinon.stub();
1794
1808
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
1795
- meeting.webex.meetings.reachability = {
1796
- isAnyPublicClusterReachable: sinon.stub().resolves(true),
1797
- getReachabilityResults: sinon.stub().resolves(undefined),
1798
- };
1799
1809
  meeting.roap.doTurnDiscovery = sinon
1800
1810
  .stub()
1801
1811
  .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
@@ -36,6 +36,7 @@ import { forEach } from 'lodash';
36
36
  import PasswordError from '@webex/plugin-meetings/src/common/errors/password-error';
37
37
  import PermissionError from '@webex/plugin-meetings/src/common/errors/permission';
38
38
  import {NoiseReductionEffect,VirtualBackgroundEffect} from '@webex/media-helpers';
39
+ import NoMeetingInfoError from '../../../../src/common/errors/no-meeting-info';
39
40
 
40
41
  describe('plugin-meetings', () => {
41
42
  const logger = {
@@ -662,15 +663,28 @@ describe('plugin-meetings', () => {
662
663
  });
663
664
  });
664
665
 
665
- it('calls createMeeting and returns its promise', async () => {
666
- const FAKE_USE_RANDOM_DELAY = true;
667
- const correlationId = 'my-correlationId';
668
- const create = webex.meetings.create(test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId);
666
+ const FAKE_USE_RANDOM_DELAY = true;
667
+ const correlationId = 'my-correlationId';
668
+
669
+ const checkCallCreateMeeting = async (createParameters, createMeetingParameters) => {
670
+ const create = webex.meetings.create(...createParameters);
669
671
 
670
672
  assert.exists(create.then);
671
673
  await create;
672
674
  assert.calledOnce(webex.meetings.createMeeting);
673
- assert.calledWith(webex.meetings.createMeeting, test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId);
675
+ assert.calledWith(webex.meetings.createMeeting, ...createMeetingParameters);
676
+ }
677
+
678
+ it('calls createMeeting and returns its promise', async () => {
679
+ checkCallCreateMeeting([test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, true], [test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, true]);
680
+ });
681
+
682
+ it('calls createMeeting when failOnMissingMeetinginfo is undefined and returns its promise', async () => {
683
+ checkCallCreateMeeting([test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, undefined], [test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, false]);
684
+ });
685
+
686
+ it('calls createMeeting when failOnMissingMeetinginfo is false and returns its promise', async () => {
687
+ checkCallCreateMeeting([test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, false], [test1, test2, FAKE_USE_RANDOM_DELAY, {}, correlationId, false]);
674
688
  });
675
689
 
676
690
  it('calls createMeeting with extra info params and returns its promise', async () => {
@@ -1346,37 +1360,63 @@ describe('plugin-meetings', () => {
1346
1360
  webex.meetings.meetingInfo.fetchMeetingInfo = sinon
1347
1361
  .stub()
1348
1362
  .returns(Promise.reject(new Error('test')));
1363
+ webex.meetings.destroy = sinon
1364
+ .stub()
1365
+ .returns(Promise.resolve());
1366
+ webex.meetings.createMeeting = sinon.spy(webex.meetings.createMeeting);
1349
1367
  });
1350
- it('creates the meeting from a rejected meeting info fetch', async () => {
1351
- const meeting = await webex.meetings.createMeeting('test destination', 'test type');
1352
1368
 
1353
- assert.instanceOf(
1354
- meeting,
1355
- Meeting,
1356
- 'createMeeting should eventually resolve to a Meeting Object'
1357
- );
1358
- assert.calledOnce(webex.meetings.meetingInfo.fetchMeetingInfo);
1359
- assert.calledOnce(MeetingsUtil.getMeetingAddedType);
1360
- assert.calledThrice(TriggerProxy.trigger);
1361
- assert.calledWith(
1362
- webex.meetings.meetingInfo.fetchMeetingInfo,
1363
- 'test destination',
1364
- 'test type'
1365
- );
1366
- assert.calledWith(MeetingsUtil.getMeetingAddedType, 'test type');
1367
- assert.calledWith(
1368
- TriggerProxy.trigger,
1369
- sinon.match.instanceOf(Meetings),
1370
- {
1371
- file: 'meetings',
1372
- function: 'createMeeting',
1373
- },
1374
- 'meeting:added',
1375
- {
1376
- meeting: sinon.match.instanceOf(Meeting),
1377
- type: 'test meeting added type',
1369
+ const checkCreateMeetingWithNoMeetingInfo = async (failOnMissingMeetingInfo, destroy) => {
1370
+ try {
1371
+ const meeting = await webex.meetings.createMeeting('test destination', 'test type', undefined, undefined, undefined, failOnMissingMeetingInfo);
1372
+
1373
+ assert.instanceOf(
1374
+ meeting,
1375
+ Meeting,
1376
+ 'createMeeting should eventually resolve to a Meeting Object'
1377
+ );
1378
+ assert.calledOnce(webex.meetings.meetingInfo.fetchMeetingInfo);
1379
+ assert.calledOnce(MeetingsUtil.getMeetingAddedType);
1380
+ assert.calledThrice(TriggerProxy.trigger);
1381
+ assert.calledWith(
1382
+ webex.meetings.meetingInfo.fetchMeetingInfo,
1383
+ 'test destination',
1384
+ 'test type'
1385
+ );
1386
+
1387
+ if (destroy) {
1388
+ assert.calledWith(webex.meetings.destroy, sinon.match.instanceOf(Meeting), 'MISSING_MEETING_INFO')
1389
+ assert.notCalled(MeetingsUtil.getMeetingAddedType);
1390
+ assert.notCalled(TriggerProxy.trigger);
1391
+ assert.throw(webex.meetings.createMeeting, 'meeting information not found');
1392
+ } else {
1393
+ assert.notCalled(webex.meetings.destroy);
1394
+ assert.calledWith(MeetingsUtil.getMeetingAddedType, 'test type');
1395
+ assert.calledWith(
1396
+ TriggerProxy.trigger,
1397
+ sinon.match.instanceOf(Meetings),
1398
+ {
1399
+ file: 'meetings',
1400
+ function: 'createMeeting',
1401
+ },
1402
+ 'meeting:added',
1403
+ {
1404
+ meeting: sinon.match.instanceOf(Meeting),
1405
+ type: 'test meeting added type',
1406
+ }
1407
+ );
1378
1408
  }
1379
- );
1409
+ } catch (err) {
1410
+ assert.instanceOf(err, NoMeetingInfoError);
1411
+ }
1412
+ }
1413
+
1414
+ it('creates the meeting from a rejected meeting info fetch', async () => {
1415
+ checkCreateMeetingWithNoMeetingInfo(false, false);
1416
+ });
1417
+
1418
+ it('creates the meeting from a rejected meeting info fetch and destroys it if failOnMissingMeetingInfo', async () => {
1419
+ checkCreateMeetingWithNoMeetingInfo(true, true);
1380
1420
  });
1381
1421
  });
1382
1422