UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

544 lines 21.5 kB
"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}@gb.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