Skip to content

Commit

Permalink
fix: add doctor diagnostic test from plugin-source (#901)
Browse files Browse the repository at this point in the history
* fix: add doctor diagnostic test from plugin-source

* fix: add test for the diagnostic
  • Loading branch information
shetzel authored Feb 12, 2024
1 parent ce15543 commit fa20f92
Show file tree
Hide file tree
Showing 5 changed files with 560 additions and 8 deletions.
19 changes: 19 additions & 0 deletions messages/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# apiVersionMismatch

The sourceApiVersion in sfdx-project.json doesn't match the apiVersion. The commands that deploy and retrieve source use the sourceApiVersion in this case. The version mismatch isn't a problem, as long as it's the behavior you actually want.

# apiVersionUnset

Neither sourceApiVersion nor apiVersion are defined. The commands that deploy and retrieve source use the max apiVersion of the target org in this case. The issue isn't a problem, as long as it's the behavior you actually want.

# maxApiVersionMismatch

The max apiVersion of the default DevHub org doesn't match the max apiVersion of the default target org. This mismatch means that the default target orgs are running different API versions. Be sure you explicitly set the apiVersion when you deploy or retrieve source, or you will likely run into problems.

# sourceApiVersionMaxMismatch

The sourceApiVersion in sfdx-project.json doesn't match the max apiVersion of the default target org. As a result, you're not using the latest features available in API version %s. The version mismatch isn't a problem, as long as it's the behavior you actually want.

# apiVersionMaxMismatch

The apiVersion doesn't match the max apiVersion of the default target org. As a result, you're not using the latest features available in API version %s. The version mismatch isn't a problem, as long as it's the behavior you actually want.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@salesforce/apex-node": "^3.0.2",
"@salesforce/core": "^6.4.4",
"@salesforce/kit": "^3.0.15",
"@salesforce/plugin-info": "^3.0.21",
"@salesforce/sf-plugins-core": "^7.1.9",
"@salesforce/source-deploy-retrieve": "^10.2.13",
"@salesforce/source-tracking": "^5.1.11",
Expand Down Expand Up @@ -91,6 +92,9 @@
}
}
},
"hooks": {
"sf-doctor-@salesforce/plugin-deploy-retrieve": "./lib/hooks/diagnostics"
},
"flexibleTaxonomy": true
},
"repository": "salesforcecli/plugin-deploy-retrieve",
Expand Down
143 changes: 143 additions & 0 deletions src/hooks/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { ConfigAggregator, Lifecycle, Logger, Messages, SfProject, OrgConfigProperties, Org } from '@salesforce/core';
import { SfDoctor } from '@salesforce/plugin-info';

type HookFunction = (options: { doctor: SfDoctor }) => Promise<[void]>;

let logger: Logger;
const getLogger = (): Logger => {
if (!logger) {
logger = Logger.childFromRoot('plugin-deploy-retrieve-diagnostics');
}
return logger;
};

const pluginName = '@salesforce/plugin-deploy-retrieve';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages(pluginName, 'diagnostics');

export const hook: HookFunction = async (options) => {
getLogger().debug(`Running SfDoctor diagnostics for ${pluginName}`);
return Promise.all([apiVersionTest(options.doctor)]);
};

// ============================
// *** DIAGNOSTIC TESTS ***
// ============================

