@webex/webex-core 3.11.0 → 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 +217 -31
- 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 +221 -30
- 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 +192 -17
- package/src/lib/services-v2/service-catalog.ts +6 -11
- package/src/lib/services-v2/services-v2.ts +195 -17
- package/test/fixtures/activation-email.ts +22 -0
- package/test/integration/spec/services/services.js +44 -8
- package/test/integration/spec/services-v2/services-v2.js +44 -8
- 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 +358 -2
- package/test/unit/spec/services-v2/service-catalog.ts +93 -11
- package/test/unit/spec/services-v2/services-v2.ts +362 -0
|
@@ -101,7 +101,24 @@ describe('webex-core', () => {
|
|
|
101
101
|
const domains = [];
|
|
102
102
|
|
|
103
103
|
beforeEach(() => {
|
|
104
|
-
|
|
104
|
+
// Shaped like a real catalog rather than a tidy list: the commercial
|
|
105
|
+
// domains the sdk ships with, a multi-part suffix, sites added at
|
|
106
|
+
// runtime from meeting preferences, an entry that overlaps another,
|
|
107
|
+
// and an unset entry, which callers can add and which must match
|
|
108
|
+
// nothing.
|
|
109
|
+
domains.push(
|
|
110
|
+
'wbx2.com',
|
|
111
|
+
'ciscospark.com',
|
|
112
|
+
'webex.com',
|
|
113
|
+
'webexapis.com',
|
|
114
|
+
'broadcloud.com.au',
|
|
115
|
+
'webexgov.us',
|
|
116
|
+
'example-a.com',
|
|
117
|
+
'example-b.com',
|
|
118
|
+
'example-c.com',
|
|
119
|
+
'go.example-a.com',
|
|
120
|
+
''
|
|
121
|
+
);
|
|
105
122
|
|
|
106
123
|
catalog.setAllowedDomains(domains);
|
|
107
124
|
});
|
|
@@ -110,10 +127,60 @@ describe('webex-core', () => {
|
|
|
110
127
|
domains.length = 0;
|
|
111
128
|
});
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
[
|
|
131
|
+
// real service hosts resolve to the entry that covers them
|
|
132
|
+
['https://u2c-a.wbx2.com/u2c/api/v1/limited/catalog', 'wbx2.com'],
|
|
133
|
+
['https://conv-a.wbx2.com/conversation/api/v1/conversations', 'wbx2.com'],
|
|
134
|
+
['https://foobar.ciscospark.com/resource/id', 'ciscospark.com'],
|
|
135
|
+
['https://idbroker.webex.com/idb/token', 'webex.com'],
|
|
136
|
+
['https://webexapis.com/v1/people/me', 'webexapis.com'],
|
|
137
|
+
// an entry is not required to be a bare registrable domain
|
|
138
|
+
['https://foo.broadcloud.com.au/resource/id', 'broadcloud.com.au'],
|
|
139
|
+
['https://a.b.webexgov.us/resource/id', 'webexgov.us'],
|
|
140
|
+
// the domain itself and its subdomains match
|
|
141
|
+
['https://example-a.com/resource/id', 'example-a.com'],
|
|
142
|
+
['https://sub.example-b.com/resource/id', 'example-b.com'],
|
|
143
|
+
['https://deep.sub.example-c.com/resource/id', 'example-c.com'],
|
|
144
|
+
// overlapping entries resolve to the first covering entry in the list
|
|
145
|
+
['https://go.example-a.com/resource/id', 'example-a.com'],
|
|
146
|
+
// an explicit port must not defeat the match
|
|
147
|
+
['https://example-a.com:8443/resource/id', 'example-a.com'],
|
|
148
|
+
['https://u2c-a.wbx2.com:8000/resource/id', 'wbx2.com'],
|
|
149
|
+
// hostname comparison is case insensitive
|
|
150
|
+
['https://U2C-A.WBX2.COM/resource/id', 'wbx2.com'],
|
|
151
|
+
// a trailing dot is the same name
|
|
152
|
+
['https://u2c-a.wbx2.com./resource/id', 'wbx2.com'],
|
|
153
|
+
// a sibling registrable domain must not match
|
|
154
|
+
['https://notwebex.com/resource/id', undefined],
|
|
155
|
+
['https://mywebexgov.us/resource/id', undefined],
|
|
156
|
+
['https://webex.company/resource/id', undefined],
|
|
157
|
+
['https://webex.com-unrelated.example/resource/id', undefined],
|
|
158
|
+
['https://example-a.community/resource/id', undefined],
|
|
159
|
+
// a partial label must not match, even against a multi-part entry
|
|
160
|
+
['https://broadcloud.com/resource/id', undefined],
|
|
161
|
+
// the domain matches only as a suffix, on a label boundary
|
|
162
|
+
['https://webex.com.unrelated.example/resource/id', undefined],
|
|
163
|
+
['https://unrelated.example/webex.com/resource/id', undefined],
|
|
164
|
+
['https://unrelated.example/?next=https://webex.com', undefined],
|
|
165
|
+
// userinfo is not the host
|
|
166
|
+
['https://webex.com@unrelated.example/resource/id', undefined],
|
|
167
|
+
// an encoded or unusual separator is not a label boundary, and the url
|
|
168
|
+
// parsers used by the transports do not agree on where these end the
|
|
169
|
+
// host, so they must not resolve to an allowed domain
|
|
170
|
+
['https://webex.com%2eunrelated.example/resource/id', undefined],
|
|
171
|
+
['https://webex.com%2Eunrelated.example/resource/id', undefined],
|
|
172
|
+
['https://unrelated.example%2ewebex.com/resource/id', undefined],
|
|
173
|
+
['https://unrelated.example%2Ewebex.com/resource/id', undefined],
|
|
174
|
+
['https://unrelated.example;.webex.com/resource/id', undefined],
|
|
175
|
+
// an empty allowed domain entry matches nothing
|
|
176
|
+
['https://unrelated.example/resource/id', undefined],
|
|
177
|
+
// unparseable urls
|
|
178
|
+
['', undefined],
|
|
179
|
+
['not a url', undefined],
|
|
180
|
+
].forEach(([url, expected]) => {
|
|
181
|
+
it(`returns ${expected} for ${url || '<empty>'}`, () => {
|
|
182
|
+
assert.equal(catalog.findAllowedDomain(url), expected);
|
|
183
|
+
});
|
|
117
184
|
});
|
|
118
185
|
});
|
|
119
186
|
|
|
@@ -121,7 +188,7 @@ describe('webex-core', () => {
|
|
|
121
188
|
const domains = [];
|
|
122
189
|
|
|
123
190
|
beforeEach(() => {
|
|
124
|
-
domains.push('example-a', 'example-b', 'example-c');
|
|
191
|
+
domains.push('example-a.com', 'example-b.com', 'example-c.com');
|
|
125
192
|
|
|
126
193
|
catalog.setAllowedDomains(domains);
|
|
127
194
|
});
|
|
@@ -160,7 +227,7 @@ describe('webex-core', () => {
|
|
|
160
227
|
const domains = [];
|
|
161
228
|
|
|
162
229
|
beforeEach(() => {
|
|
163
|
-
domains.push('example-a', 'example-b', 'example-c');
|
|
230
|
+
domains.push('example-a.com', 'example-b.com', 'example-c.com');
|
|
164
231
|
|
|
165
232
|
catalog.setAllowedDomains(domains);
|
|
166
233
|
});
|
|
@@ -170,19 +237,31 @@ describe('webex-core', () => {
|
|
|
170
237
|
});
|
|
171
238
|
|
|
172
239
|
it('sets the allowed domain entries to new values', () => {
|
|
173
|
-
const newValues = ['example-d', 'example-e', 'example-f'];
|
|
240
|
+
const newValues = ['example-d.com', 'example-e.com', 'example-f.com'];
|
|
174
241
|
|
|
175
242
|
catalog.setAllowedDomains(newValues);
|
|
176
243
|
|
|
177
244
|
assert.notDeepInclude(domains, newValues);
|
|
178
245
|
});
|
|
246
|
+
|
|
247
|
+
it('canonicalizes entries and discards those that are not usable', () => {
|
|
248
|
+
catalog.setAllowedDomains([
|
|
249
|
+
'Example-D.COM',
|
|
250
|
+
'example-d.com',
|
|
251
|
+
'example-e.com.',
|
|
252
|
+
'',
|
|
253
|
+
undefined,
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
assert.deepEqual(catalog.getAllowedDomains(), ['example-d.com', 'example-e.com']);
|
|
257
|
+
});
|
|
179
258
|
});
|
|
180
259
|
|
|
181
260
|
describe('#addAllowedDomains()', () => {
|
|
182
261
|
const domains = [];
|
|
183
262
|
|
|
184
263
|
beforeEach(() => {
|
|
185
|
-
domains.push('example-a', 'example-b', 'example-c');
|
|
264
|
+
domains.push('example-a.com', 'example-b.com', 'example-c.com');
|
|
186
265
|
|
|
187
266
|
catalog.setAllowedDomains(domains);
|
|
188
267
|
});
|
|
@@ -192,13 +271,16 @@ describe('webex-core', () => {
|
|
|
192
271
|
});
|
|
193
272
|
|
|
194
273
|
it('merge the allowed domain entries with new values', () => {
|
|
195
|
-
const newValues = ['example-c', 'example-e', 'example-f'];
|
|
274
|
+
const newValues = ['example-c.com', 'example-e.com', 'example-f.com'];
|
|
196
275
|
|
|
197
276
|
catalog.addAllowedDomains(newValues);
|
|
198
277
|
|
|
199
278
|
const list = catalog.getAllowedDomains();
|
|
200
279
|
|
|
201
|
-
assert.match(
|
|
280
|
+
assert.match(
|
|
281
|
+
['example-a.com', 'example-b.com', 'example-c.com', 'example-e.com', 'example-f.com'],
|
|
282
|
+
list
|
|
283
|
+
);
|
|
202
284
|
});
|
|
203
285
|
});
|
|
204
286
|
|
|
@@ -46,6 +46,7 @@ describe('webex-core', () => {
|
|
|
46
46
|
services.initialize();
|
|
47
47
|
|
|
48
48
|
// call the onReady callback
|
|
49
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
49
50
|
services.listenToOnce.getCall(1).args[2]();
|
|
50
51
|
await waitForAsync();
|
|
51
52
|
|
|
@@ -59,6 +60,7 @@ describe('webex-core', () => {
|
|
|
59
60
|
services.initialize();
|
|
60
61
|
|
|
61
62
|
// call the onReady callback
|
|
63
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
62
64
|
services.listenToOnce.getCall(1).args[2]();
|
|
63
65
|
await waitForAsync();
|
|
64
66
|
|
|
@@ -81,6 +83,7 @@ describe('webex-core', () => {
|
|
|
81
83
|
services.initialize();
|
|
82
84
|
|
|
83
85
|
// call the onReady callback
|
|
86
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
84
87
|
services.listenToOnce.getCall(1).args[2]();
|
|
85
88
|
|
|
86
89
|
await waitForAsync();
|
|
@@ -112,6 +115,7 @@ describe('webex-core', () => {
|
|
|
112
115
|
services.initialize();
|
|
113
116
|
|
|
114
117
|
// call the onReady callback
|
|
118
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
115
119
|
services.listenToOnce.getCall(1).args[2]();
|
|
116
120
|
|
|
117
121
|
await waitForAsync();
|
|
@@ -122,6 +126,307 @@ describe('webex-core', () => {
|
|
|
122
126
|
`services: failed to init initial services when credentials available, ${expectedMessage}`
|
|
123
127
|
);
|
|
124
128
|
});
|
|
129
|
+
|
|
130
|
+
it('sets services.ready to true when waitForCatalogInit is disabled (default)', () => {
|
|
131
|
+
services.listenToOnce = sinon.stub();
|
|
132
|
+
services.initialize();
|
|
133
|
+
|
|
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]();
|
|
137
|
+
|
|
138
|
+
assert.isTrue(services.ready, 'services.ready should be true so it does not block webex.ready');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('listens on the "ready" event when waitForCatalogInit is disabled (default)', () => {
|
|
142
|
+
services.listenToOnce = sinon.stub();
|
|
143
|
+
services.initialize();
|
|
144
|
+
|
|
145
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
146
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
147
|
+
|
|
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
|
+
});
|
|
153
|
+
|
|
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
|
+
});
|
|
166
|
+
|
|
167
|
+
it('leaves services.ready=false after initialize (does not flip until finalize)', () => {
|
|
168
|
+
services.listenToOnce = sinon.stub();
|
|
169
|
+
services.initialize();
|
|
170
|
+
|
|
171
|
+
assert.isFalse(services.ready, 'services.ready should block webex.ready until finalize');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('listens on the "loaded" event (not "ready") to avoid deadlock', () => {
|
|
175
|
+
services.listenToOnce = sinon.stub();
|
|
176
|
+
services.initialize();
|
|
177
|
+
|
|
178
|
+
// Fire change:config to trigger the mode-specific listener registration.
|
|
179
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
180
|
+
|
|
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
|
+
});
|
|
185
|
+
|
|
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
|
+
};
|
|
192
|
+
|
|
193
|
+
services.initialize();
|
|
194
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
195
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
196
|
+
await waitForAsync();
|
|
197
|
+
|
|
198
|
+
assert.isTrue(services.ready);
|
|
199
|
+
});
|
|
200
|
+
|
|
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());
|
|
204
|
+
|
|
205
|
+
services.initialize();
|
|
206
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
207
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
208
|
+
await waitForAsync();
|
|
209
|
+
|
|
210
|
+
assert.isTrue(services.ready);
|
|
211
|
+
});
|
|
212
|
+
|
|
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();
|
|
220
|
+
|
|
221
|
+
services.initialize();
|
|
222
|
+
services.listenToOnce.getCall(0).args[2]();
|
|
223
|
+
services.listenToOnce.getCall(1).args[2]();
|
|
224
|
+
await waitForAsync();
|
|
225
|
+
|
|
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);
|
|
429
|
+
});
|
|
125
430
|
});
|
|
126
431
|
|
|
127
432
|
describe('#initServiceCatalogs', () => {
|
|
@@ -922,6 +1227,57 @@ describe('webex-core', () => {
|
|
|
922
1227
|
});
|
|
923
1228
|
});
|
|
924
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
|
+
|
|
925
1281
|
describe('U2C catalog cache behavior', () => {
|
|
926
1282
|
let webex;
|
|
927
1283
|
let services;
|
|
@@ -990,7 +1346,7 @@ describe('webex-core', () => {
|
|
|
990
1346
|
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
991
1347
|
// Cause ready callback to run immediately
|
|
992
1348
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
993
|
-
if (event === 'ready') cb();
|
|
1349
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
994
1350
|
});
|
|
995
1351
|
|
|
996
1352
|
// Act
|
|
@@ -1032,7 +1388,7 @@ describe('webex-core', () => {
|
|
|
1032
1388
|
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1033
1389
|
// Cause ready callback to run immediately
|
|
1034
1390
|
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1035
|
-
if (event === 'ready') cb();
|
|
1391
|
+
if (event === 'change:config' || event === 'ready') cb();
|
|
1036
1392
|
});
|
|
1037
1393
|
|
|
1038
1394
|
// Act
|