@5minds/node-red-contrib-processcube-tools 1.0.1 → 1.0.2-develop-e3d5d9-mfm8oea8
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/.env.template +4 -0
- package/README.md +1 -1
- package/email-receiver/email-receiver.html +219 -0
- package/email-receiver/email-receiver.js +250 -0
- package/email-sender/email-sender.html +306 -0
- package/email-sender/email-sender.js +174 -0
- package/package.json +25 -4
- package/{processcube-html-to-text.html → processcube-html-to-text/processcube-html-to-text.html} +9 -9
- package/{processcube-html-to-text.js → processcube-html-to-text/processcube-html-to-text.js} +0 -3
- package/test/helpers/email-receiver.mocks.js +447 -0
- package/test/helpers/email-sender.mocks.js +368 -0
- package/test/integration/email-receiver.integration.test.js +515 -0
- package/test/integration/email-sender.integration.test.js +239 -0
- package/test/unit/email-receiver.unit.test.js +304 -0
- package/test/unit/email-sender.unit.test.js +570 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Helper functions and mocks for email-sender tests
|
|
2
|
+
const { EventEmitter } = require('events');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create mock Node-RED object for unit testing
|
|
6
|
+
*/
|
|
7
|
+
function createMockNodeRED(options = {}) {
|
|
8
|
+
const mockRED = {
|
|
9
|
+
nodes: {
|
|
10
|
+
createNode: function (node, config) {
|
|
11
|
+
nodeInstance = node; // Capture the node instance
|
|
12
|
+
|
|
13
|
+
// Apply config properties to node
|
|
14
|
+
Object.assign(node, {
|
|
15
|
+
id: config.id || 'mock-node-id',
|
|
16
|
+
type: config.type || 'email-sender',
|
|
17
|
+
name: config.name || 'Mock Node',
|
|
18
|
+
on: function (event, callback) {
|
|
19
|
+
if (event === 'input') {
|
|
20
|
+
storedInputCallback = callback;
|
|
21
|
+
// Store the callback on the node instance for easy access
|
|
22
|
+
node.inputCallback = callback;
|
|
23
|
+
}
|
|
24
|
+
// Call the original onHandler if provided
|
|
25
|
+
if (options.onHandler) {
|
|
26
|
+
options.onHandler.call(node, event, callback);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
status: options.statusHandler || function () {},
|
|
30
|
+
error: options.errorHandler || function () {},
|
|
31
|
+
send: options.sendHandler || function () {},
|
|
32
|
+
log: options.logHandler || function () {},
|
|
33
|
+
warn: options.warnHandler || function () {},
|
|
34
|
+
debug: options.debugHandler || function () {},
|
|
35
|
+
});
|
|
36
|
+
return node;
|
|
37
|
+
},
|
|
38
|
+
registerType: function (type, constructor) {
|
|
39
|
+
// Store registration for verification in tests
|
|
40
|
+
this.lastRegisteredType = type;
|
|
41
|
+
this.lastRegisteredConstructor = constructor;
|
|
42
|
+
},
|
|
43
|
+
// Helper method to get the stored input callback
|
|
44
|
+
getInputCallback: function () {
|
|
45
|
+
return storedInputCallback;
|
|
46
|
+
},
|
|
47
|
+
// Helper method to get the node instance
|
|
48
|
+
getNodeInstance: function () {
|
|
49
|
+
return nodeInstance;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
util: {
|
|
53
|
+
evaluateNodeProperty: function (value, type, node, msg, callback) {
|
|
54
|
+
if (type === 'json') {
|
|
55
|
+
try {
|
|
56
|
+
// Simulate parsing a JSON string into an object
|
|
57
|
+
return JSON.parse(JSON.stringify(value));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (callback) {
|
|
60
|
+
callback(e, null);
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Simple mock implementation
|
|
67
|
+
if (callback) {
|
|
68
|
+
callback(null, value);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
encrypt: function (value) {
|
|
73
|
+
return 'encrypted:' + value;
|
|
74
|
+
},
|
|
75
|
+
decrypt: function (value) {
|
|
76
|
+
return value.replace('encrypted:', '');
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
log: {
|
|
80
|
+
info: options.logInfo || function () {},
|
|
81
|
+
warn: options.logWarn || function () {},
|
|
82
|
+
error: options.logError || function () {},
|
|
83
|
+
debug: options.logDebug || function () {},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return mockRED;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createMockNodemailer(options = {}) {
|
|
91
|
+
const settings = Object.assign(
|
|
92
|
+
{
|
|
93
|
+
shouldFail: false,
|
|
94
|
+
// New options for different email statuses
|
|
95
|
+
rejectedEmails: [], // Array of emails to mark as rejected
|
|
96
|
+
pendingEmails: [], // Array of emails to mark as pending
|
|
97
|
+
acceptedEmails: [], // Array of emails to mark as accepted (overrides default)
|
|
98
|
+
},
|
|
99
|
+
options,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
createTransport: () => ({
|
|
104
|
+
sendMail: (mailOptions, callback) => {
|
|
105
|
+
if (settings.onSendMail) {
|
|
106
|
+
settings.onSendMail(mailOptions);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (settings.shouldFail === true) {
|
|
110
|
+
const error = new Error('Mock sendMail error');
|
|
111
|
+
error.code = 'ECONNREFUSED';
|
|
112
|
+
return callback(error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Determine email status based on configuration
|
|
116
|
+
const toEmail = Array.isArray(mailOptions.to) ? mailOptions.to[0] : mailOptions.to;
|
|
117
|
+
let accepted = [];
|
|
118
|
+
let rejected = [];
|
|
119
|
+
let pending = [];
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
settings.rejectedEmails.length > 0 ||
|
|
123
|
+
settings.pendingEmails.length > 0 ||
|
|
124
|
+
settings.acceptedEmails.length > 0
|
|
125
|
+
) {
|
|
126
|
+
// Use explicit configuration
|
|
127
|
+
if (settings.rejectedEmails.includes(toEmail)) {
|
|
128
|
+
rejected = [toEmail];
|
|
129
|
+
} else if (settings.pendingEmails.includes(toEmail)) {
|
|
130
|
+
pending = [toEmail];
|
|
131
|
+
} else if (settings.acceptedEmails.includes(toEmail)) {
|
|
132
|
+
accepted = [toEmail];
|
|
133
|
+
} else {
|
|
134
|
+
// Default behavior - accept if not explicitly configured
|
|
135
|
+
accepted = [toEmail];
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Original behavior - accept all emails (backwards compatibility)
|
|
139
|
+
accepted = [mailOptions.to];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Set appropriate response message based on status
|
|
143
|
+
let responseMessage = '250 OK: Message accepted';
|
|
144
|
+
if (rejected.length > 0) {
|
|
145
|
+
responseMessage = '550 Mailbox unavailable';
|
|
146
|
+
} else if (pending.length > 0) {
|
|
147
|
+
responseMessage = '451 Requested action aborted: local error';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
callback(null, {
|
|
151
|
+
messageId: '<mock-message-id@test.com>',
|
|
152
|
+
response: responseMessage,
|
|
153
|
+
accepted: accepted,
|
|
154
|
+
rejected: rejected,
|
|
155
|
+
pending: pending,
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
restore: function () {
|
|
160
|
+
// Cleanup method
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Aktualisiere setupModuleMocks um die neue Implementation zu nutzen
|
|
166
|
+
function setupModuleMocks() {
|
|
167
|
+
const mockNodemailerModule = createMockNodemailer();
|
|
168
|
+
|
|
169
|
+
delete require.cache[require.resolve('nodemailer')];
|
|
170
|
+
require.cache[require.resolve('nodemailer')] = {
|
|
171
|
+
exports: mockNodemailerModule,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return function cleanup() {
|
|
175
|
+
delete require.cache[require.resolve('nodemailer')];
|
|
176
|
+
if (mockNodemailerModule.restore) {
|
|
177
|
+
mockNodemailerModule.restore();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Custom mock node
|
|
183
|
+
function getMockNode() {
|
|
184
|
+
// Create an EventEmitter instance to get the .on and .emit methods
|
|
185
|
+
const mock = Object.assign(new EventEmitter(), {
|
|
186
|
+
status: () => {},
|
|
187
|
+
error: () => {},
|
|
188
|
+
warn: () => {},
|
|
189
|
+
log: () => {},
|
|
190
|
+
send: () => {},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
mock.status = (...args) => {
|
|
194
|
+
mock.status.called = true;
|
|
195
|
+
mock.status.args = args;
|
|
196
|
+
};
|
|
197
|
+
mock.status.called = false;
|
|
198
|
+
mock.status.args = [];
|
|
199
|
+
mock.status.calledWith = (expectedArgs) => {
|
|
200
|
+
return mock.status.called && JSON.stringify(mock.status.args) === JSON.stringify(expectedArgs);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
mock.send = (...args) => {
|
|
204
|
+
mock.send.called = true;
|
|
205
|
+
mock.send.args = args;
|
|
206
|
+
mock.send.callCount++;
|
|
207
|
+
};
|
|
208
|
+
mock.send.called = false;
|
|
209
|
+
mock.send.args = [];
|
|
210
|
+
mock.send.callCount = 0;
|
|
211
|
+
mock.send.calledWith = (expectedArgs) => {
|
|
212
|
+
return mock.send.called && JSON.stringify(mock.send.args) === JSON.stringify([expectedArgs]);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return mock;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Test configurations for the email sender node.
|
|
220
|
+
*/
|
|
221
|
+
const emailSenderConfigs = {
|
|
222
|
+
valid: {
|
|
223
|
+
id: 'test-node-6',
|
|
224
|
+
type: 'email-sender',
|
|
225
|
+
name: 'Test Email Sender',
|
|
226
|
+
sender: 'Test Sender',
|
|
227
|
+
senderType: 'str',
|
|
228
|
+
address: 'test.sender@example.com',
|
|
229
|
+
addressType: 'str',
|
|
230
|
+
to: 'recipient@example.com',
|
|
231
|
+
toType: 'str',
|
|
232
|
+
cc: '',
|
|
233
|
+
ccType: 'str',
|
|
234
|
+
bcc: '',
|
|
235
|
+
bccType: 'str',
|
|
236
|
+
subject: 'Test Subject',
|
|
237
|
+
subjectType: 'str',
|
|
238
|
+
htmlContent: '<b>Hello World</b>',
|
|
239
|
+
htmlContentType: 'str',
|
|
240
|
+
attachments: '',
|
|
241
|
+
attachmentsType: 'str',
|
|
242
|
+
host: 'smtp.example.com',
|
|
243
|
+
hostType: 'str',
|
|
244
|
+
port: 587,
|
|
245
|
+
portType: 'num',
|
|
246
|
+
user: 'user',
|
|
247
|
+
userType: 'str',
|
|
248
|
+
password: 'password',
|
|
249
|
+
passwordType: 'str',
|
|
250
|
+
secure: false,
|
|
251
|
+
secureType: 'bool',
|
|
252
|
+
rejectUnauthorized: true,
|
|
253
|
+
rejectUnauthorizedType: 'bool',
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
invalid: {
|
|
257
|
+
id: 'test-node-7',
|
|
258
|
+
type: 'email-sender',
|
|
259
|
+
name: 'Invalid Email Sender',
|
|
260
|
+
sender: '', // Missing sender
|
|
261
|
+
senderType: 'str',
|
|
262
|
+
address: 'test.sender@example.com',
|
|
263
|
+
addressType: 'str',
|
|
264
|
+
to: 'recipient@example.com',
|
|
265
|
+
toType: 'str',
|
|
266
|
+
subject: 'Invalid Test Subject',
|
|
267
|
+
subjectType: 'str',
|
|
268
|
+
htmlContent: 'Invalid Test Content',
|
|
269
|
+
htmlContentType: 'str',
|
|
270
|
+
host: '', // Missing host
|
|
271
|
+
hostType: 'str',
|
|
272
|
+
port: 587,
|
|
273
|
+
portType: 'str', // Incorrect type
|
|
274
|
+
user: 'user',
|
|
275
|
+
userType: 'str',
|
|
276
|
+
password: '', // Missing password
|
|
277
|
+
passwordType: 'str',
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
minimal: {
|
|
281
|
+
id: 'test-node-8',
|
|
282
|
+
type: 'email-sender',
|
|
283
|
+
name: 'Minimal Email Sender',
|
|
284
|
+
to: 'recipient@example.com',
|
|
285
|
+
toType: 'str',
|
|
286
|
+
subject: 'Minimal Subject',
|
|
287
|
+
subjectType: 'str',
|
|
288
|
+
htmlContent: 'Minimal content.',
|
|
289
|
+
htmlContentType: 'str',
|
|
290
|
+
host: 'smtp.minimal.com',
|
|
291
|
+
hostType: 'str',
|
|
292
|
+
port: 587,
|
|
293
|
+
portType: 'num',
|
|
294
|
+
user: 'minimal-user',
|
|
295
|
+
userType: 'str',
|
|
296
|
+
password: 'minimal-password',
|
|
297
|
+
passwordType: 'str',
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create test flows for Node-RED integration tests
|
|
303
|
+
*/
|
|
304
|
+
const testFlows = {
|
|
305
|
+
single: [emailSenderConfigs.valid],
|
|
306
|
+
|
|
307
|
+
withHelper: [emailSenderConfigs.valid, { id: 'h1', type: 'helper' }],
|
|
308
|
+
|
|
309
|
+
connected: [
|
|
310
|
+
{ ...emailSenderConfigs.valid, wires: [['h1']] },
|
|
311
|
+
{ id: 'h1', type: 'helper' },
|
|
312
|
+
],
|
|
313
|
+
|
|
314
|
+
multiOutput: [
|
|
315
|
+
{ ...emailSenderConfigs.valid, wires: [['h1', 'h2']] },
|
|
316
|
+
{ id: 'h1', type: 'helper' },
|
|
317
|
+
{ id: 'h2', type: 'helper' },
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Utility functions for test assertions and email simulation
|
|
323
|
+
*/
|
|
324
|
+
const testUtils = {
|
|
325
|
+
/**
|
|
326
|
+
* Wait for a specified amount of time
|
|
327
|
+
*/
|
|
328
|
+
wait: (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Create a promise that resolves when a node receives a message
|
|
332
|
+
*/
|
|
333
|
+
waitForMessage: (node, timeout = 1000) => {
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
const timer = setTimeout(() => {
|
|
336
|
+
reject(new Error('Timeout waiting for message'));
|
|
337
|
+
}, timeout);
|
|
338
|
+
|
|
339
|
+
node.on('input', (msg) => {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
resolve(msg);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
/**
|
|
346
|
+
* Verify that a message has expected properties
|
|
347
|
+
*/
|
|
348
|
+
verifyMessage: (msg, expectedProps = {}) => {
|
|
349
|
+
const should = require('should');
|
|
350
|
+
should.exist(msg);
|
|
351
|
+
|
|
352
|
+
Object.keys(expectedProps).forEach((prop) => {
|
|
353
|
+
if (expectedProps[prop] !== undefined) {
|
|
354
|
+
msg.should.have.property(prop, expectedProps[prop]);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
createMockNodeRED,
|
|
362
|
+
setupModuleMocks,
|
|
363
|
+
getMockNode,
|
|
364
|
+
emailSenderConfigs,
|
|
365
|
+
createMockNodemailer,
|
|
366
|
+
testFlows,
|
|
367
|
+
testUtils,
|
|
368
|
+
};
|