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