Skip to content
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

Getting Org connection from Salesforce Extensions services #55

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"activationEvents": [
"onStartupFinished"
],
"extensionDependencies": [
"salesforce.salesforcedx-vscode-core"
],
"main": "./out/extension.js",
"l10n": "./l10n",
"contributes": {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/wizard/deployToOrgCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import path = require('path');
import * as path from 'path';
import { Uri, commands, window, workspace, l10n } from 'vscode';

export class DeployToOrgCommand {
Expand Down
15 changes: 15 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,23 @@
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import * as onboardingWizard from './commands/wizard/onboardingWizard';
import { CoreExtensionService } from './services/CoreExtensionService';

export function activate(context: vscode.ExtensionContext) {
// We need to do this first in case any other services need access to those provided by the core extension
try {
CoreExtensionService.loadDependencies();
} catch (err) {
console.error(err);
vscode.window.showErrorMessage(
vscode.l10n.t(
'Failed to activate the extension! Could not load required services from the Salesforce Extension Pack: {0}',
(err as Error).message
)
);
return;
}

onboardingWizard.registerCommand(context);
onboardingWizard.onActivate(context);
}
Expand Down
83 changes: 83 additions & 0 deletions src/services/CoreExtensionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { extensions } from 'vscode';
import { satisfies, valid } from 'semver';
import type { CoreExtensionApi, WorkspaceContext } from '../types';
import {
CORE_EXTENSION_ID,
MINIMUM_REQUIRED_VERSION_CORE_EXTENSION
} from '../utils/constants';

const NOT_INITIALIZED_ERROR = 'CoreExtensionService not initialized';
const CORE_EXTENSION_NOT_FOUND = 'Core extension not found';
const WORKSPACE_CONTEXT_NOT_FOUND = 'Workspace Context not found';
const coreExtensionMinRequiredVersionError =
'You are running an older version of the Salesforce CLI Integration VSCode Extension. Please update the Salesforce Extension pack and try again.';

export class CoreExtensionService {
private static initialized = false;
private static workspaceContext: WorkspaceContext;

static loadDependencies() {
if (!CoreExtensionService.initialized) {
const coreExtension = extensions.getExtension(CORE_EXTENSION_ID);
if (!coreExtension) {
throw new Error(CORE_EXTENSION_NOT_FOUND);
}
const coreExtensionVersion = coreExtension.packageJSON.version;
if (
!CoreExtensionService.isAboveMinimumRequiredVersion(
MINIMUM_REQUIRED_VERSION_CORE_EXTENSION,
coreExtensionVersion
)
) {
throw new Error(coreExtensionMinRequiredVersionError);
}

const coreExtensionApi = coreExtension.exports as CoreExtensionApi;

CoreExtensionService.initializeWorkspaceContext(
coreExtensionApi?.services.WorkspaceContext
);

CoreExtensionService.initialized = true;
}
}

private static initializeWorkspaceContext(
workspaceContext: WorkspaceContext | undefined
) {
if (!workspaceContext) {
throw new Error(WORKSPACE_CONTEXT_NOT_FOUND);
}
CoreExtensionService.workspaceContext =
workspaceContext.getInstance(false);
}

private static isAboveMinimumRequiredVersion(
minRequiredVersion: string,
actualVersion: string
) {
// Check to see if version is in the expected MAJOR.MINOR.PATCH format
if (!valid(actualVersion)) {
console.debug(
'Invalid version format found for the Core Extension.' +
`\nActual version: ${actualVersion}` +
`\nMinimum required version: ${minRequiredVersion}`
);
}
return satisfies(actualVersion, '>=' + minRequiredVersion);
}

static getWorkspaceContext(): WorkspaceContext {
if (CoreExtensionService.initialized) {
return CoreExtensionService.workspaceContext;
}
throw new Error(NOT_INITIALIZED_ERROR);
}
}
7 changes: 7 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
export * from './CoreExtensionService';
12 changes: 12 additions & 0 deletions src/test/TestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { mkdtemp, rm, stat } from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import * as process from 'process';
import * as sinon from 'sinon';
import { WorkspaceUtils } from '../utils/workspaceUtils';

export class TempProjectDirManager {
readonly projectDir: string;
Expand Down Expand Up @@ -65,3 +67,13 @@ export function createPlatformAbsolutePath(...pathArgs: string[]): string {
}
return absPath;
}

// Create a stub of WorkspaceUtis.getWorkspaceDir() that returns a path to
// a temporary directory.
export function setupTempWorkspaceDirectoryStub(
projectDirManager: TempProjectDirManager
): sinon.SinonStub<[], string> {
const getWorkspaceDirStub = sinon.stub(WorkspaceUtils, 'getWorkspaceDir');
getWorkspaceDirStub.returns(projectDirManager.projectDir);
return getWorkspaceDirStub;
}
46 changes: 43 additions & 3 deletions src/test/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

import * as path from 'path';

import { runTests } from '@vscode/test-electron';
import {
downloadAndUnzipVSCode,
resolveCliArgsFromVSCodeExecutablePath,
runTests
} from '@vscode/test-electron';
import { spawnSync } from 'child_process';
import { CORE_EXTENSION_ID } from '../utils/constants';

async function main() {
try {
Expand All @@ -24,8 +30,42 @@ async function main() {
process.env['CODE_COVERAGE'] = '1';
}

// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
// Download VS Code, unzip it and run the integration tests.
// NB: We'll use the 'stable' version of VSCode for tests, to catch
// potential incompatibilities in newer versions than the minmum we
// support in the `engines` section of our package.
const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
const [cliPath, ...args] =
resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);

// Install the Salesforce Extensions, which is a pre-req for our
// extension. Bail if there's an error.
const installExtensionDepsOuput = spawnSync(
cliPath,
[...args, '--install-extension', CORE_EXTENSION_ID],
{ stdio: 'inherit', encoding: 'utf-8' }
);
if (installExtensionDepsOuput.error) {
console.error(
`Error installing Salesforce Extensions in test: ${installExtensionDepsOuput.error.message}`
);
throw installExtensionDepsOuput.error;
}
if (
installExtensionDepsOuput.status &&
installExtensionDepsOuput.status !== 0
) {
const installNonZeroError = `Install of Salesforce Extensions finished with status ${installExtensionDepsOuput.status}. See console output for more information.`;
console.error(installNonZeroError);
throw new Error(installNonZeroError);
}

// All clear! Should be able to run the tests.
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
vscodeExecutablePath
});
} catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
Expand Down
70 changes: 43 additions & 27 deletions src/test/suite/commands/wizard/lwcGenerationCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import * as fs from 'fs';
import * as path from 'path';
import { afterEach, beforeEach } from 'mocha';
import {
LwcGenerationCommand,
SObjectQuickActionStatus
} from '../../../../commands/wizard/lwcGenerationCommand';
import { WorkspaceUtils } from '../../../../utils/workspaceUtils';
import { TempProjectDirManager } from '../../../TestHelper';

