Skip to content

Commit 6cb410d

Browse files
committed
Add unit test
1 parent 81aec3c commit 6cb410d

File tree

5 files changed

+153
-5
lines changed

5 files changed

+153
-5
lines changed

packages/amazonq/src/lsp/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@ export async function startLanguageServer(
178178
await client.onReady()
179179
AuthUtil.create(new auth2.LanguageClientAuth(client, clientId, encryptionKey))
180180

181+
// Migrate SSO connections from old Auth to the LSP identity server
182+
// This function only migrates connections once
183+
// This call can be removed once all/most users have updated to the latest AmazonQ version
181184
try {
182185
await AuthUtil.instance.migrateSsoConnectionToLsp(clientName)
183186
} catch (e) {

packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
import assert from 'assert'
77
import * as sinon from 'sinon'
8-
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
8+
import * as path from 'path'
9+
import { AuthUtil, amazonQScopes } from 'aws-core-vscode/codewhisperer'
910
import { createTestAuthUtil } from 'aws-core-vscode/test'
10-
import { constants } from 'aws-core-vscode/auth'
11+
import { constants, cache } from 'aws-core-vscode/auth'
1112
import { auth2 } from 'aws-core-vscode/auth'
13+
import { mementoUtils, makeTemporaryToolkitFolder, tryRemoveFolder, fs } from 'aws-core-vscode/shared'
1214

1315
describe('AuthUtil', async function () {
1416
let auth: any
@@ -193,4 +195,145 @@ describe('AuthUtil', async function () {
193195
assert.ok(clearCacheSpy.called)
194196
})
195197
})
198+
199+
describe('migrateSsoConnectionToLsp', function () {
200+
let memento: any
201+
let cacheDir: string
202+
let fromRegistrationFile: string
203+
let fromTokenFile: string
204+
205+
const validProfile = {
206+
type: 'sso',
207+
startUrl: 'https://test2.com',
208+
ssoRegion: 'us-east-1',
209+
scopes: amazonQScopes,
210+
metadata: {
211+
connectionState: 'valid',
212+
},
213+
}
214+
beforeEach(async function () {
215+
memento = {
216+
get: sinon.stub(),
217+
update: sinon.stub().resolves(),
218+
}
219+
cacheDir = await makeTemporaryToolkitFolder()
220+
221+
sinon.stub(mementoUtils, 'getEnvironmentSpecificMemento').returns(memento)
222+
sinon.stub(cache, 'getCacheDir').returns(cacheDir)
223+
224+
fromTokenFile = cache.getTokenCacheFile(cacheDir, 'profile1')
225+
const registrationKey = {
226+
startUrl: validProfile.startUrl,
227+
region: validProfile.ssoRegion,
228+
scopes: amazonQScopes,
229+
}
230+
fromRegistrationFile = cache.getRegistrationCacheFile(cacheDir, registrationKey)
231+
232+
const registrationData = { test: 'registration' }
233+
const tokenData = { test: 'token' }
234+
235+
await fs.writeFile(fromRegistrationFile, JSON.stringify(registrationData))
236+
await fs.writeFile(fromTokenFile, JSON.stringify(tokenData))
237+
})
238+
239+
afterEach(async function () {
240+
await Promise.all([tryRemoveFolder(cacheDir)])
241+
sinon.restore()
242+
})
243+
244+
it('migrates valid SSO connection', async function () {
245+
memento.get.returns({ profile1: validProfile })
246+
247+
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()
248+
249+
await auth.migrateSsoConnectionToLsp('test-client')
250+
251+
assert.ok(updateProfileStub.calledOnce)
252+
assert.ok(memento.update.calledWith('auth.profiles', undefined))
253+
254+
const files = await fs.readdir(cacheDir)
255+
assert.strictEqual(files.length, 2) // Should have both the token and registration file
256+
257+
// Verify file contents were preserved
258+
const newFiles = files.map((f) => path.join(cacheDir, f[0]))
259+
for (const file of newFiles) {
260+
const content = await fs.readFileText(file)
261+
const parsed = JSON.parse(content)
262+
assert.ok(parsed.test === 'registration' || parsed.test === 'token')
263+
}
264+
})
265+
266+
it('does not migrate if no matching SSO profile exists', async function () {
267+
const mockProfiles = {
268+
'test-profile': {
269+
type: 'iam',
270+
startUrl: 'https://test.com',
271+
ssoRegion: 'us-east-1',
272+
},
273+
}
274+
memento.get.returns(mockProfiles)
275+
276+
await auth.migrateSsoConnectionToLsp('test-client')
277+
278+
// Assert that the file names have not updated
279+
const files = await fs.readdir(cacheDir)
280+
assert.ok(files.length === 2)
281+
assert.ok(await fs.exists(fromRegistrationFile))
282+
assert.ok(await fs.exists(fromTokenFile))
283+
assert.ok(!memento.update.called)
284+
})
285+
286+
it('migrates only profile with matching scopes', async function () {
287+
const mockProfiles = {
288+
profile1: validProfile,
289+
profile2: {
290+
type: 'sso',
291+
startUrl: 'https://test.com',
292+
ssoRegion: 'us-east-1',
293+
scopes: ['different:scope'],
294+
metadata: {
295+
connectionState: 'valid',
296+
},
297+
},
298+
}
299+
memento.get.returns(mockProfiles)
300+
301+
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()
302+
303+
await auth.migrateSsoConnectionToLsp('test-client')
304+
305+
assert.ok(updateProfileStub.calledOnce)
306+
assert.ok(memento.update.calledWith('auth.profiles', undefined))
307+
assert.deepStrictEqual(updateProfileStub.firstCall.args[0], {
308+
startUrl: validProfile.startUrl,
309+
region: validProfile.ssoRegion,
310+
scopes: validProfile.scopes,
311+
})
312+
})
313+
314+
it('uses valid connection state when multiple profiles exist', async function () {
315+
const mockProfiles = {
316+
profile2: {
317+
...validProfile,
318+
metadata: {
319+
connectionState: 'invalid',
320+
},
321+
},
322+
profile1: validProfile,
323+
}
324+
memento.get.returns(mockProfiles)
325+
326+
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()
327+
328+
await auth.migrateSsoConnectionToLsp('test-client')
329+
330+
assert.ok(
331+
updateProfileStub.calledWith({
332+
startUrl: validProfile.startUrl,
333+
region: validProfile.ssoRegion,
334+
scopes: validProfile.scopes,
335+
})
336+
)
337+
})
338+
})
196339
})

