@webex/webex-core 3.11.0-webex-services-ready.1 → 3.12.0-llmrefactor.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 +15 -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 +208 -51
- 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 +207 -46
- 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 +17 -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 +174 -37
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +173 -33
- 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 +49 -24
- package/test/integration/spec/services-v2/services-v2.js +49 -24
- 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 +458 -322
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +403 -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,334 @@ 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
|
+
});
|
|
152
|
+
});
|
|
208
153
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
154
|
+
describe('#initialize (waitForCatalogInit=true)', () => {
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
services.webex.config = {
|
|
157
|
+
...(services.webex.config || {}),
|
|
158
|
+
services: {waitForCatalogInit: true, catalogInitTimeout: 15000},
|
|
159
|
+
};
|
|
160
|
+
// Reset ready flag: the outer beforeEach constructs services which
|
|
161
|
+
// runs initialize() once with no config (ungated path, flips ready to
|
|
162
|
+
// true). We reset here so tests that call initialize() a second time
|
|
163
|
+
// with the flag enabled observe the gated behavior from a clean state.
|
|
164
|
+
services.ready = false;
|
|
165
|
+
});
|
|
212
166
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
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 = {};
|
|
167
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
168
|
+
services.listenToOnce = sinon.stub();
|
|
169
|
+
services.initialize();
|
|
221
170
|
|
|
222
|
-
|
|
171
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
172
|
+
});
|
|
223
173
|
|
|
224
|
-
|
|
225
|
-
|
|
174
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
175
|
+
services.listenToOnce = sinon.stub();
|
|
176
|
+
services.initialize();
|
|
226
177
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await waitForAsync();
|
|
178
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
179
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
230
180
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
181
|
+
// Call 0 is 'change:config'; call 1 is the catalog init listener.
|
|
182
|
+
const [, event] = services.listenToOnce.getCall(1).args;
|
|
183
|
+
assert.equal(event, 'loaded', 'gated path must listen on loaded to avoid deadlock');
|
|
184
|
+
});
|
|
234
185
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
186
|
+
it('flips services.ready=true after init succeeds with credentials', async () => {
|
|
187
|
+
services.listenToOnce = sinon.stub();
|
|
188
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
189
|
+
services.webex.credentials = {
|
|
190
|
+
supertoken: {access_token: 'token'},
|
|
191
|
+
};
|
|
238
192
|
|
|
239
|
-
|
|
240
|
-
|
|
193
|
+
services.initialize();
|
|
194
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
195
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
196
|
+
await waitForAsync();
|
|
241
197
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(false));
|
|
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 = {};
|
|
198
|
+
assert.isTrue(services.ready);
|
|
199
|
+
});
|
|
251
200
|
|
|
252
|
-
|
|
201
|
+
it('flips services.ready=true after init succeeds without credentials (preauth path)', async () => {
|
|
202
|
+
services.listenToOnce = sinon.stub();
|
|
203
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
253
204
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
205
|
+
services.initialize();
|
|
206
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
207
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
208
|
+
await waitForAsync();
|
|
257
209
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await waitForAsync();
|
|
210
|
+
assert.isTrue(services.ready);
|
|
211
|
+
});
|
|
261
212
|
|
|
262
|
-
|
|
263
|
-
|
|
213
|
+
it('flips services.ready=true even when initServiceCatalogs errors, so webex.ready can fire', async () => {
|
|
214
|
+
services.listenToOnce = sinon.stub();
|
|
215
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
216
|
+
services.webex.credentials = {
|
|
217
|
+
supertoken: {access_token: 'token'},
|
|
218
|
+
};
|
|
219
|
+
services.logger.error = sinon.stub();
|
|
264
220
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
221
|
+
services.initialize();
|
|
222
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
223
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
224
|
+
await waitForAsync();
|
|
268
225
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
226
|
+
assert.isTrue(services.initFailed);
|
|
227
|
+
assert.isTrue(services.ready);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('flips services.ready=true even when collectPreauthCatalog errors', async () => {
|
|
231
|
+
services.listenToOnce = sinon.stub();
|
|
232
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.reject(new Error('boom')));
|
|
233
|
+
services.logger.error = sinon.stub();
|
|
234
|
+
|
|
235
|
+
services.initialize();
|
|
236
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
237
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
238
|
+
await waitForAsync();
|
|
239
|
+
|
|
240
|
+
assert.isTrue(services.initFailed);
|
|
241
|
+
assert.isTrue(services.ready);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('flips services.ready=true when init times out', async () => {
|
|
245
|
+
const clock = sinon.useFakeTimers();
|
|
246
|
+
services.listenToOnce = sinon.stub();
|
|
247
|
+
// Never resolves - the timeout must win the race.
|
|
248
|
+
services.initServiceCatalogs = sinon.stub().returns(new Promise(() => {}));
|
|
249
|
+
services.webex.credentials = {
|
|
250
|
+
supertoken: {access_token: 'token'},
|
|
251
|
+
};
|
|
252
|
+
services.logger.error = sinon.stub();
|
|
253
|
+
|
|
254
|
+
services.initialize();
|
|
255
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
256
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
257
|
+
|
|
258
|
+
// Advance past the 15s init timeout and flush microtasks so the
|
|
259
|
+
// rejected timeout can propagate through .catch and .finally into
|
|
260
|
+
// _finalizeReady().
|
|
261
|
+
await clock.tickAsync(15_001);
|
|
262
|
+
clock.restore();
|
|
263
|
+
|
|
264
|
+
assert.isTrue(services.initFailed);
|
|
265
|
+
assert.isTrue(services.ready);
|
|
266
|
+
sinon.assert.calledWithMatch(
|
|
267
|
+
services.logger.error,
|
|
268
|
+
/services: init timed out after 15000ms/
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('awaits an in-flight credentials refresh before flipping services.ready=true', async () => {
|
|
273
|
+
services.listenToOnce = sinon.stub();
|
|
274
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
275
|
+
|
|
276
|
+
let onChangeIsRefreshing;
|
|
277
|
+
services.webex.credentials = {
|
|
278
|
+
supertoken: {access_token: 'token'},
|
|
279
|
+
isRefreshing: true,
|
|
280
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
281
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
services.initialize();
|
|
286
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
287
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
288
|
+
await waitForAsync();
|
|
289
|
+
|
|
290
|
+
assert.isFalse(services.ready, 'must not flip ready while refresh is in flight');
|
|
291
|
+
assert.isFunction(onChangeIsRefreshing, 'must subscribe to change:isRefreshing');
|
|
292
|
+
sinon.assert.calledWith(services.webex.credentials.once, 'change:isRefreshing');
|
|
293
|
+
|
|
294
|
+
// Simulate refresh completing.
|
|
295
|
+
onChangeIsRefreshing();
|
|
296
|
+
await waitForAsync();
|
|
297
|
+
|
|
298
|
+
assert.isTrue(services.ready);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('registers a change:canAuthorize listener when there is no supertoken (fresh-login path)', async () => {
|
|
302
|
+
services.listenToOnce = sinon.stub();
|
|
303
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
304
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
305
|
+
|
|
306
|
+
services.initialize();
|
|
307
|
+
// initialize() creates a new catalog and replaces the WeakMap entry.
|
|
308
|
+
const currentCatalog = services._getCatalog();
|
|
309
|
+
// Fire the 'loaded' listener (index 1; index 0 was 'change:config').
|
|
310
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
311
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
312
|
+
await waitForAsync();
|
|
313
|
+
|
|
314
|
+
const freshLoginCall = services.listenToOnce
|
|
315
|
+
.getCalls()
|
|
316
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
317
|
+
assert.isDefined(freshLoginCall, 'expected change:canAuthorize listener to be registered');
|
|
318
|
+
|
|
319
|
+
// Simulate OAuth completing.
|
|
320
|
+
services.webex.canAuthorize = true;
|
|
321
|
+
currentCatalog.status.postauth.ready = false;
|
|
322
|
+
freshLoginCall.args[2]();
|
|
323
|
+
|
|
324
|
+
assert.isTrue(
|
|
325
|
+
services.initServiceCatalogs.called,
|
|
326
|
+
'expected postauth catalog init after canAuthorize flips'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// `catalog.isReady` is now set inside initServiceCatalogs (after the
|
|
330
|
+
// postauth updateServices) rather than by this listener, so it is not
|
|
331
|
+
// asserted here where initServiceCatalogs is stubbed.
|
|
332
|
+
await waitForAsync();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('does not re-init postauth catalog when canAuthorize fires but catalog is already ready', async () => {
|
|
336
|
+
services.listenToOnce = sinon.stub();
|
|
337
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
338
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
339
|
+
|
|
340
|
+
services.initialize();
|
|
341
|
+
// initialize() creates a new catalog; grab the current one.
|
|
342
|
+
const currentCatalog = services._getCatalog();
|
|
343
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
344
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
345
|
+
await waitForAsync();
|
|
346
|
+
|
|
347
|
+
const freshLoginCall = services.listenToOnce
|
|
348
|
+
.getCalls()
|
|
349
|
+
.find((call) => call.args[1] === 'change:canAuthorize');
|
|
350
|
+
|
|
351
|
+
services.webex.canAuthorize = true;
|
|
352
|
+
currentCatalog.status.postauth.ready = true; // already collected
|
|
353
|
+
freshLoginCall.args[2]();
|
|
354
|
+
|
|
355
|
+
assert.isFalse(
|
|
356
|
+
services.initServiceCatalogs.called,
|
|
357
|
+
'must not re-fetch postauth catalog if it is already ready'
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('finalizes ready when cache is warm (skips network fetch)', async () => {
|
|
362
|
+
services.listenToOnce = sinon.stub();
|
|
363
|
+
services._loadCatalogFromCache = sinon.stub().returns(Promise.resolve(true));
|
|
364
|
+
services.initServiceCatalogs = sinon.stub().returns(Promise.resolve());
|
|
365
|
+
services.collectPreauthCatalog = sinon.stub().returns(Promise.resolve());
|
|
366
|
+
|
|
367
|
+
services.initialize();
|
|
368
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
369
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
370
|
+
await waitForAsync();
|
|
371
|
+
await waitForAsync();
|
|
372
|
+
|
|
373
|
+
assert.isTrue(services.ready);
|
|
374
|
+
assert.isFalse(
|
|
375
|
+
services.initServiceCatalogs.called,
|
|
376
|
+
'must skip network fetch when cache is warm'
|
|
377
|
+
);
|
|
378
|
+
assert.isFalse(
|
|
379
|
+
services.collectPreauthCatalog.called,
|
|
380
|
+
'must skip preauth fetch when cache is warm'
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('#_finalizeReady', () => {
|
|
386
|
+
beforeEach(() => {
|
|
387
|
+
// Outer beforeEach construction already ran initialize() (ungated,
|
|
388
|
+
// sets ready=true). Reset so _finalizeReady's flip is observable.
|
|
389
|
+
services.ready = false;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('sets ready=true immediately when credentials are not refreshing', async () => {
|
|
393
|
+
services.webex.credentials = {isRefreshing: false};
|
|
394
|
+
assert.isFalse(services.ready);
|
|
395
|
+
|
|
396
|
+
await services._finalizeReady();
|
|
397
|
+
|
|
398
|
+
assert.isTrue(services.ready);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('sets ready=true when credentials are missing', async () => {
|
|
402
|
+
services.webex.credentials = undefined;
|
|
403
|
+
|
|
404
|
+
await services._finalizeReady();
|
|
405
|
+
|
|
406
|
+
assert.isTrue(services.ready);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('awaits change:isRefreshing before setting ready=true', async () => {
|
|
410
|
+
let onChangeIsRefreshing;
|
|
411
|
+
services.webex.credentials = {
|
|
412
|
+
isRefreshing: true,
|
|
413
|
+
once: sinon.stub().callsFake((event, cb) => {
|
|
414
|
+
if (event === 'change:isRefreshing') onChangeIsRefreshing = cb;
|
|
415
|
+
}),
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const settled = services._finalizeReady();
|
|
419
|
+
// Let the async body reach its await point.
|
|
420
|
+
await waitForAsync();
|
|
421
|
+
|
|
422
|
+
assert.isFalse(services.ready, 'ready must not flip while refresh is in flight');
|
|
423
|
+
assert.isFunction(onChangeIsRefreshing);
|
|
424
|
+
|
|
425
|
+
onChangeIsRefreshing();
|
|
426
|
+
await settled;
|
|
427
|
+
|
|
428
|
+
assert.isTrue(services.ready);
|
|
274
429
|
});
|
|
275
430
|
});
|
|
276
431
|
|
|
@@ -309,7 +464,7 @@ describe('webex-core', () => {
|
|
|
309
464
|
|
|
310
465
|
services.collectPreauthCatalog = sinon.stub().callsFake(() => {
|
|
311
466
|
return Promise.resolve();
|
|
312
|
-
})
|
|
467
|
+
})
|
|
313
468
|
|
|
314
469
|
services.updateServices = sinon.stub().callsFake(() => {
|
|
315
470
|
return Promise.reject(error);
|
|
@@ -404,7 +559,7 @@ describe('webex-core', () => {
|
|
|
404
559
|
discovery: {
|
|
405
560
|
sqdiscovery: 'https://test.ciscospark.com/v1/region',
|
|
406
561
|
},
|
|
407
|
-
}
|
|
562
|
+
}
|
|
408
563
|
};
|
|
409
564
|
});
|
|
410
565
|
|
|
@@ -424,8 +579,8 @@ describe('webex-core', () => {
|
|
|
424
579
|
assert.calledWith(webex.request, {
|
|
425
580
|
uri: 'https://test.ciscospark.com/v1/region',
|
|
426
581
|
addAuthHeader: false,
|
|
427
|
-
headers: {'spark-user-agent': null},
|
|
428
|
-
timeout: 5000
|
|
582
|
+
headers: { 'spark-user-agent': null },
|
|
583
|
+
timeout: 5000
|
|
429
584
|
});
|
|
430
585
|
});
|
|
431
586
|
});
|
|
@@ -482,6 +637,7 @@ describe('webex-core', () => {
|
|
|
482
637
|
});
|
|
483
638
|
|
|
484
639
|
describe('#_fetchNewServiceHostmap()', () => {
|
|
640
|
+
|
|
485
641
|
beforeEach(() => {
|
|
486
642
|
sinon.spy(webex.internal.newMetrics.callDiagnosticLatencies, 'measureLatency');
|
|
487
643
|
});
|
|
@@ -495,20 +651,17 @@ describe('webex-core', () => {
|
|
|
495
651
|
|
|
496
652
|
sinon.stub(services, '_formatReceivedHostmap').resolves(mapResponse);
|
|
497
653
|
sinon.stub(services, 'request').resolves({});
|
|
498
|
-
|
|
654
|
+
|
|
499
655
|
const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
|
|
500
656
|
|
|
501
657
|
assert.calledOnceWithExactly(services.request, {
|
|
502
658
|
method: 'GET',
|
|
503
659
|
service: 'u2c',
|
|
504
660
|
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'
|
|
661
|
+
qs: {format: 'hostmap'}
|
|
662
|
+
}
|
|
511
663
|
);
|
|
664
|
+
assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
|
|
512
665
|
});
|
|
513
666
|
|
|
514
667
|
it('checks service request rejects', async () => {
|
|
@@ -516,7 +669,7 @@ describe('webex-core', () => {
|
|
|
516
669
|
|
|
517
670
|
sinon.spy(services, '_formatReceivedHostmap');
|
|
518
671
|
sinon.stub(services, 'request').rejects(error);
|
|
519
|
-
|
|
672
|
+
|
|
520
673
|
const promise = services._fetchNewServiceHostmap({from: 'limited'});
|
|
521
674
|
const rejectedValue = await assert.isRejected(promise);
|
|
522
675
|
|
|
@@ -528,13 +681,10 @@ describe('webex-core', () => {
|
|
|
528
681
|
method: 'GET',
|
|
529
682
|
service: 'u2c',
|
|
530
683
|
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'
|
|
684
|
+
qs: {format: 'hostmap'}
|
|
685
|
+
}
|
|
537
686
|
);
|
|
687
|
+
assert.calledOnceWithExactly(webex.internal.newMetrics.callDiagnosticLatencies.measureLatency, sinon.match.func, 'internal.get.u2c.time');
|
|
538
688
|
});
|
|
539
689
|
});
|
|
540
690
|
|
|
@@ -565,6 +715,7 @@ describe('webex-core', () => {
|
|
|
565
715
|
});
|
|
566
716
|
|
|
567
717
|
it('returns the original uri if the hostmap has no hosts for the host', () => {
|
|
718
|
+
|
|
568
719
|
services._hostCatalog = {
|
|
569
720
|
'example.com': [],
|
|
570
721
|
};
|
|
@@ -728,7 +879,8 @@ describe('webex-core', () => {
|
|
|
728
879
|
id: '0:0:0:different-e-x',
|
|
729
880
|
},
|
|
730
881
|
],
|
|
731
|
-
'example-f.com': [
|
|
882
|
+
'example-f.com': [
|
|
883
|
+
],
|
|
732
884
|
},
|
|
733
885
|
format: 'hostmap',
|
|
734
886
|
};
|
|
@@ -936,7 +1088,7 @@ describe('webex-core', () => {
|
|
|
936
1088
|
defaultUrl: 'https://example-g.com/api/v1',
|
|
937
1089
|
hosts: [],
|
|
938
1090
|
name: 'example-g',
|
|
939
|
-
}
|
|
1091
|
+
}
|
|
940
1092
|
]);
|
|
941
1093
|
});
|
|
942
1094
|
|
|
@@ -990,59 +1142,34 @@ describe('webex-core', () => {
|
|
|
990
1142
|
assert.equal(webex.config.credentials.authorizeUrl, authUrl);
|
|
991
1143
|
});
|
|
992
1144
|
});
|
|
993
|
-
|
|
1145
|
+
|
|
994
1146
|
describe('#getMobiusClusters', () => {
|
|
995
1147
|
it('returns unique mobius host entries from hostCatalog', () => {
|
|
996
1148
|
// Arrange: two hostCatalog keys, with duplicate mobius host across keys
|
|
997
1149
|
services._hostCatalog = {
|
|
998
1150
|
'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
|
-
],
|
|
1151
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
|
|
1152
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
|
|
1153
|
+
],
|
|
1012
1154
|
|
|
1013
1155
|
'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': [
|
|
1156
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
|
|
1157
|
+
],
|
|
1158
|
+
'wdm-a.webex.com' : [
|
|
1022
1159
|
{host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
|
|
1023
|
-
]
|
|
1160
|
+
]
|
|
1024
1161
|
};
|
|
1025
|
-
|
|
1162
|
+
|
|
1026
1163
|
// Act
|
|
1027
1164
|
const clusters = services.getMobiusClusters();
|
|
1028
|
-
|
|
1165
|
+
|
|
1029
1166
|
// Assert
|
|
1030
1167
|
// deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
|
|
1031
1168
|
assert.deepEqual(
|
|
1032
1169
|
clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
|
|
1033
1170
|
[
|
|
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
|
-
},
|
|
1171
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
|
|
1172
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
|
|
1046
1173
|
]
|
|
1047
1174
|
);
|
|
1048
1175
|
});
|
|
@@ -1051,31 +1178,31 @@ describe('webex-core', () => {
|
|
|
1051
1178
|
describe('#isValidHost', () => {
|
|
1052
1179
|
beforeEach(() => {
|
|
1053
1180
|
// 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
|
-
|
|
1181
|
+
services._hostCatalog = {
|
|
1182
|
+
"audit-ci-m.wbx2.com": [
|
|
1183
|
+
{
|
|
1184
|
+
"host": "audit-ci-m.wbx2.com",
|
|
1185
|
+
"ttl": -1,
|
|
1186
|
+
"priority": 5,
|
|
1187
|
+
"id": "urn:IDENTITY:PA61:adminAudit"
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
"host": "audit-ci-m.wbx2.com",
|
|
1191
|
+
"ttl": -1,
|
|
1192
|
+
"priority": 5,
|
|
1193
|
+
"id": "urn:IDENTITY:PA61:adminAuditV2"
|
|
1194
|
+
}
|
|
1195
|
+
],
|
|
1196
|
+
"mercury-connection-partition0-r.wbx2.com": [
|
|
1197
|
+
{
|
|
1198
|
+
"host": "mercury-connection-partition0-r.wbx2.com",
|
|
1199
|
+
"ttl": -1,
|
|
1200
|
+
"priority": 5,
|
|
1201
|
+
"id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
|
|
1202
|
+
}
|
|
1203
|
+
],
|
|
1204
|
+
"empty.com": []
|
|
1205
|
+
};
|
|
1079
1206
|
});
|
|
1080
1207
|
afterAll(() => {
|
|
1081
1208
|
// Clean up the mock host catalog
|
|
@@ -1100,13 +1227,64 @@ describe('webex-core', () => {
|
|
|
1100
1227
|
});
|
|
1101
1228
|
});
|
|
1102
1229
|
|
|
1230
|
+
describe('#isIntegrationEnvironment', () => {
|
|
1231
|
+
it('returns true when u2c URL contains "intb"', () => {
|
|
1232
|
+
services.webex.config = {
|
|
1233
|
+
services: {
|
|
1234
|
+
discovery: {
|
|
1235
|
+
u2c: 'https://u2c-intb.ciscospark.com/u2c/api/v1',
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
};
|
|
1239
|
+
assert.isTrue(services.isIntegrationEnvironment());
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it('returns false when u2c URL does not contain "intb" (production)', () => {
|
|
1243
|
+
services.webex.config = {
|
|
1244
|
+
services: {
|
|
1245
|
+
discovery: {
|
|
1246
|
+
u2c: 'https://u2c.wbx2.com/u2c/api/v1',
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
};
|
|
1250
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it('returns false when u2c URL is for FedRAMP', () => {
|
|
1254
|
+
services.webex.config = {
|
|
1255
|
+
services: {
|
|
1256
|
+
discovery: {
|
|
1257
|
+
u2c: 'https://u2c.gov.ciscospark.com/u2c/api/v1',
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
};
|
|
1261
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('returns false when u2c URL is undefined', () => {
|
|
1265
|
+
services.webex.config = {
|
|
1266
|
+
services: {
|
|
1267
|
+
discovery: {
|
|
1268
|
+
u2c: undefined,
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
};
|
|
1272
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
it('returns false when config is not available', () => {
|
|
1276
|
+
services.webex.config = undefined;
|
|
1277
|
+
assert.isFalse(services.isIntegrationEnvironment());
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1103
1281
|
describe('U2C catalog cache behavior', () => {
|
|
1104
1282
|
let webex;
|
|
1105
1283
|
let services;
|
|
1106
1284
|
let catalog;
|
|
1107
1285
|
let localStorageBackup;
|
|
1108
1286
|
let windowBackup;
|
|
1109
|
-
|
|
1287
|
+
|
|
1110
1288
|
const makeLocalStorageShim = () => {
|
|
1111
1289
|
const store = new Map();
|
|
1112
1290
|
return {
|
|
@@ -1116,16 +1294,13 @@ describe('webex-core', () => {
|
|
|
1116
1294
|
_store: store,
|
|
1117
1295
|
};
|
|
1118
1296
|
};
|
|
1119
|
-
|
|
1297
|
+
|
|
1120
1298
|
beforeEach(() => {
|
|
1121
1299
|
// Build a fresh webex instance
|
|
1122
|
-
webex = new MockWebex({
|
|
1123
|
-
children: {services: Services},
|
|
1124
|
-
config: {credentials: {federation: true}},
|
|
1125
|
-
});
|
|
1300
|
+
webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
|
|
1126
1301
|
services = webex.internal.services;
|
|
1127
1302
|
catalog = services._getCatalog();
|
|
1128
|
-
|
|
1303
|
+
|
|
1129
1304
|
// enable U2C caching feature flag in tests that rely on localStorage writes/reads
|
|
1130
1305
|
services.webex.config = services.webex.config || {};
|
|
1131
1306
|
services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
|
|
@@ -1137,15 +1312,13 @@ describe('webex-core', () => {
|
|
|
1137
1312
|
global.window.localStorage = makeLocalStorageShim();
|
|
1138
1313
|
// Ensure code under test uses our shim via util method
|
|
1139
1314
|
sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
|
|
1140
|
-
|
|
1315
|
+
|
|
1141
1316
|
// 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
|
-
]);
|
|
1317
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1318
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1319
|
+
]);
|
|
1147
1320
|
});
|
|
1148
|
-
|
|
1321
|
+
|
|
1149
1322
|
afterEach(() => {
|
|
1150
1323
|
global.window.localStorage = localStorageBackup || undefined;
|
|
1151
1324
|
if (!windowBackup) {
|
|
@@ -1158,7 +1331,7 @@ describe('webex-core', () => {
|
|
|
1158
1331
|
services._getLocalStorageSafe.restore();
|
|
1159
1332
|
}
|
|
1160
1333
|
});
|
|
1161
|
-
|
|
1334
|
+
|
|
1162
1335
|
it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
|
|
1163
1336
|
// Arrange: authenticated credentials and spies
|
|
1164
1337
|
services.webex.credentials = {
|
|
@@ -1170,20 +1343,10 @@ describe('webex-core', () => {
|
|
|
1170
1343
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1171
1344
|
const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
|
|
1172
1345
|
// 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
|
|
1346
|
+
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
1347
|
+
// Cause ready callback to run immediately
|
|
1185
1348
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1186
|
-
if (event === '
|
|
1349
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1187
1350
|
});
|
|
1188
1351
|
|
|
1189
1352
|
// Act
|
|
@@ -1223,9 +1386,9 @@ describe('webex-core', () => {
|
|
|
1223
1386
|
|
|
1224
1387
|
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
1225
1388
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1226
|
-
// Cause
|
|
1389
|
+
// Cause ready callback to run immediately
|
|
1227
1390
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1228
|
-
if (event === '
|
|
1391
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1229
1392
|
});
|
|
1230
1393
|
|
|
1231
1394
|
// Act
|
|
@@ -1233,18 +1396,9 @@ describe('webex-core', () => {
|
|
|
1233
1396
|
await waitForAsync();
|
|
1234
1397
|
|
|
1235
1398
|
// 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
|
-
);
|
|
1399
|
+
assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
|
|
1400
|
+
assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
|
|
1401
|
+
assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
|
|
1248
1402
|
assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
|
|
1249
1403
|
|
|
1250
1404
|
// Cleanup
|
|
@@ -1260,29 +1414,26 @@ describe('webex-core', () => {
|
|
|
1260
1414
|
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1261
1415
|
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1262
1416
|
};
|
|
1263
|
-
|
|
1417
|
+
|
|
1264
1418
|
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
|
|
1265
|
-
|
|
1419
|
+
|
|
1266
1420
|
const warmed = await services._loadCatalogFromCache();
|
|
1267
|
-
|
|
1421
|
+
|
|
1268
1422
|
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
|
-
);
|
|
1423
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
|
|
1273
1424
|
assert.isFalse(catalog.status.preauth.ready);
|
|
1274
1425
|
assert.isFalse(catalog.status.postauth.ready);
|
|
1275
1426
|
});
|
|
1276
|
-
|
|
1427
|
+
|
|
1277
1428
|
it('clearCatalogCache() removes the cached entry', async () => {
|
|
1278
1429
|
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1279
1430
|
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
|
|
1280
|
-
|
|
1431
|
+
|
|
1281
1432
|
await services.clearCatalogCache();
|
|
1282
|
-
|
|
1433
|
+
|
|
1283
1434
|
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
|
|
1284
1435
|
});
|
|
1285
|
-
|
|
1436
|
+
|
|
1286
1437
|
it('still fetches when forceRefresh=true even if ready', async () => {
|
|
1287
1438
|
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1288
1439
|
window.localStorage.setItem(
|
|
@@ -1294,24 +1445,20 @@ describe('webex-core', () => {
|
|
|
1294
1445
|
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1295
1446
|
})
|
|
1296
1447
|
);
|
|
1297
|
-
|
|
1448
|
+
|
|
1298
1449
|
// warm from cache
|
|
1299
1450
|
const warmed = await services._loadCatalogFromCache();
|
|
1300
1451
|
assert.isTrue(warmed);
|
|
1301
1452
|
assert.isTrue(catalog.status.preauth.ready);
|
|
1302
1453
|
assert.isTrue(catalog.status.postauth.ready);
|
|
1303
|
-
|
|
1454
|
+
|
|
1304
1455
|
const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
|
|
1305
|
-
|
|
1456
|
+
|
|
1306
1457
|
// 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
|
-
});
|
|
1458
|
+
await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
|
|
1312
1459
|
// pass an empty query to avoid spreading undefined in qs construction
|
|
1313
1460
|
await services.updateServices({forceRefresh: true});
|
|
1314
|
-
|
|
1461
|
+
|
|
1315
1462
|
assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
|
|
1316
1463
|
fetchSpy.restore();
|
|
1317
1464
|
});
|
|
@@ -1372,11 +1519,9 @@ describe('webex-core', () => {
|
|
|
1372
1519
|
);
|
|
1373
1520
|
// formatter returns at least one entry to mark ready
|
|
1374
1521
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1375
|
-
sinon
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1379
|
-
]);
|
|
1522
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1523
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1524
|
+
]);
|
|
1380
1525
|
|
|
1381
1526
|
const warmed = await services._loadCatalogFromCache();
|
|
1382
1527
|
assert.isTrue(warmed);
|
|
@@ -1398,11 +1543,9 @@ describe('webex-core', () => {
|
|
|
1398
1543
|
})
|
|
1399
1544
|
);
|
|
1400
1545
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1401
|
-
sinon
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1405
|
-
]);
|
|
1546
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1547
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1548
|
+
]);
|
|
1406
1549
|
|
|
1407
1550
|
const warmed = await services._loadCatalogFromCache();
|
|
1408
1551
|
// function returns true if overall cache path succeeded; we only verify group readiness
|
|
@@ -1429,17 +1572,12 @@ describe('webex-core', () => {
|
|
|
1429
1572
|
})
|
|
1430
1573
|
);
|
|
1431
1574
|
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1432
|
-
sinon
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1436
|
-
]);
|
|
1575
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1576
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1577
|
+
]);
|
|
1437
1578
|
|
|
1438
1579
|
await services._loadCatalogFromCache();
|
|
1439
|
-
assert.isFalse(
|
|
1440
|
-
catalog.status.preauth.ready,
|
|
1441
|
-
'preauth should not warm on selection mismatch'
|
|
1442
|
-
);
|
|
1580
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
|
|
1443
1581
|
});
|
|
1444
1582
|
|
|
1445
1583
|
it('skips warming when environment fingerprint mismatches', async () => {
|
|
@@ -1450,10 +1588,7 @@ describe('webex-core', () => {
|
|
|
1450
1588
|
JSON.stringify({
|
|
1451
1589
|
cachedAt: Date.now(),
|
|
1452
1590
|
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
|
-
},
|
|
1591
|
+
preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
|
|
1457
1592
|
})
|
|
1458
1593
|
);
|
|
1459
1594
|
// current env
|
|
@@ -1468,6 +1603,7 @@ describe('webex-core', () => {
|
|
|
1468
1603
|
assert.isFalse(catalog.status.postauth.ready);
|
|
1469
1604
|
});
|
|
1470
1605
|
});
|
|
1606
|
+
|
|
1471
1607
|
});
|
|
1472
1608
|
});
|
|
1473
1609
|
/* eslint-enable no-underscore-dangle */
|