@5minds/node-red-contrib-processcube-tools 1.0.1-feature-df88cf-mfdx610k → 1.0.1-feature-050c1a-mfe18hnk

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube-tools",
3
- "version": "1.0.1-feature-df88cf-mfdx610k",
3
+ "version": "1.0.1-feature-050c1a-mfe18hnk",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED tools nodes for ProcessCube",
6
6
  "scripts": {
@@ -1,16 +1,25 @@
1
1
  const should = require('should');
2
2
  const helper = require('node-red-node-test-helper');
3
-
4
- describe('Email Receiver Node - Integration Tests', function() {
3
+ const {
4
+ createMockImap,
5
+ createMockMailparser,
6
+ createMockNodeRED,
7
+ setupModuleMocks,
8
+ testConfigs,
9
+ testFlows,
10
+ testUtils
11
+ } = require('../helpers/email-receiver.mocks.js');
12
+
13
+ describe('Email Receiver Node - Integration Tests with Helpers', function() {
5
14
  // Set a reasonable timeout for integration tests
6
15
  this.timeout(10000);
7
16
 
8
17
  let emailReceiverNode;
9
- let originalLoad;
18
+ let cleanupMocks;
10
19
 
11
20
  before(function(done) {
12
- // Set up mocks for dependencies before loading the node
13
- setupMocks();
21
+ // Set up mocks using helper
22
+ cleanupMocks = setupModuleMocks();
14
23
 
15
24
  // Load the node with mocked dependencies
16
25
  emailReceiverNode = require('../../email-receiver/email-receiver.js');
@@ -21,10 +30,9 @@ describe('Email Receiver Node - Integration Tests', function() {
21
30
  });
22
31
 
23
32
  after(function() {
24
- // Restore original module loading
25
- if (originalLoad) {
26
- const Module = require('module');
27
- Module._load = originalLoad;
33
+ // Clean up mocks using helper cleanup function
34
+ if (cleanupMocks) {
35
+ cleanupMocks();
28
36
  }
29
37
  });
30
38
 
@@ -37,105 +45,18 @@ describe('Email Receiver Node - Integration Tests', function() {
37
45
  helper.stopServer(done);
38
46
  });
39
47
 
40
- function setupMocks() {
41
- // Create mock IMAP module
42
- const mockImap = function(config) {
43
- this.config = config;
44
- this.connect = () => {
45
- // Simulate a successful connection by immediately emitting 'ready'
46
- if (this.events && this.events.ready) {
47
- this.events.ready();
48
- }
49
- };
50
- this.openBox = (folder, readOnly, callback) => {
51
- callback(null, { messages: { total: 1 } });
52
- };
53
- this.search = (criteria, callback) => {
54
- callback(null, [123]);
55
- };
56
- this.fetch = (results, options) => {
57
- return {
58
- on: (event, cb) => {
59
- if (event === 'message') {
60
- cb({ on: (e, bodyCb) => { if (e === 'body') bodyCb({}); } });
61
- }
62
- },
63
- once: (event, cb) => {
64
- if (event === 'end') { cb(); }
65
- }
66
- };
67
- };
68
- this.end = () => {};
69
- this.once = (event, callback) => {
70
- if (!this.events) this.events = {};
71
- this.events[event] = callback;
72
- };
73
- return this;
74
- };
75
-
76
- // Create mock mailparser module
77
- const mockMailparser = {
78
- simpleParser: function() {
79
- return Promise.resolve({
80
- subject: 'test integration email',
81
- text: 'test integration body',
82
- html: '<p>test integration</p>',
83
- from: { text: 'integration@test.com' },
84
- date: new Date(),
85
- headers: new Map(),
86
- attachments: []
87
- });
88
- }
89
- };
90
-
91
- const mockModules = {
92
- 'node-imap': mockImap,
93
- 'mailparser': mockMailparser
94
- };
95
-
96
- // Override require to use mocks
97
- const Module = require('module');
98
- originalLoad = Module._load;
99
- Module._load = function(request, parent) {
100
- if (mockModules[request]) {
101
- return mockModules[request];
102
- }
103
- return originalLoad.apply(this, arguments);
104
- };
105
- }
106
-
107
48
  describe('Node Loading', function() {
108
49
  it('should load in Node-RED test environment', function(done) {
109
- // ARRANGE: Set up Node-RED flow with proper configuration
110
- const flow = [
111
- {
112
- id: "n1",
113
- type: "email-receiver",
114
- name: "test node",
115
- host: "imap.test.com",
116
- hostType: "str",
117
- port: "993",
118
- portType: "str",
119
- tls: true,
120
- tlsType: "bool",
121
- user: "test@example.com",
122
- userType: "str",
123
- password: "testpass",
124
- passwordType: "str",
125
- folder: "INBOX",
126
- folderType: "str",
127
- markseen: true,
128
- markseenType: "bool"
129
- }
130
- ];
50
+ // ARRANGE: Use test flow from helpers
51
+ const flow = [testConfigs.valid];
131
52
 
132
53
  // ACT: Load the node in the test helper environment
133
54
  helper.load(emailReceiverNode, flow, function() {
134
55
  try {
135
56
  // ASSERT: Verify the node loaded correctly
136
- const n1 = helper.getNode("n1");
57
+ const n1 = helper.getNode(testConfigs.valid.id);
137
58
  should.exist(n1);
138
- n1.should.have.property('name', 'test node');
59
+ n1.should.have.property('name', testConfigs.valid.name);
139
60
  n1.should.have.property('type', 'email-receiver');
140
61
  done();
141
62
  } catch (err) {
@@ -145,29 +66,14 @@ describe('Email Receiver Node - Integration Tests', function() {
145
66
  });
146
67
 
147
68
  it('should load with minimal configuration', function(done) {
148
- // ARRANGE: Set up minimal flow configuration
149
- const flow = [
150
- {
151
- id: "n1",
152
- type: "email-receiver",
153
- host: "imap.minimal.com",
154
- hostType: "str",
155
- port: "993",
156
- portType: "str",
157
- user: "minimal@test.com",
158
- userType: "str",
159
- password: "minimalpass",
160
- passwordType: "str",
161
- folder: "INBOX",
162
- folderType: "str"
163
- }
164
- ];
69
+ // ARRANGE: Use minimal test config from helpers
70
+ const flow = [testConfigs.minimal];
165
71
 
166
72
  // ACT: Load the node
167
73
  helper.load(emailReceiverNode, flow, function() {
168
74
  try {
169
75
  // ASSERT: Verify the node loaded with minimal config
170
- const n1 = helper.getNode("n1");
76
+ const n1 = helper.getNode(testConfigs.minimal.id);
171
77
  should.exist(n1);
172
78
  n1.should.have.property('type', 'email-receiver');
173
79
  done();
@@ -176,46 +82,60 @@ describe('Email Receiver Node - Integration Tests', function() {
176
82
  }
177
83
  });
178
84
  });
85
+
86
+ it('should load with string folders configuration', function(done) {
87
+ // ARRANGE: Use string folders config from helpers
88
+ const flow = [testConfigs.stringFolders];
89
+
90
+ // ACT: Load the node
91
+ helper.load(emailReceiverNode, flow, function() {
92
+ try {
93
+ // ASSERT: Verify the node loaded with string folders
94
+ const n1 = helper.getNode(testConfigs.stringFolders.id);
95
+ should.exist(n1);
96
+ n1.should.have.property('name', testConfigs.stringFolders.name);
97
+ done();
98
+ } catch (err) {
99
+ done(err);
100
+ }
101
+ });
102
+ });
103
+
104
+ it('should load with array folders configuration', function(done) {
105
+ // ARRANGE: Use array folders config from helpers
106
+ const flow = [testConfigs.arrayFolders];
107
+
108
+ // ACT: Load the node
109
+ helper.load(emailReceiverNode, flow, function() {
110
+ try {
111
+ // ASSERT: Verify the node loaded with array folders
112
+ const n1 = helper.getNode(testConfigs.arrayFolders.id);
113
+ should.exist(n1);
114
+ n1.should.have.property('name', testConfigs.arrayFolders.name);
115
+ done();
116
+ } catch (err) {
117
+ done(err);
118
+ }
119
+ });
120
+ });
179
121
  });
180
122
 
181
123
  describe('Node Connections', function() {
182
124
  it('should create wired connections correctly', function(done) {
183
- // ARRANGE: Set up flow with helper node to catch output
184
- const flow = [
185
- {
186
- id: "n1",
187
- type: "email-receiver",
188
- name: "test node",
189
- host: "imap.test.com",
190
- hostType: "str",
191
- port: "993",
192
- portType: "str",
193
- tls: true,
194
- tlsType: "bool",
195
- user: "test@example.com",
196
- userType: "str",
197
- password: "testpass",
198
- passwordType: "str",
199
- folder: "INBOX",
200
- folderType: "str",
201
- markseen: true,
202
- markseenType: "bool",
203
- wires: [["n2"]]
204
- },
205
- { id: "n2", type: "helper" }
206
- ];
125
+ // ARRANGE: Use connected test flow from helpers
126
+ const flow = testFlows.connected;
207
127
 
208
128
  // ACT: Load nodes and verify connections
209
129
  helper.load(emailReceiverNode, flow, function() {
210
130
  try {
211
- const n1 = helper.getNode("n1");
212
- const n2 = helper.getNode("n2");
131
+ const n1 = helper.getNode(testConfigs.valid.id);
132
+ const h1 = helper.getNode('h1');
213
133
 
214
134
  // ASSERT: Both nodes should exist and be connected
215
135
  should.exist(n1);
216
- should.exist(n2);
217
- n1.should.have.property('name', 'test node');
218
- n2.should.have.property('type', 'helper');
136
+ should.exist(h1);
137
+ n1.should.have.property('name', testConfigs.valid.name);
138
+ h1.should.have.property('type', 'helper');
219
139
 
220
140
  done();
221
141
  } catch (err) {
@@ -225,42 +145,21 @@ describe('Email Receiver Node - Integration Tests', function() {
225
145
  });
226
146
 
227
147
  it('should handle multiple output connections', function(done) {
228
- // ARRANGE: Set up flow with multiple helper nodes
229
- const flow = [
230
- {
231
- id: "n1",
232
- type: "email-receiver",
233
- name: "multi-output node",
234
- host: "imap.test.com",
235
- hostType: "str",
236
- port: "993",
237
- portType: "str",
238
- user: "test@example.com",
239
- userType: "str",
240
- password: "testpass",
241
- passwordType: "str",
242
- folder: "INBOX",
243
- folderType: "str",
244
- markseen: true,
245
- markseenType: "bool",
246
- wires: [["n2", "n3"]]
247
- },
248
- { id: "n2", type: "helper" },
249
- { id: "n3", type: "helper" }
250
- ];
148
+ // ARRANGE: Use multi-output test flow from helpers
149
+ const flow = testFlows.multiOutput;
251
150
 
252
151
  // ACT: Load nodes
253
152
  helper.load(emailReceiverNode, flow, function() {
254
153
  try {
255
- const n1 = helper.getNode("n1");
256
- const n2 = helper.getNode("n2");
257
- const n3 = helper.getNode("n3");
154
+ const n1 = helper.getNode(testConfigs.valid.id);
155
+ const h1 = helper.getNode('h1');
156
+ const h2 = helper.getNode('h2');
258
157
 
259
158
  // ASSERT: All nodes should exist
260
159
  should.exist(n1);
261
- should.exist(n2);
262
- should.exist(n3);
263
- n1.should.have.property('name', 'multi-output node');
160
+ should.exist(h1);
161
+ should.exist(h2);
162
+ n1.should.have.property('name', testConfigs.valid.name);
264
163
 
265
164
  done();
266
165
  } catch (err) {
@@ -271,87 +170,53 @@ describe('Email Receiver Node - Integration Tests', function() {
271
170
  });
272
171
 
273
172
  describe('Message Flow', function() {
274
- it('should handle input without crashing', function(done) {
275
- // ARRANGE: Set up minimal flow
276
- const flow = [
277
- {
278
- id: "n1",
279
- type: "email-receiver",
280
- name: "test node",
281
- host: "imap.test.com",
282
- hostType: "str",
283
- port: "993",
284
- portType: "str",
285
- tls: true,
286
- tlsType: "bool",
287
- user: "test@example.com",
288
- userType: "str",
289
- password: "testpass",
290
- passwordType: "str",
291
- folder: "INBOX",
292
- folderType: "str",
293
- markseen: true,
294
- markseenType: "bool"
295
- }
296
- ];
297
-
298
- // ACT: Load node and send input
299
- helper.load(emailReceiverNode, flow, function() {
300
- try {
301
- const n1 = helper.getNode("n1");
302
- should.exist(n1);
303
-
304
- // Send input - this should not crash due to mocked IMAP
305
- n1.receive({ payload: "test input" });
306
-
307
- // ASSERT: If we reach here, the node handled input gracefully
308
- setTimeout(() => {
309
- done(); // Success if no errors thrown
310
- }, 500);
311
-
312
- } catch (err) {
313
- done(err);
314
- }
173
+ it('should handle input without crashing', async function() {
174
+ // ARRANGE: Use test flow from helpers
175
+ const flow = testFlows.single;
176
+
177
+ return new Promise((resolve, reject) => {
178
+ helper.load(emailReceiverNode, flow, function() {
179
+ try {
180
+ const n1 = helper.getNode(testConfigs.valid.id);
181
+ should.exist(n1);
182
+
183
+ // Send input - this should not crash due to mocked IMAP
184
+ n1.receive({ payload: "test input" });
185
+
186
+ // ASSERT: If we reach here, the node handled input gracefully
187
+ testUtils.wait(500).then(() => {
188
+ resolve(); // Success if no errors thrown
189
+ });
190
+
191
+ } catch (err) {
192
+ reject(err);
193
+ }
194
+ });
315
195
  });
316
196
  });
317
197
 
318
198
  it('should process messages through connected nodes', function(done) {
319
- // ARRANGE: Set up flow with helper to capture output
320
- const flow = [
321
- {
322
- id: "n1",
323
- type: "email-receiver",
324
- name: "sender node",
325
- host: "imap.test.com",
326
- hostType: "str",
327
- port: "993",
328
- portType: "str",
329
- user: "test@example.com",
330
- userType: "str",
331
- password: "testpass",
332
- passwordType: "str",
333
- folder: "INBOX",
334
- folderType: "str",
335
- markseen: true,
336
- markseenType: "bool",
337
- wires: [["n2"]]
338
- },
339
- { id: "n2", type: "helper" }
340
- ];
199
+ // ARRANGE: Use connected test flow from helpers
200
+ const flow = testFlows.connected;
341
201
 
342
202
  // ACT: Load nodes and set up message listener
343
203
  helper.load(emailReceiverNode, flow, function() {
344
204
  try {
345
- const n1 = helper.getNode("n1");
346
- const n2 = helper.getNode("n2");
205
+ const n1 = helper.getNode(testConfigs.valid.id);
206
+ const h1 = helper.getNode('h1');
347
207
 
348
208
  // Set up listener for messages from email receiver
349
- n2.on("input", function(msg) {
209
+ h1.on("input", function(msg) {
350
210
  try {
351
211
  // ASSERT: Should receive a message with expected properties
352
212
  should.exist(msg);
353
213
  should.exist(msg.payload);
354
- msg.payload.should.equal('test integration body');
214
+
215
+ // Use helper utility to verify message
216
+ testUtils.verifyMessage(msg, {
217
+ payload: 'This is a mock email body for testing purposes.'
218
+ });
219
+
355
220
  done();
356
221
  } catch (err) {
357
222
  done(err);
@@ -366,33 +231,50 @@ describe('Email Receiver Node - Integration Tests', function() {
366
231
  }
367
232
  });
368
233
  });
234
+
235
+ it('should handle message timeout gracefully', async function() {
236
+ // ARRANGE: Use connected test flow
237
+ const flow = testFlows.connected;
238
+
239
+ return new Promise((resolve, reject) => {
240
+ helper.load(emailReceiverNode, flow, function() {
241
+ try {
242
+ const n1 = helper.getNode(testConfigs.valid.id);
243
+ const h1 = helper.getNode('h1');
244
+
245
+ // Use testUtils.waitForMessage with timeout
246
+ testUtils.waitForMessage(h1, 1000)
247
+ .then((msg) => {
248
+ // ASSERT: Should receive message within timeout
249
+ should.exist(msg);
250
+ resolve();
251
+ })
252
+ .catch((err) => {
253
+ // ASSERT: Should handle timeout appropriately
254
+ err.message.should.containEql('Timeout waiting for message');
255
+ resolve(); // This is expected behavior for this test
256
+ });
257
+
258
+ // Trigger the node but don't send a message (to test timeout)
259
+ // n1.receive({ payload: "trigger" }); // Commented out to test timeout
260
+
261
+ } catch (err) {
262
+ reject(err);
263
+ }
264
+ });
265
+ });
266
+ });
369
267
  });
370
268
 
371
269
  describe('Configuration Validation', function() {
372
270
  it('should handle invalid configuration gracefully', function(done) {
373
- // ARRANGE: Set up flow with missing required config
374
- const flow = [
375
- {
376
- id: "n1",
377
- type: "email-receiver",
378
- name: "invalid config node",
379
- host: "", // Missing host
380
- hostType: "str",
381
- port: "993",
382
- portType: "str",
383
- user: "test@example.com",
384
- userType: "str",
385
- password: "testpass",
386
- passwordType: "str",
387
- folder: "INBOX",
388
- folderType: "str"
389
- }
390
- ];
271
+ // ARRANGE: Use invalid config from helpers
272
+ const flow = [testConfigs.invalidConfig];
391
273
 
392
274
  // ACT: Load node with invalid config
393
275
  helper.load(emailReceiverNode, flow, function() {
394
276
  try {
395
- const n1 = helper.getNode("n1");
277
+ const n1 = helper.getNode(testConfigs.invalidConfig.id);
396
278
  should.exist(n1);
397
279
 
398
280
  // ASSERT: Node should exist but handle invalid config appropriately
@@ -400,9 +282,9 @@ describe('Email Receiver Node - Integration Tests', function() {
400
282
  n1.receive({ payload: "test" });
401
283
 
402
284
  // If we get here without crashing, the validation worked
403
- setTimeout(() => {
285
+ testUtils.wait(300).then(() => {
404
286
  done();
405
- }, 300);
287
+ });
406
288
 
407
289
  } catch (err) {
408
290
  done(err);
@@ -410,78 +292,183 @@ describe('Email Receiver Node - Integration Tests', function() {
410
292
  });
411
293
  });
412
294
 
413
- it('should load with different folder configurations', function(done) {
414
- // ARRANGE: Set up flow with array folder config
415
- const flow = [
416
- {
417
- id: "n1",
418
- type: "email-receiver",
419
- name: "array folder node",
420
- host: "imap.test.com",
421
- hostType: "str",
422
- port: "993",
423
- portType: "str",
424
- user: "test@example.com",
425
- userType: "str",
426
- password: "testpass",
427
- passwordType: "str",
428
- folder: ["INBOX", "Sent", "Drafts"],
429
- folderType: "json",
430
- markseen: true,
431
- markseenType: "bool"
432
- }
295
+ it('should validate folder configurations properly', async function() {
296
+ // ARRANGE: Test different folder configurations
297
+ const folderConfigs = [
298
+ testConfigs.stringFolders,
299
+ testConfigs.arrayFolders
433
300
  ];
434
301
 
435
- // ACT: Load node with array folder config
436
- helper.load(emailReceiverNode, flow, function() {
437
- try {
438
- const n1 = helper.getNode("n1");
302
+ for (const config of folderConfigs) {
303
+ await new Promise((resolve, reject) => {
304
+ const flow = [config];
439
305
 
440
- // ASSERT: Node should load successfully with array config
441
- should.exist(n1);
442
- n1.should.have.property('name', 'array folder node');
306
+ helper.load(emailReceiverNode, flow, function() {
307
+ try {
308
+ const n1 = helper.getNode(config.id);
309
+
310
+ // ASSERT: Node should load successfully with different folder configs
311
+ should.exist(n1);
312
+ n1.should.have.property('name', config.name);
313
+
314
+ helper.unload();
315
+ resolve();
316
+ } catch (err) {
317
+ reject(err);
318
+ }
319
+ });
320
+ });
321
+ }
322
+ });
323
+ });
324
+
325
+ describe('Mock Integration Verification', function() {
326
+ it('should work with createMockImap from helpers', function(done) {
327
+ // ARRANGE: Create IMAP mock instance directly
328
+ const mockImap = createMockImap();
329
+ const imapInstance = new mockImap({
330
+ host: testConfigs.valid.host,
331
+ port: testConfigs.valid.port,
332
+ secure: true
333
+ });
334
+
335
+ // ACT: Test IMAP mock behavior
336
+ let readyFired = false;
337
+ imapInstance.once('ready', () => {
338
+ readyFired = true;
339
+
340
+ // Test openBox functionality
341
+ imapInstance.openBox('INBOX', false, (err, box) => {
342
+ should.not.exist(err);
343
+ should.exist(box);
344
+ box.should.have.property('messages');
345
+
346
+ // ASSERT: Mock IMAP should work as expected
347
+ readyFired.should.be.true();
443
348
  done();
349
+ });
350
+ });
444
351
 
445
- } catch (err) {
446
- done(err);
447
- }
352
+ imapInstance.connect();
353
+ });
354
+
355
+ it('should work with createMockMailparser from helpers', async function() {
356
+ // ARRANGE: Create mailparser mock instance directly
357
+ const mockMailparser = createMockMailparser();
358
+
359
+ // ACT: Test mailparser mock behavior
360
+ const result = await mockMailparser.simpleParser('test content', {
361
+ subject: 'Integration Test Email',
362
+ from: 'integration@test.com'
448
363
  });
364
+
365
+ // ASSERT: Mock mailparser should return expected structure
366
+ result.should.have.property('subject', 'Integration Test Email');
367
+ result.should.have.property('from');
368
+ result.from.should.have.property('text', 'integration@test.com');
369
+ result.should.have.property('headers');
370
+ result.headers.should.be.instanceOf(Map);
449
371
  });
450
372
  });
451
373
 
452
374
  describe('Node Lifecycle', function() {
453
- it('should clean up properly on unload', function(done) {
454
- // ARRANGE: Set up flow
455
- const flow = [
456
- {
457
- id: "n1",
458
- type: "email-receiver",
459
- name: "cleanup test node",
460
- host: "imap.test.com",
461
- hostType: "str",
462
- port: "993",
463
- portType: "str",
464
- user: "test@example.com",
465
- userType: "str",
466
- password: "testpass",
467
- passwordType: "str",
468
- folder: "INBOX",
469
- folderType: "str"
470
- }
471
- ];
375
+ it('should clean up properly on unload', async function() {
376
+ // ARRANGE: Use test flow from helpers
377
+ const flow = testFlows.single;
378
+
379
+ return new Promise((resolve, reject) => {
380
+ helper.load(emailReceiverNode, flow, function() {
381
+ try {
382
+ const n1 = helper.getNode(testConfigs.valid.id);
383
+ should.exist(n1);
384
+
385
+ // Simulate some activity
386
+ n1.receive({ payload: "test" });
387
+
388
+ // Wait a bit for any async operations
389
+ testUtils.wait(100).then(() => {
390
+ // ASSERT: Unloading should not throw errors
391
+ helper.unload();
392
+ resolve();
393
+ });
394
+
395
+ } catch (err) {
396
+ reject(err);
397
+ }
398
+ });
399
+ });
400
+ });
401
+
402
+ it('should handle multiple load/unload cycles', async function() {
403
+ // ARRANGE: Test multiple cycles
404
+ const flow = testFlows.single;
405
+ const cycles = 3;
406
+
407
+ for (let i = 0; i < cycles; i++) {
408
+ await new Promise((resolve, reject) => {
409
+ helper.load(emailReceiverNode, flow, function() {
410
+ try {
411
+ const n1 = helper.getNode(testConfigs.valid.id);
412
+ should.exist(n1);
413
+
414
+ // Quick activity simulation
415
+ n1.receive({ payload: `test cycle ${i}` });
416
+
417
+ testUtils.wait(50).then(() => {
418
+ helper.unload();
419
+ resolve();
420
+ });
421
+ } catch (err) {
422
+ reject(err);
423
+ }
424
+ });
425
+ });
426
+ }
427
+
428
+ // ASSERT: If we complete all cycles without error, lifecycle handling works
429
+ // This assertion is implicit in the successful completion of the loop
430
+ });
431
+ });
432
+
433
+ describe('Advanced Flow Testing', function() {
434
+ it('should handle complex message flows with multiple helpers', function(done) {
435
+ // ARRANGE: Use multi-output flow from helpers
436
+ const flow = testFlows.multiOutput;
437
+ let receivedMessages = [];
472
438
 
473
- // ACT: Load and then unload the node
474
439
  helper.load(emailReceiverNode, flow, function() {
475
440
  try {
476
- const n1 = helper.getNode("n1");
477
- should.exist(n1);
441
+ const n1 = helper.getNode(testConfigs.valid.id);
442
+ const h1 = helper.getNode('h1');
443
+ const h2 = helper.getNode('h2');
444
+
445
+ // Set up listeners for both helper nodes
446
+ h1.on("input", function(msg) {
447
+ receivedMessages.push({ node: 'h1', msg: msg });
448
+ checkCompletion();
449
+ });
478
450
 
479
- // Simulate some activity
480
- n1.receive({ payload: "test" });
451
+ h2.on("input", function(msg) {
452
+ receivedMessages.push({ node: 'h2', msg: msg });
453
+ checkCompletion();
454
+ });
481
455
 
482
- // ASSERT: Unloading should not throw errors
483
- helper.unload();
484
- done();
456
+ function checkCompletion() {
457
+ if (receivedMessages.length >= 2) {
458
+ // ASSERT: Both helpers should receive messages
459
+ receivedMessages.length.should.equal(2);
460
+
461
+ receivedMessages.forEach(item => {
462
+ should.exist(item.msg);
463
+ should.exist(item.msg.payload);
464
+ });
465
+
466
+ done();
467
+ }
468
+ }
469
+
470
+ // Trigger the email receiver
471
+ n1.receive({ payload: "multi-trigger" });
485
472
 
486
473
  } catch (err) {
487
474
  done(err);
@@ -1,332 +1,279 @@
1
1
  const should = require('should');
2
+ const {
3
+ createMockImap,
4
+ createMockMailparser,
5
+ createMockNodeRED,
6
+ setupModuleMocks,
7
+ testConfigs,
8
+ testUtils
9
+ } = require('../helpers/email-receiver.mocks.js');
2
10
 
3
- describe('Email Receiver Node - Unit Tests', function() {
4
- // Set a reasonable timeout
11
+ describe('Email Receiver Node - Unit Tests with Helpers', function() {
5
12
  this.timeout(10000);
6
13
 
7
- // Module and mocking setup
8
14
  let emailReceiverNode;
9
- let originalLoad;
10
- let mockImap;
11
- let mockMailparser;
15
+ let cleanupMocks;
12
16
 
13
17
  before(function() {
14
- // Create mock modules with correct behavior
15
- mockImap = function(config) {
16
- this.config = config;
17
- this.connect = () => {
18
- // Simulate a successful connection by immediately emitting 'ready'
19
- if (this.events && this.events.ready) {
20
- this.events.ready();
21
- }
22
- };
23
- this.openBox = (folder, readOnly, callback) => { callback(null, { messages: { total: 1 } }); };
24
- this.search = (criteria, callback) => { callback(null, [123]); };
25
- this.fetch = (results, options) => {
26
- return {
27
- on: (event, cb) => {
28
- if (event === 'message') {
29
- cb({ on: (e, bodyCb) => { if (e === 'body') bodyCb({}); } });
30
- }
31
- },
32
- once: (event, cb) => {
33
- if (event === 'end') { cb(); }
34
- }
35
- };
36
- };
37
- this.end = () => {};
38
- this.once = (event, callback) => {
39
- if (!this.events) this.events = {};
40
- this.events[event] = callback;
41
- };
42
- return this;
43
- };
44
-
45
- mockMailparser = {
46
- simpleParser: function() {
47
- return Promise.resolve({
48
- subject: 'test',
49
- text: 'test body',
50
- html: '<p>test</p>',
51
- from: { text: 'test@test.com' },
52
- date: new Date(),
53
- headers: new Map(),
54
- attachments: []
55
- });
56
- }
57
- };
58
-
59
- const mockModules = {
60
- 'node-imap': mockImap,
61
- 'mailparser': mockMailparser
62
- };
63
-
64
- // Override require
65
- const Module = require('module');
66
- originalLoad = Module._load;
67
- Module._load = function(request, parent) {
68
- if (mockModules[request]) {
69
- return mockModules[request];
70
- }
71
- return originalLoad.apply(this, arguments);
72
- };
18
+ // Set up module mocks using helper
19
+ cleanupMocks = setupModuleMocks();
73
20
 
74
21
  // Load the node with mocked dependencies
75
22
  emailReceiverNode = require('../../email-receiver/email-receiver.js');
76
23
  });
77
24
 
78
25
  after(function() {
79
- // Restore original module loading
80
- if (originalLoad) {
81
- const Module = require('module');
82
- Module._load = originalLoad;
26
+ // Clean up mocks
27
+ if (cleanupMocks) {
28
+ cleanupMocks();
83
29
  }
84
30
  });
85
31
 
86
32
  describe('Module Export', function() {
87
33
  it('should export a function', function() {
88
- // ARRANGE: Node module is already loaded
89
-
90
- // ACT: Check the type of the exported module
91
-
92
- // ASSERT: Should be a function
93
34
  emailReceiverNode.should.be.type('function');
94
35
  });
95
36
  });
96
37
 
97
38
  describe('Node Registration', function() {
98
39
  it('should register node type without errors', function() {
99
- // ARRANGE: Set up mock RED object and capture registration calls
100
- let registeredType;
101
- let registeredConstructor;
102
-
103
- const mockRED = {
104
- nodes: {
105
- createNode: function(node, config) {
106
- node.id = config.id;
107
- node.type = config.type;
108
- node.name = config.name;
109
- node.on = function() {};
110
- node.status = function() {};
111
- node.error = function() {};
112
- node.send = function() {};
113
- return node;
114
- },
115
- registerType: function(type, constructor) {
116
- registeredType = type;
117
- registeredConstructor = constructor;
118
- }
119
- },
120
- util: {
121
- evaluateNodeProperty: function(value, type) {
122
- return value;
123
- },
124
- encrypt: function(value) {
125
- return 'encrypted:' + value;
126
- }
127
- }
128
- };
40
+ // ARRANGE: Create mock Node-RED with tracking
41
+ const mockRED = createMockNodeRED();
129
42
 
130
- // ACT: Call the node registration function
43
+ // ACT: Register the node
131
44
  emailReceiverNode(mockRED);
132
45
 
133
- // ASSERT: Verify registration was called correctly
134
- registeredType.should.equal('email-receiver');
135
- registeredConstructor.should.be.type('function');
46
+ // ASSERT: Verify registration
47
+ mockRED.nodes.lastRegisteredType.should.equal('email-receiver');
48
+ mockRED.nodes.lastRegisteredConstructor.should.be.type('function');
136
49
  });
137
50
  });
138
51
 
139
52
  describe('Node Instantiation', function() {
140
53
  it('should handle node instantiation with valid config', function() {
141
- // ARRANGE: Set up mock RED object and node instance tracking
142
- let nodeInstance;
143
-
144
- const mockRED = {
145
- nodes: {
146
- createNode: function(node, config) {
147
- nodeInstance = node;
148
- node.id = config.id;
149
- node.type = config.type;
150
- node.name = config.name;
151
- node.on = function() {};
152
- node.status = function() {};
153
- node.error = function() {};
154
- node.send = function() {};
155
- return node;
156
- },
157
- registerType: function(type, NodeConstructor) {
158
- // Simulate creating a node instance with valid config
159
- const config = {
160
- id: 'test-node',
161
- type: 'email-receiver',
162
- name: 'Test Email Receiver',
163
- host: 'imap.test.com',
164
- hostType: 'str',
165
- port: 993,
166
- portType: 'num',
167
- user: 'test@test.com',
168
- userType: 'str',
169
- password: 'testpass',
170
- passwordType: 'str',
171
- folder: ["INBOX"],
172
- folderType: 'json',
173
- markseen: true,
174
- markseenType: 'bool'
175
- };
176
-
177
- new NodeConstructor(config);
178
- }
179
- },
180
- util: {
181
- evaluateNodeProperty: function(value, type) {
182
- return value;
183
- },
184
- encrypt: function(value) {
185
- return 'encrypted:' + value;
186
- }
54
+ // ARRANGE: Track node creation
55
+ let createdNode = null;
56
+ const mockRED = createMockNodeRED({
57
+ onHandler: function(event, callback) {
58
+ createdNode = this;
187
59
  }
188
- };
60
+ });
189
61
 
190
- // ACT: Register the node and create an instance
62
+ // ACT: Register and create node instance
191
63
  emailReceiverNode(mockRED);
64
+ new mockRED.nodes.lastRegisteredConstructor(testConfigs.valid);
192
65
 
193
- // ASSERT: Verify the node instance was created with correct properties
194
- should.exist(nodeInstance);
195
- nodeInstance.should.have.property('name', 'Test Email Receiver');
66
+ // ASSERT: Verify node was created with correct properties
67
+ should.exist(createdNode);
68
+ createdNode.should.have.property('name', testConfigs.valid.name);
69
+ createdNode.should.have.property('id', testConfigs.valid.id);
70
+ });
71
+
72
+ it('should handle minimal config', function() {
73
+ // ARRANGE: Use minimal test config
74
+ let createdNode = null;
75
+ const mockRED = createMockNodeRED({
76
+ onHandler: function(event, callback) {
77
+ createdNode = this;
78
+ }
79
+ });
80
+
81
+ // ACT: Register and create node with minimal config
82
+ emailReceiverNode(mockRED);
83
+ new mockRED.nodes.lastRegisteredConstructor(testConfigs.minimal);
84
+
85
+ // ASSERT: Verify node creation
86
+ should.exist(createdNode);
87
+ createdNode.should.have.property('id', testConfigs.minimal.id);
196
88
  });
197
89
  });
198
90
 
199
91
  describe('Folder Configuration', function() {
200
- it('should handle an array of folders', function(done) {
201
- // ARRANGE: Mock the Node-RED environment
202
- let nodeInstance;
203
- let inputCallback;
204
- let messagesSent = 0;
205
- const expectedMessages = 2;
206
-
207
- const mockRED = {
208
- nodes: {
209
- createNode: function(node, config) {
210
- nodeInstance = node;
211
- node.on = (event, callback) => { if (event === 'input') inputCallback = callback; };
212
- node.status = () => {};
213
- node.error = () => {};
214
- node.send = (msg) => {
215
- should.exist(msg);
216
- msg.payload.should.equal('test body');
217
- messagesSent++;
218
- if (messagesSent === expectedMessages) {
219
- done();
220
- }
221
- };
222
- return node;
223
- },
224
- registerType: (type, constructor) => {
225
- const mockNode = {
226
- id: 'mock-node-id',
227
- on: (event, callback) => {
228
- if (event === 'input') {
229
- inputCallback = callback;
230
- }
231
- },
232
- status: () => {},
233
- error: () => {},
234
- send: (msg) => {
235
- should.exist(msg);
236
- msg.payload.should.equal('test body');
237
- messagesSent++;
238
- if (messagesSent === expectedMessages) {
239
- done();
240
- }
241
- },
242
- };
243
- // Create an instance of the node with the test configuration
244
- const node = new constructor(mockNode, {
245
- host: "imap.test.com", hostType: "str",
246
- port: 993, portType: "num",
247
- user: "test@test.com", userType: "str",
248
- password: "testpass", passwordType: "str",
249
- folder: ["INBOX", "Junk"], folderType: 'json',
250
- markseen: true, markseenType: 'bool'
251
- });
252
- }
253
- },
254
- util: { evaluateNodeProperty: (value) => value },
255
- };
256
-
257
- // ACT: Register the node, then simulate input
258
- emailReceiverNode(mockRED);
259
- // After the node is registered, simulate an input to trigger the flow
260
- setTimeout(() => {
261
- if (inputCallback) {
262
- inputCallback({});
263
- } else {
264
- done(new Error("Input callback not set up."));
92
+ it('should handle array of folders', async function() {
93
+ // ARRANGE: Set up message tracking
94
+ let sentMessage = null;
95
+ const mockRED = createMockNodeRED({
96
+ sendHandler: function(msg) {
97
+ sentMessage = msg;
265
98
  }
266
- }, 50); // Use a small timeout to ensure the mocks are fully set up
99
+ });
100
+
101
+ // ACT: Register node and create instance with array folders
102
+ emailReceiverNode(mockRED);
103
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
104
+ const nodeInstance = new nodeConstructor(testConfigs.arrayFolders);
105
+
106
+ // Wait for processing
107
+ await testUtils.wait(50);
108
+
109
+ // ASSERT: Should handle array folders without error
110
+ should.exist(nodeInstance);
111
+ nodeInstance.should.have.property('name', testConfigs.arrayFolders.name);
112
+ });
267
113
  });
268
- });
269
114
 
270
115
  describe('Error Handling', function() {
271
116
  it('should call node.error for invalid folder type', function(done) {
272
- // ARRANGE: Mock the node instance to capture errors
273
- let errorCalled = false;
274
- const nodeInstance = {
275
- config: { folder: 123, folderType: 'num' },
276
- on: (event, callback) => { if (event === 'input') nodeInstance.inputCallback = callback; },
277
- status: () => {},
278
- error: (err) => {
279
- errorCalled = true;
280
- err.should.containEql('The \'folders\' property must be an array of strings');
117
+ // ARRANGE: Set up error tracking
118
+ const mockRED = createMockNodeRED({
119
+ errorHandler: function(err) {
120
+ // ASSERT: Should receive appropriate error message
121
+ err.should.containEql("The 'folders' property must be an array of strings.");
281
122
  done();
282
- },
283
- send: () => {},
284
- };
285
- const mockRED = {
286
- nodes: {
287
- createNode: (node, config) => Object.assign(node, { on: nodeInstance.on, status: nodeInstance.status, error: nodeInstance.error, send: nodeInstance.send }),
288
- registerType: (type, constructor) => new constructor(nodeInstance.config),
289
- },
290
- util: { evaluateNodeProperty: (value, type) => value },
291
- };
123
+ }
124
+ });
292
125
 
293
- // ACT: Register and instantiate the node, then simulate an input message
126
+ // ACT: Register node and create instance with invalid config
294
127
  emailReceiverNode(mockRED);
295
- nodeInstance.inputCallback({});
128
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
129
+ new nodeConstructor(testConfigs.invalidConfig);
296
130
  });
297
131
 
298
132
  it('should call node.error for missing config', function(done) {
299
- // ARRANGE: Mock the node instance to capture errors
300
- let errorCalled = false;
133
+ // ARRANGE: Set up error and status tracking
301
134
  let statusCalled = false;
302
- const nodeInstance = {
303
- config: {
304
- host: "imap.test.com", hostType: "str",
305
- port: 993, portType: "num",
306
- user: "test@test.com", userType: "str",
307
- password: "", passwordType: "str", // Empty password should trigger error
308
- folder: ["INBOX"], folderType: "json"
135
+ const mockRED = createMockNodeRED({
136
+ statusHandler: function(status) {
137
+ statusCalled = true;
138
+ if (status.fill) {
139
+ status.fill.should.equal('red');
140
+ }
309
141
  },
310
- on: (event, callback) => { if (event === 'input') nodeInstance.inputCallback = callback; },
311
- status: (s) => { statusCalled = true; s.fill.should.equal('red'); },
312
- error: (err) => {
313
- errorCalled = true;
142
+ errorHandler: function(err) {
143
+ // ASSERT: Should receive config error
314
144
  err.should.containEql('Missing required IMAP config');
145
+ statusCalled.should.be.true();
146
+ done();
147
+ }
148
+ });
149
+
150
+ // ACT: Register node and create instance with invalid config
151
+ emailReceiverNode(mockRED);
152
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
153
+ new nodeConstructor(testConfigs.invalidConfig);
154
+ });
155
+ });
156
+
157
+ describe('Message Processing', function() {
158
+ it('should process email message correctly', async function() {
159
+ // ARRANGE: Set up message capture
160
+ let processedMessage = null;
161
+ const mockRED = createMockNodeRED({
162
+ sendHandler: function(msg) {
163
+ processedMessage = msg;
164
+ }
165
+ });
166
+
167
+ // ACT: Create node and simulate email processing
168
+ emailReceiverNode(mockRED);
169
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
170
+ const nodeInstance = new nodeConstructor(testConfigs.valid);
171
+
172
+ // Simulate input trigger (this would depend on your node's implementation)
173
+ // The actual trigger mechanism would need to match your node's design
174
+
175
+ await testUtils.wait(100);
176
+
177
+ // ASSERT: Message processing behavior would be verified here
178
+ // The specific assertions depend on your node's output format
179
+ should.exist(nodeInstance);
180
+ });
181
+
182
+ it('should handle multiple messages', async function() {
183
+ // ARRANGE: Set up message collection
184
+ const messages = [];
185
+ const mockRED = createMockNodeRED({
186
+ sendHandler: function(msg) {
187
+ messages.push(msg);
188
+ }
189
+ });
190
+
191
+ // ACT: Create node and simulate multiple emails
192
+ emailReceiverNode(mockRED);
193
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
194
+ const nodeInstance = new nodeConstructor(testConfigs.valid);
195
+
196
+ await testUtils.wait(150);
197
+
198
+ // ASSERT: Should handle multiple messages appropriately
199
+ should.exist(nodeInstance);
200
+ });
201
+ });
202
+
203
+ describe('IMAP Connection', function() {
204
+ it('should handle connection success', function(done) {
205
+ // ARRANGE: Set up connection tracking
206
+ const mockRED = createMockNodeRED({
207
+ statusHandler: function(status) {
208
+ if (status.fill === 'green') {
209
+ // ASSERT: Should show connected status
210
+ status.text.should.containEql('connected');
211
+ done();
212
+ }
213
+ }
214
+ });
215
+
216
+ // ACT: Create node which should attempt connection
217
+ emailReceiverNode(mockRED);
218
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
219
+ new nodeConstructor(testConfigs.valid);
220
+ });
221
+
222
+ it('should handle connection errors', function(done) {
223
+ // ARRANGE: Set up error tracking
224
+ const mockRED = createMockNodeRED({
225
+ errorHandler: function(err) {
226
+ // ASSERT: Should handle connection errors gracefully
227
+ should.exist(err);
315
228
  done();
316
229
  },
317
- send: () => {},
318
- };
319
- const mockRED = {
320
- nodes: {
321
- createNode: (node, config) => Object.assign(node, { on: nodeInstance.on, status: nodeInstance.status, error: nodeInstance.error, send: nodeInstance.send }),
322
- registerType: (type, constructor) => new constructor(nodeInstance.config),
323
- },
324
- util: { evaluateNodeProperty: (value, type) => value },
325
- };
230
+ statusHandler: function(status) {
231
+ if (status.fill === 'red') {
232
+ // Connection failed status
233
+ status.text.should.containEql('error');
234
+ }
235
+ }
236
+ });
326
237
 
327
- // ACT: Register and instantiate the node, then simulate an input message
238
+ // ACT: Create node with config that should fail
328
239
  emailReceiverNode(mockRED);
329
- nodeInstance.inputCallback({});
240
+ const nodeConstructor = mockRED.nodes.lastRegisteredConstructor;
241
+
242
+ // Use invalid config to trigger connection error
243
+ const invalidConfig = { ...testConfigs.valid, host: 'invalid.host.com' };
244
+ new nodeConstructor(invalidConfig);
245
+ });
246
+ });
247
+
248
+ describe('Message Verification Utilities', function() {
249
+ it('should verify message properties using testUtils', function() {
250
+ // ARRANGE: Create a test message
251
+ const testMessage = {
252
+ payload: 'test content',
253
+ topic: 'email/received',
254
+ from: 'test@example.com'
255
+ };
256
+
257
+ // ACT & ASSERT: Use helper to verify message properties
258
+ testUtils.verifyMessage(testMessage, {
259
+ payload: 'test content',
260
+ topic: 'email/received'
261
+ });
262
+
263
+ // Should not throw any errors if verification passes
264
+ testMessage.should.have.property('from', 'test@example.com');
265
+ });
266
+
267
+ it('should use wait utility for async operations', async function() {
268
+ // ARRANGE: Record start time
269
+ const startTime = Date.now();
270
+
271
+ // ACT: Use the wait utility
272
+ await testUtils.wait(100);
273
+
274
+ // ASSERT: Should have waited approximately the right amount of time
275
+ const elapsed = Date.now() - startTime;
276
+ elapsed.should.be.approximately(100, 50); // Allow 50ms tolerance
330
277
  });
331
278
  });
332
279
  });