suite('LWC Generation Command Test Suite', () => {
beforeEach(function () {});
Expand Down Expand Up @@ -94,43 +96,57 @@ suite('LWC Generation Command Test Suite', () => {
});

test('Should return error status for landing page with invalid json', async () => {
const dirManager = await TempProjectDirManager.createTempProjectDir();
const getWorkspaceDirStub = sinon.stub(
WorkspaceUtils,
'getStaticResourcesDir'
);
getWorkspaceDirStub.returns(Promise.resolve('.'));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach was breaking for me locally. Always dicey to use . as a working directory, as it's hard to know where the code is executed from. Moved to creating a temporary directory for this I/O.

const fsAccess = sinon.stub(fs, 'access');
fsAccess.returns();
const invalidJsonFile = 'landing_page.json';
const invalidJsonContents = 'invalid_json_here';
fs.writeFileSync(invalidJsonFile, invalidJsonContents, 'utf8');

const status = await LwcGenerationCommand.getSObjectsFromLandingPage();

assert.ok(status.error && status.error.length > 0);

fs.unlinkSync(invalidJsonFile);
try {
getWorkspaceDirStub.returns(Promise.resolve(dirManager.projectDir));
const invalidJsonFile = 'landing_page.json';
const invalidJsonContents = 'invalid_json_here';
fs.writeFileSync(
path.join(dirManager.projectDir, invalidJsonFile),
invalidJsonContents,
'utf8'
);

const status =
await LwcGenerationCommand.getSObjectsFromLandingPage();

assert.ok(status.error && status.error.length > 0);
} finally {
getWorkspaceDirStub.restore();
await dirManager.removeDir();
}
});

test('Should return 2 sObjects', async () => {
const dirManager = await TempProjectDirManager.createTempProjectDir();
const getWorkspaceDirStub = sinon.stub(
WorkspaceUtils,
'getStaticResourcesDir'
);
getWorkspaceDirStub.returns(Promise.resolve('.'));
const fsAccess = sinon.stub(fs, 'access');
fsAccess.returns();
const validJsonFile = 'landing_page.json';
const jsonContents =
'{ "definition": "mcf/list", "properties": { "objectApiName": "Account" }, "nested": { "definition": "mcf/timedList", "properties": { "objectApiName": "Contact"} } }';
fs.writeFileSync(validJsonFile, jsonContents, 'utf8');

const status = await LwcGenerationCommand.getSObjectsFromLandingPage();

assert.equal(status.sobjects.length, 2);
assert.equal(status.sobjects[0], 'Account');
assert.equal(status.sobjects[1], 'Contact');

fs.unlinkSync(validJsonFile);
try {
getWorkspaceDirStub.returns(Promise.resolve(dirManager.projectDir));
const validJsonFile = 'landing_page.json';
const jsonContents =
'{ "definition": "mcf/list", "properties": { "objectApiName": "Account" }, "nested": { "definition": "mcf/timedList", "properties": { "objectApiName": "Contact"} } }';
fs.writeFileSync(
path.join(dirManager.projectDir, validJsonFile),
jsonContents,
'utf8'
);

const status =
await LwcGenerationCommand.getSObjectsFromLandingPage();

assert.equal(status.sobjects.length, 2);
assert.equal(status.sobjects[0], 'Account');
assert.equal(status.sobjects[1], 'Contact');
} finally {
getWorkspaceDirStub.restore();
await dirManager.removeDir();
}
});
});
39 changes: 22 additions & 17 deletions src/test/suite/utils/orgUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import { afterEach, beforeEach } from 'mocha';
import {
ConfigAggregator,
Connection,
Org,
OrgConfigProperties
} from '@salesforce/core';
import {
DescribeGlobalResult,
DescribeSObjectResult,
Field as FieldType
} from 'jsforce';
import { CoreExtensionService } from '../../../services';

