Skip to content

Commit e386e4f

Browse files
committed
unit tests and handling of 'false' string
1 parent 64f1d42 commit e386e4f

File tree

2 files changed

+330
-18
lines changed

2 files changed

+330
-18
lines changed

packages/aws-cdk/lib/cli/cli.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,23 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
121121
* systems, even though technically we maybe could.
122122
*/
123123
const isSafeToWriteNotices = !isCI() || Boolean(ciSystemIsStdErrSafe());
124-
const shouldDisplayNotices = isSafeToWriteNotices && (configuration.settings.get(['notices']) !== false);
124+
125+
// Determine if notices should be displayed based on CLI args and configuration
126+
let shouldDisplayNotices: boolean;
127+
if (argv.notices !== undefined) {
128+
// CLI argument takes precedence
129+
shouldDisplayNotices = argv.notices;
130+
} else {
131+
// Fall back to configuration file setting, then autodetection
132+
const configNotices = configuration.settings.get(['notices']);
133+
if (configNotices !== undefined) {
134+
// Consider string "false" to be falsy in this context
135+
shouldDisplayNotices = configNotices !== "false" && Boolean(configNotices);
136+
} else {
137+
// Default autodetection behavior
138+
shouldDisplayNotices = isSafeToWriteNotices;
139+
}
140+
}
125141

126142
// Notices either go to stderr, or nowhere
127143
ioHost.noticesDestination = shouldDisplayNotices ? 'stderr' : 'drop';

packages/aws-cdk/test/cli/cli.test.ts

Lines changed: 313 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { exec } from '../../lib/cli/cli';
22
import { CliIoHost } from '../../lib/cli/io-host';
33
import { Configuration } from '../../lib/cli/user-configuration';
44
import { TestIoHost } from '../_helpers/io-host';
5+
import { Notices } from '../../lib/api/notices';
56

67
// Store original version module exports so we don't conflict with other tests
78
const originalVersion = jest.requireActual('../../lib/cli/version');
@@ -30,24 +31,26 @@ const actualUserConfig = jest.requireActual('../../lib/cli/user-configuration');
3031
Configuration.fromArgs = jest.fn().mockImplementation(() => actualUserConfig.Configuration.fromArgs(ioHelper));
3132
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => actualUserConfig.Configuration.fromArgs(ioHelper));
3233

33-
jest.mock('../../lib/api/notices', () => ({
34-
Notices: {
35-
create: jest.fn().mockReturnValue({
36-
refresh: jest.fn().mockResolvedValue(undefined),
37-
display: jest.fn(),
38-
}),
39-
},
40-
}));
41-
4234
jest.mock('../../lib/cli/parse-command-line-arguments', () => ({
43-
parseCommandLineArguments: jest.fn().mockImplementation((args) => Promise.resolve({
44-
_: ['version'],
45-
verbose: args.includes('-v') ? (
46-
args.filter((arg: string) => arg === '-v').length
47-
) : args.includes('--verbose') ? (
48-
parseInt(args[args.indexOf('--verbose') + 1]) || true
49-
) : undefined,
50-
})),
35+
parseCommandLineArguments: jest.fn().mockImplementation((args) => {
36+
const result: any = {
37+
_: ['version'],
38+
verbose: args.includes('-v') ? (
39+
args.filter((arg: string) => arg === '-v').length
40+
) : args.includes('--verbose') ? (
41+
parseInt(args[args.indexOf('--verbose') + 1]) || true
42+
) : undefined,
43+
};
44+
45+
// Handle notices flags
46+
if (args.includes('--notices')) {
47+
result.notices = true;
48+
} else if (args.includes('--no-notices')) {
49+
result.notices = false;
50+
}
51+
52+
return Promise.resolve(result);
53+
}),
5154
}));
5255

