Skip to content

Commit 38d9da2

Browse files
authored
Merge pull request #520 from gitKrystan/better-args
Handle 'true'/'false' stringified boolean options and better config error handling
2 parents f8d8fd2 + cdb8b11 commit 38d9da2

File tree

10 files changed

+587
-46
lines changed

10 files changed

+587
-46
lines changed

README.md

Lines changed: 11 additions & 11 deletions
Large diffs are not rendered by default.

test/helpers/expect-logs.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { expect, jest } from '@jest/globals';
2+
import logger from '../../transforms/helpers/log-helper';
3+
4+
/**
5+
* Spies on all logger log levels for messages matching those passed in the
6+
* config.
7+
*
8+
* @param callback The callback expected to trigger (or not) the logs.
9+
* @param config An optional object with an array of expected messages for each
10+
* log level. If no array is passed, no messages will be expected for that
11+
* level. If no object is passed, the function will expect that there are no
12+
* logs.
13+
*/
14+
export function expectLogs(
15+
callback: () => void,
16+
{
17+
info = [],
18+
warn = [],
19+
error = [],
20+
}: {
21+
info?: string[];
22+
warn?: string[];
23+
error?: string[];
24+
} = {}
25+
): void {
26+
const infoConfig = {
27+
level: 'info' as const,
28+
expectedMessages: info,
29+
restoreAllMocks: false,
30+
};
31+
const warnConfig = {
32+
level: 'warn' as const,
33+
expectedMessages: warn,
34+
restoreAllMocks: false,
35+
};
36+
const errorConfig = {
37+
level: 'error' as const,
38+
expectedMessages: error,
39+
restoreAllMocks: true,
40+
};
41+
42+
expectLogLevel(() => {
43+
expectLogLevel(() => {
44+
expectLogLevel(callback, infoConfig);
45+
}, warnConfig);
46+
}, errorConfig);
47+
48+
jest.restoreAllMocks();
49+
}
50+
51+
/**
52+
* Spies on the logger for messages matching those passed in the config.
53+
*
54+
* @param callback The callback expected to trigger (or not) the logs.
55+
* @param config An optional object with an specified log `level`, an array of
56+
* `expectedMessages` for that log level, and an option to run
57+
* `jest.restoreAllMocks()` after the callback and expectations are complete.
58+
* If no object is passed, will default to spying on the `'error'` log level,
59+
* expect that no messages are sent, and will restore all mocks after the test.
60+
*/
61+
function expectLogLevel(
62+
callback: () => void,
63+
{
64+
level = 'error',
65+
expectedMessages = [],
66+
restoreAllMocks = true,
67+
}: {
68+
level?: 'info' | 'warn' | 'error';
69+
expectedMessages?: string[];
70+
restoreAllMocks?: boolean;
71+
} = {}
72+
): void {
73+
const spy = jest.spyOn(logger, level);
74+
75+
callback();
76+
77+
if (expectedMessages.length > 0) {
78+
expect(spy).toHaveBeenCalledTimes(expectedMessages.length);
79+
for (const [index, expectedError] of expectedMessages.entries()) {
80+
expect(spy).toHaveBeenNthCalledWith(
81+
index + 1,
82+
expect.stringMatching(expectedError)
83+
);
84+
}
85+
} else {
86+
expect(spy).not.toHaveBeenCalled();
87+
}
88+
89+
if (restoreAllMocks) {
90+
jest.restoreAllMocks();
91+
}
92+
}
93+
94+
/**
95+
* Makes a regexp pattern to match logs. String arguments passed to
96+
* `makeLogMatcher` will be escaped then merged together into a regexp that will
97+
* match partial lines of multi-line logs when paired with Jest
98+
* `expect.stringMatching`.
99+
*
100+
* @example
101+
* ```
102+
* const expected = makeLogMatcher('Line 1', 'Line 2', '3')
103+
* //=> 'Line 1[\S\s]*Line 2[\S\s]*3'
104+
*
105+
* expect('Line 1\nLine 2\nLine 3').toEqual(expect.stringMatching(expected));
106+
* //=> passes
107+
* ```
108+
*/
109+
export function makeLogMatcher(...parts: string[]): string {
110+
return parts.map(escapeRegExp).join('[\\S\\s]*');
111+
}
112+
113+
/**
114+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
115+
*/
116+
function escapeRegExp(string: string): string {
117+
return string.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&'); // $& means the whole matched string
118+
}