// Gathers and compares the following API versions:
// 1. apiVersion (if set) from the sfdx config, including environment variable
// 2. sourceApiVersion (if set) from sfdx-project.json
// 3. max apiVersion of the default target dev hub org (if set)
// 4. max apiVersion of the default target org (if set)
//
// Warns if:
// 1. apiVersion and sourceApiVersion are set but not equal
// 2. apiVersion and sourceApiVersion are both not set
// 3. default devhub target org and default target org have different max apiVersions
// 4. sourceApiVersion is set and does not match max apiVersion of default target org
// 5. apiVersion is set and does not match max apiVersion of default target org
const apiVersionTest = async (doctor: SfDoctor): Promise<void> => {
getLogger().debug('Running API Version tests');

// check org-api-version from ConfigAggregator
const aggregator = await ConfigAggregator.create();
const apiVersion = aggregator.getPropertyValue<string>(OrgConfigProperties.ORG_API_VERSION);

const sourceApiVersion = await getSourceApiVersion();

const targetDevHub = aggregator.getPropertyValue<string>(OrgConfigProperties.TARGET_DEV_HUB);
const targetOrg = aggregator.getPropertyValue<string>(OrgConfigProperties.TARGET_ORG);
const targetDevHubApiVersion = targetDevHub && (await getMaxApiVersion(aggregator, targetDevHub));
const targetOrgApiVersion = targetOrg && (await getMaxApiVersion(aggregator, targetOrg));

doctor.addPluginData(pluginName, {
apiVersion,
sourceApiVersion,
targetDevHubApiVersion,
targetOrgApiVersion,
});

const testName1 = `[${pluginName}] sourceApiVersion matches apiVersion`;
let status1 = 'pass';
if (diff(sourceApiVersion, apiVersion)) {
status1 = 'warn';
doctor.addSuggestion(messages.getMessage('apiVersionMismatch'));
}
if (sourceApiVersion === undefined && apiVersion === undefined) {
status1 = 'warn';
doctor.addSuggestion(messages.getMessage('apiVersionUnset'));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName1, status: status1 });

if (targetDevHubApiVersion && targetOrgApiVersion) {
const testName2 = `[${pluginName}] default target DevHub max apiVersion matches default target org max apiVersion`;
let status2 = 'pass';
if (diff(targetDevHubApiVersion, targetOrgApiVersion)) {
status2 = 'warn';
doctor.addSuggestion(messages.getMessage('maxApiVersionMismatch'));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName2, status: status2 });
}

// Only run this test if both sourceApiVersion and the default target org max version are set.
if (sourceApiVersion?.length && targetOrgApiVersion?.length) {
const testName3 = `[${pluginName}] sourceApiVersion matches default target org max apiVersion`;
let status3 = 'pass';
if (diff(sourceApiVersion, targetOrgApiVersion)) {
status3 = 'warn';
doctor.addSuggestion(messages.getMessage('sourceApiVersionMaxMismatch', [targetOrgApiVersion]));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName3, status: status3 });
}

// Only run this test if both apiVersion and the default target org max version are set.
if (apiVersion?.length && targetOrgApiVersion?.length) {
const testName4 = `[${pluginName}] apiVersion matches default target org max apiVersion`;
let status4 = 'pass';
if (diff(apiVersion, targetOrgApiVersion)) {
status4 = 'warn';
doctor.addSuggestion(messages.getMessage('apiVersionMaxMismatch', [targetOrgApiVersion]));
}
void Lifecycle.getInstance().emit('Doctor:diagnostic', { testName: testName4, status: status4 });
}
};

// check sfdx-project.json for sourceApiVersion
const getSourceApiVersion = async (): Promise<string | undefined> => {
try {
const project = SfProject.getInstance();
const projectJson = await project.resolveProjectConfig();
return projectJson.sourceApiVersion as string | undefined;
} catch (error) {
const errMsg = (error as Error).message;
getLogger().debug(`Cannot determine sourceApiVersion due to: ${errMsg}`);
}
};

// check max API version for default orgs
const getMaxApiVersion = async (aggregator: ConfigAggregator, aliasOrUsername: string): Promise<string | undefined> => {
try {
const org = await Org.create({ aliasOrUsername, aggregator });
return await org.retrieveMaxApiVersion();
} catch (error) {
const errMsg = (error as Error).message;
getLogger().debug(`Cannot determine the max ApiVersion for org: [${aliasOrUsername}] due to: ${errMsg}`);
}
};