5356
describe('exec verbose flag tests', () => {
@@ -97,3 +100,296 @@ describe('exec verbose flag tests', () => {
97100
expect(CliIoHost.instance().logLevel).toBe('trace');
98101
});
99102
});
103+
104+
describe('notices configuration tests', () => {
105+
let mockNoticesCreate: jest.SpyInstance;
106+
107+
beforeEach(() => {
108+
jest.clearAllMocks();
109+
110+
// Mock the Notices.create method
111+
mockNoticesCreate = jest.spyOn(Notices, 'create').mockReturnValue({
112+
refresh: jest.fn().mockResolvedValue(undefined),
113+
display: jest.fn(),
114+
} as any);
115+
116+
// Set up version module for our tests
117+
jest.mock('../../lib/cli/version', () => ({
118+
...originalVersion,
119+
DISPLAY_VERSION: 'test-version',
120+
displayVersionMessage: jest.fn().mockResolvedValue(undefined),
121+
}));
122+
});
123+
124+
afterEach(() => {
125+
mockNoticesCreate.mockRestore();
126+
// Restore the version module to its original state
127+
jest.resetModules();
128+
jest.setMock('../../lib/cli/version', originalVersion);
129+
});
130+
131+
test('should send notices to "stderr" when passing --notices flag in CLI', async () => {
132+
await exec(['--notices', 'version']);
133+
134+
expect(mockNoticesCreate).toHaveBeenCalledWith(
135+
expect.objectContaining({
136+
"ioHost": expect.objectContaining({
137+
"noticesDestination": "stderr",
138+
}),
139+
})
140+
);
141+
});
142+
143+
test('should send notices to "drop" when passing --no-notices in CLI', async () => {
144+
await exec(['--no-notices', 'version']);
145+
146+
expect(mockNoticesCreate).toHaveBeenCalledWith(
147+
expect.objectContaining({
148+
"ioHost": expect.objectContaining({
149+
"noticesDestination": "drop",
150+
}),
151+
})
152+
);
153+
});
154+
155+
test('should send notices to "drop" when notices: false in settings and no CLI flag is provided', async () => {
156+
// Mock configuration to return notices: false
157+
const mockConfig = {
158+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
159+
settings: {
160+
get: jest.fn().mockImplementation((key: string[]) => {
161+
if (key[0] === 'notices') return false;
162+
return undefined;
163+
}),
164+
},
165+
context: {
166+
get: jest.fn().mockReturnValue([]),
167+
},
168+
};
169+
170+
(Configuration as any).mockImplementation(() => mockConfig);
171+
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => mockConfig);
172+
173+
await exec(['version']);
174+
175+
expect(mockNoticesCreate).toHaveBeenCalledWith(
176+
expect.objectContaining({
177+
"ioHost": expect.objectContaining({
178+
"noticesDestination": "drop",
179+
}),
180+
})
181+
);
182+
});
183+
184+
test.each([
185+
{
186+
envVar: 'TEAMCITY_VERSION',
187+
description: 'TeamCity',
188+
scenarios: [
189+
{
190+
name: 'should send notices to "drop" by default when no settings or CLI flags are provided',
191+
configNotices: undefined,
192+
cliArgs: ['version'],
193+
expectedDestination: 'drop'
194+
},
195+
{
196+
name: 'should send notices to "stderr" when config setting notices=true',
197+
configNotices: true,
198+
cliArgs: ['version'],
199+
expectedDestination: 'stderr'
200+
},
201+
{
202+
name: 'should send notices to "stderr" when passing --notices CLI flag',
203+
configNotices: undefined,
204+
cliArgs: ['--notices', 'version'],
205+
expectedDestination: 'stderr'
206+
},
207+
{
208+
name: 'should send notices to "drop" when passing --no-notices CLI flag, even when config has notices=true',
209+
configNotices: true,
210+
cliArgs: ['--no-notices', 'version'],
211+
expectedDestination: 'drop'
212+
}
213+
]
214+
},
215+
{
216+
envVar: 'TF_BUILD',
217+
description: 'Azure DevOps',
218+
scenarios: [
219+
{
220+
name: 'should send notices to "drop" when no settings or CLI flags are provided',
221+
configNotices: undefined,
222+
cliArgs: ['version'],
223+
expectedDestination: 'drop'
224+
},
225+
{
226+
name: 'should send notices to "stderr" config setting notices=true',
227+
configNotices: true,
228+
cliArgs: ['version'],
229+
expectedDestination: 'stderr'
230+
},
231+
{
232+
name: 'should send notices to "stderr" --notices CLI flag',
233+
configNotices: undefined,
234+
cliArgs: ['--notices', 'version'],
235+
expectedDestination: 'stderr'
236+
},
237+
{
238+
name: 'should send notices to "drop" when passing --no-notices CLI flag, even when config has notices=true',
239+
configNotices: true,
240+
cliArgs: ['--no-notices', 'version'],
241+
expectedDestination: 'drop'
242+
}
243+
]
244+
},
245+
])('CI environment with $description', async ({ envVar, scenarios }) => {
246+
for (const scenario of scenarios) {
247+
// Store original environment variables
248+
const originalCI = process.env.CI;
249+
const originalEnvVar = process.env[envVar];
250+
251+
// Set CI environment variables
252+
process.env.CI = '1';
253+
process.env[envVar] = '1';
254+
255+
try {
256+
// Mock configuration
257+
const mockConfig = {
258+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
259+
settings: {
260+
get: jest.fn().mockImplementation((key: string[]) => {
261+
if (key[0] === 'notices') return scenario.configNotices;
262+
return undefined;
263+
}),
264+
},
265+
context: {
266+
get: jest.fn().mockReturnValue([]),
267+
},
268+
};
269+
270+
(Configuration as any).mockImplementation(() => mockConfig);
271+
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => mockConfig);
272+
273+
await exec(scenario.cliArgs);
274+
275+
expect(mockNoticesCreate).toHaveBeenCalledWith(
276+
expect.objectContaining({
277+
"ioHost": expect.objectContaining({
278+
"noticesDestination": scenario.expectedDestination,
279+
}),
280+
})
281+
);
282+
} finally {
283+
// Restore original environment variables
284+
if (originalCI !== undefined) {
285+
process.env.CI = originalCI;
286+
} else {
287+
delete process.env.CI;
288+
}
289+
if (originalEnvVar !== undefined) {
290+
process.env[envVar] = originalEnvVar;
291+
} else {
292+
delete process.env[envVar];
293+
}
294+
}
295+
}
296+
});
297+
298+
test('should read notices=true setting from configuration', async () => {
299+
// Mock configuration to return notices: true
300+
const mockConfig = {
301+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
302+
settings: {
303+
get: jest.fn().mockImplementation((key: string) => {
304+
if (key[0] === 'notices') return true;
305+
return undefined;
306+
}),
307+
},
308+
context: {
309+
get: jest.fn().mockReturnValue([]),
310+
},
311+
};
312+
313+
(Configuration as any).mockImplementation(() => mockConfig);
314+
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => mockConfig);
315+
316+
await exec(['version']);
317+
318+
expect(mockNoticesCreate).toHaveBeenCalledWith(
319+
expect.objectContaining({
320+
"ioHost": expect.objectContaining({
321+
"noticesDestination": "stderr",
322+
}),
323+
})
324+
);
325+
});
326+
327+
test('should send notices to "drop" when passing --no-notices in CLI and config set to notices: false', async () => {
328+
// Mock configuration to return notices: true, but CLI flag should override
329+
const mockConfig = {
330+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
331+
settings: {
332+
get: jest.fn().mockImplementation((key: string) => {
333+
if (key[0] === 'notices') return true;
334+
return undefined;
335+
}),
336+
},
337+
context: {
338+
get: jest.fn().mockReturnValue([]),
339+
},
340+
};
341+
342+
(Configuration as any).mockImplementation(() => mockConfig);
343+
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => mockConfig);
344+
345+
await exec(['--no-notices', 'version']);
346+
347+
expect(mockNoticesCreate).toHaveBeenCalledWith(
348+
expect.objectContaining({
349+
"ioHost": expect.objectContaining({
350+
"noticesDestination": "drop",
351+
}),
352+
})
353+
);
354+
});
355+
356+
test.each([
357+
{ value: undefined, expected: 'stderr', description: 'undefined (autodetection)' },
358+
{ value: false, expected: 'drop', description: 'boolean false' },
359+
{ value: true, expected: 'stderr', description: 'boolean true' },
360+
// support string "false" as false
361+
{ value: 'false', expected: 'drop', description: 'string "false"' },
362+
{ value: 'truthy', expected: 'stderr', description: 'string "truthy"' },
363+
{ value: 0, expected: 'drop', description: 'numeric 0' },
364+
{ value: 1, expected: 'stderr', description: 'numeric 1' },
365+
{ value: '', expected: 'drop', description: 'empty string' },
366+
{ value: null, expected: 'drop', description: 'null' },
367+
])('should send notices to "$expected" config value: $description', async ({ value, expected }) => {
368+
// Mock configuration to return the test value
369+
const mockConfig = {
370+
loadConfigFiles: jest.fn().mockResolvedValue(undefined),
371+
settings: {
372+
get: jest.fn().mockImplementation((key: string[]) => {
373+
if (key[0] === 'notices') return value;
374+
return undefined;
375+
}),
376+
},
377+
context: {
378+
get: jest.fn().mockReturnValue([]),
379+
},
380+
};
381+
382+
(Configuration as any).mockImplementation(() => mockConfig);
383+
Configuration.fromArgsAndFiles = jest.fn().mockImplementation(() => mockConfig);
384+
385+
await exec(['version']);
386+
387+
expect(mockNoticesCreate).toHaveBeenCalledWith(
388+
expect.objectContaining({
389+
"ioHost": expect.objectContaining({
390+
"noticesDestination": expected,
391+
}),
392+
})
393+
);
394+
});
395+
});

0 commit comments

Comments
 (0)