Skip to content

Commit

Permalink
NEW (Extension) @W-16096256@ Send class content as request to ApexGur…
Browse files Browse the repository at this point in the history
…u and get requestId back (#114)
  • Loading branch information
jag-j authored Aug 5, 2024
1 parent 5d043e0 commit f8e2644
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 101 deletions.
95 changes: 95 additions & 0 deletions src/apexguru/apex-guru-service.ts
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
})
});

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[];
}
24 changes: 3 additions & 21 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import {ScanRunner} from './lib/scanner';
import {SettingsManager} from './lib/settings';
import {SfCli} from './lib/sf-cli';

import {RuleResult, ApexGuruAuthResponse} from './types';
import {RuleResult} from './types';
import {DiagnosticManager} from './lib/diagnostics';
import {messages} from './lib/messages';
import {Fixer} from './lib/fixer';
import { CoreExtensionService, TelemetryService } from './lib/core-extension-service';
import * as Constants from './lib/constants';
import * as path from 'path';
import { SIGKILL } from 'constants';
import * as ApexGuruFunctions from './apexguru/apex-guru-service'

export type RunInfo = {
diagnosticCollection?: vscode.DiagnosticCollection;
Expand Down Expand Up @@ -53,7 +54,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode
await CoreExtensionService.loadDependencies(context, outputChannel);

// Set the necessary flags to control showing the command
await vscode.commands.executeCommand('setContext', 'sfca.apexGuruEnabled', Constants.APEX_GURU_FEATURE_FLAG_ENABLED && await _isApexGuruEnabledInOrg());
await vscode.commands.executeCommand('setContext', 'sfca.apexGuruEnabled', Constants.APEX_GURU_FEATURE_FLAG_ENABLED && await ApexGuruFunctions.isApexGuruEnabledInOrg(outputChannel));

// Define a diagnostic collection in the `activate()` scope so it can be used repeatedly.
diagnosticCollection = vscode.languages.createDiagnosticCollection('sfca');
Expand Down Expand Up @@ -141,25 +142,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<vscode
return Promise.resolve(context);
}

export async function _isApexGuruEnabledInOrg(): 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;
}
}

async function _runDfa(context: vscode.ExtensionContext) {
if (violationsCacheExists()) {
const choice = await vscode.window.showQuickPick(
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const WORKSPACE_DFA_PROCESS = 'dfaScanProcess';

// apex guru APIS
export const APEX_GURU_AUTH_ENDPOINT = '/services/data/v62.0/apexguru/validate'
export const APEX_GURU_REQUEST = '/services/data/v62.0/apexguru/request'

// feature gates
export const APEX_GURU_FEATURE_FLAG_ENABLED = false;
149 changes: 149 additions & 0 deletions src/test/suite/apexguru/apex-guru-service.test.ts
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);
}
});
});
});
76 changes: 1 addition & 75 deletions src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {expect} from 'chai';
import path = require('path');
import {SfCli} from '../../lib/sf-cli';
import Sinon = require('sinon');
import { _runAndDisplayPathless, _runAndDisplayDfa, _clearDiagnostics, _shouldProceedWithDfaRun, _stopExistingDfaRun, _isValidFileForAnalysis, _isApexGuruEnabledInOrg, verifyPluginInstallation, _clearDiagnosticsForSelectedFiles, _removeDiagnosticsInRange, RunInfo } from '../../extension';
import { _runAndDisplayPathless, _runAndDisplayDfa, _clearDiagnostics, _shouldProceedWithDfaRun, _stopExistingDfaRun, _isValidFileForAnalysis, verifyPluginInstallation, _clearDiagnosticsForSelectedFiles, _removeDiagnosticsInRange, RunInfo } from '../../extension';
import {messages} from '../../lib/messages';
import {CoreExtensionService, TelemetryService} from '../../lib/core-extension-service';
import * as Constants from '../../lib/constants';
Expand Down Expand Up @@ -386,80 +386,6 @@ suite('Extension Test Suite', () => {
});
});

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 _isApexGuruEnabledInOrg();

// ===== 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 _isApexGuruEnabledInOrg();

// ===== 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 _isApexGuruEnabledInOrg();

// ===== 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('#isValidFileForAnalysis', () => {
test('Returns true for valid files', async() => {
// ===== SETUP ===== and ===== ASSERTIONS =====
Expand Down
Loading

0 comments on commit f8e2644

Please sign in to comment.