Skip to content

Commit 1b3681c

Browse files
authored
feat(amazonq lsp): add authFollowUpClicked implementation (aws#6886)
## Problem There was no implementation for the `authFollowUpClicked` message received from LSP for Amazon Q. Because of that, the `authenticate` button that appears in case of expired SSO connection had no effect. ## Solution I added the implementation as follows: * If the received `authFollowUpType` is `'re-auth'` or `'missing_scopes'`, initiate a re-authentication * If the received `authFollowUpType` is `'full-auth'` or `'use-supported-auth'`, delete the connection so a new full authentication flow kicks off I added a unit test file for this message type, which is extendable for every command handled in `registerLanguageServerEventListener` --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 708f538 commit 1b3681c

File tree

3 files changed

+158
-2
lines changed

3 files changed

+158
-2
lines changed

packages/amazonq/src/lsp/chat/messages.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AUTH_FOLLOW_UP_CLICKED,
1010
CHAT_OPTIONS,
1111
COPY_TO_CLIPBOARD,
12+
AuthFollowUpType,
1213
} from '@aws/chat-client-ui-types'
1314
import {
1415
ChatResult,
@@ -25,6 +26,7 @@ import { window } from 'vscode'
2526
import { Disposable, LanguageClient, Position, State, TextDocumentIdentifier } from 'vscode-languageclient'
2627
import * as jose from 'jose'
2728
import { AmazonQChatViewProvider } from './webviewProvider'
29+
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
2830

2931
export function registerLanguageServerEventListener(languageClient: LanguageClient, provider: AmazonQChatViewProvider) {
3032
languageClient.onDidChangeState(({ oldState, newState }) => {
@@ -77,10 +79,33 @@ export function registerMessageListeners(
7779
})
7880
break
7981
}
80-
case AUTH_FOLLOW_UP_CLICKED:
81-
// TODO hook this into auth
82+
case AUTH_FOLLOW_UP_CLICKED: {
8283
languageClient.info('[VSCode Client] AuthFollowUp clicked')
84+
const authType = message.params.authFollowupType
85+
const reAuthTypes: AuthFollowUpType[] = ['re-auth', 'missing_scopes']
86+
const fullAuthTypes: AuthFollowUpType[] = ['full-auth', 'use-supported-auth']
87+
88+
if (reAuthTypes.includes(authType)) {
89+
try {
90+
await AuthUtil.instance.reauthenticate()
91+
} catch (e) {
92+
languageClient.error(
93+
`[VSCode Client] Failed to re-authenticate after AUTH_FOLLOW_UP_CLICKED: ${(e as Error).message}`
94+
)
95+
}
96+
}
97+
98+
if (fullAuthTypes.includes(authType)) {
99+
try {
100+
await AuthUtil.instance.secondaryAuth.deleteConnection()
101+
} catch (e) {
102+
languageClient.error(
103+
`[VSCode Client] Failed to authenticate after AUTH_FOLLOW_UP_CLICKED: ${(e as Error).message}`
104+
)
105+
}
106+
}
83107
break
108+
}
84109
case chatRequestType.method: {
85110
const partialResultToken = uuidv4()
86111
const chatDisposable = languageClient.onProgress(chatRequestType, partialResultToken, (partialResult) =>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as sinon from 'sinon'
7+
import { LanguageClient } from 'vscode-languageclient'
8+
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
9+
import { registerMessageListeners } from '../../../../../src/lsp/chat/messages'
10+
import { AmazonQChatViewProvider } from '../../../../../src/lsp/chat/webviewProvider'
11+
import { secondaryAuth, authConnection, AuthFollowUpType } from 'aws-core-vscode/amazonq'
12+
13+
describe('registerMessageListeners', () => {
14+
let languageClient: LanguageClient
15+
let provider: AmazonQChatViewProvider
16+
let sandbox: sinon.SinonSandbox
17+
let messageHandler: (message: any) => void | Promise<void>
18+
let errorStub: sinon.SinonStub
19+
20+
beforeEach(() => {
21+
sandbox = sinon.createSandbox()
22+
errorStub = sandbox.stub()
23+
24+
languageClient = {
25+
info: sandbox.stub(),
26+
error: errorStub,
27+
sendNotification: sandbox.stub(),
28+
} as unknown as LanguageClient
29+
30+
provider = {
31+
webview: {
32+
onDidReceiveMessage: (callback: (message: any) => void | Promise<void>) => {
33+
messageHandler = callback
34+
return {
35+
dispose: (): void => {},
36+
}
37+
},
38+
},
39+
} as any
40+
41+
registerMessageListeners(languageClient, provider, Buffer.from('test-key'))
42+
})
43+
44+
afterEach(() => {
45+
sandbox.restore()
46+
})
47+
48+
describe('AUTH_FOLLOW_UP_CLICKED', () => {
49+
let mockAuthUtil: AuthUtil
50+
let deleteConnectionStub: sinon.SinonStub
51+
let reauthenticateStub: sinon.SinonStub
52+
53+
const authFollowUpClickedCommand = 'authFollowUpClicked'
54+
55+
interface TestCase {
56+
authType: AuthFollowUpType
57+
stubToReject: sinon.SinonStub
58+
errorMessage: string
59+
}
60+
61+
const testFailure = async (testCase: TestCase) => {
62+
testCase.stubToReject.rejects(new Error())
63+
64+
await messageHandler({
65+
command: authFollowUpClickedCommand,
66+
params: {
67+
authFollowupType: testCase.authType,
68+
},
69+
})
70+
71+
sinon.assert.calledOnce(errorStub)
72+
sinon.assert.calledWith(errorStub, sinon.match(testCase.errorMessage))
73+
}
74+
75+
beforeEach(() => {
76+
deleteConnectionStub = sandbox.stub().resolves()
77+
reauthenticateStub = sandbox.stub().resolves()
78+
79+
mockAuthUtil = {
80+
reauthenticate: reauthenticateStub,
81+
secondaryAuth: {
82+
deleteConnection: deleteConnectionStub,
83+
} as unknown as secondaryAuth.SecondaryAuth<authConnection.Connection>,
84+
} as unknown as AuthUtil
85+
86+
sandbox.replaceGetter(AuthUtil, 'instance', () => mockAuthUtil)
87+
})
88+
89+
it('handles re-authentication request', async () => {
90+
await messageHandler({
91+
command: authFollowUpClickedCommand,
92+
params: {
93+
authFollowupType: 're-auth',
94+
},
95+
})
96+
97+
sinon.assert.calledOnce(reauthenticateStub)
98+
sinon.assert.notCalled(deleteConnectionStub)
99+
})
100+
101+
it('handles full authentication request', async () => {
102+
await messageHandler({
103+
command: authFollowUpClickedCommand,
104+
params: {
105+
authFollowupType: 'full-auth',
106+
},
107+
})
108+
109+
sinon.assert.notCalled(reauthenticateStub)
110+
sinon.assert.calledOnce(deleteConnectionStub)
111+
})
112+
113+
it('logs error if re-authentication fails', async () => {
114+
await testFailure({
115+
authType: 're-auth',
116+
stubToReject: reauthenticateStub,
117+
errorMessage: 'Failed to re-authenticate',
118+
})
119+
})
120+
121+
it('logs error if full authentication fails', async () => {
122+
await testFailure({
123+
authType: 'full-auth',
124+
stubToReject: deleteConnectionStub,
125+
errorMessage: 'Failed to authenticate',
126+
})
127+
})
128+
})
129+
})

packages/core/src/amazonq/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export { extractAuthFollowUp } from './util/authUtils'
4646
export { Messenger } from './commons/connector/baseMessenger'
4747
export * from './lsp/config'
4848
export * as WorkspaceLspInstaller from './lsp/workspaceInstaller'
49+
export * as secondaryAuth from '../auth/secondaryAuth'
50+
export * as authConnection from '../auth/connection'
4951
import { FeatureContext } from '../shared/featureConfig'
5052

5153
/**

0 commit comments

Comments
 (0)