suite('Org Utils Test Suite', () => {
const describeGlobalResult: DescribeGlobalResult = {
Expand Down Expand Up @@ -118,18 +118,11 @@ suite('Org Utils Test Suite', () => {
});

test('Returns list of sobjects', async () => {
const orgStub: SinonStub = sinon.stub(Org, 'create');
const stubConnection = sinon.createStubInstance(Connection);
const stubConnection = stubWorkspaceContextConnection();
stubConnection.describeGlobal.returns(
Promise.resolve(describeGlobalResult)
);

orgStub.returns({
getConnection: () => {
return stubConnection;
}
});

const sobjects = await OrgUtils.getSobjects();

assert.equal(sobjects.length, 1);
Expand Down Expand Up @@ -293,18 +286,11 @@ suite('Org Utils Test Suite', () => {
supportedScopes: null
};

const orgStub: SinonStub = sinon.stub(Org, 'create');
const stubConnection = sinon.createStubInstance(Connection);
const stubConnection = stubWorkspaceContextConnection();
stubConnection.describe
.withArgs('SomeObject')
.returns(Promise.resolve(describeSobjectResult));

orgStub.returns({
getConnection: () => {
return stubConnection;
}
});

const fields = await OrgUtils.getFieldsForSObject(
describeSobjectResult.name
);
Expand Down Expand Up @@ -380,4 +366,23 @@ suite('Org Utils Test Suite', () => {
writeRequiresMasterRead: true
};
}

function stubWorkspaceContextConnection(): sinon.SinonStubbedInstance<
Connection<any>
> {
const stubConnection = sinon.createStubInstance(Connection);
const getWorkspaceContextInstance = {
getConnection: () => {
return Promise.resolve(stubConnection);
},
onOrgChange: sinon.stub(),
getInstance: sinon.stub(),
username: sinon.stub(),
alias: sinon.stub()
};
sinon
.stub(CoreExtensionService, 'getWorkspaceContext')
.returns(getWorkspaceContextInstance);
return stubConnection;
}
});
Loading