@webex/plugin-authorization-browser 3.0.0-beta.9 → 3.0.0-beta.91
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/authorization.js +9 -77
- package/dist/authorization.js.map +1 -1
- package/dist/config.js +0 -3
- package/dist/config.js.map +1 -1
- package/dist/index.js +1 -9
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
- package/src/authorization.js +28 -24
- package/src/config.js +2 -2
- package/src/index.js +2 -5
- package/test/automation/fixtures/app.js +25 -24
- package/test/automation/fixtures/index.html +21 -16
- package/test/automation/spec/authorization-code-grant.js +74 -66
- package/test/automation/spec/implicit-grant.js +48 -41
- package/test/integration/spec/authorization.js +30 -31
- package/test/unit/spec/authorization.js +169 -132
|
@@ -22,41 +22,48 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
22
22
|
history: {
|
|
23
23
|
replaceState(a, b, location) {
|
|
24
24
|
mockWindow.location.href = location;
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
26
|
},
|
|
27
27
|
location: {
|
|
28
|
-
href
|
|
28
|
+
href,
|
|
29
29
|
},
|
|
30
30
|
sessionStorage: {
|
|
31
31
|
getItem: sinon.stub().returns(csrfToken),
|
|
32
32
|
removeItem: sinon.spy(),
|
|
33
|
-
setItem: sinon.spy()
|
|
34
|
-
}
|
|
33
|
+
setItem: sinon.spy(),
|
|
34
|
+
},
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const webex = new MockWebex({
|
|
38
38
|
children: {
|
|
39
39
|
authorization: Authorization,
|
|
40
|
-
credentials: Credentials
|
|
40
|
+
credentials: Credentials,
|
|
41
41
|
},
|
|
42
|
-
config: merge(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
config: merge(
|
|
43
|
+
{
|
|
44
|
+
credentials: {
|
|
45
|
+
authorizeUrl: `${
|
|
46
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
47
|
+
}/idb/oauth2/v1/authorize`,
|
|
48
|
+
logoutUrl: `${
|
|
49
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
50
|
+
}/idb/oauth2/v1/logout`,
|
|
51
|
+
// eslint-disable-next-line camelcase
|
|
52
|
+
client_id: 'fake',
|
|
53
|
+
// eslint-disable-next-line camelcase
|
|
54
|
+
client_secret: 'fake',
|
|
55
|
+
// eslint-disable-next-line camelcase
|
|
56
|
+
redirect_uri: 'http://example.com',
|
|
57
|
+
// eslint-disable-next-line camelcase
|
|
58
|
+
scope: 'scope:one',
|
|
59
|
+
refreshCallback: () => Promise.resolve(),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
config
|
|
63
|
+
),
|
|
57
64
|
getWindow() {
|
|
58
65
|
return mockWindow;
|
|
59
|
-
}
|
|
66
|
+
},
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
return webex;
|
|
@@ -65,31 +72,32 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
65
72
|
describe('#initialize()', () => {
|
|
66
73
|
describe('when there is a token in the url', () => {
|
|
67
74
|
it('sets the token and sets ready', () => {
|
|
68
|
-
const webex = makeWebexCore(
|
|
75
|
+
const webex = makeWebexCore(
|
|
76
|
+
'http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer'
|
|
77
|
+
);
|
|
69
78
|
|
|
70
79
|
assert.isFalse(webex.authorization.ready);
|
|
71
80
|
assert.isFalse(webex.credentials.canAuthorize);
|
|
72
81
|
|
|
73
|
-
return webex.authorization.when('change:ready')
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
82
|
+
return webex.authorization.when('change:ready').then(() => {
|
|
83
|
+
assert.isTrue(webex.authorization.ready);
|
|
84
|
+
assert.isTrue(webex.credentials.canAuthorize);
|
|
85
|
+
});
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
describe('when url parsing is disabled', () => {
|
|
81
89
|
it('sets ready', () => {
|
|
82
90
|
const webex = new MockWebex({
|
|
83
91
|
children: {
|
|
84
|
-
credentials: Credentials
|
|
92
|
+
credentials: Credentials,
|
|
85
93
|
},
|
|
86
94
|
getWindow() {
|
|
87
95
|
return {
|
|
88
96
|
location: {
|
|
89
|
-
href: 'http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer'
|
|
90
|
-
}
|
|
97
|
+
href: 'http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer',
|
|
98
|
+
},
|
|
91
99
|
};
|
|
92
|
-
}
|
|
100
|
+
},
|
|
93
101
|
});
|
|
94
102
|
|
|
95
103
|
webex.authorization = new Authorization({parse: false}, {parent: webex});
|
|
@@ -100,18 +108,19 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
100
108
|
});
|
|
101
109
|
|
|
102
110
|
it('sets the token, refresh token and sets ready', () => {
|
|
103
|
-
const webex = makeWebexCore(
|
|
111
|
+
const webex = makeWebexCore(
|
|
112
|
+
'http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000'
|
|
113
|
+
);
|
|
104
114
|
|
|
105
115
|
assert.isFalse(webex.authorization.ready);
|
|
106
116
|
assert.isFalse(webex.credentials.canAuthorize);
|
|
107
117
|
assert.isFalse(webex.credentials.canRefresh);
|
|
108
118
|
|
|
109
|
-
return webex.authorization.when('change:ready')
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
119
|
+
return webex.authorization.when('change:ready').then(() => {
|
|
120
|
+
assert.isTrue(webex.authorization.ready);
|
|
121
|
+
assert.isTrue(webex.credentials.canAuthorize);
|
|
122
|
+
assert.isTrue(webex.credentials.canRefresh);
|
|
123
|
+
});
|
|
115
124
|
});
|
|
116
125
|
|
|
117
126
|
it('validates the csrf token', () => {
|
|
@@ -119,44 +128,70 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
119
128
|
|
|
120
129
|
assert.throws(() => {
|
|
121
130
|
// eslint-disable-next-line no-unused-vars
|
|
122
|
-
const webex = makeWebexCore(
|
|
131
|
+
const webex = makeWebexCore(
|
|
132
|
+
`http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000&state=${base64.encode(
|
|
133
|
+
JSON.stringify({csrf_token: 'someothertoken'})
|
|
134
|
+
)}`,
|
|
135
|
+
csrfToken
|
|
136
|
+
);
|
|
123
137
|
}, /CSRF token someothertoken does not match stored token abcd/);
|
|
124
138
|
|
|
125
139
|
assert.throws(() => {
|
|
126
140
|
// eslint-disable-next-line no-unused-vars
|
|
127
|
-
const webex = makeWebexCore(
|
|
141
|
+
const webex = makeWebexCore(
|
|
142
|
+
`http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000&state=${base64.encode(
|
|
143
|
+
JSON.stringify({})
|
|
144
|
+
)}`,
|
|
145
|
+
csrfToken
|
|
146
|
+
);
|
|
128
147
|
}, /Expected CSRF token abcd, but not found in redirect hash/);
|
|
129
148
|
|
|
130
149
|
assert.throws(() => {
|
|
131
150
|
// eslint-disable-next-line no-unused-vars
|
|
132
|
-
const webex = makeWebexCore(
|
|
151
|
+
const webex = makeWebexCore(
|
|
152
|
+
'http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000',
|
|
153
|
+
csrfToken
|
|
154
|
+
);
|
|
133
155
|
}, /Expected CSRF token abcd, but not found in redirect hash/);
|
|
134
156
|
|
|
135
|
-
const webex = makeWebexCore(
|
|
157
|
+
const webex = makeWebexCore(
|
|
158
|
+
`http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000&state=${base64.encode(
|
|
159
|
+
JSON.stringify({csrf_token: csrfToken})
|
|
160
|
+
)}`,
|
|
161
|
+
csrfToken
|
|
162
|
+
);
|
|
136
163
|
|
|
137
|
-
return webex.authorization.when('change:ready')
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
164
|
+
return webex.authorization.when('change:ready').then(() => {
|
|
165
|
+
assert.isTrue(webex.credentials.canAuthorize);
|
|
166
|
+
assert.called(webex.getWindow().sessionStorage.removeItem);
|
|
167
|
+
});
|
|
142
168
|
});
|
|
143
169
|
|
|
144
170
|
it('removes the oauth parameters from the url', () => {
|
|
145
171
|
const csrfToken = 'abcd';
|
|
146
172
|
|
|
147
|
-
const webex = makeWebexCore(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
const webex = makeWebexCore(
|
|
174
|
+
`http://example.com/#access_token=AT&expires_in=3600&token_type=Bearer&refresh_token=RT&refresh_token_expires_in=36000&state=${base64.encode(
|
|
175
|
+
JSON.stringify({csrf_token: csrfToken, something: true})
|
|
176
|
+
)}`,
|
|
177
|
+
csrfToken
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return webex.authorization.when('change:ready').then(() => {
|
|
181
|
+
assert.isTrue(webex.credentials.canAuthorize);
|
|
182
|
+
assert.called(webex.getWindow().sessionStorage.removeItem);
|
|
183
|
+
assert.equal(
|
|
184
|
+
webex.getWindow().location.href,
|
|
185
|
+
`http://example.com/#state=${base64.encode(JSON.stringify({something: true}))}`
|
|
186
|
+
);
|
|
187
|
+
});
|
|
155
188
|
});
|
|
156
189
|
|
|
157
190
|
it('throws a grant error when the url contains one', () => {
|
|
158
191
|
assert.throws(() => {
|
|
159
|
-
makeWebexCore(
|
|
192
|
+
makeWebexCore(
|
|
193
|
+
'http://127.0.0.1:8000/?error=invalid_scope&error_description=The%20requested%20scope%20is%20invalid.'
|
|
194
|
+
);
|
|
160
195
|
}, /The requested scope is invalid./);
|
|
161
196
|
});
|
|
162
197
|
});
|
|
@@ -176,42 +211,44 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
176
211
|
it('calls #initiateImplicitGrant()', () => {
|
|
177
212
|
const webex = makeWebexCore(undefined, undefined, {
|
|
178
213
|
credentials: {
|
|
179
|
-
clientType: 'public'
|
|
180
|
-
}
|
|
214
|
+
clientType: 'public',
|
|
215
|
+
},
|
|
181
216
|
});
|
|
182
217
|
|
|
183
218
|
sinon.spy(webex.authorization, 'initiateImplicitGrant');
|
|
184
219
|
|
|
185
|
-
return webex.authorization.initiateLogin()
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
220
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
221
|
+
assert.called(webex.authorization.initiateImplicitGrant);
|
|
222
|
+
assert.include(webex.getWindow().location, 'response_type=token');
|
|
223
|
+
});
|
|
190
224
|
});
|
|
191
225
|
|
|
192
226
|
it('adds a csrf_token to the login url and sessionStorage', () => {
|
|
193
227
|
const webex = makeWebexCore(undefined, undefined, {
|
|
194
228
|
credentials: {
|
|
195
|
-
clientType: 'public'
|
|
196
|
-
}
|
|
229
|
+
clientType: 'public',
|
|
230
|
+
},
|
|
197
231
|
});
|
|
198
232
|
|
|
199
233
|
sinon.spy(webex.authorization, 'initiateImplicitGrant');
|
|
200
234
|
|
|
201
|
-
return webex.authorization.initiateLogin()
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
236
|
+
assert.called(webex.authorization.initiateImplicitGrant);
|
|
237
|
+
assert.include(webex.getWindow().location, 'response_type=token');
|
|
238
|
+
const {query} = url.parse(webex.getWindow().location, true);
|
|
239
|
+
let {state} = query;
|
|
240
|
+
|
|
241
|
+
state = JSON.parse(base64.decode(state));
|
|
242
|
+
assert.property(state, 'csrf_token');
|
|
243
|
+
assert.isDefined(state.csrf_token);
|
|
244
|
+
assert.match(state.csrf_token, patterns.uuid);
|
|
245
|
+
assert.called(webex.getWindow().sessionStorage.setItem);
|
|
246
|
+
assert.calledWith(
|
|
247
|
+
webex.getWindow().sessionStorage.setItem,
|
|
248
|
+
'oauth2-csrf-token',
|
|
249
|
+
state.csrf_token
|
|
250
|
+
);
|
|
251
|
+
});
|
|
215
252
|
});
|
|
216
253
|
});
|
|
217
254
|
|
|
@@ -219,50 +256,52 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
219
256
|
it('calls #initiateAuthorizationCodeGrant()', () => {
|
|
220
257
|
const webex = makeWebexCore(undefined, undefined, {
|
|
221
258
|
credentials: {
|
|
222
|
-
clientType: 'confidential'
|
|
223
|
-
}
|
|
259
|
+
clientType: 'confidential',
|
|
260
|
+
},
|
|
224
261
|
});
|
|
225
262
|
|
|
226
263
|
sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant');
|
|
227
264
|
|
|
228
|
-
return webex.authorization.initiateLogin()
|
|
229
|
-
.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
265
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
266
|
+
assert.called(webex.authorization.initiateAuthorizationCodeGrant);
|
|
267
|
+
assert.include(webex.getWindow().location, 'response_type=code');
|
|
268
|
+
});
|
|
233
269
|
});
|
|
234
270
|
|
|
235
271
|
it('adds a csrf_token to the login url and sessionStorage', () => {
|
|
236
272
|
const webex = makeWebexCore(undefined, undefined, {
|
|
237
273
|
credentials: {
|
|
238
|
-
clientType: 'confidential'
|
|
239
|
-
}
|
|
274
|
+
clientType: 'confidential',
|
|
275
|
+
},
|
|
240
276
|
});
|
|
241
277
|
|
|
242
278
|
sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant');
|
|
243
279
|
|
|
244
|
-
return webex.authorization.initiateLogin()
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
281
|
+
assert.called(webex.authorization.initiateAuthorizationCodeGrant);
|
|
282
|
+
assert.include(webex.getWindow().location, 'response_type=code');
|
|
283
|
+
const {query} = url.parse(webex.getWindow().location, true);
|
|
284
|
+
let {state} = query;
|
|
285
|
+
|
|
286
|
+
state = JSON.parse(base64.decode(state));
|
|
287
|
+
assert.property(state, 'csrf_token');
|
|
288
|
+
assert.isDefined(state.csrf_token);
|
|
289
|
+
assert.match(state.csrf_token, patterns.uuid);
|
|
290
|
+
assert.called(webex.getWindow().sessionStorage.setItem);
|
|
291
|
+
assert.calledWith(
|
|
292
|
+
webex.getWindow().sessionStorage.setItem,
|
|
293
|
+
'oauth2-csrf-token',
|
|
294
|
+
state.csrf_token
|
|
295
|
+
);
|
|
296
|
+
});
|
|
258
297
|
});
|
|
259
298
|
});
|
|
260
299
|
|
|
261
300
|
it('sets #isAuthorizing', () => {
|
|
262
301
|
const webex = makeWebexCore(undefined, undefined, {
|
|
263
302
|
credentials: {
|
|
264
|
-
clientType: 'confidential'
|
|
265
|
-
}
|
|
303
|
+
clientType: 'confidential',
|
|
304
|
+
},
|
|
266
305
|
});
|
|
267
306
|
|
|
268
307
|
assert.isFalse(webex.authorization.isAuthorizing);
|
|
@@ -276,8 +315,8 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
276
315
|
it('sets #isAuthenticating', () => {
|
|
277
316
|
const webex = makeWebexCore(undefined, undefined, {
|
|
278
317
|
credentials: {
|
|
279
|
-
clientType: 'confidential'
|
|
280
|
-
}
|
|
318
|
+
clientType: 'confidential',
|
|
319
|
+
},
|
|
281
320
|
});
|
|
282
321
|
|
|
283
322
|
assert.isFalse(webex.authorization.isAuthenticating);
|
|
@@ -293,8 +332,8 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
293
332
|
it('removes the state parameter when it is empty', () => {
|
|
294
333
|
const webex = makeWebexCore(undefined, undefined, {
|
|
295
334
|
credentials: {
|
|
296
|
-
clientType: 'confidential'
|
|
297
|
-
}
|
|
335
|
+
clientType: 'confidential',
|
|
336
|
+
},
|
|
298
337
|
});
|
|
299
338
|
|
|
300
339
|
sinon.spy(webex.authorization, '_cleanUrl');
|
|
@@ -309,17 +348,17 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
309
348
|
it('removes the state parameter when only token is present', () => {
|
|
310
349
|
const webex = makeWebexCore(undefined, undefined, {
|
|
311
350
|
credentials: {
|
|
312
|
-
clientType: 'confidential'
|
|
313
|
-
}
|
|
351
|
+
clientType: 'confidential',
|
|
352
|
+
},
|
|
314
353
|
});
|
|
315
354
|
|
|
316
355
|
sinon.spy(webex.authorization, '_cleanUrl');
|
|
317
356
|
const location = {
|
|
318
357
|
hash: {
|
|
319
358
|
state: {
|
|
320
|
-
csrf_token: 'token'
|
|
321
|
-
}
|
|
322
|
-
}
|
|
359
|
+
csrf_token: 'token',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
323
362
|
};
|
|
324
363
|
|
|
325
364
|
webex.authorization._cleanUrl(location);
|
|
@@ -329,16 +368,16 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
329
368
|
it('keeps the state parameter when it has keys', () => {
|
|
330
369
|
const webex = makeWebexCore(undefined, undefined, {
|
|
331
370
|
credentials: {
|
|
332
|
-
clientType: 'confidential'
|
|
333
|
-
}
|
|
371
|
+
clientType: 'confidential',
|
|
372
|
+
},
|
|
334
373
|
});
|
|
335
374
|
const location = {
|
|
336
375
|
hash: {
|
|
337
376
|
state: {
|
|
338
377
|
csrf_token: 'token',
|
|
339
|
-
key: 'value'
|
|
340
|
-
}
|
|
341
|
-
}
|
|
378
|
+
key: 'value',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
342
381
|
};
|
|
343
382
|
|
|
344
383
|
sinon.spy(webex.authorization, '_cleanUrl');
|
|
@@ -355,17 +394,16 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
355
394
|
it('redirects to the login page with response_type=token', () => {
|
|
356
395
|
const webex = makeWebexCore(undefined, undefined, {
|
|
357
396
|
credentials: {
|
|
358
|
-
clientType: 'public'
|
|
359
|
-
}
|
|
397
|
+
clientType: 'public',
|
|
398
|
+
},
|
|
360
399
|
});
|
|
361
400
|
|
|
362
401
|
sinon.spy(webex.authorization, 'initiateImplicitGrant');
|
|
363
402
|
|
|
364
|
-
return webex.authorization.initiateLogin()
|
|
365
|
-
.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
});
|
|
403
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
404
|
+
assert.called(webex.authorization.initiateImplicitGrant);
|
|
405
|
+
assert.include(webex.getWindow().location, 'response_type=token');
|
|
406
|
+
});
|
|
369
407
|
});
|
|
370
408
|
});
|
|
371
409
|
|
|
@@ -373,17 +411,16 @@ browserOnly(describe)('plugin-authorization-browser', () => {
|
|
|
373
411
|
it('redirects to the login page with response_type=code', () => {
|
|
374
412
|
const webex = makeWebexCore(undefined, undefined, {
|
|
375
413
|
credentials: {
|
|
376
|
-
clientType: 'confidential'
|
|
377
|
-
}
|
|
414
|
+
clientType: 'confidential',
|
|
415
|
+
},
|
|
378
416
|
});
|
|
379
417
|
|
|
380
418
|
sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant');
|
|
381
419
|
|
|
382
|
-
return webex.authorization.initiateLogin()
|
|
383
|
-
.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
});
|
|
420
|
+
return webex.authorization.initiateLogin().then(() => {
|
|
421
|
+
assert.called(webex.authorization.initiateAuthorizationCodeGrant);
|
|
422
|
+
assert.include(webex.getWindow().location, 'response_type=code');
|
|
423
|
+
});
|
|
387
424
|
});
|
|
388
425
|
});
|
|
389
426
|
});
|