@nestjs/testing 12.1.0 → 12.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './interfaces/index.js';
2
+ export * from './services/capturing-logger.service.js';
2
3
  export * from './test.js';
3
4
  export * from './testing-module.js';
4
5
  export * from './testing-module.builder.js';
package/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * MIT Licensed
6
6
  */
7
7
  export * from './interfaces/index.js';
8
+ export * from './services/capturing-logger.service.js';
8
9
  export * from './test.js';
9
10
  export * from './testing-module.js';
10
11
  export * from './testing-module.builder.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/testing",
3
- "version": "12.1.0",
3
+ "version": "12.1.1",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@testing)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -41,5 +41,5 @@
41
41
  "optional": true
42
42
  }
43
43
  },
44
- "gitHead": "6739656fa3e8c98bd55883f0363a2254e6412232"
44
+ "gitHead": "9feedffde8ef8c86f0cd521153740575dc579d88"
45
45
  }
@@ -0,0 +1,129 @@
1
+ import { ConsoleLogger, type ConsoleLoggerOptions, type LogLevel } from '@nestjs/common';
2
+ /**
3
+ * A log call recorded by `CapturingLogger`.
4
+ *
5
+ * @publicApi
6
+ */
7
+ export interface CapturedLog {
8
+ level: LogLevel;
9
+ /**
10
+ * The message. A lazy message (a function) is evaluated, and when an
11
+ * `Error` is logged as the message, this is the error's message.
12
+ */
13
+ message: unknown;
14
+ /**
15
+ * The context, e.g. the one passed to `new Logger('Context')`.
16
+ */
17
+ context?: string;
18
+ /**
19
+ * The plain objects passed after the message, merged.
20
+ */
21
+ params?: Record<string, any>;
22
+ /**
23
+ * The first `Error` passed to the call, as the message or after it.
24
+ */
25
+ error?: Error;
26
+ /**
27
+ * The stack trace passed as a string, as in `logger.error(message, stack)`.
28
+ */
29
+ stack?: string;
30
+ /**
31
+ * The arguments, as passed to the logger (including the context).
32
+ */
33
+ args: unknown[];
34
+ }
35
+ /**
36
+ * Criteria for `CapturingLogger` queries. Every criterion that is set must
37
+ * match.
38
+ *
39
+ * @publicApi
40
+ */
41
+ export interface CapturedLogFilter {
42
+ level?: LogLevel;
43
+ /**
44
+ * A string matches the context exactly.
45
+ */
46
+ context?: string | RegExp;
47
+ /**
48
+ * A string matches when the message contains it. Only string messages
49
+ * match (an `Error` logged as the message counts, through its message).
50
+ */
51
+ message?: string | RegExp;
52
+ /**
53
+ * Matches when each of these params is deeply equal to the logged one.
54
+ */
55
+ params?: Record<string, unknown>;
56
+ }
57
+ /**
58
+ * @publicApi
59
+ */
60
+ export type CapturedLogMatcher = CapturedLogFilter | ((entry: CapturedLog) => boolean);
61
+ /**
62
+ * @publicApi
63
+ */
64
+ export interface CapturingLoggerOptions extends ConsoleLoggerOptions {
65
+ /**
66
+ * If enabled, logs are also printed, as a `ConsoleLogger` with the same
67
+ * options would print them.
68
+ * @default false
69
+ */
70
+ print?: boolean;
71
+ }
72
+ /**
73
+ * A logger for tests that records every log call with its structured data,
74
+ * so that tests can assert on it. Pass it to `setLogger()` of the testing
75
+ * module builder, or to `app.useLogger()`, to capture the framework's logs and
76
+ * the logs of `Logger` instances.
77
+ *
78
+ * Every level is captured unless `logLevels` is set; the `NEST_LOG_LEVEL`
79
+ * environment variable is ignored.
80
+ *
81
+ * @example
82
+ * const logger = new CapturingLogger();
83
+ * const moduleRef = await Test.createTestingModule({ providers: [PaymentsService] })
84
+ * .setLogger(logger)
85
+ * .compile();
86
+ *
87
+ * await moduleRef.get(PaymentsService).charge(order);
88
+ *
89
+ * logger.assertLogged({ level: 'warn', context: 'PaymentsService', message: /retrying/ });
90
+ *
91
+ * @publicApi
92
+ */
93
+ export declare class CapturingLogger extends ConsoleLogger {
94
+ private readonly captured;
95
+ private readonly shouldPrint;
96
+ constructor(options?: CapturingLoggerOptions);
97
+ /**
98
+ * The recorded entries, oldest first.
99
+ */
100
+ get entries(): CapturedLog[];
101
+ log(message: any, ...optionalParams: any[]): void;
102
+ error(message: any, ...optionalParams: any[]): void;
103
+ warn(message: any, ...optionalParams: any[]): void;
104
+ debug(message: any, ...optionalParams: any[]): void;
105
+ verbose(message: any, ...optionalParams: any[]): void;
106
+ fatal(message: any, ...optionalParams: any[]): void;
107
+ /**
108
+ * Returns the entries that match.
109
+ */
110
+ filter(matcher?: CapturedLogMatcher): CapturedLog[];
111
+ /**
112
+ * Returns the first entry that matches.
113
+ */
114
+ find(matcher: CapturedLogMatcher): CapturedLog | undefined;
115
+ /**
116
+ * Throws an error listing the captured entries if no entry matches.
117
+ * @returns The first entry that matches.
118
+ */
119
+ assertLogged(matcher: CapturedLogMatcher): CapturedLog;
120
+ /**
121
+ * Throws an error listing the matching entries if any entry matches.
122
+ */
123
+ assertNotLogged(matcher: CapturedLogMatcher): void;
124
+ /**
125
+ * Removes the recorded entries.
126
+ */
127
+ clear(): void;
128
+ private capture;
129
+ }
@@ -0,0 +1,179 @@
1
+ import { ConsoleLogger, LOG_LEVELS, } from '@nestjs/common';
2
+ import { inspect, isDeepStrictEqual } from 'util';
3
+ /**
4
+ * A logger for tests that records every log call with its structured data,
5
+ * so that tests can assert on it. Pass it to `setLogger()` of the testing
6
+ * module builder, or to `app.useLogger()`, to capture the framework's logs and
7
+ * the logs of `Logger` instances.
8
+ *
9
+ * Every level is captured unless `logLevels` is set; the `NEST_LOG_LEVEL`
10
+ * environment variable is ignored.
11
+ *
12
+ * @example
13
+ * const logger = new CapturingLogger();
14
+ * const moduleRef = await Test.createTestingModule({ providers: [PaymentsService] })
15
+ * .setLogger(logger)
16
+ * .compile();
17
+ *
18
+ * await moduleRef.get(PaymentsService).charge(order);
19
+ *
20
+ * logger.assertLogged({ level: 'warn', context: 'PaymentsService', message: /retrying/ });
21
+ *
22
+ * @publicApi
23
+ */
24
+ export class CapturingLogger extends ConsoleLogger {
25
+ captured = [];
26
+ shouldPrint;
27
+ constructor(options = {}) {
28
+ const { print = false, ...consoleLoggerOptions } = options;
29
+ super({
30
+ ...consoleLoggerOptions,
31
+ logLevels: consoleLoggerOptions.logLevels ?? [...LOG_LEVELS],
32
+ });
33
+ this.shouldPrint = print;
34
+ }
35
+ /**
36
+ * The recorded entries, oldest first.
37
+ */
38
+ get entries() {
39
+ return [...this.captured];
40
+ }
41
+ log(message, ...optionalParams) {
42
+ this.capture('log', [message, ...optionalParams]);
43
+ }
44
+ error(message, ...optionalParams) {
45
+ this.capture('error', [message, ...optionalParams]);
46
+ }
47
+ warn(message, ...optionalParams) {
48
+ this.capture('warn', [message, ...optionalParams]);
49
+ }
50
+ debug(message, ...optionalParams) {
51
+ this.capture('debug', [message, ...optionalParams]);
52
+ }
53
+ verbose(message, ...optionalParams) {
54
+ this.capture('verbose', [message, ...optionalParams]);
55
+ }
56
+ fatal(message, ...optionalParams) {
57
+ this.capture('fatal', [message, ...optionalParams]);
58
+ }
59
+ /**
60
+ * Returns the entries that match.
61
+ */
62
+ filter(matcher = {}) {
63
+ const predicate = toPredicate(matcher);
64
+ return this.captured.filter(entry => predicate(entry));
65
+ }
66
+ /**
67
+ * Returns the first entry that matches.
68
+ */
69
+ find(matcher) {
70
+ return this.captured.find(toPredicate(matcher));
71
+ }
72
+ /**
73
+ * Throws an error listing the captured entries if no entry matches.
74
+ * @returns The first entry that matches.
75
+ */
76
+ assertLogged(matcher) {
77
+ const entry = this.find(matcher);
78
+ if (!entry) {
79
+ throw new Error(`Expected a log entry matching ${describeMatcher(matcher)}, but none of the ${this.captured.length} captured entries matched.${formatEntries(this.captured)}`);
80
+ }
81
+ return entry;
82
+ }
83
+ /**
84
+ * Throws an error listing the matching entries if any entry matches.
85
+ */
86
+ assertNotLogged(matcher) {
87
+ const entries = this.filter(matcher);
88
+ if (entries.length > 0) {
89
+ throw new Error(`Expected no log entry matching ${describeMatcher(matcher)}, but ${entries.length} matched.${formatEntries(entries)}`);
90
+ }
91
+ }
92
+ /**
93
+ * Removes the recorded entries.
94
+ */
95
+ clear() {
96
+ this.captured.length = 0;
97
+ }
98
+ capture(level, args) {
99
+ if (!this.isLevelEnabled(level)) {
100
+ return;
101
+ }
102
+ const parsed = level === 'error' || level === 'fatal'
103
+ ? this.getContextAndStackAndMessagesToPrint(args)
104
+ : this.getContextAndMessagesToPrint(args);
105
+ // Evaluated once, for the entry and for printing.
106
+ const resolvedMessages = parsed.messages.map(message => this.resolveMessage(message));
107
+ const { messages, error } = this.extractJsonError(resolvedMessages);
108
+ const params = 'params' in parsed ? parsed.params : undefined;
109
+ const stack = 'stack' in parsed && typeof parsed.stack === 'string'
110
+ ? parsed.stack
111
+ : undefined;
112
+ const entry = { level, message: messages[0], args };
113
+ if (parsed.context) {
114
+ entry.context = parsed.context;
115
+ }
116
+ if (params) {
117
+ entry.params = params;
118
+ }
119
+ if (error) {
120
+ entry.error = error;
121
+ }
122
+ if (stack) {
123
+ entry.stack = stack;
124
+ }
125
+ this.captured.push(entry);
126
+ if (this.shouldPrint) {
127
+ // What the ConsoleLogger method for this level does once the arguments
128
+ // are parsed.
129
+ const isError = level === 'error' || level === 'fatal';
130
+ this.printMessages(resolvedMessages, parsed.context, level, isError ? 'stderr' : 'stdout', stack, params);
131
+ if (isError) {
132
+ this.printStackTrace(stack);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ function toPredicate(matcher) {
138
+ if (typeof matcher === 'function') {
139
+ return matcher;
140
+ }
141
+ const { level, context, message, params } = matcher;
142
+ return entry => (level === undefined || entry.level === level) &&
143
+ (context === undefined ||
144
+ (typeof context === 'string'
145
+ ? entry.context === context
146
+ : entry.context !== undefined &&
147
+ entry.context.search(context) !== -1)) &&
148
+ (message === undefined ||
149
+ (typeof entry.message === 'string' &&
150
+ (typeof message === 'string'
151
+ ? entry.message.includes(message)
152
+ : entry.message.search(message) !== -1))) &&
153
+ (params === undefined ||
154
+ Object.entries(params).every(([key, value]) => isDeepStrictEqual(entry.params?.[key], value)));
155
+ }
156
+ function describeMatcher(matcher) {
157
+ return typeof matcher === 'function'
158
+ ? 'the predicate'
159
+ : inspect(matcher, { breakLength: Infinity });
160
+ }
161
+ const MAX_LISTED_ENTRIES = 50;
162
+ function formatEntries(entries) {
163
+ if (entries.length === 0) {
164
+ return '';
165
+ }
166
+ const lines = entries.slice(-MAX_LISTED_ENTRIES).map(entry => {
167
+ const context = entry.context ? ` [${entry.context}]` : '';
168
+ const message = typeof entry.message === 'string'
169
+ ? entry.message
170
+ : inspect(entry.message, { breakLength: Infinity, depth: 2 });
171
+ const params = entry.params
172
+ ? ` ${inspect(entry.params, { breakLength: Infinity, depth: 2 })}`
173
+ : '';
174
+ return ` ${entry.level.toUpperCase()}${context} ${message}${params}`;
175
+ });
176
+ const omitted = entries.length - lines.length;
177
+ const header = omitted > 0 ? `\n (${omitted} earlier entries omitted)` : '';
178
+ return `\nCaptured entries:${header}\n${lines.join('\n')}`;
179
+ }