-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NEW (Extension) @W-16096256@ Send class content as request to ApexGuru and get requestId back #114
Changes from all commits
200b474
a7e840d
aab9d47
b495788
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, Inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
import * as vscode from 'vscode'; | ||
import * as fspromises from 'fs/promises'; | ||
import { CoreExtensionService } from '../lib/core-extension-service'; | ||
import * as Constants from '../lib/constants'; | ||
|
||
export async function isApexGuruEnabledInOrg(outputChannel: vscode.LogOutputChannel): Promise<boolean> { | ||
try { | ||
const connection = await CoreExtensionService.getConnection(); | ||
const response:ApexGuruAuthResponse = await connection.request({ | ||
method: 'GET', | ||
url: Constants.APEX_GURU_AUTH_ENDPOINT, | ||
body: '' | ||
}); | ||
return response.status == 'Success'; | ||
} catch(e) { | ||
// This could throw an error for a variety of reasons. The API endpoint has not been deployed to the instance, org has no perms, timeouts etc,. | ||
// In all of these scenarios, we return false. | ||
const errMsg = e instanceof Error ? e.message : e as string; | ||
outputChannel.error('***ApexGuru perm check failed with error:***' + errMsg); | ||
outputChannel.show(); | ||
return false; | ||
} | ||
} | ||
|
||
export async function runApexGuruOnFile(selection: vscode.Uri, outputChannel: vscode.LogOutputChannel) { | ||
try { | ||
const requestId = await initiateApexGuruRequest(selection, outputChannel); | ||
// TODO: Logging the request Id for easy QA. Future stories will use this requestId to poll and retrieve the Apex Guru report. | ||
outputChannel.appendLine('***Apex Guru request Id:***' + requestId); | ||
} catch (e) { | ||
const errMsg = e instanceof Error ? e.message : e as string; | ||
outputChannel.appendLine('***Apex Guru initiate request failed***'); | ||
outputChannel.appendLine(errMsg); | ||
} | ||
} | ||
|
||
export async function initiateApexGuruRequest(selection: vscode.Uri, outputChannel: vscode.LogOutputChannel): Promise<string> { | ||
const fileContent = await fileSystem.readFile(selection.fsPath); | ||
const base64EncodedContent = Buffer.from(fileContent).toString('base64'); | ||
const connection = await CoreExtensionService.getConnection(); | ||
const response: ApexGuruInitialResponse = await connection.request({ | ||
method: 'POST', | ||
url: Constants.APEX_GURU_REQUEST, | ||
body: JSON.stringify({ | ||
classContent: base64EncodedContent | ||
}) | ||
}); | ||
Comment on lines
+48
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like you are assuming that the response will always be of type ApexGuruInitialResponse. But is it possible that you do not get a response back that has a status field? I suggest you move your ApexGuruInitialResponse and other types into typescript officially (in a .ts - not a .d.ts file) to be compiled, then this will complain unless you cast with "as ApexGuruInitialResponse" to the request output. But do we really want to do this casting? I would guess that you would want to first check the status code of the response. But now I see that the request method is templated so it is inferring the output type. Do we know if the response object universally as something like a response code that can be used? Typically when making http requests, we validate the response to see if it is a 2** or a 4** or a 5** before we try to parse the response body. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved the ApexGuruInitialResponse out out of the d.ts file. I noticed we don't get 2**/4** responses as part of the response. I'll bring this up with the ApexGuru team tomorrow in the meeting. I hope it should be easy fix for them. Btw, the connection request also does not give the status codes. So, I'll have to leave this as is for now. |
||
|
||
if (response.status != 'new' && response.status != 'success') { | ||
outputChannel.warn('***Apex Guru returned unexpected response:***' + response.status); | ||
throw Error('***Apex Guru returned unexpected response:***' + response.status); | ||
} | ||
|
||
const requestId = response.requestId; | ||
return requestId; | ||
} | ||
|
||
export const fileSystem = { | ||
readFile: (path: string) => fspromises.readFile(path, 'utf8') | ||
}; | ||
|
||
export type ApexGuruAuthResponse = { | ||
status: string; | ||
} | ||
|
||
export type ApexGuruInitialResponse = { | ||
status: string; | ||
requestId: string; | ||
message: string; | ||
} | ||
|
||
export type ApexGuruQueryResponse = { | ||
status: string; | ||
message: string; | ||
report: string; | ||
} | ||
|
||
export type ApexGuruProperty = { | ||
name: string; | ||
value: string; | ||
}; | ||
|
||
export type ApexGuruReport = { | ||
id: string; | ||
type: string; | ||
value: string; | ||
properties: ApexGuruProperty[]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
/* | ||
* Copyright (c) 2024, Salesforce, Inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: BSD-3-Clause | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
*/ | ||
|
||
import {expect} from 'chai'; | ||
import Sinon = require('sinon'); | ||
import {CoreExtensionService} from '../../../lib/core-extension-service'; | ||
import * as Constants from '../../../lib/constants'; | ||
import * as ApexGuruFunctions from '../../../apexguru/apex-guru-service' | ||
|
||
// You can import and use all API from the 'vscode' module | ||
// as well as import your extension to test it | ||
import * as vscode from 'vscode'; | ||
|
||
suite('Apex Guru Test Suite', () => { | ||
let outputChannel = vscode.window.createOutputChannel('Salesforce Code Analyzer', {log: true}); | ||
|
||
suite('#_isApexGuruEnabledInOrg', () => { | ||
let getConnectionStub: Sinon.SinonStub; | ||
let requestStub: Sinon.SinonStub; | ||
|
||
setup(() => { | ||
getConnectionStub = Sinon.stub(CoreExtensionService, 'getConnection'); | ||
requestStub = Sinon.stub(); | ||
}); | ||
|
||
teardown(() => { | ||
Sinon.restore(); | ||
}); | ||
|
||
test('Returns true if response status is Success', async () => { | ||
// ===== SETUP ===== | ||
getConnectionStub.resolves({ | ||
request: requestStub.resolves({ status: 'Success' }) | ||
}); | ||
|
||
// ===== TEST ===== | ||
const result = await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel); | ||
|
||
// ===== ASSERTIONS ===== | ||
expect(result).to.be.true; | ||
Sinon.assert.calledOnce(getConnectionStub); | ||
Sinon.assert.calledOnce(requestStub); | ||
Sinon.assert.calledWith(requestStub, { | ||
method: 'GET', | ||
url: Constants.APEX_GURU_AUTH_ENDPOINT, | ||
body: '' | ||
}); | ||
}); | ||
|
||
test('Returns false if response status is not Success', async () => { | ||
// ===== SETUP ===== | ||
getConnectionStub.resolves({ | ||
request: requestStub.resolves({ status: 'Failure' }) | ||
}); | ||
|
||
// ===== TEST ===== | ||
const result = await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel); | ||
|
||
// ===== ASSERTIONS ===== | ||
expect(result).to.be.false; | ||
Sinon.assert.calledOnce(getConnectionStub); | ||
Sinon.assert.calledOnce(requestStub); | ||
Sinon.assert.calledWith(requestStub, { | ||
method: 'GET', | ||
url: Constants.APEX_GURU_AUTH_ENDPOINT, | ||
body: '' | ||
}); | ||
}); | ||
|
||
test('Returns false if an error is thrown', async () => { | ||
// ===== SETUP ===== | ||
getConnectionStub.resolves({ | ||
request: requestStub.rejects(new Error('Resource not found')) | ||
}); | ||
|
||
// ===== TEST ===== | ||
const result = await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel); | ||
|
||
// ===== ASSERTIONS ===== | ||
expect(result).to.be.false; | ||
Sinon.assert.calledOnce(getConnectionStub); | ||
Sinon.assert.calledOnce(requestStub); | ||
Sinon.assert.calledWith(requestStub, { | ||
method: 'GET', | ||
url: Constants.APEX_GURU_AUTH_ENDPOINT, | ||
body: '' | ||
}); | ||
}); | ||
}); | ||
suite('#initiateApexGuruRequest', () => { | ||
let getConnectionStub: Sinon.SinonStub; | ||
let requestStub: Sinon.SinonStub; | ||
let readFileStub: Sinon.SinonStub; | ||
|
||
setup(() => { | ||
getConnectionStub = Sinon.stub(CoreExtensionService, 'getConnection'); | ||
requestStub = Sinon.stub(); | ||
readFileStub = Sinon.stub(ApexGuruFunctions.fileSystem, 'readFile'); | ||
}); | ||
|
||
teardown(() => { | ||
Sinon.restore(); | ||
}); | ||
|
||
test('Returns requestId if response status is new', async () => { | ||
// ===== SETUP ===== | ||
getConnectionStub.resolves({ | ||
request: requestStub.resolves({ status: 'new', requestId: '12345' }) | ||
}); | ||
readFileStub.resolves('console.log("Hello World");'); | ||
|
||
// ===== TEST ===== | ||
const result = await ApexGuruFunctions.initiateApexGuruRequest(vscode.Uri.file('dummyPath'), outputChannel); | ||
|
||
// ===== ASSERTIONS ===== | ||
expect(result).to.equal('12345'); | ||
Sinon.assert.calledOnce(getConnectionStub); | ||
Sinon.assert.calledOnce(requestStub); | ||
Sinon.assert.calledOnce(readFileStub); | ||
Sinon.assert.calledWith(requestStub, Sinon.match({ | ||
method: 'POST', | ||
url: Constants.APEX_GURU_REQUEST, | ||
body: Sinon.match.string | ||
})); | ||
}); | ||
|
||
test('Logs warning if response status is not new', async () => { | ||
// ===== SETUP ===== | ||
getConnectionStub.resolves({ | ||
request: requestStub.resolves({ status: 'failed' }) | ||
}); | ||
readFileStub.resolves('console.log("Hello World");'); | ||
const outputChannelSpy = Sinon.spy(outputChannel, 'warn'); | ||
|
||
// ===== TEST ===== | ||
try { | ||
await ApexGuruFunctions.initiateApexGuruRequest(vscode.Uri.file('dummyPath'), outputChannel); | ||
} catch (e) { | ||
// ===== ASSERTIONS ===== | ||
Sinon.assert.calledOnce(outputChannelSpy); | ||
Sinon.assert.calledWith(outputChannelSpy, Sinon.match.string); | ||
} | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we should move all these messages eventually to a message catalog. A super lightweight version that I created for the 5.x world is https://github.com/forcedotcom/code-analyzer-core/blob/dev/packages/code-analyzer-engine-api/src/messages.ts#L18 and an example of how it is used: https://github.com/forcedotcom/code-analyzer-core/blob/dev/packages/code-analyzer-eslint-engine/src/messages.ts and https://github.com/forcedotcom/code-analyzer-core/blob/dev/packages/code-analyzer-eslint-engine/src/engine.ts#L117
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! I would like to follow the pattern that IDX team follows that helps with i18n as well. I am not doing it as part of this. But definitely worth doing it when the 5.x integration work happens in the extension.