@techstream/quark-core 1.1.0
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/.turbo/turbo-lint.log +7 -0
- package/.turbo/turbo-test.log +1376 -0
- package/README.md +419 -0
- package/package.json +29 -0
- package/src/auth/index.js +127 -0
- package/src/auth/password.js +9 -0
- package/src/auth.test.js +90 -0
- package/src/authorization.js +235 -0
- package/src/authorization.test.js +314 -0
- package/src/cache.js +137 -0
- package/src/cache.test.js +217 -0
- package/src/csrf.js +118 -0
- package/src/csrf.test.js +157 -0
- package/src/email.js +140 -0
- package/src/email.test.js +259 -0
- package/src/error-reporter.js +266 -0
- package/src/error-reporter.test.js +236 -0
- package/src/errors.js +192 -0
- package/src/errors.test.js +128 -0
- package/src/index.js +32 -0
- package/src/logger.js +182 -0
- package/src/logger.test.js +287 -0
- package/src/mailhog.js +43 -0
- package/src/queue/index.js +214 -0
- package/src/rate-limiter.js +253 -0
- package/src/rate-limiter.test.js +130 -0
- package/src/redis.js +96 -0
- package/src/testing/factories.js +93 -0
- package/src/testing/helpers.js +266 -0
- package/src/testing/index.js +46 -0
- package/src/testing/mocks.js +480 -0
- package/src/testing/testing.test.js +543 -0
- package/src/types.js +74 -0
- package/src/utils.js +219 -0
- package/src/utils.test.js +193 -0
- package/src/validation.js +26 -0
- package/test-imports.mjs +21 -0
package/src/logger.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-core - Logger Module
|
|
3
|
+
* Lightweight structured logger with zero external dependencies
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @enum {number} */
|
|
7
|
+
const LOG_LEVELS = {
|
|
8
|
+
debug: 10,
|
|
9
|
+
info: 20,
|
|
10
|
+
warn: 30,
|
|
11
|
+
error: 40,
|
|
12
|
+
fatal: 50,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const LEVEL_NAMES = /** @type {const} */ ({
|
|
16
|
+
10: "debug",
|
|
17
|
+
20: "info",
|
|
18
|
+
30: "warn",
|
|
19
|
+
40: "error",
|
|
20
|
+
50: "fatal",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** @type {Record<string, string>} */
|
|
24
|
+
const LEVEL_COLORS = {
|
|
25
|
+
debug: "\x1b[36m", // cyan
|
|
26
|
+
info: "\x1b[32m", // green
|
|
27
|
+
warn: "\x1b[33m", // yellow
|
|
28
|
+
error: "\x1b[31m", // red
|
|
29
|
+
fatal: "\x1b[35m", // magenta
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RESET = "\x1b[0m";
|
|
33
|
+
const DIM = "\x1b[2m";
|
|
34
|
+
|
|
35
|
+
const isProduction = () => process.env.NODE_ENV === "production";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the minimum log level from options and environment
|
|
39
|
+
* @param {string} [optionLevel] - Level from options
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
const resolveLevel = (optionLevel) => {
|
|
43
|
+
const level =
|
|
44
|
+
optionLevel || process.env.LOG_LEVEL || (isProduction() ? "info" : "debug");
|
|
45
|
+
return LOG_LEVELS[level] ?? LOG_LEVELS.info;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Formats a log entry for dev (colorized, human-readable)
|
|
50
|
+
* @param {object} entry - The log entry
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
const formatDev = (entry) => {
|
|
54
|
+
const { timestamp, level, name, msg, ...rest } = entry;
|
|
55
|
+
const color = LEVEL_COLORS[level] || "";
|
|
56
|
+
const time = timestamp.slice(11, 23); // HH:mm:ss.SSS
|
|
57
|
+
const contextStr =
|
|
58
|
+
Object.keys(rest).length > 0
|
|
59
|
+
? ` ${DIM}${JSON.stringify(rest)}${RESET}`
|
|
60
|
+
: "";
|
|
61
|
+
return `${DIM}${time}${RESET} ${color}${level.toUpperCase().padEnd(5)}${RESET} ${DIM}[${name}]${RESET} ${msg}${contextStr}`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Formats a log entry for production (single-line JSON)
|
|
66
|
+
* @param {object} entry - The log entry
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
const formatProd = (entry) => {
|
|
70
|
+
return JSON.stringify(entry);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the console method for a given level
|
|
75
|
+
* @param {string} level - The log level name
|
|
76
|
+
* @returns {Function}
|
|
77
|
+
*/
|
|
78
|
+
const getTransport = (level) => {
|
|
79
|
+
if (level === "error" || level === "fatal") return console.error;
|
|
80
|
+
if (level === "warn") return console.warn;
|
|
81
|
+
return console.log;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {Object} LoggerOptions
|
|
86
|
+
* @property {string} [name] - Logger name (e.g. "web", "worker", "db")
|
|
87
|
+
* @property {string} [level] - Minimum log level (default: "info" in production, "debug" in dev)
|
|
88
|
+
* @property {Object} [context] - Default context merged into every log entry
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {Object} Logger
|
|
93
|
+
* @property {(msg: string, data?: Object) => void} debug - Log at debug level
|
|
94
|
+
* @property {(msg: string, data?: Object) => void} info - Log at info level
|
|
95
|
+
* @property {(msg: string, data?: Object) => void} warn - Log at warn level
|
|
96
|
+
* @property {(msg: string, data?: Object) => void} error - Log at error level
|
|
97
|
+
* @property {(msg: string, data?: Object) => void} fatal - Log at fatal level
|
|
98
|
+
* @property {(context: Object) => Logger} child - Create a child logger with merged context
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a structured logger instance
|
|
103
|
+
* @param {LoggerOptions} [options] - Logger configuration
|
|
104
|
+
* @returns {Logger}
|
|
105
|
+
*/
|
|
106
|
+
export const createLogger = (options = {}) => {
|
|
107
|
+
const {
|
|
108
|
+
name = "app",
|
|
109
|
+
level: optionLevel,
|
|
110
|
+
context: defaultContext = {},
|
|
111
|
+
} = options;
|
|
112
|
+
const minLevel = resolveLevel(optionLevel);
|
|
113
|
+
const format = isProduction() ? formatProd : formatDev;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Writes a log entry if the level is at or above the minimum
|
|
117
|
+
* @param {string} levelName - The level name
|
|
118
|
+
* @param {string} msg - The log message
|
|
119
|
+
* @param {Object} [data] - Additional data
|
|
120
|
+
*/
|
|
121
|
+
const write = (levelName, msg, data = {}) => {
|
|
122
|
+
const numeric = LOG_LEVELS[levelName];
|
|
123
|
+
if (numeric < minLevel) return;
|
|
124
|
+
|
|
125
|
+
const entry = {
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
127
|
+
level: levelName,
|
|
128
|
+
name,
|
|
129
|
+
msg,
|
|
130
|
+
...defaultContext,
|
|
131
|
+
...data,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const transport = getTransport(levelName);
|
|
135
|
+
transport(format(entry));
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
debug: (msg, data) => write("debug", msg, data),
|
|
140
|
+
info: (msg, data) => write("info", msg, data),
|
|
141
|
+
warn: (msg, data) => write("warn", msg, data),
|
|
142
|
+
error: (msg, data) => write("error", msg, data),
|
|
143
|
+
fatal: (msg, data) => write("fatal", msg, data),
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates a child logger with merged context
|
|
147
|
+
* @param {Object} childContext - Additional context for the child logger
|
|
148
|
+
* @returns {Logger}
|
|
149
|
+
*/
|
|
150
|
+
child: (childContext = {}) => {
|
|
151
|
+
return createLogger({
|
|
152
|
+
name,
|
|
153
|
+
level: LEVEL_NAMES[minLevel],
|
|
154
|
+
context: { ...defaultContext, ...childContext },
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Creates a request logger middleware helper.
|
|
162
|
+
* Returns a function that accepts a request object and produces
|
|
163
|
+
* a child logger enriched with request-specific fields.
|
|
164
|
+
* @param {Logger} parentLogger - The parent logger instance
|
|
165
|
+
* @returns {(request: { method?: string, url?: string, headers?: Object }) => Logger}
|
|
166
|
+
*/
|
|
167
|
+
export const requestLogger = (parentLogger) => {
|
|
168
|
+
return (request) => {
|
|
169
|
+
const requestId = request.headers?.["x-request-id"] || crypto.randomUUID();
|
|
170
|
+
const method = request.method || "UNKNOWN";
|
|
171
|
+
const path = request.url
|
|
172
|
+
? new URL(request.url, "http://localhost").pathname
|
|
173
|
+
: "/";
|
|
174
|
+
|
|
175
|
+
const child = parentLogger.child({ requestId, method, path });
|
|
176
|
+
child.info("request started");
|
|
177
|
+
return child;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Default logger instance */
|
|
182
|
+
export const logger = createLogger({ name: "quark" });
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, beforeEach, describe, test } from "node:test";
|
|
3
|
+
import { createLogger, requestLogger } from "../src/logger.js";
|
|
4
|
+
|
|
5
|
+
/** Helper: captures console output during a callback */
|
|
6
|
+
const captureConsole = async (fn) => {
|
|
7
|
+
const output = { log: [], warn: [], error: [] };
|
|
8
|
+
const orig = {
|
|
9
|
+
log: console.log,
|
|
10
|
+
warn: console.warn,
|
|
11
|
+
error: console.error,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
console.log = (...args) => output.log.push(args.join(" "));
|
|
15
|
+
console.warn = (...args) => output.warn.push(args.join(" "));
|
|
16
|
+
console.error = (...args) => output.error.push(args.join(" "));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await fn();
|
|
20
|
+
} finally {
|
|
21
|
+
console.log = orig.log;
|
|
22
|
+
console.warn = orig.warn;
|
|
23
|
+
console.error = orig.error;
|
|
24
|
+
}
|
|
25
|
+
return output;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe("Logger", () => {
|
|
29
|
+
let originalNodeEnv;
|
|
30
|
+
let originalLogLevel;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
originalNodeEnv = process.env.NODE_ENV;
|
|
34
|
+
originalLogLevel = process.env.LOG_LEVEL;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
if (originalNodeEnv === undefined) {
|
|
39
|
+
delete process.env.NODE_ENV;
|
|
40
|
+
} else {
|
|
41
|
+
process.env.NODE_ENV = originalNodeEnv;
|
|
42
|
+
}
|
|
43
|
+
if (originalLogLevel === undefined) {
|
|
44
|
+
delete process.env.LOG_LEVEL;
|
|
45
|
+
} else {
|
|
46
|
+
process.env.LOG_LEVEL = originalLogLevel;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("creates logger with default options", () => {
|
|
51
|
+
const log = createLogger();
|
|
52
|
+
assert.ok(log.debug);
|
|
53
|
+
assert.ok(log.info);
|
|
54
|
+
assert.ok(log.warn);
|
|
55
|
+
assert.ok(log.error);
|
|
56
|
+
assert.ok(log.fatal);
|
|
57
|
+
assert.ok(log.child);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("default logger instance exists with name 'quark'", async () => {
|
|
61
|
+
process.env.NODE_ENV = "production";
|
|
62
|
+
const log = createLogger({ name: "quark" });
|
|
63
|
+
const output = await captureConsole(() => {
|
|
64
|
+
log.info("hello");
|
|
65
|
+
});
|
|
66
|
+
const entry = JSON.parse(output.log[0]);
|
|
67
|
+
assert.equal(entry.name, "quark");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("log level filtering: debug hidden at info level", async () => {
|
|
71
|
+
process.env.NODE_ENV = "production";
|
|
72
|
+
const log = createLogger({ name: "test", level: "info" });
|
|
73
|
+
|
|
74
|
+
const output = await captureConsole(() => {
|
|
75
|
+
log.debug("should be hidden");
|
|
76
|
+
log.info("should be visible");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.equal(output.log.length, 1);
|
|
80
|
+
const entry = JSON.parse(output.log[0]);
|
|
81
|
+
assert.equal(entry.msg, "should be visible");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("log level filtering: debug visible at debug level", async () => {
|
|
85
|
+
process.env.NODE_ENV = "production";
|
|
86
|
+
const log = createLogger({ name: "test", level: "debug" });
|
|
87
|
+
|
|
88
|
+
const output = await captureConsole(() => {
|
|
89
|
+
log.debug("visible");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert.equal(output.log.length, 1);
|
|
93
|
+
const entry = JSON.parse(output.log[0]);
|
|
94
|
+
assert.equal(entry.msg, "visible");
|
|
95
|
+
assert.equal(entry.level, "debug");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("respects LOG_LEVEL env var", async () => {
|
|
99
|
+
process.env.NODE_ENV = "production";
|
|
100
|
+
process.env.LOG_LEVEL = "warn";
|
|
101
|
+
const log = createLogger({ name: "test" });
|
|
102
|
+
|
|
103
|
+
const output = await captureConsole(() => {
|
|
104
|
+
log.info("hidden");
|
|
105
|
+
log.warn("visible");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(output.log.length, 0);
|
|
109
|
+
assert.equal(output.warn.length, 1);
|
|
110
|
+
const entry = JSON.parse(output.warn[0]);
|
|
111
|
+
assert.equal(entry.msg, "visible");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("child logger inherits parent context", async () => {
|
|
115
|
+
process.env.NODE_ENV = "production";
|
|
116
|
+
const parent = createLogger({
|
|
117
|
+
name: "parent",
|
|
118
|
+
level: "info",
|
|
119
|
+
context: { service: "web" },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const child = parent.child({ requestId: "abc-123" });
|
|
123
|
+
|
|
124
|
+
const output = await captureConsole(() => {
|
|
125
|
+
child.info("child message");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const entry = JSON.parse(output.log[0]);
|
|
129
|
+
assert.equal(entry.name, "parent");
|
|
130
|
+
assert.equal(entry.service, "web");
|
|
131
|
+
assert.equal(entry.requestId, "abc-123");
|
|
132
|
+
assert.equal(entry.msg, "child message");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("child logger merges context without mutating parent", async () => {
|
|
136
|
+
process.env.NODE_ENV = "production";
|
|
137
|
+
const parent = createLogger({
|
|
138
|
+
name: "parent",
|
|
139
|
+
level: "info",
|
|
140
|
+
context: { a: 1 },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
parent.child({ b: 2 });
|
|
144
|
+
|
|
145
|
+
const output = await captureConsole(() => {
|
|
146
|
+
parent.info("parent only");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const entry = JSON.parse(output.log[0]);
|
|
150
|
+
assert.equal(entry.a, 1);
|
|
151
|
+
assert.equal(entry.b, undefined);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("structured output contains required fields", async () => {
|
|
155
|
+
process.env.NODE_ENV = "production";
|
|
156
|
+
const log = createLogger({ name: "fields-test", level: "debug" });
|
|
157
|
+
|
|
158
|
+
const output = await captureConsole(() => {
|
|
159
|
+
log.info("test message", { extra: "data" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const entry = JSON.parse(output.log[0]);
|
|
163
|
+
assert.ok(entry.timestamp);
|
|
164
|
+
assert.equal(entry.level, "info");
|
|
165
|
+
assert.equal(entry.name, "fields-test");
|
|
166
|
+
assert.equal(entry.msg, "test message");
|
|
167
|
+
assert.equal(entry.extra, "data");
|
|
168
|
+
// timestamp should be ISO 8601
|
|
169
|
+
assert.ok(!Number.isNaN(Date.parse(entry.timestamp)));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("error and fatal use console.error", async () => {
|
|
173
|
+
process.env.NODE_ENV = "production";
|
|
174
|
+
const log = createLogger({ name: "test", level: "debug" });
|
|
175
|
+
|
|
176
|
+
const output = await captureConsole(() => {
|
|
177
|
+
log.error("err msg");
|
|
178
|
+
log.fatal("fatal msg");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
assert.equal(output.error.length, 2);
|
|
182
|
+
const errEntry = JSON.parse(output.error[0]);
|
|
183
|
+
assert.equal(errEntry.level, "error");
|
|
184
|
+
const fatalEntry = JSON.parse(output.error[1]);
|
|
185
|
+
assert.equal(fatalEntry.level, "fatal");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("warn uses console.warn", async () => {
|
|
189
|
+
process.env.NODE_ENV = "production";
|
|
190
|
+
const log = createLogger({ name: "test", level: "debug" });
|
|
191
|
+
|
|
192
|
+
const output = await captureConsole(() => {
|
|
193
|
+
log.warn("warn msg");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
assert.equal(output.warn.length, 1);
|
|
197
|
+
const entry = JSON.parse(output.warn[0]);
|
|
198
|
+
assert.equal(entry.level, "warn");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("dev mode outputs colorized non-JSON string", async () => {
|
|
202
|
+
delete process.env.NODE_ENV;
|
|
203
|
+
const log = createLogger({ name: "dev-test", level: "debug" });
|
|
204
|
+
|
|
205
|
+
const output = await captureConsole(() => {
|
|
206
|
+
log.info("hello dev");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
assert.equal(output.log.length, 1);
|
|
210
|
+
const line = output.log[0];
|
|
211
|
+
// Should NOT be valid JSON in dev mode
|
|
212
|
+
assert.throws(() => JSON.parse(line));
|
|
213
|
+
// Should contain the message and logger name
|
|
214
|
+
assert.ok(line.includes("hello dev"));
|
|
215
|
+
assert.ok(line.includes("dev-test"));
|
|
216
|
+
assert.ok(line.includes("INFO"));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("call-site data overrides default context", async () => {
|
|
220
|
+
process.env.NODE_ENV = "production";
|
|
221
|
+
const log = createLogger({
|
|
222
|
+
name: "test",
|
|
223
|
+
level: "info",
|
|
224
|
+
context: { region: "us-east" },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const output = await captureConsole(() => {
|
|
228
|
+
log.info("override", { region: "eu-west" });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const entry = JSON.parse(output.log[0]);
|
|
232
|
+
assert.equal(entry.region, "eu-west");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("requestLogger", () => {
|
|
237
|
+
test("creates child logger with request fields", async () => {
|
|
238
|
+
process.env.NODE_ENV = "production";
|
|
239
|
+
const parent = createLogger({ name: "web", level: "info" });
|
|
240
|
+
const getReqLogger = requestLogger(parent);
|
|
241
|
+
|
|
242
|
+
const request = {
|
|
243
|
+
method: "GET",
|
|
244
|
+
url: "/api/users?page=1",
|
|
245
|
+
headers: { "x-request-id": "req-456" },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const output = await captureConsole(() => {
|
|
249
|
+
const reqLog = getReqLogger(request);
|
|
250
|
+
reqLog.info("handling request");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// First log is "request started", second is "handling request"
|
|
254
|
+
assert.equal(output.log.length, 2);
|
|
255
|
+
|
|
256
|
+
const startEntry = JSON.parse(output.log[0]);
|
|
257
|
+
assert.equal(startEntry.msg, "request started");
|
|
258
|
+
assert.equal(startEntry.requestId, "req-456");
|
|
259
|
+
assert.equal(startEntry.method, "GET");
|
|
260
|
+
assert.equal(startEntry.path, "/api/users");
|
|
261
|
+
|
|
262
|
+
const handleEntry = JSON.parse(output.log[1]);
|
|
263
|
+
assert.equal(handleEntry.msg, "handling request");
|
|
264
|
+
assert.equal(handleEntry.requestId, "req-456");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("generates requestId when header is missing", async () => {
|
|
268
|
+
process.env.NODE_ENV = "production";
|
|
269
|
+
const parent = createLogger({ name: "web", level: "info" });
|
|
270
|
+
const getReqLogger = requestLogger(parent);
|
|
271
|
+
|
|
272
|
+
const request = {
|
|
273
|
+
method: "POST",
|
|
274
|
+
url: "/api/data",
|
|
275
|
+
headers: {},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const output = await captureConsole(() => {
|
|
279
|
+
getReqLogger(request);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const entry = JSON.parse(output.log[0]);
|
|
283
|
+
assert.ok(entry.requestId);
|
|
284
|
+
assert.equal(entry.method, "POST");
|
|
285
|
+
assert.equal(entry.path, "/api/data");
|
|
286
|
+
});
|
|
287
|
+
});
|
package/src/mailhog.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds MAILHOG_SMTP_URL from individual environment variables if not explicitly provided.
|
|
3
|
+
* Allows configuration via individual MAILHOG_* vars instead of a single MAILHOG_SMTP_URL.
|
|
4
|
+
*/
|
|
5
|
+
function getMailhogSmtpUrl() {
|
|
6
|
+
if (process.env.MAILHOG_SMTP_URL) {
|
|
7
|
+
return process.env.MAILHOG_SMTP_URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const host = process.env.MAILHOG_HOST || "localhost";
|
|
11
|
+
const port = process.env.MAILHOG_SMTP_PORT || "1025";
|
|
12
|
+
|
|
13
|
+
return `smtp://${host}:${port}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds Mailhog Web UI URL from individual environment variables.
|
|
18
|
+
* Useful for displaying the URL in logs or configuration.
|
|
19
|
+
*/
|
|
20
|
+
function getMailhogUiUrl() {
|
|
21
|
+
const host = process.env.MAILHOG_HOST || "localhost";
|
|
22
|
+
const port = process.env.MAILHOG_UI_PORT || "8025";
|
|
23
|
+
|
|
24
|
+
return `http://${host}:${port}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets Mailhog SMTP configuration for email clients.
|
|
29
|
+
*/
|
|
30
|
+
export const getMailhogSmtpConfig = () => {
|
|
31
|
+
const url = getMailhogSmtpUrl();
|
|
32
|
+
const match = url.match(/smtp:\/\/([^:]+):(\d+)/);
|
|
33
|
+
const host = match ? match[1] : (process.env.MAILHOG_HOST || "localhost");
|
|
34
|
+
const port = match ? match[2] : (process.env.MAILHOG_SMTP_PORT || "1025");
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
host,
|
|
38
|
+
port: parseInt(port, 10),
|
|
39
|
+
url,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { getMailhogSmtpUrl, getMailhogUiUrl };
|