4
4
*/
5
5
6
6
import * as assert from 'assert'
7
- import { AuthNode , isSsoConnection , promptForConnection , SsoConnection } from '../../credentials/auth'
7
+ import * as sinon from 'sinon'
8
+ import {
9
+ AuthNode ,
10
+ Connection ,
11
+ isIamConnection ,
12
+ isSsoConnection ,
13
+ promptForConnection ,
14
+ ssoAccountAccessScopes ,
15
+ } from '../../credentials/auth'
8
16
import { ToolkitError } from '../../shared/errors'
9
17
import { assertTreeItem } from '../shared/treeview/testUtil'
10
18
import { getTestWindow } from '../shared/vscode/window'
11
19
import { captureEventOnce } from '../testUtil'
12
- import { createSsoProfile , createTestAuth } from './testUtil'
20
+ import { createBuilderIdProfile , createSsoProfile , createTestAuth } from './testUtil'
21
+ import { toCollection } from '../../shared/utilities/asyncCollection'
22
+ import globals from '../../shared/extensionGlobals'
13
23
14
24
const ssoProfile = createSsoProfile ( )
15
25
const scopedSsoProfile = createSsoProfile ( { scopes : [ 'foo' ] } )
@@ -181,15 +191,15 @@ describe('Auth', function () {
181
191
assert . ok ( await conn . getToken ( ) )
182
192
} )
183
193
184
- describe ( 'SSO Connections' , function ( ) {
185
- async function runExpiredGetTokenFlow ( conn : SsoConnection , selection : string | RegExp ) {
186
- const token = conn . getToken ( )
187
- const message = await getTestWindow ( ) . waitForMessage ( expiredConnPattern )
188
- message . selectItem ( selection )
194
+ async function runExpiredConnectionFlow ( conn : Connection , selection : string | RegExp ) {
195
+ const creds = conn . type === 'sso' ? conn . getToken ( ) : conn . getCredentials ( )
196
+ const message = await getTestWindow ( ) . waitForMessage ( expiredConnPattern )
197
+ message . selectItem ( selection )
189
198
190
- return token
191
- }
199
+ return creds
200
+ }
192
201
202
+ describe ( 'SSO Connections' , function ( ) {
193
203
it ( 'creates a new token if one does not exist' , async function ( ) {
194
204
const conn = await auth . createConnection ( ssoProfile )
195
205
const provider = auth . getTestTokenProvider ( conn )
@@ -198,7 +208,7 @@ describe('Auth', function () {
198
208
199
209
it ( 'prompts the user if the token is invalid or expired' , async function ( ) {
200
210
const conn = await auth . createInvalidSsoConnection ( ssoProfile )
201
- const token = await runExpiredGetTokenFlow ( conn , / l o g i n / i)
211
+ const token = await runExpiredConnectionFlow ( conn , / l o g i n / i)
202
212
assert . notStrictEqual ( token , undefined )
203
213
} )
204
214
@@ -207,7 +217,7 @@ describe('Auth', function () {
207
217
await auth . useConnection ( conn )
208
218
await auth . invalidateCachedCredentials ( conn )
209
219
210
- const token = runExpiredGetTokenFlow ( conn , / n o / i)
220
+ const token = runExpiredConnectionFlow ( conn , / n o / i)
211
221
await assert . rejects ( token , ToolkitError )
212
222
213
223
assert . strictEqual ( auth . activeConnection ?. state , 'invalid' )
@@ -217,12 +227,139 @@ describe('Auth', function () {
217
227
const err1 = new ToolkitError ( 'test' , { code : 'test' } )
218
228
const conn = await auth . createConnection ( ssoProfile )
219
229
auth . getTestTokenProvider ( conn ) ?. getToken . rejects ( err1 )
220
- const err2 = await runExpiredGetTokenFlow ( conn , / n o / i) . catch ( e => e )
230
+ const err2 = await runExpiredConnectionFlow ( conn , / n o / i) . catch ( e => e )
221
231
assert . ok ( err2 instanceof ToolkitError )
222
232
assert . strictEqual ( err2 . cause , err1 )
223
233
} )
224
234
} )
225
235
236
+ describe ( 'Linked Connections' , function ( ) {
237
+ const linkedSsoProfile = createSsoProfile ( { scopes : ssoAccountAccessScopes } )
238
+ const accountRoles = [
239
+ { accountId : '1245678910' , roleName : 'foo' } ,
240
+ { accountId : '9876543210' , roleName : 'foo' } ,
241
+ { accountId : '9876543210' , roleName : 'bar' } ,
242
+ ]
243
+
244
+ beforeEach ( function ( ) {
245
+ auth . ssoClient . listAccounts . returns (
246
+ toCollection ( async function * ( ) {
247
+ yield [ { accountId : '1245678910' } , { accountId : '9876543210' } ]
248
+ } )
249
+ )
250
+
251
+ auth . ssoClient . listAccountRoles . callsFake ( req =>
252
+ toCollection ( async function * ( ) {
253
+ yield accountRoles . filter ( i => i . accountId === req . accountId )
254
+ } )
255
+ )
256
+
257
+ auth . ssoClient . getRoleCredentials . resolves ( {
258
+ accessKeyId : 'xxx' ,
259
+ secretAccessKey : 'xxx' ,
260
+ expiration : new Date ( Date . now ( ) + 1000000 ) ,
261
+ } )
262
+
263
+ sinon . stub ( globals . loginManager , 'validateCredentials' ) . resolves ( '' )
264
+ } )
265
+
266
+ afterEach ( function ( ) {
267
+ sinon . restore ( )
268
+ } )
269
+
270
+ it ( 'lists linked conections for SSO connections' , async function ( ) {
271
+ await auth . createConnection ( linkedSsoProfile )
272
+ const connections = await auth . listAndTraverseConnections ( ) . promise ( )
273
+ assert . deepStrictEqual (
274
+ connections . map ( c => c . type ) ,
275
+ [ 'sso' , 'iam' , 'iam' , 'iam' ]
276
+ )
277
+ } )
278
+
279
+ it ( 'does not gather linked accounts when calling `listConnections`' , async function ( ) {
280
+ await auth . createConnection ( linkedSsoProfile )
281
+ const connections = await auth . listConnections ( )
282
+ assert . deepStrictEqual (
283
+ connections . map ( c => c . type ) ,
284
+ [ 'sso' ]
285
+ )
286
+ } )
287
+
288
+ it ( 'caches linked conections when the source connection becomes invalid' , async function ( ) {
289
+ const conn = await auth . createConnection ( linkedSsoProfile )
290
+ await auth . listAndTraverseConnections ( ) . promise ( )
291
+ await auth . invalidateCachedCredentials ( conn )
292
+
293
+ const connections = await auth . listConnections ( )
294
+ assert . deepStrictEqual (
295
+ connections . map ( c => c . type ) ,
296
+ [ 'sso' , 'iam' , 'iam' , 'iam' ]
297
+ )
298
+ } )
299
+
300
+ it ( 'gracefully handles source connections becoming invalid when discovering linked accounts' , async function ( ) {
301
+ await auth . createConnection ( linkedSsoProfile )
302
+ auth . ssoClient . listAccounts . rejects ( new Error ( 'No access' ) )
303
+ const connections = await auth . listAndTraverseConnections ( ) . promise ( )
304
+ assert . deepStrictEqual (
305
+ connections . map ( c => c . type ) ,
306
+ [ 'sso' ]
307
+ )
308
+ } )
309
+
310
+ it ( 'removes linked connections when the source connection is deleted' , async function ( ) {
311
+ const conn = await auth . createConnection ( linkedSsoProfile )
312
+ await auth . listAndTraverseConnections ( ) . promise ( )
313
+ await auth . deleteConnection ( conn )
314
+
315
+ assert . deepStrictEqual ( await auth . listAndTraverseConnections ( ) . promise ( ) , [ ] )
316
+ } )
317
+
318
+ it ( 'prompts the user to reauthenticate if the source connection becomes invalid' , async function ( ) {
319
+ const source = await auth . createConnection ( linkedSsoProfile )
320
+ const conn = await auth . listAndTraverseConnections ( ) . find ( c => isIamConnection ( c ) && c . id . includes ( 'sso' ) )
321
+ assert . ok ( conn )
322
+ await auth . useConnection ( conn )
323
+ await auth . reauthenticate ( conn )
324
+ await auth . invalidateCachedCredentials ( conn )
325
+ await auth . invalidateCachedCredentials ( source )
326
+
327
+ await runExpiredConnectionFlow ( conn , / l o g i n / i)
328
+ assert . strictEqual ( auth . getConnectionState ( source ) , 'valid' )
329
+ assert . strictEqual ( auth . getConnectionState ( conn ) , 'valid' )
330
+ } )
331
+
332
+ describe ( 'Multiple Connections' , function ( ) {
333
+ const otherProfile = createBuilderIdProfile ( { scopes : ssoAccountAccessScopes } )
334
+
335
+ // Equivalent profiles from multiple sources is a fairly rare situation right now
336
+ // Ideally they would be de-duped although the implementation can be tricky
337
+ it ( 'can handle multiple SSO connection and does not de-dupe' , async function ( ) {
338
+ await auth . createConnection ( linkedSsoProfile )
339
+ await auth . createConnection ( otherProfile )
340
+
341
+ const connections = await auth . listAndTraverseConnections ( ) . promise ( )
342
+ assert . deepStrictEqual (
343
+ connections . map ( c => c . type ) ,
344
+ [ 'sso' , 'sso' , 'iam' , 'iam' , 'iam' , 'iam' , 'iam' , 'iam' ] ,
345
+ 'Expected two SSO connections and 3 IAM connections for each SSO connection'
346
+ )
347
+ } )
348
+
349
+ it ( 'does not stop discovery if one connection fails' , async function ( ) {
350
+ const otherProfile = createBuilderIdProfile ( { scopes : ssoAccountAccessScopes } )
351
+ await auth . createConnection ( linkedSsoProfile )
352
+ await auth . createConnection ( otherProfile )
353
+ auth . ssoClient . listAccounts . onFirstCall ( ) . rejects ( new Error ( 'No access' ) )
354
+ const connections = await auth . listAndTraverseConnections ( ) . promise ( )
355
+ assert . deepStrictEqual (
356
+ connections . map ( c => c . type ) ,
357
+ [ 'sso' , 'sso' , 'iam' , 'iam' , 'iam' ]
358
+ )
359
+ } )
360
+ } )
361
+ } )
362
+
226
363
describe ( 'AuthNode' , function ( ) {
227
364
it ( 'shows a message to create a connection if no connections exist' , async function ( ) {
228
365
const node = new AuthNode ( auth )
0 commit comments