Skip to content

Commit 527287c

Browse files
authored
Merge pull request #422 from salesforcecli/wr/sfEnvVars
fix: use SF prefixed env vars, add UTs
2 parents 06173f0 + bb4200e commit 527287c

2 files changed

Lines changed: 246 additions & 9 deletions

File tree

src/commands/info/releasenotes/display.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ import { parseReleaseNotes } from '../../../shared/parseReleaseNotes';
2323
// Initialize Messages with the current plugin directory
2424
Messages.importMessagesDirectory(__dirname);
2525

26-
const HIDE_NOTES = 'SFDX_HIDE_RELEASE_NOTES';
27-
const HIDE_FOOTER = 'SFDX_HIDE_RELEASE_NOTES_FOOTER';
28-
2926
const helpers = ['stable', 'stable-rc', 'latest', 'latest-rc', 'rc'];
3027

3128
// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core,
@@ -53,13 +50,14 @@ export default class Display extends SfCommand<DisplayOutput | undefined> {
5350
};
5451

5552
public async run(): Promise<DisplayOutput | undefined> {
53+
const HIDE_NOTES = this.config.bin === 'sf' ? 'SF_HIDE_RELEASE_NOTES' : 'SFDX_HIDE_RELEASE_NOTES';
54+
const HIDE_FOOTER = this.config.bin === 'sf' ? 'SF_HIDE_RELEASE_NOTES_FOOTER' : 'SFDX_HIDE_RELEASE_NOTES_FOOTER';
55+
5656
const logger = Logger.childFromRoot(this.constructor.name);
5757
const { flags } = await this.parse(Display);
5858
const env = new Env();
5959

60-
const isHook = !!flags.hook;
61-
62-
if (env.getBoolean(HIDE_NOTES) && isHook) {
60+
if (env.getBoolean(HIDE_NOTES) && flags.hook) {
6361
// We don't ever want to exit the process for info:releasenotes:display (whatsnew)
6462
// In most cases we will log a message, but here we only trace log in case someone using stdout of the update command
6563
logger.trace(`release notes disabled via env var: ${HIDE_NOTES}`);
@@ -100,7 +98,7 @@ export default class Display extends SfCommand<DisplayOutput | undefined> {
10098
this.log(marked.parser(tokens));
10199
}
102100

103-
if (isHook) {
101+
if (flags.hook) {
104102
if (env.getBoolean(HIDE_FOOTER)) {
105103
await Lifecycle.getInstance().emitTelemetry({ eventName: 'FOOTER_HIDDEN' });
106104
} else {
@@ -109,7 +107,7 @@ export default class Display extends SfCommand<DisplayOutput | undefined> {
109107
}
110108
}
111109
} catch (err) {
112-
if (isHook) {
110+
if (flags.hook) {
113111
// Do not throw error if --hook is passed, just warn so we don't exit any processes.
114112
// --hook is passed in the post install/update scripts
115113
const { message, stack, name } = err as Error;

test/commands/info/releasenotes/display.test.ts

Lines changed: 240 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Display from '../../../../src/commands/info/releasenotes/display';
2525
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2626
chaiUse(SinonChai);
2727

28-
describe('info:releasenotes:display', () => {
28+
describe('sfdx info:releasenotes:display', () => {
2929
const sandbox = Sinon.createSandbox();
3030

3131
let mockInfoConfig: getInfoConfig.InfoConfig;
@@ -264,3 +264,242 @@ describe('info:releasenotes:display', () => {
264264
expect(json).to.deep.equal(expected);
265265
});
266266
});
267+
describe('sf info:releasenotes:display', () => {
268+
const sandbox = Sinon.createSandbox();
269+
270+
let mockInfoConfig: getInfoConfig.InfoConfig;
271+
let getBooleanStub: sinon.SinonStub;
272+
let uxLogStub: sinon.SinonStub;
273+
let uxWarnStub: sinon.SinonStub;
274+
let getInfoConfigStub: Sinon.SinonStub;
275+
let getReleaseNotesStub: Sinon.SinonStub;
276+
let getDistTagVersionStub: Sinon.SinonStub;
277+
let parseReleaseNotesSpy: Sinon.SinonSpy;
278+
let markedParserSpy: Sinon.SinonSpy;
279+
280+
const oclifConfigStub = fromStub(stubInterface<Config>(sandbox));
281+
282+
class TestDisplay extends Display {
283+
public async runIt() {
284+
await this.init();
285+
return this.run();
286+
}
287+
}
288+
289+
const runDisplayCmd = async (params: string[]) => {
290+
oclifConfigStub.bin = 'sf';
291+
292+
const cmd = new TestDisplay(params, oclifConfigStub);
293+
294+
uxLogStub = stubMethod(sandbox, SfCommand.prototype, 'log');
295+
uxWarnStub = stubMethod(sandbox, SfCommand.prototype, 'warn');
296+
297+
return cmd.runIt();
298+
};
299+
300+
beforeEach(() => {
301+
mockInfoConfig = {
302+
releasenotes: {
303+
distTagUrl: 'https://registry.npmjs.org/-/package/sfdx-cli/dist-tags',
304+
releaseNotesPath: 'https://github.com/forcedotcom/cli/tree/main/releasenotes/sfdx',
305+
releaseNotesFilename: 'README.md',
306+
},
307+
};
308+
309+
oclifConfigStub.pjson.version = '3.3.3';
310+
oclifConfigStub.root = '/root/path';
311+
312+
getBooleanStub = stubMethod(sandbox, Env.prototype, 'getBoolean');
313+
getBooleanStub.withArgs('SF_HIDE_RELEASE_NOTES').returns(false);
314+
getBooleanStub.withArgs('SF_HIDE_RELEASE_NOTES_FOOTER').returns(false);
315+
316+
getInfoConfigStub = stubMethod(sandbox, getInfoConfig, 'getInfoConfig').returns(mockInfoConfig);
317+
getReleaseNotesStub = stubMethod(sandbox, getReleaseNotes, 'getReleaseNotes').returns('## Release notes for 3.3.3');
318+
getDistTagVersionStub = stubMethod(sandbox, getDistTagVersion, 'getDistTagVersion').returns('1.2.3');
319+
parseReleaseNotesSpy = spyMethod(sandbox, parseReleaseNotes, 'parseReleaseNotes');
320+
markedParserSpy = spyMethod(sandbox, marked, 'parser');
321+
});
322+
323+
afterEach(() => {
324+
sandbox.restore();
325+
});
326+
327+
it('allows you to suppress release notes output with env var', async () => {
328+
// This env var is only honored when the --hook flag is passed.
329+
// If someone is running the command directly, we show notes regardless.
330+
getBooleanStub.withArgs('SF_HIDE_RELEASE_NOTES').returns(true);
331+
332+
const lifecycleStub = stubMethod(sandbox, Lifecycle.prototype, 'emitTelemetry');
333+
334+
await runDisplayCmd(['--hook']);
335+
336+
expect(lifecycleStub.calledOnce).to.equal(true);
337+
expect(uxLogStub.called).to.be.false;
338+
expect(uxWarnStub.called).to.be.false;
339+
});
340+
341+
it('ignores hide release notes env var if running command directly (without --hook)', async () => {
342+
getBooleanStub.withArgs('SF_HIDE_RELEASE_NOTES').returns(true);
343+
344+
await runDisplayCmd([]);
345+
346+
expect(uxLogStub.args[0][0]).to.contain('## Release notes for 3.3.3');
347+
});
348+
349+
it('calls getInfoConfig with config root', async () => {
350+
await runDisplayCmd([]);
351+
352+
expect(getInfoConfigStub.args[0][0]).to.equal('/root/path');
353+
});
354+
355+
it('does not render emoji', async () => {
356+
getReleaseNotesStub.returns('## 3.3.3 :tada:');
357+
358+
await runDisplayCmd([]);
359+
360+
expect(uxLogStub.args[0][0]).to.contain('## 3.3.3 :tada:');
361+
});
362+
363+
it('throws an error if info config lookup fails', async () => {
364+
getInfoConfigStub.throws(new Error('info config error'));
365+
366+
try {
367+
await shouldThrow(runDisplayCmd([]));
368+
} catch (err) {
369+
expect((err as Error).message).to.contain('info config error');
370+
}
371+
});
372+
373+
it('does not call getDistTagVersion if helper is not passed', async () => {
374+
await runDisplayCmd([]);
375+
376+
expect(getDistTagVersionStub.called).to.be.false;
377+
});
378+
379+
it('calls getDistTagVersion with correct are if helpers are used', async () => {
380+
await runDisplayCmd(['-v', 'latest-rc', '--hook']);
381+
382+
expect(getDistTagVersionStub.args[0]).to.deep.equal([mockInfoConfig.releasenotes.distTagUrl, 'latest-rc']);
383+
});
384+
385+
it('throws an error if dist tag lookup fails', async () => {
386+
getDistTagVersionStub.throws(new Error('dist tag error'));
387+
388+
try {
389+
await shouldThrow(runDisplayCmd(['-v', 'latest-rc']));
390+
} catch (err) {
391+
expect((err as Error).message).to.contain('dist tag error');
392+
}
393+
});
394+
395+
it('calls getReleaseNotes with version returned from getDistTagVersion', async () => {
396+
await runDisplayCmd(['-v', 'latest-rc', '--hook']);
397+
398+
const expected = [
399+
mockInfoConfig.releasenotes.releaseNotesPath,
400+
mockInfoConfig.releasenotes.releaseNotesFilename,
401+
'1.2.3',
402+
];
403+
404+
expect(getReleaseNotesStub.args[0]).to.deep.equal(expected);
405+
});
406+
407+
it('logs logs a header with cli bin', async () => {
408+
await runDisplayCmd([]);
409+
410+
expect(uxLogStub.args[0][0]).to.contain("# Release notes for 'sf':");
411+
});
412+
413+
it('calls getReleaseNotes with passed version', async () => {
414+
await runDisplayCmd(['-v', '4.5.6', '--hook']);
415+
416+
expect(getReleaseNotesStub.args[0][2]).to.equal('4.5.6');
417+
});
418+
419+
it('calls getReleaseNotes with installed version if no arg is passed', async () => {
420+
await runDisplayCmd([]);
421+
422+
expect(getReleaseNotesStub.args[0][2]).to.equal('3.3.3');
423+
});
424+
425+
it('throws an error if getReleaseNotes lookup fails', async () => {
426+
getReleaseNotesStub.throws(new Error('release notes error'));
427+
428+
try {
429+
await shouldThrow(runDisplayCmd([]));
430+
} catch (err) {
431+
expect((err as Error).message).to.contain('release notes error');
432+
}
433+
});
434+
435+
it('parseReleaseNotes is called with the correct args', async () => {
436+
await runDisplayCmd([]);
437+
438+
expect(parseReleaseNotesSpy.args[0]).to.deep.equal([
439+
'## Release notes for 3.3.3',
440+
'3.3.3',
441+
mockInfoConfig.releasenotes.releaseNotesPath,
442+
]);
443+
});
444+
445+
it('parser is called with tokens', async () => {
446+
await runDisplayCmd([]);
447+
448+
const tokens = parseReleaseNotesSpy.returnValues[0] as marked.Token;
449+
450+
expect(markedParserSpy.calledOnce).to.be.true;
451+
expect(markedParserSpy.args[0][0]).to.deep.equal(tokens);
452+
});
453+
454+
it('logs markdown on the command line', async () => {
455+
await runDisplayCmd([]);
456+
457+
expect(uxLogStub.args[0][0]).to.contain('## Release notes for 3.3.3');
458+
});
459+
460+
it('throws an error if parsing fails', async () => {
461+
try {
462+
await shouldThrow(runDisplayCmd(['-v', '4.5.6']));
463+
} catch (err) {
464+
expect((err as Error).message).to.contain(
465+
`Didn't find version '4.5.6'. View release notes online at: ${mockInfoConfig.releasenotes.releaseNotesPath}`
466+
);
467+
}
468+
});
469+
470+
it('does not throw an error if --hook is set', async () => {
471+
getReleaseNotesStub.throws(new Error('release notes error'));
472+
const lifecycleStub = stubMethod(sandbox, Lifecycle.prototype, 'emitTelemetry');
473+
474+
await runDisplayCmd(['--hook']);
475+
expect(lifecycleStub.calledOnce).to.be.true;
476+
expect(uxWarnStub.args[0][0]).to.contain('release notes error');
477+
});
478+
479+
it('renders a footer if --hook is set', async () => {
480+
await runDisplayCmd(['--hook']);
481+
482+
expect(uxLogStub.args[1][0]).to.contain('to manually view the current release notes');
483+
});
484+
485+
it('hides footer if env var is set', async () => {
486+
getBooleanStub.withArgs('SF_HIDE_RELEASE_NOTES_FOOTER').returns(true);
487+
488+
const lifecycleStub = stubMethod(sandbox, Lifecycle.prototype, 'emitTelemetry');
489+
490+
await runDisplayCmd(['--hook']);
491+
expect(lifecycleStub.calledOnce).to.be.true;
492+
expect(uxLogStub.args[2]).to.be.undefined;
493+
});
494+
495+
it('supports json output', async () => {
496+
const json = await runDisplayCmd(['--json']);
497+
498+
const expected = {
499+
body: `# Release notes for 'sf':${os.EOL}## Release notes for 3.3.3`,
500+
url: mockInfoConfig.releasenotes.releaseNotesPath,
501+
};
502+
503+
expect(json).to.deep.equal(expected);
504+
});
505+
});

0 commit comments

Comments
 (0)