test/options.test.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import { describe, expect, test } from '@jest/globals';
2+
import { DEFAULT_OPTIONS, parseConfig } from '../transforms/helpers/options';
3+
import { expectLogs, makeLogMatcher } from './helpers/expect-logs';
4+
5+
describe('options', () => {
6+
describe('parseConfig', () => {
7+
test('it parses an empty config', () => {
8+
expectLogs(() => {
9+
const config = parseConfig('test', {});
10+
expect(config).toStrictEqual({});
11+
});
12+
});
13+
14+
test('it parses the DEFAULT_OPTIONS', () => {
15+
expectLogs(() => {
16+
const config = parseConfig('test', DEFAULT_OPTIONS);
17+
expect(config).toStrictEqual(DEFAULT_OPTIONS);
18+
});
19+
});
20+
21+
describe('decorators', () => {
22+
test('it parses `{ decorators: true }`', () => {
23+
expectLogs(() => {
24+
const config = parseConfig('test', { decorators: true });
25+
expect(config).toStrictEqual({
26+
decorators: { inObjectLiterals: [] },
27+
});
28+
});
29+
});
30+
31+
test('it parses `{ decorators: "true" }`', () => {
32+
expectLogs(() => {
33+
const config = parseConfig('test', { decorators: 'true' });
34+
expect(config).toStrictEqual({
35+
decorators: { inObjectLiterals: [] },
36+
});
37+
});
38+
});
39+
40+
test('it parses `{ decorators: false }`', () => {
41+
expectLogs(() => {
42+
const config = parseConfig('test', { decorators: false });
43+
expect(config).toStrictEqual({ decorators: false });
44+
});
45+
});
46+
47+
test('it parses `{ decorators: "false" }`', () => {
48+
expectLogs(() => {
49+
const config = parseConfig('test', { decorators: 'false' });
50+
expect(config).toStrictEqual({ decorators: false });
51+
});
52+
});
53+
54+
test('it parses DecoratorOptions.inObjectLiterals with array of strings', () => {
55+
expectLogs(() => {
56+
const config = parseConfig('test', {
57+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
58+
});
59+
expect(config).toStrictEqual({
60+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
61+
});
62+
});
63+
});
64+
65+
test('it parses DecoratorOptions.inObjectLiterals with string of strings', () => {
66+
expectLogs(() => {
67+
const config = parseConfig('test', {
68+
decorators: { inObjectLiterals: 'one,two , three' },
69+
});
70+
expect(config).toStrictEqual({
71+
decorators: { inObjectLiterals: ['one', 'two', 'three'] },
72+
});
73+
});
74+
});
75+
76+
test('it logs an error for invalid `decorators` config', () => {
77+
expectLogs(
78+
() => {
79+
const config = parseConfig('test', { decorators: 'oops' });
80+
expect(config).toStrictEqual({});
81+
},
82+
{
83+
error: [
84+
makeLogMatcher(
85+
'[test]: CONFIG ERROR:',
86+
"[decorators] Expected DecoratorOptions object or boolean, received 'oops'"
87+
),
88+
],
89+
}
90+
);
91+
});
92+
});
93+
94+
describe.each(['classFields', 'classicDecorator', 'partialTransforms'])(
95+
'%s (StringBooleanSchema)',
96+
(fieldName) => {
97+
test(`it parses \`{ ${fieldName}: true }\``, () => {
98+
expectLogs(() => {
99+
const config = parseConfig('test', { [fieldName]: true });
100+
expect(config).toStrictEqual({ [fieldName]: true });
101+
});
102+
});
103+
104+
test(`it parses \`{ ${fieldName}: "true" }\``, () => {
105+
expectLogs(() => {
106+
const config = parseConfig('test', { [fieldName]: 'true' });
107+
expect(config).toStrictEqual({ [fieldName]: true });
108+
});
109+
});
110+
111+
test(`it parses \`{ ${fieldName}: false }\``, () => {
112+
expectLogs(() => {
113+
const config = parseConfig('test', { [fieldName]: false });
114+
expect(config).toStrictEqual({ [fieldName]: false });
115+
});
116+
});
117+
118+
test(`it parses \`{ ${fieldName}: "false" }\``, () => {
119+
expectLogs(() => {
120+
const config = parseConfig('test', { [fieldName]: 'false' });
121+
expect(config).toStrictEqual({ [fieldName]: false });
122+
});
123+
});
124+
125+
test(`it logs an error for invalid \`${fieldName}\` config`, () => {
126+
expectLogs(
127+
() => {
128+
const config = parseConfig('test', { [fieldName]: 'oops' });
129+
expect(config).toStrictEqual({});
130+
},
131+
{
132+
error: [
133+
makeLogMatcher(
134+
'[test]: CONFIG ERROR:',
135+
`[${fieldName}] Expected boolean, received string`
136+
),
137+
],
138+
}
139+
);
140+
});
141+
}
142+
);
143+
144+
describe('quote', () => {
145+
test('it parses `{ quote: "single" }`', () => {
146+
expectLogs(() => {
147+
const config = parseConfig('test', { quote: 'single' });
148+
expect(config).toStrictEqual({ quote: 'single' });
149+
});
150+
});
151+
152+
test('it parses `{ quote: "double" }`', () => {
153+
expectLogs(() => {
154+
const config = parseConfig('test', { quote: 'double' });
155+
expect(config).toStrictEqual({ quote: 'double' });
156+
});
157+
});
158+
159+
test('it logs an error for invalid `quote` config', () => {
160+
expectLogs(
161+
() => {
162+
const config = parseConfig('test', { quote: 'oops' });
163+
expect(config).toStrictEqual({});
164+
},
165+
{
166+
error: [
167+
makeLogMatcher(
168+
'[test]: CONFIG ERROR:',
169+
"[quote] Expected 'single' or 'double', received 'oops"
170+
),
171+
],
172+
}
173+
);
174+
});
175+
});
176+
177+
describe('ignoreLeakingState', () => {
178+
test('it parses `ignoreLeakingState` with an empty array', () => {
179+
expectLogs(() => {
180+
const config = parseConfig('test', { ignoreLeakingState: [] });
181+
expect(config).toStrictEqual({ ignoreLeakingState: [] });
182+
});
183+
});
184+
185+
test('it parses `ignoreLeakingState` with array of strings', () => {
186+
expectLogs(() => {
187+
const config = parseConfig('test', {
188+
ignoreLeakingState: ['one', 'two', 'three'],
189+
});
190+
expect(config).toStrictEqual({
191+
ignoreLeakingState: ['one', 'two', 'three'],
192+
});
193+
});
194+
});
195+
196+
test('it parses `ignoreLeakingState` with string of strings', () => {
197+
expectLogs(() => {
198+
const config = parseConfig('test', {
199+
ignoreLeakingState: 'one,two , three',
200+
});
201+
expect(config).toStrictEqual({
202+
ignoreLeakingState: ['one', 'two', 'three'],
203+
});
204+
});
205+
});
206+
207+
test('it logs an error for invalid `ignoreLeakingState` config', () => {
208+
expectLogs(
209+
() => {
210+
const config = parseConfig('test', { ignoreLeakingState: false });
211+
expect(config).toStrictEqual({});
212+
},
213+
{
214+
error: [
215+
makeLogMatcher(
216+
'[test]: CONFIG ERROR:',
217+
'[ignoreLeakingState] Expected array of strings or comma-separated string, received false'
218+
),
219+
],
220+
}
221+
);
222+
});
223+
});
224+
225+
describe('type', () => {
226+
test.each(['services', 'routes', 'components', 'controllers'])(
227+
'it parses `{ type: "%s" }`',
228+
(type) => {
229+
expectLogs(() => {
230+
const config = parseConfig('test', { type });
231+
expect(config).toStrictEqual({ type });
232+
});
233+
}
234+
);
235+
236+
test('it logs an error for invalid `type` config', () => {
237+
expectLogs(
238+
() => {
239+
const config = parseConfig('test', { type: 'oops' });
240+
expect(config).toStrictEqual({});
241+
},
242+
{
243+
error: [
244+
makeLogMatcher(
245+
'[test]: CONFIG ERROR:',
246+
"[type] Expected 'services', 'routes', 'components', or 'controllers', received 'oops"
247+
),
248+
],
249+
}
250+
);
251+
});
252+
});
253+
});
254+
});

0 commit comments

Comments
 (0)