@upyo/mock 0.1.0-dev.17
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/README.md +186 -0
- package/dist/index.cjs +336 -0
- package/dist/index.d.cts +276 -0
- package/dist/index.d.ts +276 -0
- package/dist/index.js +335 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
//#region src/config.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a resolved mock configuration by applying default values to optional fields.
|
|
4
|
+
*
|
|
5
|
+
* This function takes a partial mock configuration and returns a complete
|
|
6
|
+
* configuration with all optional fields filled with sensible defaults.
|
|
7
|
+
*
|
|
8
|
+
* @param config - The mock configuration with optional fields
|
|
9
|
+
* @returns A resolved configuration with all defaults applied
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const resolved = createMockConfig({
|
|
14
|
+
* delay: 100
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // resolved.defaultResponse will be { successful: true, messageId: "mock-message-id" }
|
|
18
|
+
* // resolved.failureRate will be 0 (default)
|
|
19
|
+
* // resolved.generateUniqueMessageIds will be true (default)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function createMockConfig(config = {}) {
|
|
23
|
+
return {
|
|
24
|
+
defaultResponse: config.defaultResponse ?? {
|
|
25
|
+
successful: true,
|
|
26
|
+
messageId: "mock-message-id"
|
|
27
|
+
},
|
|
28
|
+
delay: config.delay ?? 0,
|
|
29
|
+
randomDelayRange: config.randomDelayRange ?? {
|
|
30
|
+
min: 0,
|
|
31
|
+
max: 0
|
|
32
|
+
},
|
|
33
|
+
failureRate: config.failureRate ?? 0,
|
|
34
|
+
generateUniqueMessageIds: config.generateUniqueMessageIds ?? true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/mock-transport.ts
|
|
40
|
+
/**
|
|
41
|
+
* A mock transport implementation for testing purposes.
|
|
42
|
+
*
|
|
43
|
+
* This transport doesn't actually send emails but stores them in memory,
|
|
44
|
+
* making it useful for unit testing email functionality. It provides
|
|
45
|
+
* comprehensive testing capabilities including message verification,
|
|
46
|
+
* behavior simulation, and async utilities.
|
|
47
|
+
*/
|
|
48
|
+
var MockTransport = class {
|
|
49
|
+
/**
|
|
50
|
+
* The resolved configuration used by this mock transport.
|
|
51
|
+
*/
|
|
52
|
+
config;
|
|
53
|
+
sentMessages = [];
|
|
54
|
+
nextResponse = null;
|
|
55
|
+
messageIdCounter = 1;
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new MockTransport instance.
|
|
58
|
+
*
|
|
59
|
+
* @param config Configuration options for the mock transport behavior.
|
|
60
|
+
*/
|
|
61
|
+
constructor(config = {}) {
|
|
62
|
+
this.config = createMockConfig(config);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sends an email message through the mock transport.
|
|
66
|
+
*
|
|
67
|
+
* The message is stored in memory and can be retrieved for testing verification.
|
|
68
|
+
* Respects configured delays, failure rates, and response overrides.
|
|
69
|
+
*
|
|
70
|
+
* @param message The email message to send.
|
|
71
|
+
* @param options Transport options including abort signal.
|
|
72
|
+
* @returns A promise that resolves to a receipt indicating success or failure.
|
|
73
|
+
* @throws {DOMException} When the operation is aborted via `AbortSignal`.
|
|
74
|
+
*/
|
|
75
|
+
async send(message, options) {
|
|
76
|
+
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
77
|
+
await this.applyDelay();
|
|
78
|
+
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
79
|
+
this.sentMessages.push(message);
|
|
80
|
+
const response = this.getResponse();
|
|
81
|
+
return response;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sends multiple email messages through the mock transport.
|
|
85
|
+
*
|
|
86
|
+
* Each message is processed sequentially using the send() method, respecting
|
|
87
|
+
* all configured behavior including delays and failure rates.
|
|
88
|
+
*
|
|
89
|
+
* @param messages An iterable or async iterable of messages to send.
|
|
90
|
+
* @param options Transport options including abort signal.
|
|
91
|
+
* @returns An async iterable of receipts, one for each message.
|
|
92
|
+
* @throws {DOMException} When the operation is aborted via `AbortSignal`.
|
|
93
|
+
*/
|
|
94
|
+
async *sendMany(messages, options) {
|
|
95
|
+
for await (const message of messages) {
|
|
96
|
+
if (options?.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
97
|
+
yield await this.send(message, options);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get all messages that have been "sent" through this transport.
|
|
102
|
+
*
|
|
103
|
+
* @returns A readonly array containing copies of all sent messages.
|
|
104
|
+
*/
|
|
105
|
+
getSentMessages() {
|
|
106
|
+
return [...this.sentMessages];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get the last message that was sent, or undefined if no messages have been sent.
|
|
110
|
+
*
|
|
111
|
+
* @returns The most recently sent message, or undefined if none.
|
|
112
|
+
*/
|
|
113
|
+
getLastSentMessage() {
|
|
114
|
+
return this.sentMessages[this.sentMessages.length - 1];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the total number of messages that have been sent.
|
|
118
|
+
*
|
|
119
|
+
* @returns The count of messages sent through this transport.
|
|
120
|
+
*/
|
|
121
|
+
getSentMessagesCount() {
|
|
122
|
+
return this.sentMessages.length;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clear all stored messages.
|
|
126
|
+
*
|
|
127
|
+
* This removes all messages from the internal storage but does not
|
|
128
|
+
* reset other configuration like delays or failure rates.
|
|
129
|
+
*/
|
|
130
|
+
clearSentMessages() {
|
|
131
|
+
this.sentMessages = [];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Set the response that will be returned for the next send operation.
|
|
135
|
+
*
|
|
136
|
+
* After being used once, it will revert to the default response.
|
|
137
|
+
* This is useful for testing specific success or failure scenarios.
|
|
138
|
+
*
|
|
139
|
+
* @param receipt The receipt to return for the next send operation.
|
|
140
|
+
*/
|
|
141
|
+
setNextResponse(receipt) {
|
|
142
|
+
this.nextResponse = receipt;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Set the default response that will be returned for send operations.
|
|
146
|
+
*
|
|
147
|
+
* This response is used when no next response is set and random failures
|
|
148
|
+
* are not triggered.
|
|
149
|
+
*
|
|
150
|
+
* @param receipt The default receipt to return for send operations.
|
|
151
|
+
*/
|
|
152
|
+
setDefaultResponse(receipt) {
|
|
153
|
+
this.config = {
|
|
154
|
+
...this.config,
|
|
155
|
+
defaultResponse: receipt
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Set the failure rate (0.0 to 1.0). When set, sends will randomly fail
|
|
160
|
+
* at the specified rate instead of using the configured responses.
|
|
161
|
+
*
|
|
162
|
+
* @param rate The failure rate as a decimal between 0.0 and 1.0.
|
|
163
|
+
* @throws {RangeError} When rate is not between 0.0 and 1.0.
|
|
164
|
+
*/
|
|
165
|
+
setFailureRate(rate) {
|
|
166
|
+
if (rate < 0 || rate > 1) throw new RangeError("Failure rate must be between 0.0 and 1.0");
|
|
167
|
+
this.config = {
|
|
168
|
+
...this.config,
|
|
169
|
+
failureRate: rate
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Set a fixed delay in milliseconds for all send operations.
|
|
174
|
+
*
|
|
175
|
+
* This overrides any random delay range that was previously configured.
|
|
176
|
+
*
|
|
177
|
+
* @param milliseconds The delay in milliseconds (must be non-negative).
|
|
178
|
+
* @throws {RangeError} When milliseconds is negative.
|
|
179
|
+
*/
|
|
180
|
+
setDelay(milliseconds) {
|
|
181
|
+
if (milliseconds < 0) throw new RangeError("Delay must be non-negative");
|
|
182
|
+
this.config = {
|
|
183
|
+
...this.config,
|
|
184
|
+
delay: milliseconds,
|
|
185
|
+
randomDelayRange: {
|
|
186
|
+
min: 0,
|
|
187
|
+
max: 0
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Set a random delay range in milliseconds for send operations.
|
|
193
|
+
*
|
|
194
|
+
* This overrides any fixed delay that was previously configured.
|
|
195
|
+
*
|
|
196
|
+
* @param min The minimum delay in milliseconds.
|
|
197
|
+
* @param max The maximum delay in milliseconds.
|
|
198
|
+
* @throws {RangeError} When min or max is negative, or min > max.
|
|
199
|
+
*/
|
|
200
|
+
setRandomDelay(min, max) {
|
|
201
|
+
if (min < 0 || max < 0 || min > max) throw new RangeError("Invalid delay range");
|
|
202
|
+
this.config = {
|
|
203
|
+
...this.config,
|
|
204
|
+
delay: 0,
|
|
205
|
+
randomDelayRange: {
|
|
206
|
+
min,
|
|
207
|
+
max
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Find the first message matching the given predicate.
|
|
213
|
+
*
|
|
214
|
+
* @param predicate A function that tests each message.
|
|
215
|
+
* @returns The first matching message, or undefined if none found.
|
|
216
|
+
*/
|
|
217
|
+
findMessageBy(predicate) {
|
|
218
|
+
return this.sentMessages.find(predicate);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Find all messages matching the given predicate.
|
|
222
|
+
*
|
|
223
|
+
* @param predicate A function that tests each message.
|
|
224
|
+
* @returns An array of all matching messages.
|
|
225
|
+
*/
|
|
226
|
+
findMessagesBy(predicate) {
|
|
227
|
+
return this.sentMessages.filter(predicate);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get all messages sent to a specific email address.
|
|
231
|
+
*
|
|
232
|
+
* Searches through To, CC, and BCC recipients to find messages
|
|
233
|
+
* that were sent to the specified email address.
|
|
234
|
+
*
|
|
235
|
+
* @param email The email address to search for.
|
|
236
|
+
* @returns An array of messages sent to the specified address.
|
|
237
|
+
*/
|
|
238
|
+
getMessagesTo(email) {
|
|
239
|
+
return this.sentMessages.filter((message) => [
|
|
240
|
+
...message.recipients,
|
|
241
|
+
...message.ccRecipients,
|
|
242
|
+
...message.bccRecipients
|
|
243
|
+
].some((addr) => addr.address === email));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get all messages with a specific subject.
|
|
247
|
+
*
|
|
248
|
+
* @param subject The exact subject line to match.
|
|
249
|
+
* @returns An array of messages with the specified subject.
|
|
250
|
+
*/
|
|
251
|
+
getMessagesBySubject(subject) {
|
|
252
|
+
return this.sentMessages.filter((message) => message.subject === subject);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Wait for a specific number of messages to be sent.
|
|
256
|
+
*
|
|
257
|
+
* This method polls the message count until the target is reached or
|
|
258
|
+
* the timeout expires. Useful for testing async email workflows where you
|
|
259
|
+
* need to wait for messages to be sent.
|
|
260
|
+
*
|
|
261
|
+
* @param count The number of messages to wait for.
|
|
262
|
+
* @param timeout The timeout in milliseconds (default: 5000).
|
|
263
|
+
* @returns A promise that resolves when the count is reached.
|
|
264
|
+
* @throws {Error} When the timeout is exceeded before reaching the target
|
|
265
|
+
* count.
|
|
266
|
+
*/
|
|
267
|
+
async waitForMessageCount(count, timeout = 5e3) {
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
while (this.sentMessages.length < count) {
|
|
270
|
+
if (Date.now() - startTime > timeout) throw new Error(`Timeout waiting for ${count} messages (got ${this.sentMessages.length})`);
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Wait for a message matching the given predicate.
|
|
276
|
+
*
|
|
277
|
+
* This method polls for a matching message until one is found or the timeout
|
|
278
|
+
* expires. Useful for testing async email workflows where you need to wait
|
|
279
|
+
* for specific messages.
|
|
280
|
+
*
|
|
281
|
+
* @param predicate A function that tests each message for a match.
|
|
282
|
+
* @param timeout The timeout in milliseconds (default: 5000).
|
|
283
|
+
* @returns A promise that resolves with the matching message.
|
|
284
|
+
* @throws {Error} When the timeout is exceeded before finding a matching
|
|
285
|
+
* message.
|
|
286
|
+
*/
|
|
287
|
+
async waitForMessage(predicate, timeout = 5e3) {
|
|
288
|
+
const startTime = Date.now();
|
|
289
|
+
while (true) {
|
|
290
|
+
const message = this.findMessageBy(predicate);
|
|
291
|
+
if (message) return message;
|
|
292
|
+
if (Date.now() - startTime > timeout) throw new Error("Timeout waiting for message");
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Reset the transport to its initial state.
|
|
298
|
+
*
|
|
299
|
+
* Clears all messages, resets all configuration to defaults, and
|
|
300
|
+
* resets the message ID counter. This is useful for test cleanup.
|
|
301
|
+
*/
|
|
302
|
+
reset() {
|
|
303
|
+
this.sentMessages = [];
|
|
304
|
+
this.nextResponse = null;
|
|
305
|
+
this.config = createMockConfig();
|
|
306
|
+
this.messageIdCounter = 1;
|
|
307
|
+
}
|
|
308
|
+
async applyDelay() {
|
|
309
|
+
let delayMs = this.config.delay;
|
|
310
|
+
if (this.config.randomDelayRange && (this.config.randomDelayRange.min > 0 || this.config.randomDelayRange.max > 0)) {
|
|
311
|
+
const { min, max } = this.config.randomDelayRange;
|
|
312
|
+
delayMs = Math.random() * (max - min) + min;
|
|
313
|
+
}
|
|
314
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
315
|
+
}
|
|
316
|
+
getResponse() {
|
|
317
|
+
if (this.config.failureRate > 0 && Math.random() < this.config.failureRate) return {
|
|
318
|
+
successful: false,
|
|
319
|
+
errorMessages: ["Simulated random failure"]
|
|
320
|
+
};
|
|
321
|
+
if (this.nextResponse) {
|
|
322
|
+
const response = this.nextResponse;
|
|
323
|
+
this.nextResponse = null;
|
|
324
|
+
return response;
|
|
325
|
+
}
|
|
326
|
+
if (this.config.defaultResponse.successful && this.config.generateUniqueMessageIds) return {
|
|
327
|
+
successful: true,
|
|
328
|
+
messageId: `mock-message-${this.messageIdCounter++}`
|
|
329
|
+
};
|
|
330
|
+
return this.config.defaultResponse;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
export { MockTransport };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@upyo/mock",
|
|
3
|
+
"version": "0.1.0-dev.17+3ffd074a",
|
|
4
|
+
"description": "Mock transport for Upyo email library—useful for testing",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"email",
|
|
7
|
+
"mail",
|
|
8
|
+
"mock",
|
|
9
|
+
"testing",
|
|
10
|
+
"test"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Hong Minhee",
|
|
15
|
+
"email": "hong@minhee.org",
|
|
16
|
+
"url": "https://hongminhee.org/"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://upyo.org/",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/dahlia/upyo.git",
|
|
22
|
+
"directory": "packages/mock/"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/dahlia/upyo/issues"
|
|
26
|
+
},
|
|
27
|
+
"funding": [
|
|
28
|
+
"https://github.com/sponsors/dahlia"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0",
|
|
32
|
+
"bun": ">=1.2.0",
|
|
33
|
+
"deno": ">=2.3.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/",
|
|
37
|
+
"package.json",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"type": "module",
|
|
41
|
+
"module": "./dist/index.js",
|
|
42
|
+
"main": "./dist/index.cjs",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": {
|
|
47
|
+
"import": "./dist/index.d.ts",
|
|
48
|
+
"require": "./dist/index.d.cts"
|
|
49
|
+
},
|
|
50
|
+
"import": "./dist/index.js",
|
|
51
|
+
"require": "./dist/index.cjs"
|
|
52
|
+
},
|
|
53
|
+
"./package.json": "./package.json"
|
|
54
|
+
},
|
|
55
|
+
"sideEffects": false,
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@upyo/core": "0.1.0-dev.17+3ffd074a"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@dotenvx/dotenvx": "^1.47.3",
|
|
61
|
+
"tsdown": "^0.12.7",
|
|
62
|
+
"typescript": "5.8.3"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"prepublish": "tsdown",
|
|
67
|
+
"test": "tsdown && dotenvx run --ignore=MISSING_ENV_FILE -- node --experimental-transform-types --test",
|
|
68
|
+
"test:bun": "tsdown && bun test --timeout=30000",
|
|
69
|
+
"test:deno": "deno test --allow-env"
|
|
70
|
+
}
|
|
71
|
+
}
|