@webex/plugin-meetings 3.11.0-next.47 → 3.11.0-next.48
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.
- package/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/interceptors/dataChannelAuthToken.js +103 -46
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
- package/dist/interceptors/utils.js +27 -0
- package/dist/interceptors/utils.js.map +1 -0
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +4 -6
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +2 -2
- package/dist/meeting/request.js.map +1 -1
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +8 -0
- package/dist/types/interceptors/utils.d.ts +1 -0
- package/dist/webinar/index.js +8 -2
- package/dist/webinar/index.js.map +1 -1
- package/package.json +4 -3
- package/src/interceptors/dataChannelAuthToken.ts +28 -0
- package/src/interceptors/utils.ts +16 -0
- package/src/meeting/index.ts +5 -5
- package/src/meeting/request.ts +4 -4
- package/src/webinar/index.ts +5 -0
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +69 -0
- package/test/unit/spec/interceptors/utils.ts +75 -0
- package/test/unit/spec/meeting/index.js +6 -6
- package/test/unit/spec/meeting/request.js +18 -12
- package/test/unit/spec/webinar/index.ts +18 -0
package/package.json
CHANGED
|
@@ -65,12 +65,12 @@
|
|
|
65
65
|
"@webex/internal-media-core": "2.23.1",
|
|
66
66
|
"@webex/internal-plugin-conversation": "3.11.0-next.11",
|
|
67
67
|
"@webex/internal-plugin-device": "3.11.0-next.8",
|
|
68
|
-
"@webex/internal-plugin-llm": "3.11.0-next.
|
|
68
|
+
"@webex/internal-plugin-llm": "3.11.0-next.12",
|
|
69
69
|
"@webex/internal-plugin-mercury": "3.11.0-next.10",
|
|
70
70
|
"@webex/internal-plugin-metrics": "3.11.0-next.8",
|
|
71
71
|
"@webex/internal-plugin-support": "3.11.0-next.11",
|
|
72
72
|
"@webex/internal-plugin-user": "3.11.0-next.8",
|
|
73
|
-
"@webex/internal-plugin-voicea": "3.11.0-next.
|
|
73
|
+
"@webex/internal-plugin-voicea": "3.11.0-next.13",
|
|
74
74
|
"@webex/media-helpers": "3.11.0-next.4",
|
|
75
75
|
"@webex/plugin-people": "3.11.0-next.10",
|
|
76
76
|
"@webex/plugin-rooms": "3.11.0-next.11",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"global": "^4.4.0",
|
|
85
85
|
"ip-anonymize": "^0.1.0",
|
|
86
86
|
"javascript-state-machine": "^3.1.0",
|
|
87
|
+
"jose": "^5.8.0",
|
|
87
88
|
"jwt-decode": "3.1.2",
|
|
88
89
|
"lodash": "^4.17.21",
|
|
89
90
|
"uuid": "^3.3.2",
|
|
@@ -93,5 +94,5 @@
|
|
|
93
94
|
"//": [
|
|
94
95
|
"TODO: upgrade jwt-decode when moving to node 18"
|
|
95
96
|
],
|
|
96
|
-
"version": "3.11.0-next.
|
|
97
|
+
"version": "3.11.0-next.48"
|
|
97
98
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import {Interceptor} from '@webex/http-core';
|
|
6
6
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
7
7
|
import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY, RETRY_INTERVAL, RETRY_KEY} from './constant';
|
|
8
|
+
import {isJwtTokenExpired} from './utils';
|
|
8
9
|
|
|
9
10
|
/*!
|
|
10
11
|
* Copyright (c) 2015-2026 Cisco Systems, Inc. See LICENSE file.
|
|
@@ -69,6 +70,33 @@ export default class DataChannelAuthTokenInterceptor extends Interceptor {
|
|
|
69
70
|
return key ? headers[key] : undefined;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Intercepts outgoing requests and refreshes the Data-Channel-Auth-Token
|
|
75
|
+
* if the current JWT token is expired before the request is sent.
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} options - The original request options.
|
|
78
|
+
* @returns {Promise<Object>} Updated request options with refreshed token if needed.
|
|
79
|
+
*/
|
|
80
|
+
async onRequest(options) {
|
|
81
|
+
const token = this.getHeader(options.headers, DATA_CHANNEL_AUTH_HEADER);
|
|
82
|
+
const enabled = await this._isDataChannelTokenEnabled();
|
|
83
|
+
|
|
84
|
+
if (!token || !enabled) {
|
|
85
|
+
return options;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isJwtTokenExpired(token)) {
|
|
89
|
+
try {
|
|
90
|
+
const newToken = await this._refreshDataChannelToken();
|
|
91
|
+
options.headers[DATA_CHANNEL_AUTH_HEADER] = newToken;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
LoggerProxy.logger.warn(`DataChannelAuthTokenInterceptor: refresh failed: ${e.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return options;
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
/**
|
|
73
101
|
* Intercept responses and, on 401/403 with `Data-Channel-Auth-Token` header,
|
|
74
102
|
* attempt to refresh the data channel token and retry the original request once.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
|
|
3
|
+
const EXPIRY_BUFFER = 30 * 1000;
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
6
|
+
export const isJwtTokenExpired = (token: string): boolean => {
|
|
7
|
+
try {
|
|
8
|
+
const payload = jose.decodeJwt(token);
|
|
9
|
+
|
|
10
|
+
if (!payload?.exp) return false;
|
|
11
|
+
|
|
12
|
+
return payload.exp * 1000 < Date.now() + EXPIRY_BUFFER;
|
|
13
|
+
} catch {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
};
|
package/src/meeting/index.ts
CHANGED
|
@@ -10317,12 +10317,12 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
10317
10317
|
} catch (e) {
|
|
10318
10318
|
const msg = e?.message || String(e);
|
|
10319
10319
|
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10320
|
+
LoggerProxy.logger.warn(
|
|
10321
|
+
`Meeting:index#refreshDataChannelToken --> DataChannel token refresh failed (likely locus changed or participant left): ${msg}`,
|
|
10322
|
+
{statusCode: e?.statusCode}
|
|
10323
|
+
);
|
|
10324
10324
|
|
|
10325
|
-
|
|
10325
|
+
return null;
|
|
10326
10326
|
}
|
|
10327
10327
|
}
|
|
10328
10328
|
|
package/src/meeting/request.ts
CHANGED
|
@@ -1159,13 +1159,13 @@ export default class MeetingRequest extends StatelessWebexPlugin {
|
|
|
1159
1159
|
method: HTTP_VERBS.GET,
|
|
1160
1160
|
uri,
|
|
1161
1161
|
}).catch((err) => {
|
|
1162
|
-
LoggerProxy.logger.
|
|
1163
|
-
`Meeting:request#fetchDatachannelToken -->
|
|
1162
|
+
LoggerProxy.logger.warn(
|
|
1163
|
+
`Meeting:request#fetchDatachannelToken --> Failed to retrieve ${
|
|
1164
1164
|
isPracticeSession ? 'practice session ' : ''
|
|
1165
|
-
}datachannel token
|
|
1165
|
+
}datachannel token: ${err?.message || err}`
|
|
1166
1166
|
);
|
|
1167
1167
|
|
|
1168
|
-
|
|
1168
|
+
return null;
|
|
1169
1169
|
});
|
|
1170
1170
|
}
|
|
1171
1171
|
}
|
package/src/webinar/index.ts
CHANGED
|
@@ -177,6 +177,8 @@ const Webinar = WebexPlugin.extend({
|
|
|
177
177
|
|
|
178
178
|
const finalToken = currentToken ?? practiceSessionDatachannelToken;
|
|
179
179
|
|
|
180
|
+
const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
|
|
181
|
+
|
|
180
182
|
if (!currentToken && practiceSessionDatachannelToken) {
|
|
181
183
|
// @ts-ignore
|
|
182
184
|
this.webex.internal.llm.setDatachannelToken(
|
|
@@ -219,6 +221,9 @@ const Webinar = WebexPlugin.extend({
|
|
|
219
221
|
);
|
|
220
222
|
// @ts-ignore - Fix type
|
|
221
223
|
this.webex.internal.voicea?.announce?.();
|
|
224
|
+
if (isCaptionBoxOn) {
|
|
225
|
+
this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
226
|
+
}
|
|
222
227
|
LoggerProxy.logger.info(
|
|
223
228
|
`Webinar:index#updatePSDataChannel --> enabled to receive relay events for default session for ${LLM_PRACTICE_SESSION}!`
|
|
224
229
|
);
|
|
@@ -5,6 +5,7 @@ import MockWebex from '@webex/test-helper-mock-webex';
|
|
|
5
5
|
import {WebexHttpError} from '@webex/webex-core';
|
|
6
6
|
import DataChannelAuthTokenInterceptor from '@webex/plugin-meetings/src/interceptors/dataChannelAuthToken';
|
|
7
7
|
import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
|
|
8
|
+
import * as utils from '@webex/plugin-meetings/src/interceptors/utils';
|
|
8
9
|
import {DATA_CHANNEL_AUTH_HEADER, MAX_RETRY} from '@webex/plugin-meetings/src/interceptors/constant';
|
|
9
10
|
|
|
10
11
|
describe('plugin-meetings', () => {
|
|
@@ -14,6 +15,10 @@ describe('plugin-meetings', () => {
|
|
|
14
15
|
|
|
15
16
|
beforeEach(() => {
|
|
16
17
|
clock = sinon.useFakeTimers();
|
|
18
|
+
sinon.stub(LoggerProxy, 'logger').value({
|
|
19
|
+
error: sinon.stub(),
|
|
20
|
+
warn: sinon.stub(),
|
|
21
|
+
});
|
|
17
22
|
|
|
18
23
|
webex = new MockWebex({children: {}});
|
|
19
24
|
webex.request = sinon.stub().resolves({});
|
|
@@ -25,6 +30,7 @@ describe('plugin-meetings', () => {
|
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
afterEach(() => {
|
|
33
|
+
sinon.restore();
|
|
28
34
|
clock.restore();
|
|
29
35
|
});
|
|
30
36
|
|
|
@@ -86,6 +92,69 @@ describe('plugin-meetings', () => {
|
|
|
86
92
|
});
|
|
87
93
|
});
|
|
88
94
|
|
|
95
|
+
describe('#onRequest', () => {
|
|
96
|
+
let isJwtTokenExpiredStub;
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
isJwtTokenExpiredStub = sinon.stub(utils, 'isJwtTokenExpired').returns(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does nothing when token is missing', async () => {
|
|
103
|
+
const options = {headers: {}};
|
|
104
|
+
|
|
105
|
+
const res = await interceptor.onRequest(options);
|
|
106
|
+
|
|
107
|
+
expect(res).to.equal(options);
|
|
108
|
+
sinon.assert.notCalled(isJwtTokenExpiredStub);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does nothing when feature is disabled', async () => {
|
|
112
|
+
interceptor._isDataChannelTokenEnabled.resolves(false);
|
|
113
|
+
|
|
114
|
+
const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'}};
|
|
115
|
+
const res = await interceptor.onRequest(options);
|
|
116
|
+
|
|
117
|
+
expect(res).to.equal(options);
|
|
118
|
+
sinon.assert.notCalled(isJwtTokenExpiredStub);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not refresh when token is not expired', async () => {
|
|
122
|
+
interceptor._isDataChannelTokenEnabled.resolves(true);
|
|
123
|
+
isJwtTokenExpiredStub.returns(false);
|
|
124
|
+
|
|
125
|
+
const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'}};
|
|
126
|
+
const res = await interceptor.onRequest(options);
|
|
127
|
+
|
|
128
|
+
sinon.assert.notCalled(interceptor._refreshDataChannelToken);
|
|
129
|
+
expect(res.headers[DATA_CHANNEL_AUTH_HEADER]).to.equal('old-token');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('refreshes token when expired', async () => {
|
|
133
|
+
interceptor._isDataChannelTokenEnabled.resolves(true);
|
|
134
|
+
isJwtTokenExpiredStub.returns(true);
|
|
135
|
+
|
|
136
|
+
interceptor._refreshDataChannelToken.resolves('new-token');
|
|
137
|
+
|
|
138
|
+
const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'}};
|
|
139
|
+
const res = await interceptor.onRequest(options);
|
|
140
|
+
|
|
141
|
+
sinon.assert.calledOnce(interceptor._refreshDataChannelToken);
|
|
142
|
+
expect(res.headers[DATA_CHANNEL_AUTH_HEADER]).to.equal('new-token');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('continues request when refresh fails', async () => {
|
|
146
|
+
interceptor._isDataChannelTokenEnabled.resolves(true);
|
|
147
|
+
isJwtTokenExpiredStub.returns(true);
|
|
148
|
+
|
|
149
|
+
interceptor._refreshDataChannelToken.rejects(new Error('refresh failed'));
|
|
150
|
+
|
|
151
|
+
const options = {headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'}};
|
|
152
|
+
const res = await interceptor.onRequest(options);
|
|
153
|
+
|
|
154
|
+
expect(res.headers[DATA_CHANNEL_AUTH_HEADER]).to.equal('old-token');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
89
158
|
describe('#refreshTokenAndRetryWithDelay', () => {
|
|
90
159
|
const options = {
|
|
91
160
|
headers: {[DATA_CHANNEL_AUTH_HEADER]: 'old-token'},
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import 'jsdom-global/register';
|
|
2
|
+
import {expect} from '@webex/test-helper-chai';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import {isJwtTokenExpired} from '@webex/plugin-meetings/src/interceptors/utils';
|
|
5
|
+
|
|
6
|
+
const makeJwt = (payload) =>
|
|
7
|
+
[
|
|
8
|
+
Buffer.from(JSON.stringify({alg: 'none', typ: 'JWT'})).toString('base64url'),
|
|
9
|
+
Buffer.from(JSON.stringify(payload)).toString('base64url'),
|
|
10
|
+
''
|
|
11
|
+
].join('.');
|
|
12
|
+
|
|
13
|
+
describe('plugin-meetings', () => {
|
|
14
|
+
describe('Interceptors', () => {
|
|
15
|
+
describe('utils - isJwtTokenExpired', () => {
|
|
16
|
+
let clock;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
clock = sinon.useFakeTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
sinon.restore();
|
|
24
|
+
clock.restore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns false when token has no exp', () => {
|
|
28
|
+
const token = makeJwt({}); // no exp
|
|
29
|
+
|
|
30
|
+
const result = isJwtTokenExpired(token);
|
|
31
|
+
|
|
32
|
+
expect(result).to.equal(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns false when token is not expired', () => {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const futureExp = Math.floor((now + 60 * 1000) / 1000);
|
|
38
|
+
|
|
39
|
+
const token = makeJwt({exp: futureExp});
|
|
40
|
+
|
|
41
|
+
const result = isJwtTokenExpired(token);
|
|
42
|
+
|
|
43
|
+
expect(result).to.equal(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns true when token is expired', () => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const pastExp = Math.floor((now - 60 * 1000) / 1000);
|
|
49
|
+
|
|
50
|
+
const token = makeJwt({exp: pastExp});
|
|
51
|
+
|
|
52
|
+
const result = isJwtTokenExpired(token);
|
|
53
|
+
|
|
54
|
+
expect(result).to.equal(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns true when token expires within EXPIRY_BUFFER', () => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const expSoon = Math.floor((now + 10 * 1000) / 1000);
|
|
60
|
+
|
|
61
|
+
const token = makeJwt({exp: expSoon});
|
|
62
|
+
|
|
63
|
+
const result = isJwtTokenExpired(token);
|
|
64
|
+
|
|
65
|
+
expect(result).to.equal(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns true when token is invalid', () => {
|
|
69
|
+
const result = isJwtTokenExpired('not-a-jwt');
|
|
70
|
+
|
|
71
|
+
expect(result).to.equal(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -13029,7 +13029,7 @@ describe('plugin-meetings', () => {
|
|
|
13029
13029
|
'a datachannel url',
|
|
13030
13030
|
'token-123'
|
|
13031
13031
|
);
|
|
13032
|
-
assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default');
|
|
13032
|
+
assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
|
|
13033
13033
|
});
|
|
13034
13034
|
it('prefers refreshed token over locus self token', async () => {
|
|
13035
13035
|
meeting.joinedWith = {state: 'JOINED'};
|
|
@@ -13039,7 +13039,7 @@ describe('plugin-meetings', () => {
|
|
|
13039
13039
|
self: {datachannelToken: 'locus-token'},
|
|
13040
13040
|
};
|
|
13041
13041
|
|
|
13042
|
-
webex.internal.llm.getDatachannelToken.withArgs('default').returns('refreshed-token');
|
|
13042
|
+
webex.internal.llm.getDatachannelToken.withArgs('llm-default-session').returns('refreshed-token');
|
|
13043
13043
|
|
|
13044
13044
|
await meeting.updateLLMConnection();
|
|
13045
13045
|
|
|
@@ -13072,7 +13072,7 @@ describe('plugin-meetings', () => {
|
|
|
13072
13072
|
'a datachannel url',
|
|
13073
13073
|
'token-123'
|
|
13074
13074
|
);
|
|
13075
|
-
assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'default');
|
|
13075
|
+
assert.calledWithExactly(webex.internal.llm.setDatachannelToken, 'token-123', 'llm-default-session');
|
|
13076
13076
|
});
|
|
13077
13077
|
|
|
13078
13078
|
describe('#clearMeetingData', () => {
|
|
@@ -14735,7 +14735,7 @@ describe('plugin-meetings', () => {
|
|
|
14735
14735
|
expect(result).to.deep.equal({
|
|
14736
14736
|
body: {
|
|
14737
14737
|
datachannelToken: 'mock-token',
|
|
14738
|
-
dataChannelTokenType: '
|
|
14738
|
+
dataChannelTokenType: 'llm-practice-session',
|
|
14739
14739
|
},
|
|
14740
14740
|
});
|
|
14741
14741
|
});
|
|
@@ -14748,7 +14748,7 @@ describe('plugin-meetings', () => {
|
|
|
14748
14748
|
|
|
14749
14749
|
const result = meeting.getDataChannelTokenType();
|
|
14750
14750
|
|
|
14751
|
-
expect(result).to.equal('
|
|
14751
|
+
expect(result).to.equal('llm-practice-session');
|
|
14752
14752
|
});
|
|
14753
14753
|
|
|
14754
14754
|
it('returns Default when not in practice session mode', () => {
|
|
@@ -14758,7 +14758,7 @@ describe('plugin-meetings', () => {
|
|
|
14758
14758
|
|
|
14759
14759
|
const result = meeting.getDataChannelTokenType();
|
|
14760
14760
|
|
|
14761
|
-
expect(result).to.equal('default');
|
|
14761
|
+
expect(result).to.equal('llm-default-session');
|
|
14762
14762
|
});
|
|
14763
14763
|
});
|
|
14764
14764
|
describe('#stopKeepAlive', () => {
|
|
@@ -924,7 +924,14 @@ describe('plugin-meetings', () => {
|
|
|
924
924
|
const locusUrl = 'https://locus.example.com/locus/api/v1/loci/123';
|
|
925
925
|
const participantId = 'participant-123';
|
|
926
926
|
|
|
927
|
+
beforeEach(() => {
|
|
928
|
+
sinon.restore();
|
|
929
|
+
locusDeltaRequestSpy = sinon.stub(meetingsRequest, 'locusDeltaRequest');
|
|
930
|
+
});
|
|
931
|
+
|
|
927
932
|
it('sends GET request to regular datachannel token endpoint', async () => {
|
|
933
|
+
locusDeltaRequestSpy.resolves({body: {}});
|
|
934
|
+
|
|
928
935
|
await meetingsRequest.fetchDatachannelToken({
|
|
929
936
|
locusUrl,
|
|
930
937
|
requestingParticipantId: participantId,
|
|
@@ -938,6 +945,8 @@ describe('plugin-meetings', () => {
|
|
|
938
945
|
});
|
|
939
946
|
|
|
940
947
|
it('sends GET request to practice session datachannel token endpoint', async () => {
|
|
948
|
+
locusDeltaRequestSpy.resolves({body: {}});
|
|
949
|
+
|
|
941
950
|
await meetingsRequest.fetchDatachannelToken({
|
|
942
951
|
locusUrl,
|
|
943
952
|
requestingParticipantId: participantId,
|
|
@@ -950,7 +959,7 @@ describe('plugin-meetings', () => {
|
|
|
950
959
|
});
|
|
951
960
|
});
|
|
952
961
|
|
|
953
|
-
it('
|
|
962
|
+
it('rejects when locusUrl or participantId is missing', async () => {
|
|
954
963
|
await assert.isRejected(
|
|
955
964
|
meetingsRequest.fetchDatachannelToken({
|
|
956
965
|
locusUrl: null,
|
|
@@ -968,18 +977,15 @@ describe('plugin-meetings', () => {
|
|
|
968
977
|
);
|
|
969
978
|
});
|
|
970
979
|
|
|
971
|
-
it('
|
|
972
|
-
|
|
973
|
-
locusDeltaRequestSpy.restore();
|
|
974
|
-
sinon.stub(meetingsRequest, 'locusDeltaRequest').rejects(error);
|
|
980
|
+
it('returns null when locusDeltaRequest fails', async () => {
|
|
981
|
+
locusDeltaRequestSpy.rejects(new Error('network error'));
|
|
975
982
|
|
|
976
|
-
await
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
);
|
|
983
|
+
const result = await meetingsRequest.fetchDatachannelToken({
|
|
984
|
+
locusUrl,
|
|
985
|
+
requestingParticipantId: participantId,
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
assert.equal(result, null);
|
|
983
989
|
});
|
|
984
990
|
});
|
|
985
991
|
});
|
|
@@ -233,6 +233,8 @@ describe('plugin-meetings', () => {
|
|
|
233
233
|
// Ensure connect path is eligible
|
|
234
234
|
webinar.selfIsPanelist = true;
|
|
235
235
|
webinar.practiceSessionEnabled = true;
|
|
236
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
237
|
+
webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
|
|
236
238
|
});
|
|
237
239
|
|
|
238
240
|
it('no-ops when practice session join eligibility is false', async () => {
|
|
@@ -342,6 +344,22 @@ describe('plugin-meetings', () => {
|
|
|
342
344
|
processRelayEvent
|
|
343
345
|
);
|
|
344
346
|
});
|
|
347
|
+
|
|
348
|
+
it('subscribes to transcription when caption intent is enabled', async () => {
|
|
349
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(true);
|
|
350
|
+
|
|
351
|
+
await webinar.updatePSDataChannel();
|
|
352
|
+
|
|
353
|
+
assert.calledOnceWithExactly(webex.internal.voicea.updateSubchannelSubscriptions, { subscribe: ['transcription'] });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('does not subscribe to transcription when caption intent is disabled', async () => {
|
|
357
|
+
webex.internal.voicea.getIsCaptionBoxOn = sinon.stub().returns(false);
|
|
358
|
+
|
|
359
|
+
await webinar.updatePSDataChannel();
|
|
360
|
+
|
|
361
|
+
assert.notCalled(webex.internal.voicea.updateSubchannelSubscriptions);
|
|
362
|
+
});
|
|
345
363
|
});
|
|
346
364
|
|
|
347
365
|
describe('#updateStatusByRole', () => {
|