// Compare 2 API versions that have values and return if they are different.
// E.g.,
// Comparing undefined with 56.0 would return false.
// Comparing undefined with undefined would return false.
// Comparing 55.0 with 55.0 would return false.
// Comparing 55.0 with 56.0 would return true.
const diff = (version1: string | undefined, version2: string | undefined): boolean => {
getLogger().debug(`Comparing API versions: [${version1},${version2}]`);
return !!version1?.length && !!version2?.length && version1 !== version2;
};
214 changes: 214 additions & 0 deletions test/hooks/diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import sinon from 'sinon';
import { expect } from 'chai';
import { fromStub, StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon';
import { SfDoctor } from '@salesforce/plugin-info';
import { ConfigAggregator, Lifecycle, Messages, Org, SfProject } from '@salesforce/core';
import { TestContext } from '@salesforce/core/lib/testSetup.js';
import { hook } from '../../src/hooks/diagnostics.js';
const pluginName = '@salesforce/plugin-deploy-retrieve';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages(pluginName, 'diagnostics');

describe('Doctor diagnostics', () => {
const sandbox = new TestContext().SANDBOX;

// Stubs for:
// 1. the Doctor class needed by the hook
// 2. ConfigAggregator for apiVersion in the config
// 3. SfProject for sourceApiVersion in sfdx-project.json
// 4. Org for maxApiVersion of default devhub and target orgs
let doctorMock: SfDoctor;
let doctorStubbedType: StubbedType<SfDoctor>;
let configAggregatorMock: ConfigAggregator;
let configAggregatorStubbedType: StubbedType<ConfigAggregator>;
let sfProjectMock: SfProject;
let sfProjectStubbedType: StubbedType<SfProject>;
let orgMock: Org;
let orgStubbedType: StubbedType<Org>;
let addPluginDataStub: sinon.SinonStub;
let getPropertyValueStub: sinon.SinonStub;
let resolveProjectConfigStub: sinon.SinonStub;
let addSuggestionStub: sinon.SinonStub;
let lifecycleEmitStub: sinon.SinonStub;
let maxApiVersionStub: sinon.SinonStub;

beforeEach(() => {
doctorStubbedType = stubInterface<SfDoctor>(sandbox);
doctorMock = fromStub(doctorStubbedType);
configAggregatorStubbedType = stubInterface<ConfigAggregator>(sandbox);
configAggregatorMock = fromStub(configAggregatorStubbedType);
stubMethod(sandbox, ConfigAggregator, 'create').resolves(configAggregatorMock);
sfProjectStubbedType = stubInterface<SfProject>(sandbox);
sfProjectMock = fromStub(sfProjectStubbedType);
stubMethod(sandbox, SfProject, 'getInstance').returns(sfProjectMock);
orgStubbedType = stubInterface<Org>(sandbox);
orgMock = fromStub(orgStubbedType);
stubMethod(sandbox, Org, 'create').resolves(orgMock);
lifecycleEmitStub = stubMethod(sandbox, Lifecycle.prototype, 'emit');

// Shortening these for brevity in tests.
addPluginDataStub = doctorStubbedType.addPluginData;
addSuggestionStub = doctorStubbedType.addSuggestion;
getPropertyValueStub = configAggregatorStubbedType.getPropertyValue;
resolveProjectConfigStub = sfProjectStubbedType.resolveProjectConfig;
maxApiVersionStub = orgStubbedType.retrieveMaxApiVersion;
});

afterEach(() => {
sandbox.restore();
});

it('should warn when apiVersion does not match sourceApiVersion', async () => {
getPropertyValueStub.onFirstCall().returns('55.0');
resolveProjectConfigStub.onFirstCall().resolves({ sourceApiVersion: '52.0' });

await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: '55.0',
sourceApiVersion: '52.0',
targetDevHubApiVersion: undefined,
targetOrgApiVersion: undefined,
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() to be called once').to.equal(1);
expect(addSuggestionStub.args[0][0]).to.equal(messages.getMessage('apiVersionMismatch'));
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[0][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[0][1]).to.deep.equal({
testName: `[${pluginName}] sourceApiVersion matches apiVersion`,
status: 'warn',
});
});

it('should pass when apiVersion matches sourceApiVersion', async () => {
getPropertyValueStub.onFirstCall().returns('55.0');
resolveProjectConfigStub.onFirstCall().resolves({ sourceApiVersion: '55.0' });

await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: '55.0',
sourceApiVersion: '55.0',
targetDevHubApiVersion: undefined,
targetOrgApiVersion: undefined,
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() NOT to be called').to.equal(0);
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[0][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[0][1]).to.deep.equal({
testName: `[${pluginName}] sourceApiVersion matches apiVersion`,
status: 'pass',
});
});

it('should warn when both apiVersion and sourceApiVersion are not set', async () => {
await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: undefined,
sourceApiVersion: undefined,
targetDevHubApiVersion: undefined,
targetOrgApiVersion: undefined,
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() to be called once').to.equal(1);
expect(addSuggestionStub.args[0][0]).to.equal(messages.getMessage('apiVersionUnset'));
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[0][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[0][1]).to.deep.equal({
testName: `[${pluginName}] sourceApiVersion matches apiVersion`,
status: 'warn',
});
});

it('should warn when default devhub target org and default target org have different max apiVersions', async () => {
getPropertyValueStub.onSecondCall().returns('devhubOrg').onThirdCall().returns('scratchOrg');
maxApiVersionStub.onFirstCall().resolves('55.0').onSecondCall().resolves('56.0');

await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: undefined,
sourceApiVersion: undefined,
targetDevHubApiVersion: '55.0',
targetOrgApiVersion: '56.0',
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() to be called twice').to.equal(2);
expect(addSuggestionStub.args[1][0]).to.equal(messages.getMessage('maxApiVersionMismatch'));
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[1][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[1][1]).to.deep.equal({
testName: `[${pluginName}] default target DevHub max apiVersion matches default target org max apiVersion`,
status: 'warn',
});
});

it('should warn when sourceApiVersion and default target org max apiVersion does not match', async () => {
const targetOrgApiVersion = '56.0';
resolveProjectConfigStub.resolves({ sourceApiVersion: '55.0' });
getPropertyValueStub.onThirdCall().returns('scratchOrg');
maxApiVersionStub.onFirstCall().resolves(targetOrgApiVersion);

await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: undefined,
sourceApiVersion: '55.0',
targetDevHubApiVersion: undefined,
targetOrgApiVersion,
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() to be called once').to.equal(1);
expect(addSuggestionStub.args[0][0]).to.equal(
messages.getMessage('sourceApiVersionMaxMismatch', [targetOrgApiVersion])
);
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[1][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[1][1]).to.deep.equal({
testName: `[${pluginName}] sourceApiVersion matches default target org max apiVersion`,
status: 'warn',
});
});

it('should warn when apiVersion and default target org max apiVersion does not match', async () => {
const targetOrgApiVersion = '56.0';
getPropertyValueStub.onFirstCall().returns('55.0');
getPropertyValueStub.onThirdCall().returns('scratchOrg');
maxApiVersionStub.onFirstCall().resolves(targetOrgApiVersion);

await hook({ doctor: doctorMock });

expect(addPluginDataStub.callCount, 'Expected doctor.addPluginData() to be called once').to.equal(1);
expect(addPluginDataStub.args[0][0]).to.equal(pluginName);
expect(addPluginDataStub.args[0][1]).to.deep.equal({
apiVersion: '55.0',
sourceApiVersion: undefined,
targetDevHubApiVersion: undefined,
targetOrgApiVersion,
});
expect(addSuggestionStub.callCount, 'Expected doctor.addSuggestion() to be called once').to.equal(1);
expect(addSuggestionStub.args[0][0]).to.equal(messages.getMessage('apiVersionMaxMismatch', [targetOrgApiVersion]));
expect(lifecycleEmitStub.called).to.be.true;
expect(lifecycleEmitStub.args[1][0]).to.equal('Doctor:diagnostic');
expect(lifecycleEmitStub.args[1][1]).to.deep.equal({
testName: `[${pluginName}] apiVersion matches default target org max apiVersion`,
status: 'warn',
});
});
});
Loading

0 comments on commit fa20f92

Please sign in to comment.