@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-auth-prejoin-fetch.1
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/README.md +5 -2
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +12 -0
- package/dist/credentials-config.js.map +1 -1
- package/dist/interceptors/redirect.js +1 -1
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/lib/batcher.js +23 -7
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +48 -4
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/token.js +1 -1
- package/dist/lib/domains.js +90 -0
- package/dist/lib/domains.js.map +1 -0
- package/dist/lib/services/service-catalog.js +6 -10
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/services.js +244 -57
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +6 -12
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +243 -52
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/webex-core.js +2 -2
- package/dist/webex-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +27 -0
- package/src/credentials-config.js +13 -0
- package/src/interceptors/redirect.js +4 -1
- package/src/lib/batcher.js +25 -10
- package/src/lib/credentials/credentials.js +50 -3
- package/src/lib/domains.ts +94 -0
- package/src/lib/services/service-catalog.js +6 -10
- package/src/lib/services/services.js +218 -46
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +218 -42
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/service-catalog.js +7 -6
- package/test/integration/spec/services/services.js +188 -128
- package/test/integration/spec/services-v2/services-v2.js +174 -114
- package/test/unit/spec/credentials/credentials.js +133 -2
- package/test/unit/spec/interceptors/auth.js +56 -0
- package/test/unit/spec/lib/batcher.js +56 -0
- package/test/unit/spec/services/service-catalog.js +93 -11
- package/test/unit/spec/services/services.js +591 -322
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +539 -214
- package/test/unit/spec/webex-core.js +0 -2
- package/test/unit/spec/webex-internal-core.js +0 -2
|
@@ -34,53 +34,6 @@ describe('webex-core', () => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
describe('#initialize', () => {
|
|
37
|
-
it('listens for "loaded" event instead of "ready" to avoid deadlock', () => {
|
|
38
|
-
services.listenToOnce = sinon.stub();
|
|
39
|
-
services.initialize();
|
|
40
|
-
|
|
41
|
-
// Second listenToOnce call should be for 'loaded' event
|
|
42
|
-
assert.equal(services.listenToOnce.getCall(1).args[1], 'loaded');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('services.ready starts as false', () => {
|
|
46
|
-
assert.isFalse(services.ready);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('sets ready to true and triggers services:initialized when initialization succeeds with credentials', async () => {
|
|
50
|
-
services.listenToOnce = sinon.stub();
|
|
51
|
-
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
52
|
-
services.trigger = sinon.stub();
|
|
53
|
-
services.webex.credentials = {
|
|
54
|
-
supertoken: {
|
|
55
|
-
access_token: 'token',
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
services.initialize();
|
|
60
|
-
|
|
61
|
-
// call the onLoaded callback
|
|
62
|
-
services.listenToOnce.getCall(1).args[2]();
|
|
63
|
-
await waitForAsync();
|
|
64
|
-
|
|
65
|
-
assert.isTrue(services.ready);
|
|
66
|
-
sinon.assert.calledWith(services.trigger, 'services:initialized');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('sets ready to true and triggers services:initialized when initialization succeeds without credentials', async () => {
|
|
70
|
-
services.listenToOnce = sinon.stub();
|
|
71
|
-
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
72
|
-
services.trigger = sinon.stub();
|
|
73
|
-
|
|
74
|
-
services.initialize();
|
|
75
|
-
|
|
76
|
-
// call the onLoaded callback
|
|
77
|
-
services.listenToOnce.getCall(1).args[2]();
|
|
78
|
-
await waitForAsync();
|
|
79
|
-
|
|
80
|
-
assert.isTrue(services.ready);
|
|
81
|
-
sinon.assert.calledWith(services.trigger, 'services:initialized');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
37
|
it('initFailed is false when initialization succeeds and credentials are available', async () => {
|
|
85
38
|
services.listenToOnce = sinon.stub();
|
|
86
39
|
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
@@ -92,7 +45,8 @@ describe('webex-core', () => {
|
|
|
92
45
|
|
|
93
46
|
services.initialize();
|
|
94
47
|
|
|
95
|
-
// call the
|
|
48
|
+
// call the onReady callback
|
|
49
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
96
50
|
services.listenToOnce.getCall(1).args[2]();
|
|
97
51
|
await waitForAsync();
|
|
98
52
|
|
|
@@ -105,7 +59,8 @@ describe('webex-core', () => {
|
|
|
105
59
|
|
|
106
60
|
services.initialize();
|
|
107
61
|
|
|
108
|
-
// call the
|
|
62
|
+
// call the onReady callback
|
|
63
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
109
64
|
services.listenToOnce.getCall(1).args[2]();
|
|
110
65
|
await waitForAsync();
|
|
111
66
|
|
|
@@ -114,9 +69,9 @@ describe('webex-core', () => {
|
|
|
114
69
|
|
|
115
70
|
it.each([
|
|
116
71
|
{error: new Error('failed'), expectedMessage: 'failed'},
|
|
117
|
-
{error: undefined, expectedMessage: undefined}
|
|
72
|
+
{error: undefined, expectedMessage: undefined}
|
|
118
73
|
])(
|
|
119
|
-
'sets initFailed to true when collectPreauthCatalog errors
|
|
74
|
+
'sets initFailed to true when collectPreauthCatalog errors',
|
|
120
75
|
async ({error, expectedMessage}) => {
|
|
121
76
|
services.collectPreauthCatalog = sinon.stub().callsFake(() => {
|
|
122
77
|
return Promise.reject(error);
|
|
@@ -124,18 +79,16 @@ describe('webex-core', () => {
|
|
|
124
79
|
|
|
125
80
|
services.listenToOnce = sinon.stub();
|
|
126
81
|
services.logger.error = sinon.stub();
|
|
127
|
-
services.trigger = sinon.stub();
|
|
128
82
|
|
|
129
83
|
services.initialize();
|
|
130
84
|
|
|
131
|
-
// call the
|
|
85
|
+
// call the onReady callback
|
|
86
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
132
87
|
services.listenToOnce.getCall(1).args[2]();
|
|
133
88
|
|
|
134
89
|
await waitForAsync();
|
|
135
90
|
|
|
136
91
|
assert.isTrue(services.initFailed);
|
|
137
|
-
assert.isTrue(services.ready);
|
|
138
|
-
sinon.assert.calledWith(services.trigger, 'services:initialized');
|
|
139
92
|
sinon.assert.calledWith(
|
|
140
93
|
services.logger.error,
|
|
141
94
|
`services: failed to init initial services when no credentials available, ${expectedMessage}`
|
|
@@ -145,132 +98,467 @@ describe('webex-core', () => {
|
|
|
145
98
|
|
|
146
99
|
it.each([
|
|
147
100
|
{error: new Error('failed'), expectedMessage: 'failed'},
|
|
148
|
-
{error: undefined, expectedMessage: undefined}
|
|
149
|
-
])(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
},
|
|
159
|
-
};
|
|
101
|
+
{error: undefined, expectedMessage: undefined}
|
|
102
|
+
])('sets initFailed to true when initServiceCatalogs errors', async ({error, expectedMessage}) => {
|
|
103
|
+
services.initServiceCatalogs = sinon.stub().callsFake(() => {
|
|
104
|
+
return Promise.reject(error);
|
|
105
|
+
});
|
|
106
|
+
services.webex.credentials = {
|
|
107
|
+
supertoken: {
|
|
108
|
+
access_token: 'token'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
160
111
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
services.trigger = sinon.stub();
|
|
112
|
+
services.listenToOnce = sinon.stub();
|
|
113
|
+
services.logger.error = sinon.stub();
|
|
164
114
|
|
|
165
|
-
|
|
115
|
+
services.initialize();
|
|
166
116
|
|
|
167
|
-
|
|
168
|
-
|
|
117
|
+
// call the onReady callback
|
|
118
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
119
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
169
120
|
|
|
170
|
-
|
|
121
|
+
await waitForAsync();
|
|
171
122
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
);
|
|
123
|
+
assert.isTrue(services.initFailed);
|
|
124
|
+
sinon.assert.calledWith(
|
|
125
|
+
services.logger.error,
|
|
126
|
+
`services: failed to init initial services when credentials available, ${expectedMessage}`
|
|
127
|
+
);
|
|
128
|
+
});
|
|
181
129
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(false));
|
|
186
|
-
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
187
|
-
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
188
|
-
services.trigger = sinon.stub();
|
|
189
|
-
// Ensure no credentials so we go to the preauth path
|
|
190
|
-
services.webex.credentials = {};
|
|
130
|
+
it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
|
|
131
|
+
services.listenToOnce = sinon.stub();
|
|
132
|
+
services.initialize();
|
|
191
133
|
|
|
192
|
-
|
|
134
|
+
// Fire the change:config handler which decides gated-vs-ungated and, in
|
|
135
|
+
// ungated mode, flips services.ready to true.
|
|
136
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
193
137
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
testCatalog.status.postauth.ready = false;
|
|
138
|
+
assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
|
|
139
|
+
});
|
|
197
140
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
141
|
+
it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
|
|
142
|
+
services.listenToOnce = sinon.stub();
|
|
143
|
+
services.initialize();
|
|
201
144
|
|
|
202
|
-
|
|
203
|
-
|
|
145
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
146
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
204
147
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
148
|
+
// Call 0 is 'change:config'; call 1 is the catalog init listener.
|
|
149
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
150
|
+
assert.equal(event, 'ready', 'default path listens on webex ready');
|
|
151
|
+
});
|
|
208
152
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
153
|
+
it('skips preauth catalog collection when skipPreauthCatalogOnUnauthenticated is true and no credentials', async () => {
|
|
154
|
+
services.webex.config = {
|
|
155
|
+
...(services.webex.config || {}),
|
|
156
|
+
services: {skipPreauthCatalogOnUnauthenticated: true},
|
|
157
|
+
};
|
|
158
|
+
services.listenToOnce = sinon.stub();
|
|
159
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
160
|
+
services.logger.info = sinon.stub();
|
|
212
161
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
218
|
-
services.trigger = sinon.stub();
|
|
219
|
-
// Ensure no credentials so we go to the preauth path
|
|
220
|
-
services.webex.credentials = {};
|
|
162
|
+
services.initialize();
|
|
163
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
164
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
165
|
+
await waitForAsync();
|
|
221
166
|
|
|
222
|
-
|
|
167
|
+
assert.isFalse(
|
|
168
|
+
services.collectPreauthCatalog.called,
|
|
169
|
+
'must not collect preauth catalog while unauthenticated when option is enabled'
|
|
170
|
+
);
|
|
171
|
+
assert.isFalse(services.initFailed);
|
|
172
|
+
sinon.assert.calledWith(
|
|
173
|
+
services.logger.info,
|
|
174
|
+
'services: skipping preauth catalog collection while unauthenticated as per the config'
|
|
175
|
+
);
|
|
176
|
+
});
|
|
223
177
|
|
|
224
|
-
|
|
225
|
-
|
|
178
|
+
it('still collects preauth catalog when skipPreauthCatalogOnUnauthenticated is false (default)', async () => {
|
|
179
|
+
services.webex.config = {
|
|
180
|
+
...(services.webex.config || {}),
|
|
181
|
+
services: {skipPreauthCatalogOnUnauthenticated: false},
|
|
182
|
+
};
|
|
183
|
+
services.listenToOnce = sinon.stub();
|
|
184
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
226
185
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
186
|
+
services.initialize();
|
|
187
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
188
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
189
|
+
await waitForAsync();
|
|
230
190
|
|
|
231
|
-
|
|
232
|
-
services.
|
|
233
|
-
|
|
191
|
+
assert.isTrue(
|
|
192
|
+
services.collectPreauthCatalog.called,
|
|
193
|
+
'must collect preauth catalog when option is disabled'
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
234
197
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await waitForAsync();
|
|
198
|
+
describe('#initialize (waitForCatalogInit=true)', () => {
|
|
199
|
+
let clock;
|
|
238
200
|
|
|
239
|
-
|
|
240
|
-
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
services.webex.config = {
|
|
203
|
+
...(services.webex.config || {}),
|
|
204
|
+
services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
|
|
205
|
+
};
|
|
206
|
+
// Reset ready flag: the outer beforeEach constructs services which
|
|
207
|
+
// runs initialize() once with no config (ungated path, flips ready to
|
|
208
|
+
// true). We reset here so tests that call initialize() a second time
|
|
209
|
+
// with the flag enabled observe the gated behavior from a clean state.
|
|
210
|
+
services.ready = false;
|
|
211
|
+
// The gated path schedules a real 15s setTimeout via _makeInitTimeout()
|
|
212
|
+
// that is not cleared when the init race resolves. Fake only setTimeout/
|
|
213
|
+
// clearTimeout (leaving setImmediate real so waitForAsync still works)
|
|
214
|
+
// so restoring in afterEach clears the pending timer and Jest can exit.
|
|
215
|
+
clock = sinon.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
|
|
216
|
+
});
|
|
241
217
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
246
|
-
services.initServiceCatalogs = sinon.stub().rejects(new Error('auth failed'));
|
|
247
|
-
services.logger.error = sinon.stub();
|
|
248
|
-
services.trigger = sinon.stub();
|
|
249
|
-
// Ensure no credentials so we go to the preauth path
|
|
250
|
-
services.webex.credentials = {};
|
|
218
|
+
afterEach(() => {
|
|
219
|
+
clock.restore();
|
|
220
|
+
});
|
|
251
221
|
|
|
252
|
-
|
|
222
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
223
|
+
services.listenToOnce = sinon.stub();
|
|
224
|
+
services.initialize();
|
|
253
225
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
testCatalog.status.postauth.ready = false;
|
|
226
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
227
|
+
});
|
|
257
228
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
229
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
230
|
+
services.listenToOnce = sinon.stub();
|
|
231
|
+
services.initialize();
|
|
261
232
|
|
|
262
|
-
|
|
263
|
-
|
|
233
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
234
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
264
235
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
236
|
+
// Call 0 is 'change:config'; call 1 is the catalog init listener.
|
|
237
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
238
|
+
assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
|
|
239
|
+
});
|
|
268
240
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
241
|
+
it('flips services.ready=true after init succeeds with credentials', async () => {
|
|
242
|
+
services.listenToOnce = sinon.stub();
|
|
243
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
244
|
+
services.webex.credentials = {
|
|
245
|
+
supertoken: {access_token: 'token'},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
services.initialize();
|
|
249
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
250
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
251
|
+
await waitForAsync();
|
|
252
|
+
|
|
253
|
+
assert.isTrue(services.ready);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
|
|
257
|
+
services.listenToOnce = sinon.stub();
|
|
258
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
259
|
+
|
|
260
|
+
services.initialize();
|
|
261
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
262
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
263
|
+
await waitForAsync();
|
|
264
|
+
|
|
265
|
+
assert.isTrue(services.ready);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
|
|
269
|
+
services.listenToOnce = sinon.stub();
|
|
270
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
271
|
+
services.webex.credentials = {
|
|
272
|
+
supertoken: {access_token: 'token'},
|
|
273
|
+
};
|
|
274
|
+
services.logger.error = sinon.stub();
|
|
275
|
+
|
|
276
|
+
services.initialize();
|
|
277
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
278
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
279
|
+
await waitForAsync();
|
|
280
|
+
|
|
281
|
+
assert.isTrue(services.initFailed);
|
|
282
|
+
assert.isTrue(services.ready);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
|
|
286
|
+
services.listenToOnce = sinon.stub();
|
|
287
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
288
|
+
services.logger.error = sinon.stub();
|
|
289
|
+
|
|
290
|
+
services.initialize();
|
|
291
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
292
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
293
|
+
await waitForAsync();
|
|
294
|
+
|
|
295
|
+
assert.isTrue(services.initFailed);
|
|
296
|
+
assert.isTrue(services.ready);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('flips services.ready=true when init times out', async () => {
|
|
300
|
+
services.listenToOnce = sinon.stub();
|
|
301
|
+
// Never resolves - the timeout must win the race.
|
|
302
|
+
services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
|
|
303
|
+
services.webex.credentials = {
|
|
304
|
+
supertoken: {access_token: 'token'},
|
|
305
|
+
};
|
|
306
|
+
services.logger.error = sinon.stub();
|
|
307
|
+
|
|
308
|
+
services.initialize();
|
|
309
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
310
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
311
|
+
|
|
312
|
+
// Advance past the 15s init timeout and flush microtasks so the
|
|
313
|
+
// rejected timeout can propagate through .catch and .finally into
|
|
314
|
+
// _finalizeReady().
|
|
315
|
+
await clock.tickAsync(15_001);
|
|
316
|
+
|
|
317
|
+
assert.isTrue(services.initFailed);
|
|
318
|
+
assert.isTrue(services.ready);
|
|
319
|
+
sinon.assert.calledWithMatch(
|
|
320
|
+
services.logger.error,
|
|
321
|
+
/services: init timed out after 15000ms/
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
|
|
326
|
+
services.listenToOnce = sinon.stub();
|
|
327
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
328
|
+
|
|
329
|
+
let onChangeIsRefreshing;
|
|
330
|
+
services.webex.credentials = {
|
|
331
|
+
supertoken: {access_token: 'token'},
|
|
332
|
+
isRefreshing: true,
|
|
333
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
334
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
335
|
+
}),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
services.initialize();
|
|
339
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
340
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
341
|
+
await waitForAsync();
|
|
342
|
+
|
|
343
|
+
assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
|
|
344
|
+
assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
|
|
345
|
+
sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
|
|
346
|
+
|
|
347
|
+
// Simulate refresh completing.
|
|
348
|
+
onChangeIsRefreshing();
|
|
349
|
+
await waitForAsync();
|
|
350
|
+
|
|
351
|
+
assert.isTrue(services.ready);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
|
|
355
|
+
services.listenToOnce = sinon.stub();
|
|
356
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
357
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
358
|
+
|
|
359
|
+
services.initialize();
|
|
360
|
+
// initialize() creates a new catalog and replaces the WeakMap entry.
|
|
361
|
+
const currentCatalog = services._getCatalog();
|
|
362
|
+
// Fire the 'loaded' listener (index 1; index 0 was 'change:config').
|
|
363
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
364
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
365
|
+
await waitForAsync();
|
|
366
|
+
|
|
367
|
+
const freshLoginCall = services.listenToOnce
|
|
368
|
+
.getCalls()
|
|
369
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
370
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
371
|
+
|
|
372
|
+
// Simulate OAuth completing.
|
|
373
|
+
services.webex.canAuthorize = true;
|
|
374
|
+
currentCatalog.status.postauth.ready = false;
|
|
375
|
+
freshLoginCall.args[2]();
|
|
376
|
+
|
|
377
|
+
assert.isTrue(
|
|
378
|
+
services.initServiceCatalogs.called,
|
|
379
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// `catalog.isReady` is now set inside initServiceCatalogs (after the
|
|
383
|
+
// postauth updateServices) rather than by this listener, so it is not
|
|
384
|
+
// asserted here where initServiceCatalogs is stubbed.
|
|
385
|
+
await waitForAsync();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
|
|
389
|
+
services.listenToOnce = sinon.stub();
|
|
390
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
391
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
392
|
+
|
|
393
|
+
services.initialize();
|
|
394
|
+
// initialize() creates a new catalog; grab the current one.
|
|
395
|
+
const currentCatalog = services._getCatalog();
|
|
396
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
397
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
398
|
+
await waitForAsync();
|
|
399
|
+
|
|
400
|
+
const freshLoginCall = services.listenToOnce
|
|
401
|
+
.getCalls()
|
|
402
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
403
|
+
|
|
404
|
+
services.webex.canAuthorize = true;
|
|
405
|
+
currentCatalog.status.postauth.ready = true; // already collected
|
|
406
|
+
freshLoginCall.args[2]();
|
|
407
|
+
|
|
408
|
+
assert.isFalse(
|
|
409
|
+
services.initServiceCatalogs.called,
|
|
410
|
+
'must not re-fetch postauth catalog if it is already ready'
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('finalizes ready when cache is warm (skips network fetch)', async () => {
|
|
415
|
+
services.listenToOnce = sinon.stub();
|
|
416
|
+
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
|
|
417
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
418
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
419
|
+
|
|
420
|
+
services.initialize();
|
|
421
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
422
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
423
|
+
await waitForAsync();
|
|
424
|
+
await waitForAsync();
|
|
425
|
+
|
|
426
|
+
assert.isTrue(services.ready);
|
|
427
|
+
assert.isFalse(
|
|
428
|
+
services.initServiceCatalogs.called,
|
|
429
|
+
'must skip network fetch when cache is warm'
|
|
430
|
+
);
|
|
431
|
+
assert.isFalse(
|
|
432
|
+
services.collectPreauthCatalog.called,
|
|
433
|
+
'must skip preauth fetch when cache is warm'
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('#initialize (waitForCatalogInit=true, skipPreauthCatalogOnUnauthenticated=true)', () => {
|
|
439
|
+
beforeEach(() => {
|
|
440
|
+
services.webex.config = {
|
|
441
|
+
...(services.webex.config || {}),
|
|
442
|
+
services: {
|
|
443
|
+
waitForCatalogInit: true,
|
|
444
|
+
catalogInitTimeout: 15000,
|
|
445
|
+
skipPreauthCatalogOnUnauthenticated: true,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
services.ready = false;
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('skips preauth catalog collection but still finalizes services.ready when unauthenticated', async () => {
|
|
452
|
+
services.listenToOnce = sinon.stub();
|
|
453
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
454
|
+
services.logger.info = sinon.stub();
|
|
455
|
+
|
|
456
|
+
services.initialize();
|
|
457
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
458
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
459
|
+
await waitForAsync();
|
|
460
|
+
|
|
461
|
+
assert.isFalse(
|
|
462
|
+
services.collectPreauthCatalog.called,
|
|
463
|
+
'must not collect preauth catalog while unauthenticated when option is enabled'
|
|
464
|
+
);
|
|
465
|
+
assert.isTrue(services.ready, 'must still finalize ready so webex.ready is not stalled');
|
|
466
|
+
sinon.assert.calledWith(
|
|
467
|
+
services.logger.info,
|
|
468
|
+
'services: skipping preauth catalog collection while unauthenticated as per the config'
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('still registers a change:canAuthorize listener when skipping preauth (fresh-login path)', async () => {
|
|
473
|
+
services.listenToOnce = sinon.stub();
|
|
474
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
475
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
476
|
+
|
|
477
|
+
services.initialize();
|
|
478
|
+
const currentCatalog = services._getCatalog();
|
|
479
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
480
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
481
|
+
await waitForAsync();
|
|
482
|
+
|
|
483
|
+
const freshLoginCall = services.listenToOnce
|
|
484
|
+
.getCalls()
|
|
485
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
486
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
487
|
+
|
|
488
|
+
services.webex.canAuthorize = true;
|
|
489
|
+
currentCatalog.status.postauth.ready = false;
|
|
490
|
+
freshLoginCall.args[2]();
|
|
491
|
+
|
|
492
|
+
assert.isTrue(
|
|
493
|
+
services.initServiceCatalogs.called,
|
|
494
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
495
|
+
);
|
|
496
|
+
await waitForAsync();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('does not schedule an init timeout when skipping preauth (no stray timer/rejection)', async () => {
|
|
500
|
+
const clock = sinon.useFakeTimers();
|
|
501
|
+
services.listenToOnce = sinon.stub();
|
|
502
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
503
|
+
services.logger.error = sinon.stub();
|
|
504
|
+
|
|
505
|
+
services.initialize();
|
|
506
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
507
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
508
|
+
|
|
509
|
+
// Advance well past the configured init timeout; since no timeout was
|
|
510
|
+
// scheduled in skip mode, no timeout error should ever be logged.
|
|
511
|
+
await clock.tickAsync(30_000);
|
|
512
|
+
clock.restore();
|
|
513
|
+
|
|
514
|
+
sinon.assert.neverCalledWithMatch(services.logger.error, /services: init timed out/);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('#_finalizeReady', () => {
|
|
519
|
+
beforeEach(() => {
|
|
520
|
+
// Outer beforeEach construction already ran initialize() (ungated,
|
|
521
|
+
// sets ready=true). Reset so _finalizeReady's flip is observable.
|
|
522
|
+
services.ready = false;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('sets ready=true immediately when credentials are not refreshing', async () => {
|
|
526
|
+
services.webex.credentials = {isRefreshing: false};
|
|
527
|
+
assert.isFalse(services.ready);
|
|
528
|
+
|
|
529
|
+
await services._finalizeReady();
|
|
530
|
+
|
|
531
|
+
assert.isTrue(services.ready);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('sets ready=true when credentials are missing', async () => {
|
|
535
|
+
services.webex.credentials = undefined;
|
|
536
|
+
|
|
537
|
+
await services._finalizeReady();
|
|
538
|
+
|
|
539
|
+
assert.isTrue(services.ready);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('awaits change:isRefreshing before setting ready=true', async () => {
|
|
543
|
+
let onChangeIsRefreshing;
|
|
544
|
+
services.webex.credentials = {
|
|
545
|
+
isRefreshing: true,
|
|
546
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
547
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
548
|
+
}),
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const settled = services._finalizeReady();
|
|
552
|
+
// Let the async body reach its await point.
|
|
553
|
+
await waitForAsync();
|
|
554
|
+
|
|
555
|
+
assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
|
|
556
|
+
assert.isFunction(onChangeIsRefreshing);
|
|
557
|
+
|
|
558
|
+
onChangeIsRefreshing();
|
|
559
|
+
await settled;
|
|
560
|
+
|
|
561
|
+
assert.isTrue(services.ready);
|
|
274
562
|
});
|
|
275
563
|
});
|
|
276
564
|
|
|
@@ -309,7 +597,7 @@ describe('webex-core', () => {
|
|
|
309
597
|
|
|
310
598
|
services.collectPreauthCatalog = sinon.stub().callsFake(() => {
|
|
311
599
|
return Promise.resolve();
|
|
312
|
-
})
|
|
600
|
+
})
|
|
313
601
|
|
|
314
602
|
services.updateServices = sinon.stub().callsFake(() => {
|
|
315
603
|
return Promise.reject(error);
|
|
@@ -404,7 +692,7 @@ describe('webex-core', () => {
|
|
|
404
692
|
discovery: {
|
|
405
693
|
sqdiscovery: 'https://test.ciscospark.com/v1/region',
|
|
406
694
|
},
|
|
407
|
-
}
|
|
695
|
+
}
|
|
408
696
|
};
|
|
409
697
|
});
|
|
410
698
|
|
|
@@ -424,8 +712,8 @@ describe('webex-core', () => {
|
|
|
424
712
|
assert.calledWith(webex.request, {
|
|
425
713
|
uri: 'https://test.ciscospark.com/v1/region',
|
|
426
714
|
addAuthHeader: false,
|
|
427
|
-
headers: {'spark-user-agent': null},
|
|
428
|
-
timeout: 5000
|
|
715
|
+
headers: { 'spark-user-agent': null },
|
|
716
|
+
timeout: 5000
|
|
429
717
|
});
|
|
430
718
|
});
|
|
431
719
|
});
|
|
@@ -482,6 +770,7 @@ describe('webex-core', () => {
|
|
|
482
770
|
});
|
|
483
771
|
|
|
484
772
|
describe('#_fetchNewServiceHostmap()', () => {
|
|
773
|
+
|
|
485
774
|
beforeEach(() => {
|
|
486
775
|
sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
|
|
487
776
|
});
|
|
@@ -495,20 +784,17 @@ describe('webex-core', () => {
|
|
|
495
784
|
|
|
496
785
|
sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
|
|
497
786
|
sinon.stub(services, 'request').resolves({});
|
|
498
|
-
|
|
787
|
+
|
|
499
788
|
const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
|
|
500
789
|
|
|
501
790
|
assert.calledOnceWithExactly(services.request, {
|
|
502
791
|
method: 'GET',
|
|
503
792
|
service: 'u2c',
|
|
504
793
|
resource: '/limited/catalog',
|
|
505
|
-
qs: {format: 'hostmap'}
|
|
506
|
-
}
|
|
507
|
-
assert.calledOnceWithExactly(
|
|
508
|
-
webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
|
|
509
|
-
sinon.match.func,
|
|
510
|
-
'internal.get.u2c.time'
|
|
794
|
+
qs: {format: 'hostmap'}
|
|
795
|
+
}
|
|
511
796
|
);
|
|
797
|
+
assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
|
|
512
798
|
});
|
|
513
799
|
|
|
514
800
|
it('checks service request rejects', async () => {
|
|
@@ -516,7 +802,7 @@ describe('webex-core', () => {
|
|
|
516
802
|
|
|
517
803
|
sinon.spy(services, '_formatReceivedHostmap');
|
|
518
804
|
sinon.stub(services, 'request').rejects(error);
|
|
519
|
-
|
|
805
|
+
|
|
520
806
|
const promise = services._fetchNewServiceHostmap({from: 'limited'});
|
|
521
807
|
const rejectedValue = await assert.isRejected(promise);
|
|
522
808
|
|
|
@@ -528,13 +814,10 @@ describe('webex-core', () => {
|
|
|
528
814
|
method: 'GET',
|
|
529
815
|
service: 'u2c',
|
|
530
816
|
resource: '/limited/catalog',
|
|
531
|
-
qs: {format: 'hostmap'}
|
|
532
|
-
}
|
|
533
|
-
assert.calledOnceWithExactly(
|
|
534
|
-
webex.internal.newMetrics.callDiagnosticLatencies.measureLatency,
|
|
535
|
-
sinon.match.func,
|
|
536
|
-
'internal.get.u2c.time'
|
|
817
|
+
qs: {format: 'hostmap'}
|
|
818
|
+
}
|
|
537
819
|
);
|
|
820
|
+
assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
|
|
538
821
|
});
|
|
539
822
|
});
|
|
540
823
|
|
|
@@ -565,6 +848,7 @@ describe('webex-core', () => {
|
|
|
565
848
|
});
|
|
566
849
|
|
|
567
850
|
it('returns the original uri if the hostmap has no hosts for the host', () => {
|
|
851
|
+
|
|
568
852
|
services._hostCatalog = {
|
|
569
853
|
'example.com': [],
|
|
570
854
|
};
|
|
@@ -728,7 +1012,8 @@ describe('webex-core', () => {
|
|
|
728
1012
|
id: '0:0:0:different-e-x',
|
|
729
1013
|
},
|
|
730
1014
|
],
|
|
731
|
-
'example-f.com': [
|
|
1015
|
+
'example-f.com': [
|
|
1016
|
+
],
|
|
732
1017
|
},
|
|
733
1018
|
format: 'hostmap',
|
|
734
1019
|
};
|
|
@@ -936,7 +1221,7 @@ describe('webex-core', () => {
|
|
|
936
1221
|
defaultUrl: 'https://example-g.com/api/v1',
|
|
937
1222
|
hosts: [],
|
|
938
1223
|
name: 'example-g',
|
|
939
|
-
}
|
|
1224
|
+
}
|
|
940
1225
|
]);
|
|
941
1226
|
});
|
|
942
1227
|
|
|
@@ -990,59 +1275,34 @@ describe('webex-core', () => {
|
|
|
990
1275
|
assert.equal(webex.config.credentials.authorizeUrl, authUrl);
|
|
991
1276
|
});
|
|
992
1277
|
});
|
|
993
|
-
|
|
1278
|
+
|
|
994
1279
|
describe('#getMobiusClusters', () => {
|
|
995
1280
|
it('returns unique mobius host entries from hostCatalog', () => {
|
|
996
1281
|
// Arrange: two hostCatalog keys, with duplicate mobius host across keys
|
|
997
1282
|
services._hostCatalog = {
|
|
998
1283
|
'mobius-us-east-2.prod.infra.webex.com': [
|
|
999
|
-
{
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
priority: 5,
|
|
1003
|
-
id: 'urn:TEAM:xyz:mobius',
|
|
1004
|
-
},
|
|
1005
|
-
{
|
|
1006
|
-
host: 'mobius-eu-central-1.prod.infra.webex.com',
|
|
1007
|
-
ttl: -1,
|
|
1008
|
-
priority: 10,
|
|
1009
|
-
id: 'urn:TEAM:xyz:mobius',
|
|
1010
|
-
},
|
|
1011
|
-
],
|
|
1284
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
|
|
1285
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
|
|
1286
|
+
],
|
|
1012
1287
|
|
|
1013
1288
|
'mobius-eu-central-1.prod.infra.webex.com': [
|
|
1014
|
-
{
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
priority: 7,
|
|
1018
|
-
id: 'urn:TEAM:xyz:mobius',
|
|
1019
|
-
}, // duplicate host
|
|
1020
|
-
],
|
|
1021
|
-
'wdm-a.webex.com': [
|
|
1289
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
|
|
1290
|
+
],
|
|
1291
|
+
'wdm-a.webex.com' : [
|
|
1022
1292
|
{host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
|
|
1023
|
-
]
|
|
1293
|
+
]
|
|
1024
1294
|
};
|
|
1025
|
-
|
|
1295
|
+
|
|
1026
1296
|
// Act
|
|
1027
1297
|
const clusters = services.getMobiusClusters();
|
|
1028
|
-
|
|
1298
|
+
|
|
1029
1299
|
// Assert
|
|
1030
1300
|
// deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
|
|
1031
1301
|
assert.deepEqual(
|
|
1032
1302
|
clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
|
|
1033
1303
|
[
|
|
1034
|
-
{
|
|
1035
|
-
|
|
1036
|
-
id: 'urn:TEAM:xyz:mobius',
|
|
1037
|
-
ttl: -1,
|
|
1038
|
-
priority: 5,
|
|
1039
|
-
},
|
|
1040
|
-
{
|
|
1041
|
-
host: 'mobius-eu-central-1.prod.infra.webex.com',
|
|
1042
|
-
id: 'urn:TEAM:xyz:mobius',
|
|
1043
|
-
ttl: -1,
|
|
1044
|
-
priority: 10,
|
|
1045
|
-
},
|
|
1304
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
|
|
1305
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
|
|
1046
1306
|
]
|
|
1047
1307
|
);
|
|
1048
1308
|
});
|
|
@@ -1051,31 +1311,31 @@ describe('webex-core', () => {
|
|
|
1051
1311
|
describe('#isValidHost', () => {
|
|
1052
1312
|
beforeEach(() => {
|
|
1053
1313
|
// Setting up a mock host catalog
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1314
|
+
services._hostCatalog = {
|
|
1315
|
+
"audit-ci-m.wbx2.com": [
|
|
1316
|
+
{
|
|
1317
|
+
"host": "audit-ci-m.wbx2.com",
|
|
1318
|
+
"ttl": -1,
|
|
1319
|
+
"priority": 5,
|
|
1320
|
+
"id": "urn:IDENTITY:PA61:adminAudit"
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
"host": "audit-ci-m.wbx2.com",
|
|
1324
|
+
"ttl": -1,
|
|
1325
|
+
"priority": 5,
|
|
1326
|
+
"id": "urn:IDENTITY:PA61:adminAuditV2"
|
|
1327
|
+
}
|
|
1328
|
+
],
|
|
1329
|
+
"mercury-connection-partition0-r.wbx2.com": [
|
|
1330
|
+
{
|
|
1331
|
+
"host": "mercury-connection-partition0-r.wbx2.com",
|
|
1332
|
+
"ttl": -1,
|
|
1333
|
+
"priority": 5,
|
|
1334
|
+
"id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
|
|
1335
|
+
}
|
|
1336
|
+
],
|
|
1337
|
+
"empty.com": []
|
|
1338
|
+
};
|
|
1079
1339
|
});
|
|
1080
1340
|
afterAll(() => {
|
|
1081
1341
|
// Clean up the mock host catalog
|
|
@@ -1100,13 +1360,64 @@ describe('webex-core', () => {
|
|
|
1100
1360
|
});
|
|
1101
1361
|
});
|
|
1102
1362
|
|
|
1363
|
+
describe('#isIntegrationEnvironment', () => {
|
|
1364
|
+
it('returns true when u2c URL contains "intb"', () => {
|
|
1365
|
+
services.webex.config = {
|
|
1366
|
+
services: {
|
|
1367
|
+
discovery: {
|
|
1368
|
+
u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
};
|
|
1372
|
+
assert.isTrue(services.isIntegrationEnvironment());
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
it('returns false when u2c URL does not contain "intb" (production)', () => {
|
|
1376
|
+
services.webex.config = {
|
|
1377
|
+
services: {
|
|
1378
|
+
discovery: {
|
|
1379
|
+
u2c: 'https://u2c.wbx2.com/u2c/api/v1',
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('returns false when u2c URL is for FedRAMP', () => {
|
|
1387
|
+
services.webex.config = {
|
|
1388
|
+
services: {
|
|
1389
|
+
discovery: {
|
|
1390
|
+
u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('returns false when u2c URL is undefined', () => {
|
|
1398
|
+
services.webex.config = {
|
|
1399
|
+
services: {
|
|
1400
|
+
discovery: {
|
|
1401
|
+
u2c: undefined,
|
|
1402
|
+
},
|
|
1403
|
+
},
|
|
1404
|
+
};
|
|
1405
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
it('returns false when config is not available', () => {
|
|
1409
|
+
services.webex.config = undefined;
|
|
1410
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1103
1414
|
describe('U2C catalog cache behavior', () => {
|
|
1104
1415
|
let webex;
|
|
1105
1416
|
let services;
|
|
1106
1417
|
let catalog;
|
|
1107
1418
|
let localStorageBackup;
|
|
1108
1419
|
let windowBackup;
|
|
1109
|
-
|
|
1420
|
+
|
|
1110
1421
|
const makeLocalStorageShim = () => {
|
|
1111
1422
|
const store = new Map();
|
|
1112
1423
|
return {
|
|
@@ -1116,16 +1427,13 @@ describe('webex-core', () => {
|
|
|
1116
1427
|
_store: store,
|
|
1117
1428
|
};
|
|
1118
1429
|
};
|
|
1119
|
-
|
|
1430
|
+
|
|
1120
1431
|
beforeEach(() => {
|
|
1121
1432
|
// Build a fresh webex instance
|
|
1122
|
-
webex = new MockWebex({
|
|
1123
|
-
children: {services: Services},
|
|
1124
|
-
config: {credentials: {federation: true}},
|
|
1125
|
-
});
|
|
1433
|
+
webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
|
|
1126
1434
|
services = webex.internal.services;
|
|
1127
1435
|
catalog = services._getCatalog();
|
|
1128
|
-
|
|
1436
|
+
|
|
1129
1437
|
// enable U2C caching feature flag in tests that rely on localStorage writes/reads
|
|
1130
1438
|
services.webex.config = services.webex.config || {};
|
|
1131
1439
|
services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
|
|
@@ -1137,15 +1445,13 @@ describe('webex-core', () => {
|
|
|
1137
1445
|
global.window.localStorage = makeLocalStorageShim();
|
|
1138
1446
|
// Ensure code under test uses our shim via util method
|
|
1139
1447
|
sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
|
|
1140
|
-
|
|
1448
|
+
|
|
1141
1449
|
// Stub the formatter so we don't need a full hostmap payload in tests
|
|
1142
|
-
sinon
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1146
|
-
]);
|
|
1450
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1451
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1452
|
+
]);
|
|
1147
1453
|
});
|
|
1148
|
-
|
|
1454
|
+
|
|
1149
1455
|
afterEach(() => {
|
|
1150
1456
|
global.window.localStorage = localStorageBackup || undefined;
|
|
1151
1457
|
if (!windowBackup) {
|
|
@@ -1158,7 +1464,7 @@ describe('webex-core', () => {
|
|
|
1158
1464
|
services._getLocalStorageSafe.restore();
|
|
1159
1465
|
}
|
|
1160
1466
|
});
|
|
1161
|
-
|
|
1467
|
+
|
|
1162
1468
|
it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
|
|
1163
1469
|
// Arrange: authenticated credentials and spies
|
|
1164
1470
|
services.webex.credentials = {
|
|
@@ -1170,20 +1476,10 @@ describe('webex-core', () => {
|
|
|
1170
1476
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1171
1477
|
const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
|
|
1172
1478
|
// Make fetch return a hostmap object and allow formatter to reduce it
|
|
1173
|
-
sinon
|
|
1174
|
-
|
|
1175
|
-
.resolves({
|
|
1176
|
-
body: {
|
|
1177
|
-
services: [],
|
|
1178
|
-
activeServices: {},
|
|
1179
|
-
timestamp: Date.now().toString(),
|
|
1180
|
-
orgId: 'urn:EXAMPLE:org',
|
|
1181
|
-
format: 'U2CV2',
|
|
1182
|
-
},
|
|
1183
|
-
});
|
|
1184
|
-
// Cause loaded callback to run immediately
|
|
1479
|
+
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
1480
|
+
// Cause ready callback to run immediately
|
|
1185
1481
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1186
|
-
if (event === '
|
|
1482
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1187
1483
|
});
|
|
1188
1484
|
|
|
1189
1485
|
// Act
|
|
@@ -1223,9 +1519,9 @@ describe('webex-core', () => {
|
|
|
1223
1519
|
|
|
1224
1520
|
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
1225
1521
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1226
|
-
// Cause
|
|
1522
|
+
// Cause ready callback to run immediately
|
|
1227
1523
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1228
|
-
if (event === '
|
|
1524
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1229
1525
|
});
|
|
1230
1526
|
|
|
1231
1527
|
// Act
|
|
@@ -1233,18 +1529,9 @@ describe('webex-core', () => {
|
|
|
1233
1529
|
await waitForAsync();
|
|
1234
1530
|
|
|
1235
1531
|
// Assert: ready path found cache and skipped initServiceCatalogs
|
|
1236
|
-
assert.isFalse(
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
);
|
|
1240
|
-
assert.isTrue(
|
|
1241
|
-
services._getCatalog().status.preauth.ready,
|
|
1242
|
-
'preauth should be ready from cache'
|
|
1243
|
-
);
|
|
1244
|
-
assert.isTrue(
|
|
1245
|
-
services._getCatalog().status.postauth.ready,
|
|
1246
|
-
'postauth should be ready from cache'
|
|
1247
|
-
);
|
|
1532
|
+
assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
|
|
1533
|
+
assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
|
|
1534
|
+
assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
|
|
1248
1535
|
assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
|
|
1249
1536
|
|
|
1250
1537
|
// Cleanup
|
|
@@ -1260,29 +1547,26 @@ describe('webex-core', () => {
|
|
|
1260
1547
|
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1261
1548
|
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1262
1549
|
};
|
|
1263
|
-
|
|
1550
|
+
|
|
1264
1551
|
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
|
|
1265
|
-
|
|
1552
|
+
|
|
1266
1553
|
const warmed = await services._loadCatalogFromCache();
|
|
1267
|
-
|
|
1554
|
+
|
|
1268
1555
|
assert.isFalse(warmed, 'stale cache must not warm');
|
|
1269
|
-
assert.isNull(
|
|
1270
|
-
window.localStorage.getItem(CATALOG_CACHE_KEY_V1),
|
|
1271
|
-
'expired cache must be cleared'
|
|
1272
|
-
);
|
|
1556
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
|
|
1273
1557
|
assert.isFalse(catalog.status.preauth.ready);
|
|
1274
1558
|
assert.isFalse(catalog.status.postauth.ready);
|
|
1275
1559
|
});
|
|
1276
|
-
|
|
1560
|
+
|
|
1277
1561
|
it('clearCatalogCache() removes the cached entry', async () => {
|
|
1278
1562
|
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1279
1563
|
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
|
|
1280
|
-
|
|
1564
|
+
|
|
1281
1565
|
await services.clearCatalogCache();
|
|
1282
|
-
|
|
1566
|
+
|
|
1283
1567
|
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
|
|
1284
1568
|
});
|
|
1285
|
-
|
|
1569
|
+
|
|
1286
1570
|
it('still fetches when forceRefresh=true even if ready', async () => {
|
|
1287
1571
|
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1288
1572
|
window.localStorage.setItem(
|
|
@@ -1294,24 +1578,20 @@ describe('webex-core', () => {
|
|
|
1294
1578
|
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1295
1579
|
})
|
|
1296
1580
|
);
|
|
1297
|
-
|
|
1581
|
+
|
|
1298
1582
|
// warm from cache
|
|
1299
1583
|
const warmed = await services._loadCatalogFromCache();
|
|
1300
1584
|
assert.isTrue(warmed);
|
|
1301
1585
|
assert.isTrue(catalog.status.preauth.ready);
|
|
1302
1586
|
assert.isTrue(catalog.status.postauth.ready);
|
|
1303
|
-
|
|
1587
|
+
|
|
1304
1588
|
const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
|
|
1305
|
-
|
|
1589
|
+
|
|
1306
1590
|
// with forceRefresh we should fetch despite ready=true
|
|
1307
|
-
await services.updateServices({
|
|
1308
|
-
from: 'limited',
|
|
1309
|
-
query: {orgId: 'urn:EXAMPLE:org'},
|
|
1310
|
-
forceRefresh: true,
|
|
1311
|
-
});
|
|
1591
|
+
await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
|
|
1312
1592
|
// pass an empty query to avoid spreading undefined in qs construction
|
|
1313
1593
|
await services.updateServices({forceRefresh: true});
|
|
1314
|
-
|
|
1594
|
+
|
|
1315
1595
|
assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
|
|
1316
1596
|
fetchSpy.restore();
|
|
1317
1597
|
});
|
|
@@ -1372,11 +1652,9 @@ describe('webex-core', () => {
|
|
|
1372
1652
|
);
|
|
1373
1653
|
// formatter returns at least one entry to mark ready
|
|
1374
1654
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1375
|
-
sinon
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1379
|
-
]);
|
|
1655
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1656
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1657
|
+
]);
|
|
1380
1658
|
|
|
1381
1659
|
const warmed = await services._loadCatalogFromCache();
|
|
1382
1660
|
assert.isTrue(warmed);
|
|
@@ -1398,11 +1676,9 @@ describe('webex-core', () => {
|
|
|
1398
1676
|
})
|
|
1399
1677
|
);
|
|
1400
1678
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1401
|
-
sinon
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1405
|
-
]);
|
|
1679
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1680
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1681
|
+
]);
|
|
1406
1682
|
|
|
1407
1683
|
const warmed = await services._loadCatalogFromCache();
|
|
1408
1684
|
// function returns true if overall cache path succeeded; we only verify group readiness
|
|
@@ -1429,17 +1705,12 @@ describe('webex-core', () => {
|
|
|
1429
1705
|
})
|
|
1430
1706
|
);
|
|
1431
1707
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1432
|
-
sinon
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1436
|
-
]);
|
|
1708
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1709
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1710
|
+
]);
|
|
1437
1711
|
|
|
1438
1712
|
await services._loadCatalogFromCache();
|
|
1439
|
-
assert.isFalse(
|
|
1440
|
-
catalog.status.preauth.ready,
|
|
1441
|
-
'preauth should not warm on selection mismatch'
|
|
1442
|
-
);
|
|
1713
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
|
|
1443
1714
|
});
|
|
1444
1715
|
|
|
1445
1716
|
it('skips warming when environment fingerprint mismatches', async () => {
|
|
@@ -1450,10 +1721,7 @@ describe('webex-core', () => {
|
|
|
1450
1721
|
JSON.stringify({
|
|
1451
1722
|
cachedAt: Date.now(),
|
|
1452
1723
|
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
|
|
1453
|
-
preauth: {
|
|
1454
|
-
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1455
|
-
meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
|
|
1456
|
-
},
|
|
1724
|
+
preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
|
|
1457
1725
|
})
|
|
1458
1726
|
);
|
|
1459
1727
|
// current env
|
|
@@ -1468,6 +1736,7 @@ describe('webex-core', () => {
|
|
|
1468
1736
|
assert.isFalse(catalog.status.postauth.ready);
|
|
1469
1737
|
});
|
|
1470
1738
|
});
|
|
1739
|
+
|
|
1471
1740
|
});
|
|
1472
1741
|
});
|
|
1473
1742
|
/* eslint-enable no-underscore-dangle */
|