UNPKG

21.5 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9exports.MockTestOrgData = exports.StreamingMockCometClient = exports.StreamingMockCometSubscription = exports.StreamingMockSubscriptionCall = exports.shouldThrow = exports.unexpectedResult = exports.testSetup = exports.restoreContext = exports.stubContext = exports.instantiateContext = void 0;
10const crypto_1 = require("crypto");
11const events_1 = require("events");
12const os_1 = require("os");
13const path_1 = require("path");
14const kit_1 = require("@salesforce/kit");
15const ts_sinon_1 = require("@salesforce/ts-sinon");
16const ts_types_1 = require("@salesforce/ts-types");
17const configAggregator_1 = require("./config/configAggregator");
18const configFile_1 = require("./config/configFile");
19const connection_1 = require("./connection");
20const crypto_2 = require("./crypto");
21const logger_1 = require("./logger");
22const messages_1 = require("./messages");
23const sfdxError_1 = require("./sfdxError");
24const sfdxProject_1 = require("./sfdxProject");
25const streamingClient_1 = require("./status/streamingClient");
26const uniqid = () => {
27 return crypto_1.randomBytes(16).toString('hex');
28};
29function getTestLocalPath(uid) {
30 return path_1.join(os_1.tmpdir(), uid, 'sfdx_core', 'local');
31}
32function getTestGlobalPath(uid) {
33 return path_1.join(os_1.tmpdir(), uid, 'sfdx_core', 'global');
34}
35function retrieveRootPathSync(isGlobal, uid = uniqid()) {
36 return isGlobal ? getTestGlobalPath(uid) : getTestLocalPath(uid);
37}
38// eslint-disable-next-line @typescript-eslint/require-await
39async function retrieveRootPath(isGlobal, uid = uniqid()) {
40 return retrieveRootPathSync(isGlobal, uid);
41}
42function defaultFakeConnectionRequest() {
43 return Promise.resolve(ts_types_1.ensureAnyJson({ records: [] }));
44}
45/**
46 * Instantiate a @salesforce/core test context. This is automatically created by `const $$ = testSetup()`
47 * but is useful if you don't want to have a global stub of @salesforce/core and you want to isolate it to
48 * a single describe.
49 *
50 * **Note:** Call `stubContext` in your beforeEach to have clean stubs of @salesforce/core every test run.
51 *
52 * @example
53 * ```
54 * const $$ = instantiateContext();
55 *
56 * beforeEach(() => {
57 * stubContext($$);
58 * });
59 *
60 * afterEach(() => {
61 * restoreContext($$);
62 * });
63 * ```
64 * @param sinon
65 */
66// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
67const instantiateContext = (sinon) => {
68 if (!sinon) {
69 try {
70 sinon = require('sinon');
71 }
72 catch (e) {
73 throw new Error('The package sinon was not found. Add it to your package.json and pass it in to testSetup(sinon.sandbox)');
74 }
75 }
76 // Import all the messages files in the sfdx-core messages dir.
77 // Messages.importMessagesDirectory(pathJoin(__dirname, '..', '..'));
78 messages_1.Messages.importMessagesDirectory(path_1.join(__dirname));
79 // Create a global sinon sandbox and a test logger instance for use within tests.
80 const defaultSandbox = sinon.createSandbox();
81 const testContext = {
82 SANDBOX: defaultSandbox,
83 SANDBOXES: {
84 DEFAULT: defaultSandbox,
85 CONFIG: sinon.createSandbox(),
86 PROJECT: sinon.createSandbox(),
87 CRYPTO: sinon.createSandbox(),
88 CONNECTION: sinon.createSandbox(),
89 },
90 TEST_LOGGER: new logger_1.Logger({
91 name: 'SFDX_Core_Test_Logger',
92 }).useMemoryLogging(),
93 id: uniqid(),
94 uniqid,
95 configStubs: {},
96 // eslint-disable-next-line @typescript-eslint/require-await
97 localPathRetriever: async (uid) => getTestLocalPath(uid),
98 localPathRetrieverSync: getTestLocalPath,
99 // eslint-disable-next-line @typescript-eslint/require-await
100 globalPathRetriever: async (uid) => getTestGlobalPath(uid),
101 globalPathRetrieverSync: getTestGlobalPath,
102 rootPathRetriever: retrieveRootPath,
103 rootPathRetrieverSync: retrieveRootPathSync,
104 fakeConnectionRequest: defaultFakeConnectionRequest,
105 getConfigStubContents(name, group) {
106 const stub = this.configStubs[name];
107 if (stub && stub.contents) {
108 if (group && stub.contents[group]) {
109 return ts_types_1.ensureJsonMap(stub.contents[group]);
110 }
111 else {
112 return stub.contents;
113 }
114 }
115 return {};
116 },
117 setConfigStubContents(name, value) {
118 if (ts_types_1.ensureString(name) && ts_types_1.isJsonMap(value)) {
119 this.configStubs[name] = value;
120 }
121 },
122 inProject(inProject = true) {
123 testContext.SANDBOXES.PROJECT.restore();
124 if (inProject) {
125 testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPath').callsFake(() => testContext.localPathRetriever(testContext.id));
126 testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPathSync').callsFake(() => testContext.localPathRetrieverSync(testContext.id));
127 }
128 else {
129 testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPath').rejects(new sfdxError_1.SfdxError('InvalidProjectWorkspace'));
130 testContext.SANDBOXES.PROJECT.stub(sfdxProject_1.SfdxProject, 'resolveProjectPathSync').throws(new sfdxError_1.SfdxError('InvalidProjectWorkspace'));
131 }
132 },
133 };
134 return testContext;
135};
136exports.instantiateContext = instantiateContext;
137/**
138 * Stub a @salesforce/core test context. This will mock out logging to a file, config file reading and writing,
139 * local and global path resolution, and http request using connection (soon)*.
140 *
141 * This is automatically stubbed in the global beforeEach created by
142 * `const $$ = testSetup()` but is useful if you don't want to have a global stub of @salesforce/core and you
143 * want to isolate it to a single describe.
144 *
145 * **Note:** Always call `restoreContext` in your afterEach.
146 *
147 * @example
148 * ```
149 * const $$ = instantiateContext();
150 *
151 * beforeEach(() => {
152 * stubContext($$);
153 * });
154 *
155 * afterEach(() => {
156 * restoreContext($$);
157 * });
158 * ```
159 * @param testContext
160 */
161const stubContext = (testContext) => {
162 // Most core files create a child logger so stub this to return our test logger.
163 ts_sinon_1.stubMethod(testContext.SANDBOX, logger_1.Logger, 'child').returns(Promise.resolve(testContext.TEST_LOGGER));
164 ts_sinon_1.stubMethod(testContext.SANDBOX, logger_1.Logger, 'childFromRoot').returns(testContext.TEST_LOGGER);
165 testContext.inProject(true);
166 testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile, 'resolveRootFolder').callsFake((isGlobal) => testContext.rootPathRetriever(isGlobal, testContext.id));
167 testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile, 'resolveRootFolderSync').callsFake((isGlobal) => testContext.rootPathRetrieverSync(isGlobal, testContext.id));
168 ts_sinon_1.stubMethod(testContext.SANDBOXES.PROJECT, sfdxProject_1.SfdxProjectJson.prototype, 'doesPackageExist').callsFake(() => true);
169 const initStubForRead = (configFile) => {
170 const stub = testContext.configStubs[configFile.constructor.name] || {};
171 // init calls read calls getPath which sets the path on the config file the first time.
172 // Since read is now stubbed, make sure to call getPath to initialize it.
173 configFile.getPath();
174 // @ts-ignore set this to true to avoid an infinite loop in tests when reading config files.
175 configFile.hasRead = true;
176 return stub;
177 };
178 const readSync = function (newContents) {
179 const stub = initStubForRead(this);
180 this.setContentsFromObject(newContents || stub.contents || {});
181 return this.getContents();
182 };
183 const read = async function () {
184 const stub = initStubForRead(this);
185 if (stub.readFn) {
186 return await stub.readFn.call(this);
187 }
188 if (stub.retrieveContents) {
189 return readSync.call(this, await stub.retrieveContents.call(this));
190 }
191 else {
192 return readSync.call(this);
193 }
194 };
195 // Mock out all config file IO for all tests. They can restore individually if they need original functionality.
196 // @ts-ignore
197 testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile.prototype, 'readSync').callsFake(readSync);
198 testContext.SANDBOXES.CONFIG.stub(configFile_1.ConfigFile.prototype, 'read').callsFake(read);
199 const writeSync = function (newContents) {
200 if (!testContext.configStubs[this.constructor.name]) {
201 testContext.configStubs[this.constructor.name] = {};
202 }
203 const stub = testContext.configStubs[this.constructor.name];
204 if (!stub)
205 return;
206 this.setContents(newContents || this.getContents());
207 stub.contents = this.toObject();
208 };
209 const write = async function (newContents) {
210 if (!testContext.configStubs[this.constructor.name]) {
211 testContext.configStubs[this.constructor.name] = {};
212 }
213 const stub = testContext.configStubs[this.constructor.name];
214 if (!stub)
215 return;
216 if (stub.writeFn) {
217 return await stub.writeFn.call(this, newContents);
218 }
219 if (stub.updateContents) {
220 writeSync.call(this, await stub.updateContents.call(this));
221 }
222 else {
223 writeSync.call(this);
224 }
225 };
226 ts_sinon_1.stubMethod(testContext.SANDBOXES.CONFIG, configFile_1.ConfigFile.prototype, 'writeSync').callsFake(writeSync);
227 ts_sinon_1.stubMethod(testContext.SANDBOXES.CONFIG, configFile_1.ConfigFile.prototype, 'write').callsFake(write);
228 ts_sinon_1.stubMethod(testContext.SANDBOXES.CRYPTO, crypto_2.Crypto.prototype, 'getKeyChain').callsFake(() => Promise.resolve({
229 setPassword: () => Promise.resolve(),
230 getPassword: (data, cb) => cb(undefined, '12345678901234567890123456789012'),
231 }));
232 ts_sinon_1.stubMethod(testContext.SANDBOXES.CONNECTION, connection_1.Connection.prototype, 'isResolvable').resolves(true);
233 ts_sinon_1.stubMethod(testContext.SANDBOXES.CONNECTION, connection_1.Connection.prototype, 'request').callsFake(function (request, options) {
234 if (request === `${this.instanceUrl}/services/data`) {
235 return Promise.resolve([{ version: '42.0' }]);
236 }
237 return testContext.fakeConnectionRequest.call(this, request, options);
238 });
239 // Always start with the default and tests beforeEach or it methods can override it.
240 testContext.fakeConnectionRequest = defaultFakeConnectionRequest;
241};
242exports.stubContext = stubContext;
243/**
244 * Restore a @salesforce/core test context. This is automatically stubbed in the global beforeEach created by
245 * `const $$ = testSetup()` but is useful if you don't want to have a global stub of @salesforce/core and you
246 * want to isolate it to a single describe.
247 *
248 * @example
249 * ```
250 * const $$ = instantiateContext();
251 *
252 * beforeEach(() => {
253 * stubContext($$);
254 * });
255 *
256 * afterEach(() => {
257 * restoreContext($$);
258 * });
259 * ```
260 * @param testContext
261 */
262const restoreContext = (testContext) => {
263 testContext.SANDBOX.restore();
264 Object.values(testContext.SANDBOXES).forEach((theSandbox) => theSandbox.restore());
265 testContext.configStubs = {};
266};
267exports.restoreContext = restoreContext;
268// eslint-disable-next-line @typescript-eslint/no-explicit-any
269const _testSetup = (sinon) => {
270 const testContext = exports.instantiateContext(sinon);
271 beforeEach(() => {
272 // Allow each test to have their own config aggregator
273 // @ts-ignore clear for testing.
274 delete configAggregator_1.ConfigAggregator.instance;
275 exports.stubContext(testContext);
276 });
277 afterEach(() => {
278 exports.restoreContext(testContext);
279 });
280 return testContext;
281};
282/**
283 * Use to mock out different pieces of sfdx-core to make testing easier. This will mock out
284 * logging to a file, config file reading and writing, local and global path resolution, and
285 * *http request using connection (soon)*.
286 *
287 * **Note:** The testSetup should be outside of the describe. If you need to stub per test, use
288 * `instantiateContext`, `stubContext`, and `restoreContext`.
289 * ```
290 * // In a mocha tests
291 * import testSetup from '@salesforce/core/lib/testSetup';
292 *
293 * const $$ = testSetup();
294 *
295 * describe(() => {
296 * it('test', () => {
297 * // Stub out your own method
298 * $$.SANDBOX.stub(MyClass.prototype, 'myMethod').returnsFake(() => {});
299 *
300 * // Set the contents that is used when aliases are read. Same for all config files.
301 * $$.configStubs.Aliases = { contents: { 'myTestAlias': 'user@company.com' } };
302 *
303 * // Will use the contents set above.
304 * const username = Aliases.fetch('myTestAlias');
305 * expect(username).to.equal('user@company.com');
306 * });
307 * });
308 * ```
309 */
310exports.testSetup = kit_1.once(_testSetup);
311/**
312 * A pre-canned error for try/catch testing.
313 *
314 * **See** {@link shouldThrow}
315 */
316exports.unexpectedResult = new sfdxError_1.SfdxError('This code was expected to fail', 'UnexpectedResult');
317/**
318 * Use for this testing pattern:
319 * ```
320 * try {
321 * await call()
322 * assert.fail('this should never happen');
323 * } catch (e) {
324 * ...
325 * }
326 *
327 * Just do this
328 *
329 * try {
330 * await shouldThrow(call()); // If this succeeds unexpectedResultError is thrown.
331 * } catch(e) {
332 * ...
333 * }
334 * ```
335 *
336 * @param f The async function that is expected to throw.
337 */
338async function shouldThrow(f) {
339 await f;
340 throw exports.unexpectedResult;
341}
342exports.shouldThrow = shouldThrow;
343/**
344 * A helper to determine if a subscription will use callback or errorback.
345 * Enable errback to simulate a subscription failure.
346 */
347var StreamingMockSubscriptionCall;
348(function (StreamingMockSubscriptionCall) {
349 StreamingMockSubscriptionCall[StreamingMockSubscriptionCall["CALLBACK"] = 0] = "CALLBACK";
350 StreamingMockSubscriptionCall[StreamingMockSubscriptionCall["ERRORBACK"] = 1] = "ERRORBACK";
351})(StreamingMockSubscriptionCall = exports.StreamingMockSubscriptionCall || (exports.StreamingMockSubscriptionCall = {}));
352/**
353 * Simulates a comet subscription to a streaming channel.
354 */
355class StreamingMockCometSubscription extends events_1.EventEmitter {
356 constructor(options) {
357 super();
358 this.options = options;
359 }
360 /**
361 * Sets up a streaming subscription callback to occur after the setTimeout event loop phase.
362 *
363 * @param callback The function to invoke.
364 */
365 callback(callback) {
366 if (this.options.subscriptionCall === StreamingMockSubscriptionCall.CALLBACK) {
367 setTimeout(() => {
368 callback();
369 super.emit(StreamingMockCometSubscription.SUBSCRIPTION_COMPLETE);
370 }, 0);
371 }
372 }
373 /**
374 * Sets up a streaming subscription errback to occur after the setTimeout event loop phase.
375 *
376 * @param callback The function to invoke.
377 */
378 errback(callback) {
379 if (this.options.subscriptionCall === StreamingMockSubscriptionCall.ERRORBACK) {
380 const error = this.options.subscriptionErrbackError;
381 if (!error)
382 return;
383 setTimeout(() => {
384 callback(error);
385 super.emit(StreamingMockCometSubscription.SUBSCRIPTION_FAILED);
386 }, 0);
387 }
388 }
389}
390exports.StreamingMockCometSubscription = StreamingMockCometSubscription;
391StreamingMockCometSubscription.SUBSCRIPTION_COMPLETE = 'subscriptionComplete';
392StreamingMockCometSubscription.SUBSCRIPTION_FAILED = 'subscriptionFailed';
393/**
394 * Simulates a comet client. To the core streaming client this mocks the internal comet impl.
395 * The uses setTimeout(0ms) event loop phase just so the client can simulate actual streaming without the response
396 * latency.
397 */
398class StreamingMockCometClient extends streamingClient_1.CometClient {
399 /**
400 * Constructor
401 *
402 * @param {StreamingMockCometSubscriptionOptions} options Extends the StreamingClient options.
403 */
404 constructor(options) {
405 super();
406 this.options = options;
407 if (!this.options.messagePlaylist) {
408 this.options.messagePlaylist = [{ id: this.options.id }];
409 }
410 }
411 /**
412 * Fake addExtension. Does nothing.
413 */
414 // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
415 addExtension(extension) { }
416 /**
417 * Fake disable. Does nothing.
418 */
419 // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
420 disable(label) { }
421 /**
422 * Fake handshake that invoke callback after the setTimeout event phase.
423 *
424 * @param callback The function to invoke.
425 */
426 handshake(callback) {
427 setTimeout(() => {
428 callback();
429 }, 0);
430 }
431 /**
432 * Fake setHeader. Does nothing,
433 */
434 // eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function
435 setHeader(name, value) { }
436 /**
437 * Fake subscription that completed after the setTimout event phase.
438 *
439 * @param channel The streaming channel.
440 * @param callback The function to invoke after the subscription completes.
441 */
442 subscribe(channel, callback) {
443 const subscription = new StreamingMockCometSubscription(this.options);
444 subscription.on('subscriptionComplete', () => {
445 if (!this.options.messagePlaylist)
446 return;
447 Object.values(this.options.messagePlaylist).forEach((message) => {
448 setTimeout(() => {
449 callback(message);
450 }, 0);
451 });
452 });
453 return subscription;
454 }
455 /**
456 * Fake disconnect. Does Nothing.
457 */
458 disconnect() {
459 return Promise.resolve();
460 }
461}
462exports.StreamingMockCometClient = StreamingMockCometClient;
463/**
464 * Mock class for OrgData.
465 */
466class MockTestOrgData {
467 constructor(id = uniqid(), options) {
468 this.testId = id;
469 this.userId = `user_id_${this.testId}`;
470 this.orgId = `${this.testId}`;
471 this.username = (options === null || options === void 0 ? void 0 : options.username) || `admin_${this.testId}@gb.org`;
472 this.loginUrl = `http://login.${this.testId}.salesforce.com`;
473 this.instanceUrl = `http://instance.${this.testId}.salesforce.com`;
474 this.clientId = `${this.testId}/client_id`;
475 this.clientSecret = `${this.testId}/client_secret`;
476 this.authcode = `${this.testId}/authcode`;
477 this.accessToken = `${this.testId}/accessToken`;
478 this.refreshToken = `${this.testId}/refreshToken`;
479 this.redirectUri = `http://${this.testId}/localhost:1717/OauthRedirect`;
480 }
481 createDevHubUsername(username) {
482 this.devHubUsername = username;
483 }
484 makeDevHub() {
485 kit_1.set(this, 'isDevHub', true);
486 }
487 createUser(user) {
488 const userMock = new MockTestOrgData();
489 userMock.username = user;
490 userMock.alias = this.alias;
491 userMock.devHubUsername = this.devHubUsername;
492 userMock.orgId = this.orgId;
493 userMock.loginUrl = this.loginUrl;
494 userMock.instanceUrl = this.instanceUrl;
495 userMock.clientId = this.clientId;
496 userMock.clientSecret = this.clientSecret;
497 userMock.redirectUri = this.redirectUri;
498 return userMock;
499 }
500 getMockUserInfo() {
501 return {
502 Id: this.userId,
503 Username: this.username,
504 LastName: `user_lastname_${this.testId}`,
505 Alias: this.alias || 'user_alias_blah',
506 TimeZoneSidKey: `user_timezonesidkey_${this.testId}`,
507 LocaleSidKey: `user_localesidkey_${this.testId}`,
508 EmailEncodingKey: `user_emailencodingkey_${this.testId}`,
509 ProfileId: `user_profileid_${this.testId}`,
510 LanguageLocaleKey: `user_languagelocalekey_${this.testId}`,
511 Email: `user_email@${this.testId}.com`,
512 };
513 }
514 async getConfig() {
515 const crypto = await crypto_2.Crypto.create();
516 const config = {};
517 config.orgId = this.orgId;
518 const accessToken = crypto.encrypt(this.accessToken);
519 if (accessToken) {
520 config.accessToken = accessToken;
521 }
522 const refreshToken = crypto.encrypt(this.refreshToken);
523 if (refreshToken) {
524 config.refreshToken = refreshToken;
525 }
526 config.instanceUrl = this.instanceUrl;
527 config.loginUrl = this.loginUrl;
528 config.username = this.username;
529 config.createdOrgInstance = 'CS1';
530 config.created = '1519163543003';
531 config.userId = this.userId;
532 // config.devHubUsername = 'tn@su-blitz.org';
533 if (this.devHubUsername) {
534 config.devHubUsername = this.devHubUsername;
535 }
536 const isDevHub = ts_types_1.getBoolean(this, 'isDevHub');
537 if (isDevHub) {
538 config.isDevHub = isDevHub;
539 }
540 return config;
541 }
542}
543exports.MockTestOrgData = MockTestOrgData;
544//# sourceMappingURL=testSetup.js.map
\No newline at end of file