packages/core/src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { Auth } from './auth'
2323
export { CredentialsStore } from './credentials/store'
2424
export { LoginManager } from './deprecated/loginManager'
2525
export * as constants from './sso/constants'
26+
export * as cache from './sso/cache'
2627
export * as authUtils from './utils'
2728
export * as auth2 from './auth2'
2829
export * as SsoAccessTokenProvider from './sso/ssoAccessTokenProvider'

packages/core/src/codewhisperer/util/authUtil.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode'
77
import * as localizedText from '../../shared/localizedText'
88
import * as nls from 'vscode-nls'
9-
import * as fs from 'fs'
9+
import { fs } from '../../shared/fs/fs'
1010
import * as path from 'path'
1111
import * as crypto from 'crypto'
1212
import { ToolkitError } from '../../shared/errors'
@@ -403,8 +403,8 @@ export class AuthUtil implements IAuthProvider {
403403
const toTokenFile = filePath(this.profileName)
404404

405405
try {
406-
fs.renameSync(fromRegistrationFile, toRegistrationFile)
407-
fs.renameSync(fromTokenFile, toTokenFile)
406+
await fs.rename(fromRegistrationFile, toRegistrationFile)
407+
await fs.rename(fromTokenFile, toTokenFile)
408408
getLogger().debug('Successfully renamed registration and token files')
409409
} catch (err) {
410410
getLogger().error(`Failed to rename files during migration: ${err}`)

packages/core/src/shared/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,4 @@ export * as BaseLspInstaller from './lsp/baseLspInstaller'
7474
export * as collectionUtil from './utilities/collectionUtils'
7575
export * from './datetime'
7676
export * from './performance/marks'
77+
export * as mementoUtils from './utilities/mementos'

0 commit comments

Comments
 (0)