@webex/webex-core 3.10.0 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/auth.js +6 -8
- package/dist/interceptors/auth.js.map +1 -1
- package/dist/interceptors/default-options.js +6 -8
- package/dist/interceptors/default-options.js.map +1 -1
- package/dist/interceptors/embargo.js +6 -8
- package/dist/interceptors/embargo.js.map +1 -1
- package/dist/interceptors/network-timing.js +6 -8
- package/dist/interceptors/network-timing.js.map +1 -1
- package/dist/interceptors/payload-transformer.js +6 -8
- package/dist/interceptors/payload-transformer.js.map +1 -1
- package/dist/interceptors/proxy.js +7 -10
- package/dist/interceptors/proxy.js.map +1 -1
- package/dist/interceptors/rate-limit.js +7 -10
- package/dist/interceptors/rate-limit.js.map +1 -1
- package/dist/interceptors/redirect.js +9 -8
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/interceptors/request-event.js +6 -8
- package/dist/interceptors/request-event.js.map +1 -1
- package/dist/interceptors/request-logger.js +6 -8
- package/dist/interceptors/request-logger.js.map +1 -1
- package/dist/interceptors/request-timing.js +6 -8
- package/dist/interceptors/request-timing.js.map +1 -1
- package/dist/interceptors/response-logger.js +6 -8
- package/dist/interceptors/response-logger.js.map +1 -1
- package/dist/interceptors/user-agent.js +8 -11
- package/dist/interceptors/user-agent.js.map +1 -1
- package/dist/interceptors/webex-tracking-id.js +6 -8
- package/dist/interceptors/webex-tracking-id.js.map +1 -1
- package/dist/interceptors/webex-user-agent.js +7 -10
- package/dist/interceptors/webex-user-agent.js.map +1 -1
- package/dist/lib/batcher.js +1 -1
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/credentials/credentials.js +4 -6
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/grant-errors.js +18 -26
- package/dist/lib/credentials/grant-errors.js.map +1 -1
- package/dist/lib/credentials/index.js.map +1 -1
- package/dist/lib/credentials/scope.js.map +1 -1
- package/dist/lib/credentials/token-collection.js.map +1 -1
- package/dist/lib/credentials/token.js +5 -5
- package/dist/lib/credentials/token.js.map +1 -1
- package/dist/lib/interceptors/hostmap.js +6 -8
- package/dist/lib/interceptors/hostmap.js.map +1 -1
- package/dist/lib/interceptors/server-error.js +6 -8
- package/dist/lib/interceptors/server-error.js.map +1 -1
- package/dist/lib/interceptors/service.js +6 -8
- package/dist/lib/interceptors/service.js.map +1 -1
- package/dist/lib/metrics.js.map +1 -1
- package/dist/lib/page.js +5 -6
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/services/index.js.map +1 -1
- package/dist/lib/services/service-catalog.js +3 -3
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/service-fed-ramp.js.map +1 -1
- package/dist/lib/services/service-host.js +1 -2
- package/dist/lib/services/service-host.js.map +1 -1
- package/dist/lib/services/service-registry.js +1 -2
- package/dist/lib/services/service-registry.js.map +1 -1
- package/dist/lib/services/service-state.js +1 -2
- package/dist/lib/services/service-state.js.map +1 -1
- package/dist/lib/services/service-url.js +11 -1
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +485 -127
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/services-v2/index.js.map +1 -1
- package/dist/lib/services-v2/metrics.js.map +1 -1
- package/dist/lib/services-v2/service-catalog.js +7 -7
- package/dist/lib/services-v2/service-catalog.js.map +1 -1
- package/dist/lib/services-v2/service-detail.js.map +1 -1
- package/dist/lib/services-v2/service-fed-ramp.js.map +1 -1
- package/dist/lib/services-v2/services-v2.js +379 -51
- package/dist/lib/services-v2/services-v2.js.map +1 -1
- package/dist/lib/services-v2/types.js.map +1 -1
- package/dist/lib/stateless-webex-plugin.js +3 -4
- package/dist/lib/stateless-webex-plugin.js.map +1 -1
- package/dist/lib/storage/decorators.js.map +1 -1
- package/dist/lib/storage/errors.js +7 -9
- package/dist/lib/storage/errors.js.map +1 -1
- package/dist/lib/storage/index.js.map +1 -1
- package/dist/lib/storage/make-webex-plugin-store.js +14 -5
- package/dist/lib/storage/make-webex-plugin-store.js.map +1 -1
- package/dist/lib/storage/make-webex-store.js +13 -5
- package/dist/lib/storage/make-webex-store.js.map +1 -1
- package/dist/lib/storage/memory-store-adapter.js.map +1 -1
- package/dist/lib/webex-core-plugin-mixin.js.map +1 -1
- package/dist/lib/webex-http-error.js +8 -11
- package/dist/lib/webex-http-error.js.map +1 -1
- package/dist/lib/webex-internal-core-plugin-mixin.js.map +1 -1
- package/dist/lib/webex-plugin.js.map +1 -1
- package/dist/plugins/logger.js +1 -1
- package/dist/plugins/logger.js.map +1 -1
- package/dist/webex-core.js +11 -11
- package/dist/webex-core.js.map +1 -1
- package/dist/webex-internal-core.js.map +1 -1
- package/package.json +13 -13
- package/src/config.js +15 -0
- package/src/interceptors/redirect.js +4 -0
- package/src/lib/services/service-url.js +9 -1
- package/src/lib/services/services.js +315 -7
- package/src/lib/services-v2/index.ts +0 -1
- package/src/lib/services-v2/service-catalog.ts +4 -4
- package/src/lib/services-v2/services-v2.ts +307 -7
- package/src/lib/services-v2/types.ts +13 -0
- package/test/fixtures/host-catalog-v2.ts +1 -1
- package/test/integration/spec/services/service-catalog.js +10 -4
- package/test/integration/spec/services/services.js +65 -9
- package/test/integration/spec/services-v2/service-catalog.js +2 -2
- package/test/integration/spec/services-v2/services-v2.js +56 -6
- package/test/unit/spec/interceptors/redirect.js +98 -0
- package/test/unit/spec/services/service-url.js +110 -0
- package/test/unit/spec/services/services.js +411 -2
- package/test/unit/spec/services-v2/services-v2.ts +316 -0
|
@@ -168,6 +168,104 @@ describe('webex-core', () => {
|
|
|
168
168
|
uri: 'http://newlocus.example.com',
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
|
+
|
|
172
|
+
it('removes authorization header when redirecting preJoin request to webex-appapi-service', () => {
|
|
173
|
+
const response = {
|
|
174
|
+
statusCode: 404,
|
|
175
|
+
headers: {},
|
|
176
|
+
body: {
|
|
177
|
+
code: 404100,
|
|
178
|
+
data: {
|
|
179
|
+
siteFullUrl: 'newlocus.example.com'
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const options = {
|
|
185
|
+
$redirectCount: 0,
|
|
186
|
+
uri: 'https://test.webex.com/meet/v1/preJoin',
|
|
187
|
+
resource: 'preJoin',
|
|
188
|
+
service: 'webex-appapi-service',
|
|
189
|
+
headers: {
|
|
190
|
+
authorization: 'Bearer token123',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
interceptor.onResponse(options, response);
|
|
195
|
+
sinon.assert.calledWith(webex.request, sinon.match({
|
|
196
|
+
$redirectCount: 1,
|
|
197
|
+
uri: 'https://newlocus.example.com/meet/v1/preJoin',
|
|
198
|
+
resource: 'preJoin',
|
|
199
|
+
service: 'webex-appapi-service',
|
|
200
|
+
headers: {
|
|
201
|
+
authorization: false,
|
|
202
|
+
},
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('keeps authorization header for non-preJoin requests on appapi redirect', () => {
|
|
207
|
+
const response = {
|
|
208
|
+
statusCode: 404,
|
|
209
|
+
headers: {},
|
|
210
|
+
body: {
|
|
211
|
+
code: 404100,
|
|
212
|
+
data: {
|
|
213
|
+
siteFullUrl: 'newlocus.example.com'
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const options = {
|
|
219
|
+
$redirectCount: 0,
|
|
220
|
+
uri: 'https://test.webex.com/meet/v1/join',
|
|
221
|
+
resource: 'join',
|
|
222
|
+
service: 'webex-appapi-service',
|
|
223
|
+
headers: {
|
|
224
|
+
authorization: 'Bearer token123',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
interceptor.onResponse(options, response);
|
|
229
|
+
sinon.assert.calledWith(webex.request, sinon.match({
|
|
230
|
+
$redirectCount: 1,
|
|
231
|
+
uri: 'https://newlocus.example.com/meet/v1/join',
|
|
232
|
+
headers: {
|
|
233
|
+
authorization: 'Bearer token123',
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('keeps authorization header for preJoin requests to non-webex-appapi-service', () => {
|
|
239
|
+
const response = {
|
|
240
|
+
statusCode: 404,
|
|
241
|
+
headers: {},
|
|
242
|
+
body: {
|
|
243
|
+
code: 404100,
|
|
244
|
+
data: {
|
|
245
|
+
siteFullUrl: 'newlocus.example.com'
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const options = {
|
|
251
|
+
$redirectCount: 0,
|
|
252
|
+
uri: 'https://test.webex.com/meet/v1/preJoin',
|
|
253
|
+
resource: 'preJoin',
|
|
254
|
+
service: 'other-service',
|
|
255
|
+
headers: {
|
|
256
|
+
authorization: 'Bearer token123',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
interceptor.onResponse(options, response);
|
|
261
|
+
sinon.assert.calledWith(webex.request, sinon.match({
|
|
262
|
+
$redirectCount: 1,
|
|
263
|
+
uri: 'https://newlocus.example.com/meet/v1/preJoin',
|
|
264
|
+
headers: {
|
|
265
|
+
authorization: 'Bearer token123',
|
|
266
|
+
},
|
|
267
|
+
}));
|
|
268
|
+
});
|
|
171
269
|
});
|
|
172
270
|
});
|
|
173
271
|
});
|
|
@@ -151,6 +151,116 @@ describe('webex-core', () => {
|
|
|
151
151
|
|
|
152
152
|
assert.isTrue(homeClusterUrls.every((host) => !host.failed));
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
describe('when hosts have negative priorities', () => {
|
|
156
|
+
it('should return defaultUrl when all hosts have negative priorities', () => {
|
|
157
|
+
const negativeServiceUrl = new ServiceUrl({
|
|
158
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
159
|
+
hosts: [
|
|
160
|
+
{
|
|
161
|
+
host: 'example-host-neg1.com',
|
|
162
|
+
priority: -1,
|
|
163
|
+
ttl: -1,
|
|
164
|
+
id: '1',
|
|
165
|
+
homeCluster: true,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
host: 'example-host-neg2.com',
|
|
169
|
+
priority: -1,
|
|
170
|
+
ttl: -1,
|
|
171
|
+
id: '2',
|
|
172
|
+
homeCluster: true,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
name: 'negative-priority-test',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.equal(
|
|
179
|
+
negativeServiceUrl._getPriorityHostUrl(),
|
|
180
|
+
'https://default.example.com/api/v1'
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return defaultUrl when all hosts have zero priority', () => {
|
|
185
|
+
const zeroServiceUrl = new ServiceUrl({
|
|
186
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
187
|
+
hosts: [
|
|
188
|
+
{
|
|
189
|
+
host: 'example-host-zero.com',
|
|
190
|
+
priority: 0,
|
|
191
|
+
ttl: -1,
|
|
192
|
+
id: '1',
|
|
193
|
+
homeCluster: true,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
name: 'zero-priority-test',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
assert.equal(zeroServiceUrl._getPriorityHostUrl(), 'https://default.example.com/api/v1');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should ignore hosts with negative priorities and return valid host', () => {
|
|
203
|
+
const mixedServiceUrl = new ServiceUrl({
|
|
204
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
205
|
+
hosts: [
|
|
206
|
+
{
|
|
207
|
+
host: 'example-host-neg.com',
|
|
208
|
+
priority: -1,
|
|
209
|
+
ttl: -1,
|
|
210
|
+
id: '1',
|
|
211
|
+
homeCluster: true,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
host: 'example-host-valid.com',
|
|
215
|
+
priority: 5,
|
|
216
|
+
ttl: -1,
|
|
217
|
+
id: '2',
|
|
218
|
+
homeCluster: true,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
name: 'mixed-priority-test',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = mixedServiceUrl._getPriorityHostUrl();
|
|
225
|
+
|
|
226
|
+
assert.include(result, 'example-host-valid.com');
|
|
227
|
+
assert.notInclude(result, 'example-host-neg.com');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should select lowest positive priority host when mixed with negative priorities', () => {
|
|
231
|
+
const mixedServiceUrl = new ServiceUrl({
|
|
232
|
+
defaultUrl: 'https://default.example.com/api/v1',
|
|
233
|
+
hosts: [
|
|
234
|
+
{
|
|
235
|
+
host: 'example-host-neg.com',
|
|
236
|
+
priority: -1,
|
|
237
|
+
ttl: -1,
|
|
238
|
+
id: '1',
|
|
239
|
+
homeCluster: true,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
host: 'example-host-p5.com',
|
|
243
|
+
priority: 5,
|
|
244
|
+
ttl: -1,
|
|
245
|
+
id: '2',
|
|
246
|
+
homeCluster: true,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
host: 'example-host-p2.com',
|
|
250
|
+
priority: 2,
|
|
251
|
+
ttl: -1,
|
|
252
|
+
id: '3',
|
|
253
|
+
homeCluster: true,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
name: 'mixed-priority-test',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const result = mixedServiceUrl._getPriorityHostUrl();
|
|
260
|
+
|
|
261
|
+
assert.include(result, 'example-host-p2.com');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
154
264
|
});
|
|
155
265
|
|
|
156
266
|
describe('#failHost()', () => {
|
|
@@ -349,8 +349,6 @@ describe('webex-core', () => {
|
|
|
349
349
|
|
|
350
350
|
const mapResult = await services._fetchNewServiceHostmap({from: 'limited'});
|
|
351
351
|
|
|
352
|
-
assert.deepEqual(mapResult, mapResponse);
|
|
353
|
-
|
|
354
352
|
assert.calledOnceWithExactly(services.request, {
|
|
355
353
|
method: 'GET',
|
|
356
354
|
service: 'u2c',
|
|
@@ -839,6 +837,417 @@ describe('webex-core', () => {
|
|
|
839
837
|
assert.equal(webex.config.credentials.authorizeUrl, authUrl);
|
|
840
838
|
});
|
|
841
839
|
});
|
|
840
|
+
|
|
841
|
+
describe('#getMobiusClusters', () => {
|
|
842
|
+
it('returns unique mobius host entries from hostCatalog', () => {
|
|
843
|
+
// Arrange: two hostCatalog keys, with duplicate mobius host across keys
|
|
844
|
+
services._hostCatalog = {
|
|
845
|
+
'mobius-us-east-2.prod.infra.webex.com': [
|
|
846
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:mobius'},
|
|
847
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', ttl: -1, priority: 10, id: 'urn:TEAM:xyz:mobius'},
|
|
848
|
+
],
|
|
849
|
+
|
|
850
|
+
'mobius-eu-central-1.prod.infra.webex.com': [
|
|
851
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', ttl: -1, priority: 7, id: 'urn:TEAM:xyz:mobius'}, // duplicate host
|
|
852
|
+
],
|
|
853
|
+
'wdm-a.webex.com' : [
|
|
854
|
+
{host: 'wdm-a.webex.com', ttl: -1, priority: 5, id: 'urn:TEAM:xyz:wdm'},
|
|
855
|
+
]
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// Act
|
|
859
|
+
const clusters = services.getMobiusClusters();
|
|
860
|
+
|
|
861
|
+
// Assert
|
|
862
|
+
// deduped; only mobius entries; keeps first seen mobius-a, then mobius-b
|
|
863
|
+
assert.deepEqual(
|
|
864
|
+
clusters.map(({host, id, ttl, priority}) => ({host, id, ttl, priority})),
|
|
865
|
+
[
|
|
866
|
+
{host: 'mobius-us-east-2.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 5},
|
|
867
|
+
{host: 'mobius-eu-central-1.prod.infra.webex.com', id: 'urn:TEAM:xyz:mobius', ttl: -1, priority: 10},
|
|
868
|
+
]
|
|
869
|
+
);
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
describe('#isValidHost', () => {
|
|
874
|
+
beforeEach(() => {
|
|
875
|
+
// Setting up a mock host catalog
|
|
876
|
+
services._hostCatalog = {
|
|
877
|
+
"audit-ci-m.wbx2.com": [
|
|
878
|
+
{
|
|
879
|
+
"host": "audit-ci-m.wbx2.com",
|
|
880
|
+
"ttl": -1,
|
|
881
|
+
"priority": 5,
|
|
882
|
+
"id": "urn:IDENTITY:PA61:adminAudit"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
"host": "audit-ci-m.wbx2.com",
|
|
886
|
+
"ttl": -1,
|
|
887
|
+
"priority": 5,
|
|
888
|
+
"id": "urn:IDENTITY:PA61:adminAuditV2"
|
|
889
|
+
}
|
|
890
|
+
],
|
|
891
|
+
"mercury-connection-partition0-r.wbx2.com": [
|
|
892
|
+
{
|
|
893
|
+
"host": "mercury-connection-partition0-r.wbx2.com",
|
|
894
|
+
"ttl": -1,
|
|
895
|
+
"priority": 5,
|
|
896
|
+
"id": "urn:TEAM:us-west-2_r:mercuryConnectionPartition0"
|
|
897
|
+
}
|
|
898
|
+
],
|
|
899
|
+
"empty.com": []
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
afterAll(() => {
|
|
903
|
+
// Clean up the mock host catalog
|
|
904
|
+
services._hostCatalog = {};
|
|
905
|
+
});
|
|
906
|
+
it('returns true if the host is in the host catalog', () => {
|
|
907
|
+
assert.isTrue(services.isValidHost('mercury-connection-partition0-r.wbx2.com'));
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('returns false if the host is not in the host catalog or has an empty entry list', () => {
|
|
911
|
+
assert.isFalse(services.isValidHost('test.com'));
|
|
912
|
+
assert.isFalse(services.isValidHost(''));
|
|
913
|
+
assert.isFalse(services.isValidHost(null));
|
|
914
|
+
assert.isFalse(services.isValidHost(undefined));
|
|
915
|
+
assert.isFalse(services.isValidHost('empty.com'));
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('returns false for non-string inputs', () => {
|
|
919
|
+
assert.isFalse(services.isValidHost(123));
|
|
920
|
+
assert.isFalse(services.isValidHost({}));
|
|
921
|
+
assert.isFalse(services.isValidHost([]));
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe('U2C catalog cache behavior', () => {
|
|
926
|
+
let webex;
|
|
927
|
+
let services;
|
|
928
|
+
let catalog;
|
|
929
|
+
let localStorageBackup;
|
|
930
|
+
let windowBackup;
|
|
931
|
+
|
|
932
|
+
const makeLocalStorageShim = () => {
|
|
933
|
+
const store = new Map();
|
|
934
|
+
return {
|
|
935
|
+
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
|
936
|
+
setItem: (k, v) => store.set(k, v),
|
|
937
|
+
removeItem: (k) => store.delete(k),
|
|
938
|
+
_store: store,
|
|
939
|
+
};
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
beforeEach(() => {
|
|
943
|
+
// Build a fresh webex instance
|
|
944
|
+
webex = new MockWebex({children: {services: Services}, config: {credentials: {federation: true}}});
|
|
945
|
+
services = webex.internal.services;
|
|
946
|
+
catalog = services._getCatalog();
|
|
947
|
+
|
|
948
|
+
// enable U2C caching feature flag in tests that rely on localStorage writes/reads
|
|
949
|
+
services.webex.config = services.webex.config || {};
|
|
950
|
+
services.webex.config.calling = {...(services.webex.config.calling || {}), cacheU2C: true};
|
|
951
|
+
|
|
952
|
+
// stub window.localStorage
|
|
953
|
+
windowBackup = global.window;
|
|
954
|
+
if (!global.window) global.window = {};
|
|
955
|
+
localStorageBackup = global.window.localStorage;
|
|
956
|
+
global.window.localStorage = makeLocalStorageShim();
|
|
957
|
+
// Ensure code under test uses our shim via util method
|
|
958
|
+
sinon.stub(services, '_getLocalStorageSafe').returns(global.window.localStorage);
|
|
959
|
+
|
|
960
|
+
// Stub the formatter so we don't need a full hostmap payload in tests
|
|
961
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
962
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
963
|
+
]);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
afterEach(() => {
|
|
967
|
+
global.window.localStorage = localStorageBackup || undefined;
|
|
968
|
+
if (!windowBackup) {
|
|
969
|
+
delete global.window;
|
|
970
|
+
} else {
|
|
971
|
+
global.window = windowBackup;
|
|
972
|
+
}
|
|
973
|
+
// Restore util stub if present
|
|
974
|
+
if (services._getLocalStorageSafe && services._getLocalStorageSafe.restore) {
|
|
975
|
+
services._getLocalStorageSafe.restore();
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('invokes initServiceCatalogs on ready, caches catalog, and stores in localStorage', async () => {
|
|
980
|
+
// Arrange: authenticated credentials and spies
|
|
981
|
+
services.webex.credentials = {
|
|
982
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
983
|
+
canAuthorize: true,
|
|
984
|
+
supertoken: {access_token: 'token'},
|
|
985
|
+
};
|
|
986
|
+
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
987
|
+
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
988
|
+
const setItemSpy = sinon.spy(global.window.localStorage, 'setItem');
|
|
989
|
+
// Make fetch return a hostmap object and allow formatter to reduce it
|
|
990
|
+
sinon.stub(services, 'request').resolves({body: {services: [], activeServices: {}, timestamp: Date.now().toString(), orgId: 'urn:EXAMPLE:org', format: 'U2CV2'}});
|
|
991
|
+
// Cause ready callback to run immediately
|
|
992
|
+
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
993
|
+
if (event === 'ready') cb();
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// Act
|
|
997
|
+
services.initialize();
|
|
998
|
+
await waitForAsync();
|
|
999
|
+
|
|
1000
|
+
// Assert: initServiceCatalogs was called because there was no cache
|
|
1001
|
+
assert.isTrue(initSpy.called, 'expected initServiceCatalogs to be invoked on ready');
|
|
1002
|
+
// _cacheCatalog is called at least once (preauth/postauth flows)
|
|
1003
|
+
assert.isTrue(cacheSpy.called, 'expected _cacheCatalog to be called');
|
|
1004
|
+
assert.isTrue(setItemSpy.called, 'expected localStorage.setItem to be called');
|
|
1005
|
+
|
|
1006
|
+
// Cleanup spies
|
|
1007
|
+
services.request.restore();
|
|
1008
|
+
initSpy.restore();
|
|
1009
|
+
cacheSpy.restore();
|
|
1010
|
+
setItemSpy.restore();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('does not invoke initServiceCatalogs on ready when cache exists and uses cached catalog', async () => {
|
|
1014
|
+
// Arrange: put a valid cache
|
|
1015
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1016
|
+
const cached = {
|
|
1017
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1018
|
+
cachedAt: Date.now(),
|
|
1019
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1020
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1021
|
+
};
|
|
1022
|
+
global.window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(cached));
|
|
1023
|
+
|
|
1024
|
+
// authenticated credentials
|
|
1025
|
+
services.webex.credentials = {
|
|
1026
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1027
|
+
canAuthorize: true,
|
|
1028
|
+
supertoken: {access_token: 'token'},
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const initSpy = sinon.spy(services, 'initServiceCatalogs');
|
|
1032
|
+
const cacheSpy = sinon.spy(services, '_cacheCatalog');
|
|
1033
|
+
// Cause ready callback to run immediately
|
|
1034
|
+
services.listenToOnce = sinon.stub().callsFake((ctx, event, cb) => {
|
|
1035
|
+
if (event === 'ready') cb();
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// Act
|
|
1039
|
+
services.initialize();
|
|
1040
|
+
await waitForAsync();
|
|
1041
|
+
|
|
1042
|
+
// Assert: ready path found cache and skipped initServiceCatalogs
|
|
1043
|
+
assert.isFalse(initSpy.called, 'expected initServiceCatalogs to be skipped with cache present');
|
|
1044
|
+
assert.isTrue(services._getCatalog().status.preauth.ready, 'preauth should be ready from cache');
|
|
1045
|
+
assert.isTrue(services._getCatalog().status.postauth.ready, 'postauth should be ready from cache');
|
|
1046
|
+
assert.isFalse(cacheSpy.called, 'should not write cache during warm-up-only path');
|
|
1047
|
+
|
|
1048
|
+
// Cleanup
|
|
1049
|
+
initSpy.restore();
|
|
1050
|
+
cacheSpy.restore();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('expires cached catalog after TTL and clears the entry', async () => {
|
|
1054
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1055
|
+
const staleCached = {
|
|
1056
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1057
|
+
cachedAt: Date.now() - (24 * 60 * 60 * 1000 + 1000), // past TTL
|
|
1058
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1059
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify(staleCached));
|
|
1063
|
+
|
|
1064
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1065
|
+
|
|
1066
|
+
assert.isFalse(warmed, 'stale cache must not warm');
|
|
1067
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'expired cache must be cleared');
|
|
1068
|
+
assert.isFalse(catalog.status.preauth.ready);
|
|
1069
|
+
assert.isFalse(catalog.status.postauth.ready);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
it('clearCatalogCache() removes the cached entry', async () => {
|
|
1073
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1074
|
+
window.localStorage.setItem(CATALOG_CACHE_KEY_V1, JSON.stringify({cachedAt: Date.now()}));
|
|
1075
|
+
|
|
1076
|
+
await services.clearCatalogCache();
|
|
1077
|
+
|
|
1078
|
+
assert.isNull(window.localStorage.getItem(CATALOG_CACHE_KEY_V1), 'cache should be cleared');
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('still fetches when forceRefresh=true even if ready', async () => {
|
|
1082
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1083
|
+
window.localStorage.setItem(
|
|
1084
|
+
CATALOG_CACHE_KEY_V1,
|
|
1085
|
+
JSON.stringify({
|
|
1086
|
+
orgId: 'urn:EXAMPLE:org',
|
|
1087
|
+
cachedAt: Date.now(),
|
|
1088
|
+
preauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1089
|
+
postauth: {serviceLinks: {}, hostCatalog: {}},
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
// warm from cache
|
|
1094
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1095
|
+
assert.isTrue(warmed);
|
|
1096
|
+
assert.isTrue(catalog.status.preauth.ready);
|
|
1097
|
+
assert.isTrue(catalog.status.postauth.ready);
|
|
1098
|
+
|
|
1099
|
+
const fetchSpy = sinon.spy(services, '_fetchNewServiceHostmap');
|
|
1100
|
+
|
|
1101
|
+
// with forceRefresh we should fetch despite ready=true
|
|
1102
|
+
await services.updateServices({from: 'limited', query: {orgId: 'urn:EXAMPLE:org'}, forceRefresh: true});
|
|
1103
|
+
// pass an empty query to avoid spreading undefined in qs construction
|
|
1104
|
+
await services.updateServices({forceRefresh: true});
|
|
1105
|
+
|
|
1106
|
+
assert.isTrue(fetchSpy.called, 'forceRefresh should bypass cache short-circuit');
|
|
1107
|
+
fetchSpy.restore();
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('stores selection metadata and env on cache write for preauth', async () => {
|
|
1111
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1112
|
+
// arrange config for env fingerprint
|
|
1113
|
+
services.webex.config = services.webex.config || {};
|
|
1114
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1115
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1116
|
+
services.webex.config.fedramp = false;
|
|
1117
|
+
|
|
1118
|
+
// write cache with meta
|
|
1119
|
+
await services._cacheCatalog(
|
|
1120
|
+
'preauth',
|
|
1121
|
+
{serviceLinks: {}, hostCatalog: {}},
|
|
1122
|
+
{selectionType: 'orgId', selectionValue: 'urn:EXAMPLE:org'}
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
const raw = window.localStorage.getItem(CATALOG_CACHE_KEY_V1);
|
|
1126
|
+
assert.isString(raw);
|
|
1127
|
+
const parsed = JSON.parse(raw);
|
|
1128
|
+
assert.equal(parsed.orgId, undefined, 'orgId not set without credentials');
|
|
1129
|
+
assert.deepEqual(parsed.env, {
|
|
1130
|
+
fedramp: false,
|
|
1131
|
+
u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1',
|
|
1132
|
+
});
|
|
1133
|
+
assert.isObject(parsed.preauth);
|
|
1134
|
+
assert.deepEqual(parsed.preauth.meta, {
|
|
1135
|
+
selectionType: 'orgId',
|
|
1136
|
+
selectionValue: 'urn:EXAMPLE:org',
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('warms preauth from cache when selection meta matches intended orgId', async () => {
|
|
1141
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1142
|
+
// stub credentials
|
|
1143
|
+
services.webex.credentials = {
|
|
1144
|
+
canAuthorize: true,
|
|
1145
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1146
|
+
};
|
|
1147
|
+
// set current env to match cached env
|
|
1148
|
+
services.webex.config = services.webex.config || {};
|
|
1149
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1150
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1151
|
+
services.webex.config.fedramp = false;
|
|
1152
|
+
// cache with matching orgId selection
|
|
1153
|
+
window.localStorage.setItem(
|
|
1154
|
+
CATALOG_CACHE_KEY_V1,
|
|
1155
|
+
JSON.stringify({
|
|
1156
|
+
cachedAt: Date.now(),
|
|
1157
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1158
|
+
preauth: {
|
|
1159
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1160
|
+
meta: {selectionType: 'orgId', selectionValue: 'urn:EXAMPLE:org'},
|
|
1161
|
+
},
|
|
1162
|
+
})
|
|
1163
|
+
);
|
|
1164
|
+
// formatter returns at least one entry to mark ready
|
|
1165
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1166
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1167
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1168
|
+
]);
|
|
1169
|
+
|
|
1170
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1171
|
+
assert.isTrue(warmed);
|
|
1172
|
+
assert.isTrue(catalog.status.preauth.ready, 'preauth should be warmed on match');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('does not warm preauth when selection meta is proximity mode', async () => {
|
|
1176
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1177
|
+
// cache with proximity mode selection
|
|
1178
|
+
window.localStorage.setItem(
|
|
1179
|
+
CATALOG_CACHE_KEY_V1,
|
|
1180
|
+
JSON.stringify({
|
|
1181
|
+
cachedAt: Date.now(),
|
|
1182
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1183
|
+
preauth: {
|
|
1184
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1185
|
+
meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'},
|
|
1186
|
+
},
|
|
1187
|
+
})
|
|
1188
|
+
);
|
|
1189
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1190
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1191
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1192
|
+
]);
|
|
1193
|
+
|
|
1194
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1195
|
+
// function returns true if overall cache path succeeded; we only verify group readiness
|
|
1196
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm for proximity mode');
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('does not warm preauth when selection meta mismatches intended selection', async () => {
|
|
1200
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1201
|
+
// authorized with org X
|
|
1202
|
+
services.webex.credentials = {
|
|
1203
|
+
canAuthorize: true,
|
|
1204
|
+
getOrgId: sinon.stub().returns('urn:EXAMPLE:org'),
|
|
1205
|
+
};
|
|
1206
|
+
// cache points to a different org
|
|
1207
|
+
window.localStorage.setItem(
|
|
1208
|
+
CATALOG_CACHE_KEY_V1,
|
|
1209
|
+
JSON.stringify({
|
|
1210
|
+
cachedAt: Date.now(),
|
|
1211
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.wbx2.com/u2c/api/v1'},
|
|
1212
|
+
preauth: {
|
|
1213
|
+
hostMap: {serviceLinks: {}, hostCatalog: {}},
|
|
1214
|
+
meta: {selectionType: 'orgId', selectionValue: 'urn:DIFF:org'},
|
|
1215
|
+
},
|
|
1216
|
+
})
|
|
1217
|
+
);
|
|
1218
|
+
services._formatReceivedHostmap.restore && services._formatReceivedHostmap.restore();
|
|
1219
|
+
sinon.stub(services, '_formatReceivedHostmap').callsFake(() => [
|
|
1220
|
+
{name: 'hydra', defaultUrl: 'https://api.ciscospark.com/v1', hosts: []},
|
|
1221
|
+
]);
|
|
1222
|
+
|
|
1223
|
+
await services._loadCatalogFromCache();
|
|
1224
|
+
assert.isFalse(catalog.status.preauth.ready, 'preauth should not warm on selection mismatch');
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('skips warming when environment fingerprint mismatches', async () => {
|
|
1228
|
+
const CATALOG_CACHE_KEY_V1 = 'services.v1.u2cHostMap';
|
|
1229
|
+
// cached env differs from current env (different U2C URL)
|
|
1230
|
+
window.localStorage.setItem(
|
|
1231
|
+
CATALOG_CACHE_KEY_V1,
|
|
1232
|
+
JSON.stringify({
|
|
1233
|
+
cachedAt: Date.now(),
|
|
1234
|
+
env: {fedramp: false, u2cDiscoveryUrl: 'https://u2c.other.com/u2c/api/v1'},
|
|
1235
|
+
preauth: {hostMap: {serviceLinks: {}, hostCatalog: {}}, meta: {selectionType: 'mode', selectionValue: 'DEFAULT_BY_PROXIMITY'}},
|
|
1236
|
+
})
|
|
1237
|
+
);
|
|
1238
|
+
// current env
|
|
1239
|
+
services.webex.config = services.webex.config || {};
|
|
1240
|
+
services.webex.config.services = services.webex.config.services || {discovery: {}};
|
|
1241
|
+
services.webex.config.services.discovery.u2c = 'https://u2c.wbx2.com/u2c/api/v1';
|
|
1242
|
+
services.webex.config.fedramp = false;
|
|
1243
|
+
|
|
1244
|
+
const warmed = await services._loadCatalogFromCache();
|
|
1245
|
+
assert.isFalse(warmed, 'env mismatch should skip warm and return false');
|
|
1246
|
+
assert.isFalse(catalog.status.preauth.ready);
|
|
1247
|
+
assert.isFalse(catalog.status.postauth.ready);
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
|
|
842
1251
|
});
|
|
843
1252
|
});
|
|
844
1253
|
/* eslint-enable no-underscore-dangle */
|