@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
544 lines • 21.5 kB
JavaScript
"use strict";
/*
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MockTestOrgData = exports.StreamingMockCometClient = exports.StreamingMockCometSubscription = exports.StreamingMockSubscriptionCall = exports.shouldThrow = exports.unexpectedResult = exports.testSetup = exports.restoreContext = exports.stubContext = exports.instantiateContext = void 0;
const crypto_1 = require("crypto");
const events_1 = require("events");
const os_1 = require("os");
const path_1 = require("path");
const kit_1 = require("@salesforce/kit");
const ts_sinon_1 = require("@salesforce/ts-sinon");
const ts_types_1 = require("@salesforce/ts-types");
const configAggregator_1 = require("./config/configAggregator");
const configFile_1 = require("./config/configFile");
const connection_1 = require("./connection");
const crypto_2 = require("./crypto");
const logger_1 = require("./logger");
const messages_1 = require("./messages");
const sfdxError_1 = require("./sfdxError");
const sfdxProject_1 = require("./sfdxProject");
const streamingClient_1 = require("./status/streamingClient");
const uniqid = () => {
return crypto_1.randomBytes(16).toString('hex');
};
function getTestLocalPath(uid) {
return path_1.join(os_1.tmpdir(), uid, 'sfdx_core', 'local');
}
function getTestGlobalPath(uid) {
return path_1.join(os_1.tmpdir(), uid, 'sfdx_core', 'global');
}
function retrieveRootPathSync(isGlobal, uid = uniqid()) {
return isGlobal ? getTestGlobalPath(uid) : getTestLocalPath(uid);
}
// eslint-disable-next-line @typescript-eslint/require-await
async function retrieveRootPath(isGlobal, uid = uniqid()) {
return retrieveRootPathSync(isGlobal, uid);
}
function defaultFakeConnectionRequest() {
return Promise.resolve(ts_types_1.ensureAnyJson({ records: [] }));
}
/**
* Instantiate a @salesforce/core test context. This is automatically created by `const $$ = testSetup()`
* but is useful if you don't want to have a global stub of @salesforce/core and you want to isolate it to
* a single describe.
*
* **Note:** Call `stubContext` in your beforeEach to have clean stubs of @salesforce/core every test run.
*
* @example
* ```
* const $$ = instantiateContext();
*
* beforeEach(() => {
* stubContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
* @param sinon
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const instantiateContext = (sinon) => {
if (!sinon) {
try {
sinon = require('sinon');
}
catch (e) {
throw new Error('The package sinon was not found. Add it to your package.json and pass it in to testSetup(sinon.sandbox)');
}
}
// Import all the messages files in the sfdx-core messages dir.
// Messages.importMessagesDirectory(pathJoin(__dirname, '..', '..'));
messages_1.Messages.importMessagesDirectory(path_1.join(__dirname));
// Create a global sinon sandbox and a test logger instance for use within tests.
const defaultSandbox = sinon.createSandbox();
const testContext = {
SANDBOX: defaultSandbox,
SANDBOXES: {
DEFAULT: defaultSandbox,
CONFIG: sinon.createSandbox(),
PROJECT: sinon.createSandbox(),
CRYPTO: sinon.createSandbox(),
CONNECTION: sinon.createSandbox(),
},
TEST_LOGGER: new logger_1.Logger({
name: 'SFDX_Core_Test_Logger',
}).useMemoryLogging(),
id: uniqid(),
uniqid,
configStubs: {},
// eslint-disable-next-line @typescript-eslint/require-await
localPathRetriever: async (uid) => getTestLocalPath(uid),
localPathRetrieverSync: getTestLocalPath,
// eslint-disable-next-line @typescript-eslint/require-await
globalPathRetriever: async (uid) => getTestGlobalPath(uid),
globalPathRetrieverSync: getTestGlobalPath,
rootPathRetriever: retrieveRootPath,
rootPathRetrieverSync: retrieveRootPathSync,
fakeConnectionRequest: defaultFakeConnectionRequest,
getConfigStubContents(name, group) {
const stub = this.configStubs[name];
if (stub && stub.contents) {
if (group && stub.contents[group]) {
return ts_types_1.ensureJsonMap(stub.contents[group]);
}
else {
return stub.contents;
}
}
return {};
},
setConfigStubContents(name, value) {
if (ts_types_1.ensureString(name) && ts_types_1.isJsonMap(value)) {
this.configStubs[name] = value;
}
},
inProject(inProject = true) {
testContext.SANDBOXES.PROJECT.restore();
if (inProject) {
testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPath').callsFake(() => testContext.localPathRetriever(testContext.id));
testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPathSync').callsFake(() => testContext.localPathRetrieverSync(testContext.id));
}
else {
testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPath').rejects(new sfdxError_1.SfdxError('InvalidProjectWorkspace'));
testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPathSync').throws(new sfdxError_1.SfdxError('InvalidProjectWorkspace'));
}
},
};
return testContext;
};
exports.instantiateContext = instantiateContext;
/**
* Stub a @salesforce/core test context. This will mock out logging to a file, config file reading and writing,
* local and global path resolution, and http request using connection (soon)*.
*
* This is automatically stubbed in the global beforeEach created by
* `const $$ = testSetup()` but is useful if you don't want to have a global stub of @salesforce/core and you
* want to isolate it to a single describe.
*
* **Note:** Always call `restoreContext` in your afterEach.
*
* @example
* ```
* const $$ = instantiateContext();
*
* beforeEach(() => {
* stubContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
* @param testContext
*/
const stubContext = (testContext) => {
// Most core files create a child logger so stub this to return our test logger.
ts_sinon_1.stubMethod(testContext.SANDBOX, logger_1.Logger, 'child').returns(Promise.resolve(testContext.TEST_LOGGER));
ts_sinon_1.stubMethod(testContext.SANDBOX, logger_1.Logger, 'childFromRoot').returns(testContext.TEST_LOGGER);
testContext.inProject(true);
testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile, 'resolveRootFolder').callsFake((isGlobal) => testContext.rootPathRetriever(isGlobal, testContext.id));
testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile, 'resolveRootFolderSync').callsFake((isGlobal) => testContext.rootPathRetrieverSync(isGlobal, testContext.id));
ts_sinon_1.stubMethod(testContext.SANDBOXES.PROJECT, sfdxProject_1.SfdxProjectJson.prototype, 'doesPackageExist').callsFake(() => true);
const initStubForRead = (configFile) => {
const stub = testContext.configStubs[configFile.constructor.name] || {};
// init calls read calls getPath which sets the path on the config file the first time.
// Since read is now stubbed, make sure to call getPath to initialize it.
configFile.getPath();
// @ts-ignore set this to true to avoid an infinite loop in tests when reading config files.
configFile.hasRead = true;
return stub;
};
const readSync = function (newContents) {
const stub = initStubForRead(this);
this.setContentsFromObject(newContents || stub.contents || {});
return this.getContents();
};
const read = async function () {
const stub = initStubForRead(this);
if (stub.readFn) {
return await stub.readFn.call(this);
}
if (stub.retrieveContents) {
return readSync.call(this, await stub.retrieveContents.call(this));
}
else {
return readSync.call(this);
}
};
// Mock out all config file IO for all tests. They can restore individually if they need original functionality.
// @ts-ignore
testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile.prototype, 'readSync').callsFake(readSync);
testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile.prototype, 'read').callsFake(read);
const writeSync = function (newContents) {
if (!testContext.configStubs[this.constructor.name]) {
testContext.configStubs[this.constructor.name] = {};
}
const stub = testContext.configStubs[this.constructor.name];
if (!stub)
return;
this.setContents(newContents || this.getContents());
stub.contents = this.toObject();
};
const write = async function (newContents) {
if (!testContext.configStubs[this.constructor.name]) {
testContext.configStubs[this.constructor.name] = {};
}
const stub = testContext.configStubs[this.constructor.name];
if (!stub)
return;
if (stub.writeFn) {
return await stub.writeFn.call(this, newContents);
}
if (stub.updateContents) {
writeSync.call(this, await stub.updateContents.call(this));
}
else {
writeSync.call(this);
}
};
ts_sinon_1.stubMethod(testContext.SANDBOXES.CONFIG, configFile_1.ConfigFile.prototype, 'writeSync').callsFake(writeSync);
ts_sinon_1.stubMethod(testContext.SANDBOXES.CONFIG, configFile_1.ConfigFile.prototype, 'write').callsFake(write);
ts_sinon_1.stubMethod(testContext.SANDBOXES.CRYPTO, crypto_2.Crypto.prototype, 'getKeyChain').callsFake(() => Promise.resolve({
setPassword: () => Promise.resolve(),
getPassword: (data, cb) => cb(undefined, '12345678901234567890123456789012'),
}));
ts_sinon_1.stubMethod(testContext.SANDBOXES.CONNECTION, connection_1.Connection.prototype, 'isResolvable').resolves(true);
ts_sinon_1.stubMethod(testContext.SANDBOXES.CONNECTION, connection_1.Connection.prototype, 'request').callsFake(function (request, options) {
if (request === `${this.instanceUrl}/services/data`) {
return Promise.resolve([{ version: '42.0' }]);
}
return testContext.fakeConnectionRequest.call(this, request, options);
});
// Always start with the default and tests beforeEach or it methods can override it.
testContext.fakeConnectionRequest = defaultFakeConnectionRequest;
};
exports.stubContext = stubContext;
/**
* Restore a @salesforce/core test context. This is automatically stubbed in the global beforeEach created by
* `const $$ = testSetup()` but is useful if you don't want to have a global stub of @salesforce/core and you
* want to isolate it to a single describe.
*
* @example
* ```
* const $$ = instantiateContext();
*
* beforeEach(() => {
* stubContext($$);
* });
*
* afterEach(() => {
* restoreContext($$);
* });
* ```
* @param testContext
*/
const restoreContext = (testContext) => {
testContext.SANDBOX.restore();
Object.values(testContext.SANDBOXES).forEach((theSandbox) => theSandbox.restore());
testContext.configStubs = {};
};
exports.restoreContext = restoreContext;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const _testSetup = (sinon) => {
const testContext = exports.instantiateContext(sinon);
beforeEach(() => {
// Allow each test to have their own config aggregator
// @ts-ignore clear for testing.
delete configAggregator_1.ConfigAggregator.instance;
exports.stubContext(testContext);
});
afterEach(() => {
exports.restoreContext(testContext);
});
return testContext;
};
/**
* Use to mock out different pieces of sfdx-core to make testing easier. This will mock out
* logging to a file, config file reading and writing, local and global path resolution, and
* *http request using connection (soon)*.
*
* **Note:** The testSetup should be outside of the describe. If you need to stub per test, use
* `instantiateContext`, `stubContext`, and `restoreContext`.
* ```
* // In a mocha tests
* import testSetup from '@salesforce/core/lib/testSetup';
*
* const $$ = testSetup();
*
* describe(() => {
* it('test', () => {
* // Stub out your own method
* $$.SANDBOX.stub(MyClass.prototype, 'myMethod').returnsFake(() => {});
*
* // Set the contents that is used when aliases are read. Same for all config files.
* $$.configStubs.Aliases = { contents: { 'myTestAlias': 'user@company.com' } };
*
* // Will use the contents set above.
* const username = Aliases.fetch('myTestAlias');
* expect(username).to.equal('user@company.com');
* });
* });
* ```
*/
exports.testSetup = kit_1.once(_testSetup);
/**
* A pre-canned error for try/catch testing.
*
* **See** {@link shouldThrow}
*/
exports.unexpectedResult = new sfdxError_1.SfdxError('This code was expected to fail', 'UnexpectedResult');
/**
* Use for this testing pattern:
* ```
* try {
* await call()
* assert.fail('this should never happen');
* } catch (e) {
* ...
* }
*
* Just do this
*
* try {
* await shouldThrow(call()); // If this succeeds unexpectedResultError is thrown.
* } catch(e) {
* ...
* }
* ```
*
* @param f The async function that is expected to throw.
*/
async function shouldThrow(f) {
await f;
throw exports.unexpectedResult;
}
exports.shouldThrow = shouldThrow;
/**
* A helper to determine if a subscription will use callback or errorback.
* Enable errback to simulate a subscription failure.
*/
var StreamingMockSubscriptionCall;
(function (StreamingMockSubscriptionCall) {
StreamingMockSubscriptionCall[StreamingMockSubscriptionCall["CALLBACK"] = 0] = "CALLBACK";
StreamingMockSubscriptionCall[StreamingMockSubscriptionCall["ERRORBACK"] = 1] = "ERRORBACK";
})(StreamingMockSubscriptionCall = exports.StreamingMockSubscriptionCall || (exports.StreamingMockSubscriptionCall = {}));
/**
* Simulates a comet subscription to a streaming channel.
*/
class StreamingMockCometSubscription extends events_1.EventEmitter {
constructor(options) {
super();
this.options = options;
}
/**
* Sets up a streaming subscription callback to occur after the setTimeout event loop phase.
*
* @param callback The function to invoke.
*/
callback(callback) {
if (this.options.subscriptionCall === StreamingMockSubscriptionCall.CALLBACK) {
setTimeout(() => {
callback();
super.emit(StreamingMockCometSubscription.SUBSCRIPTION_COMPLETE);
}, 0);
}
}
/**
* Sets up a streaming subscription errback to occur after the setTimeout event loop phase.
*
* @param callback The function to invoke.
*/
errback(callback) {
if (this.options.subscriptionCall === StreamingMockSubscriptionCall.ERRORBACK) {
const error = this.options.subscriptionErrbackError;
if (!error)
return;
setTimeout(() => {
callback(error);
super.emit(StreamingMockCometSubscription.SUBSCRIPTION_FAILED);
}, 0);
}
}
}
exports.StreamingMockCometSubscription = StreamingMockCometSubscription;
StreamingMockCometSubscription.SUBSCRIPTION_COMPLETE = 'subscriptionComplete';
StreamingMockCometSubscription.SUBSCRIPTION_FAILED = 'subscriptionFailed';
/**
* Simulates a comet client. To the core streaming client this mocks the internal comet impl.
* The uses setTimeout(0ms) event loop phase just so the client can simulate actual streaming without the response
* latency.
*/
class StreamingMockCometClient extends streamingClient_1.CometClient {
/**
* Constructor
*
* @param {StreamingMockCometSubscriptionOptions} options Extends the StreamingClient options.
*/
constructor(options) {
super();
this.options = options;
if (!this.options.messagePlaylist) {
this.options.messagePlaylist = [{ id: this.options.id }];
}
}
/**
* Fake addExtension. Does nothing.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
addExtension(extension) { }
/**
* Fake disable. Does nothing.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
disable(label) { }
/**
* Fake handshake that invoke callback after the setTimeout event phase.
*
* @param callback The function to invoke.
*/
handshake(callback) {
setTimeout(() => {
callback();
}, 0);
}
/**
* Fake setHeader. Does nothing,
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
setHeader(name, value) { }
/**
* Fake subscription that completed after the setTimout event phase.
*
* @param channel The streaming channel.
* @param callback The function to invoke after the subscription completes.
*/
subscribe(channel, callback) {
const subscription = new StreamingMockCometSubscription(this.options);
subscription.on('subscriptionComplete', () => {
if (!this.options.messagePlaylist)
return;
Object.values(this.options.messagePlaylist).forEach((message) => {
setTimeout(() => {
callback(message);
}, 0);
});
});
return subscription;
}
/**
* Fake disconnect. Does Nothing.
*/
disconnect() {
return Promise.resolve();
}
}
exports.StreamingMockCometClient = StreamingMockCometClient;
/**
* Mock class for OrgData.
*/
class MockTestOrgData {
constructor(id = uniqid(), options) {
this.testId = id;
this.userId = `user_id_${this.testId}`;
this.orgId = `${this.testId}`;
this.username = (options === null || options === void 0 ? void 0 : options.username) || `admin_${this.testId}.org`;
this.loginUrl = `http://login.${this.testId}.salesforce.com`;
this.instanceUrl = `http://instance.${this.testId}.salesforce.com`;
this.clientId = `${this.testId}/client_id`;
this.clientSecret = `${this.testId}/client_secret`;
this.authcode = `${this.testId}/authcode`;
this.accessToken = `${this.testId}/accessToken`;
this.refreshToken = `${this.testId}/refreshToken`;
this.redirectUri = `http://${this.testId}/localhost:1717/OauthRedirect`;
}
createDevHubUsername(username) {
this.devHubUsername = username;
}
makeDevHub() {
kit_1.set(this, 'isDevHub', true);
}
createUser(user) {
const userMock = new MockTestOrgData();
userMock.username = user;
userMock.alias = this.alias;
userMock.devHubUsername = this.devHubUsername;
userMock.orgId = this.orgId;
userMock.loginUrl = this.loginUrl;
userMock.instanceUrl = this.instanceUrl;
userMock.clientId = this.clientId;
userMock.clientSecret = this.clientSecret;
userMock.redirectUri = this.redirectUri;
return userMock;
}
getMockUserInfo() {
return {
Id: this.userId,
Username: this.username,
LastName: `user_lastname_${this.testId}`,
Alias: this.alias || 'user_alias_blah',
TimeZoneSidKey: `user_timezonesidkey_${this.testId}`,
LocaleSidKey: `user_localesidkey_${this.testId}`,
EmailEncodingKey: `user_emailencodingkey_${this.testId}`,
ProfileId: `user_profileid_${this.testId}`,
LanguageLocaleKey: `user_languagelocalekey_${this.testId}`,
Email: `user_email@${this.testId}.com`,
};
}
async getConfig() {
const crypto = await crypto_2.Crypto.create();
const config = {};
config.orgId = this.orgId;
const accessToken = crypto.encrypt(this.accessToken);
if (accessToken) {
config.accessToken = accessToken;
}
const refreshToken = crypto.encrypt(this.refreshToken);
if (refreshToken) {
config.refreshToken = refreshToken;
}
config.instanceUrl = this.instanceUrl;
config.loginUrl = this.loginUrl;
config.username = this.username;
config.createdOrgInstance = 'CS1';
config.created = '1519163543003';
config.userId = this.userId;
// config.devHubUsername = 'tn@su-blitz.org';
if (this.devHubUsername) {
config.devHubUsername = this.devHubUsername;
}
const isDevHub = ts_types_1.getBoolean(this, 'isDevHub');
if (isDevHub) {
config.isDevHub = isDevHub;
}
return config;
}
}
exports.MockTestOrgData = MockTestOrgData;
//# sourceMappingURL=testSetup.js.map