@5minds/node-red-contrib-processcube-tools 1.0.1-feature-607796-mfdmqdc3 → 1.0.1-feature-e7a81a-mfdq8poq

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.
@@ -123,56 +123,65 @@
123
123
  <script type="text/html" data-help-name="email-receiver">
124
124
  <p>A Node-RED node that fetches unseen emails from a specified IMAP server. Each fetched email is sent as a separate message on the output.</p>
125
125
 
126
- <p>All fields can be configured as a <strong>string</strong>, from a <strong>message property (msg)</strong>, <strong>flow</strong> or <strong>global</strong> context, or an <strong>environment variable</strong>.</p>
127
-
128
- <p><strong>Security Tip:</strong> It's a best practice to avoid storing sensitive information like passwords directly in your Node-RED flow. This prevents them from being exposed in your flow file. Instead, use an <strong>environment variable</strong>. You can define these variables in a <code>.env</code> file, which is especially useful when deploying your application.</p>
129
-
130
- <p>A <code>.env</code> file should look like this:</p>
131
- <pre>
132
- EMAIL_SEND_PORT=123
133
- EMAIL_SEND_HOST=smtp.gmail.com
134
- EMAIL_SEND_USER=myTestMail@company.com
135
- EMAIL_SEND_PASSWORD=mySecretPassword
136
- </pre>
137
-
138
- <p>In your flow, you can then access the password using the environment variable <code>EMAIL_SEND_PASSWORD</code>.</p>
139
-
140
- Inputs
141
- <dl class="message-properties">
142
- <dt>payload</dt>
143
- <dd>The node is triggered by any incoming message. The node's configuration can be overridden by properties of the incoming <code>msg</code> object.</dd>
144
- </dl>
145
-
146
- Outputs
147
- <dl class="message-properties">
148
- <dt>payload
149
- <span class="property-type">string</span>
150
- </dt>
151
- <dd>The text body of the email.</dd>
152
- </dl>
153
-
154
- Optional Message Properties
155
- <p>You can override default settings by passing the following properties in the incoming <code>msg</code> object:</p>
156
- <dl class="message-properties">
157
- <dt>msg.imap_connTimeout
158
- <span class="property-type">number</span>
159
- </dt>
160
- <dd>The connection timeout in milliseconds (default: 10000).</dd>
161
- <dt>msg.imap_authTimeout
162
- <span class="property-type">number</span>
163
- </dt>
164
- <dd>The authentication timeout in milliseconds (default: 5000).</dd>
165
- <dt>msg.imap_keepalive
166
- <span class="property-type">boolean</span>
167
- </dt>
168
- <dd>If set to true, a periodic NOOP command is sent to keep the connection alive (default: true).</dd>
169
- <dt>msg.imap_autotls
170
- <span class="property-type">string</span>
171
- </dt>
172
- <dd>Controls STARTTLS behavior. Set to never to disable it (default: never).</dd>
173
- <dt>msg.imap_tlsOptions
174
- <span class="property-type">object</span>
175
- </dt>
176
- <dd>An object containing TLS options for the connection.</dd>
177
- </dl>
126
+ <p>All fields can be configured as a <strong>string</strong>, from a <strong>message property (msg)</strong>, <strong>flow</strong> or <strong>global</strong> context, or an <strong>environment variable</strong>.</p>
127
+
128
+ <p><strong>Security Tip:</strong> It's a best practice to avoid storing sensitive information like passwords directly in your Node-RED flow. This prevents them from being exposed in your flow file. Instead, use an <strong>environment variable</strong>. You can define these variables in a <code>.env</code> file, which is especially useful when deploying your application with Docker.</p>
129
+
130
+ <p>A <code>.env</code> file should look like this:</p>
131
+ <pre>
132
+ EMAIL_SEND_PORT=123
133
+ EMAIL_SEND_HOST=smtp.gmail.com
134
+ EMAIL_SEND_USER=myTestMail@company.com
135
+ EMAIL_SEND_PASSWORD=mySecretPassword
136
+ </pre>
137
+
138
+ <p>To ensure Docker loads these variables from the <code>.env</code> file, you need to add the following line to your <code>docker-compose.yaml</code> file:</p>
139
+ <pre>
140
+ services:
141
+ your_service_name:
142
+ ...
143
+ env_file:
144
+ - .env
145
+ </pre>
146
+
147
+ <p>In your flow, you can then access the password using the environment variable <code>EMAIL_SEND_PASSWORD</code>.</p>
148
+
149
+ Inputs
150
+ <dl class="message-properties">
151
+ <dt>payload</dt>
152
+ <dd>The node is triggered by any incoming message. The node's configuration can be overridden by properties of the incoming <code>msg</code> object.</dd>
153
+ </dl>
154
+
155
+ Outputs
156
+ <dl class="message-properties">
157
+ <dt>payload
158
+ <span class="property-type">string</span>
159
+ </dt>
160
+ <dd>The text body of the email.</dd>
161
+ </dl>
162
+
163
+ Optional Message Properties
164
+ <p>You can override default settings by passing the following properties in the incoming <code>msg</code> object:</p>
165
+ <dl class="message-properties">
166
+ <dt>msg.imap_connTimeout
167
+ <span class="property-type">number</span>
168
+ </dt>
169
+ <dd>The connection timeout in milliseconds (default: 10000).</dd>
170
+ <dt>msg.imap_authTimeout
171
+ <span class="property-type">number</span>
172
+ </dt>
173
+ <dd>The authentication timeout in milliseconds (default: 5000).</dd>
174
+ <dt>msg.imap_keepalive
175
+ <span class="property-type">boolean</span>
176
+ </dt>
177
+ <dd>If set to <code>true</code>, a periodic NOOP command is sent to keep the connection alive (default: <code>true</code>).</dd>
178
+ <dt>msg.imap_autotls
179
+ <span class="property-type">string</span>
180
+ </dt>
181
+ <dd>Controls STARTTLS behavior. Set to <code>never</code> to disable it (default: <code>never</code>).</dd>
182
+ <dt>msg.imap_tlsOptions
183
+ <span class="property-type">object</span>
184
+ </dt>
185
+ <dd>An object containing TLS options for the connection.</dd>
186
+ </dl>
178
187
  </script>
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube-tools",
3
- "version": "1.0.1-feature-607796-mfdmqdc3",
3
+ "version": "1.0.1-feature-e7a81a-mfdq8poq",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED tools nodes for ProcessCube",
6
6
  "scripts": {
7
7
  "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\"",
8
- "test": "mocha test/**/*_spec.js --timeout 10000",
9
- "test:debug": "mocha test/**/*_spec.js --timeout 0 --reporter spec"
8
+ "test:unit": "mocha test/unit/**/*.test.js --timeout 10000",
9
+ "test:integration": "mocha test/integration/**/*.test.js --timeout 10000",
10
+ "test": "npm run test:unit && npm run test:integration",
11
+ "test:debug": "mocha test/unit/**/*.test.js test/integration/**/*.test.js --timeout 0 --reporter spec"
10
12
  },
11
13
  "authors": [
12
14
  {
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Shared mock objects and utilities for Email Receiver Node tests
3
+ */
4
+
5
+ /**
6
+ * Mock IMAP implementation for testing
7
+ */
8
+ function createMockImap() {
9
+ return function MockImap(config) {
10
+ this.config = config;
11
+ this.events = {};
12
+
13
+ // Simulate connection behavior
14
+ this.connect = () => {
15
+ // Simulate successful connection by emitting 'ready' event
16
+ if (this.events && this.events.ready) {
17
+ // Use setTimeout to simulate async behavior
18
+ setTimeout(() => this.events.ready(), 10);
19
+ }
20
+ };
21
+
22
+ // Simulate opening a mailbox
23
+ this.openBox = (folder, readOnly, callback) => {
24
+ setTimeout(() => {
25
+ callback(null, {
26
+ messages: { total: 1 },
27
+ name: folder,
28
+ readOnly: readOnly
29
+ });
30
+ }, 10);
31
+ };
32
+
33
+ // Simulate searching for emails
34
+ this.search = (criteria, callback) => {
35
+ setTimeout(() => {
36
+ // Return mock message IDs
37
+ callback(null, [123, 456, 789]);
38
+ }, 10);
39
+ };
40
+
41
+ // Simulate fetching email messages
42
+ this.fetch = (results, options) => {
43
+ return {
44
+ on: (event, callback) => {
45
+ if (event === 'message') {
46
+ setTimeout(() => {
47
+ const mockMessage = {
48
+ on: (messageEvent, messageCallback) => {
49
+ if (messageEvent === 'body') {
50
+ setTimeout(() => {
51
+ messageCallback(Buffer.from('mock email body'));
52
+ }, 5);
53
+ } else if (messageEvent === 'attributes') {
54
+ setTimeout(() => {
55
+ messageCallback({
56
+ uid: 123,
57
+ flags: ['\\Seen'],
58
+ date: new Date(),
59
+ size: 1024
60
+ });
61
+ }, 5);
62
+ }
63
+ },
64
+ once: (messageEvent, messageCallback) => {
65
+ if (messageEvent === 'end') {
66
+ setTimeout(() => messageCallback(), 15);
67
+ }
68
+ }
69
+ };
70
+ callback(mockMessage);
71
+ }, 10);
72
+ }
73
+ },
74
+ once: (event, callback) => {
75
+ if (event === 'end') {
76
+ setTimeout(() => callback(), 20);
77
+ } else if (event === 'error') {
78
+ // Store error callback for potential use
79
+ this.errorCallback = callback;
80
+ }
81
+ }
82
+ };
83
+ };
84
+
85
+ // Simulate closing connection
86
+ this.end = () => {
87
+ if (this.events && this.events.end) {
88
+ setTimeout(() => this.events.end(), 5);
89
+ }
90
+ };
91
+
92
+ // Event listener setup
93
+ this.once = (event, callback) => {
94
+ if (!this.events) this.events = {};
95
+ this.events[event] = callback;
96
+ };
97
+
98
+ // Additional IMAP methods that might be used
99
+ this.addFlags = (source, flags, callback) => {
100
+ setTimeout(() => callback(null), 5);
101
+ };
102
+
103
+ this.removeFlags = (source, flags, callback) => {
104
+ setTimeout(() => callback(null), 5);
105
+ };
106
+
107
+ return this;
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Mock Mailparser implementation for testing
113
+ */
114
+ function createMockMailparser() {
115
+ return {
116
+ simpleParser: function(source, options = {}) {
117
+ return Promise.resolve({
118
+ subject: options.subject || 'Mock Email Subject',
119
+ text: options.text || 'This is a mock email body for testing purposes.',
120
+ html: options.html || '<p>This is a mock email body for testing purposes.</p>',
121
+ from: {
122
+ text: options.from || 'sender@test.com',
123
+ value: [{ address: options.from || 'sender@test.com', name: 'Test Sender' }]
124
+ },
125
+ to: {
126
+ text: options.to || 'recipient@test.com',
127
+ value: [{ address: options.to || 'recipient@test.com', name: 'Test Recipient' }]
128
+ },
129
+ date: options.date || new Date(),
130
+ messageId: options.messageId || '<mock-message-id@test.com>',
131
+ headers: new Map([
132
+ ['message-id', '<mock-message-id@test.com>'],
133
+ ['subject', options.subject || 'Mock Email Subject'],
134
+ ['from', options.from || 'sender@test.com'],
135
+ ['to', options.to || 'recipient@test.com']
136
+ ]),
137
+ attachments: options.attachments || []
138
+ });
139
+ }
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Create mock Node-RED object for unit testing
145
+ */
146
+ function createMockNodeRED(options = {}) {
147
+ return {
148
+ nodes: {
149
+ createNode: function(node, config) {
150
+ // Apply config properties to node
151
+ Object.assign(node, {
152
+ id: config.id || 'mock-node-id',
153
+ type: config.type || 'email-receiver',
154
+ name: config.name || 'Mock Node',
155
+ on: options.onHandler || function() {},
156
+ status: options.statusHandler || function() {},
157
+ error: options.errorHandler || function() {},
158
+ send: options.sendHandler || function() {},
159
+ log: options.logHandler || function() {},
160
+ warn: options.warnHandler || function() {},
161
+ debug: options.debugHandler || function() {}
162
+ });
163
+ return node;
164
+ },
165
+ registerType: function(type, constructor) {
166
+ // Store registration for verification in tests
167
+ this.lastRegisteredType = type;
168
+ this.lastRegisteredConstructor = constructor;
169
+ }
170
+ },
171
+ util: {
172
+ evaluateNodeProperty: function(value, type, node, msg, callback) {
173
+ if (type === 'json') {
174
+ try {
175
+ // Simulate parsing a JSON string into an object
176
+ return JSON.parse(JSON.stringify(value));
177
+ } catch (e) {
178
+ if (callback) {
179
+ callback(e, null);
180
+ }
181
+ return null;
182
+ }
183
+ }
184
+
185
+ // Simple mock implementation
186
+ if (callback) {
187
+ callback(null, value);
188
+ }
189
+ return value;
190
+ },
191
+ encrypt: function(value) {
192
+ return 'encrypted:' + value;
193
+ },
194
+ decrypt: function(value) {
195
+ return value.replace('encrypted:', '');
196
+ }
197
+ },
198
+ log: {
199
+ info: options.logInfo || function() {},
200
+ warn: options.logWarn || function() {},
201
+ error: options.logError || function() {},
202
+ debug: options.logDebug || function() {}
203
+ }
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Set up module mocks for require() calls
209
+ */
210
+ function setupModuleMocks() {
211
+ const mockModules = {
212
+ 'node-imap': createMockImap(),
213
+ 'mailparser': createMockMailparser()
214
+ };
215
+
216
+ const Module = require('module');
217
+ const originalLoad = Module._load;
218
+
219
+ Module._load = function(request, parent) {
220
+ if (mockModules[request]) {
221
+ return mockModules[request];
222
+ }
223
+ return originalLoad.apply(this, arguments);
224
+ };
225
+
226
+ // Return cleanup function
227
+ return function cleanup() {
228
+ Module._load = originalLoad;
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Create test configurations for different scenarios
234
+ */
235
+ const testConfigs = {
236
+ valid: {
237
+ id: 'test-node-1',
238
+ type: 'email-receiver',
239
+ name: 'Test Email Receiver',
240
+ host: 'imap.test.com',
241
+ hostType: 'str',
242
+ port: 993,
243
+ portType: 'num',
244
+ tls: true,
245
+ tlsType: 'bool',
246
+ user: 'test@test.com',
247
+ userType: 'str',
248
+ password: 'testpass',
249
+ passwordType: 'str',
250
+ folder: 'INBOX',
251
+ folderType: 'str',
252
+ markseen: true,
253
+ markseenType: 'bool'
254
+ },
255
+
256
+ arrayFolders: {
257
+ id: 'test-node-3',
258
+ type: 'email-receiver',
259
+ name: 'Array Folders Test',
260
+ host: 'imap.test.com',
261
+ hostType: 'str',
262
+ port: 993,
263
+ portType: 'num',
264
+ user: 'test@test.com',
265
+ userType: 'str',
266
+ password: 'testpass',
267
+ passwordType: 'str',
268
+ folder: ['INBOX', 'Junk', 'Drafts'],
269
+ folderType: 'json',
270
+ markseen: false,
271
+ markseenType: 'bool'
272
+ },
273
+
274
+ invalidConfig: {
275
+ id: 'test-node-4',
276
+ type: 'email-receiver',
277
+ name: 'Invalid Config Test',
278
+ host: '', // Missing host
279
+ hostType: 'str',
280
+ port: 993,
281
+ portType: 'num',
282
+ user: 'test@test.com',
283
+ userType: 'str',
284
+ password: '', // Missing password
285
+ passwordType: 'str',
286
+ folder: 123, // Wrong type
287
+ folderType: 'num'
288
+ },
289
+
290
+ minimal: {
291
+ id: 'test-node-5',
292
+ type: 'email-receiver',
293
+ host: 'imap.minimal.com',
294
+ hostType: 'str',
295
+ port: 993,
296
+ portType: 'num',
297
+ user: 'minimal@test.com',
298
+ userType: 'str',
299
+ password: 'minimalpass',
300
+ passwordType: 'str',
301
+ folder: 'INBOX',
302
+ folderType: 'str'
303
+ }
304
+ };
305
+
306
+ /**
307
+ * Create test flows for Node-RED integration tests
308
+ */
309
+ const testFlows = {
310
+ single: [
311
+ testConfigs.valid
312
+ ],
313
+
314
+ withHelper: [
315
+ testConfigs.valid,
316
+ { id: 'h1', type: 'helper' }
317
+ ],
318
+
319
+ connected: [
320
+ { ...testConfigs.valid, wires: [['h1']] },
321
+ { id: 'h1', type: 'helper' }
322
+ ],
323
+
324
+ multiOutput: [
325
+ { ...testConfigs.valid, wires: [['h1', 'h2']] },
326
+ { id: 'h1', type: 'helper' },
327
+ { id: 'h2', type: 'helper' }
328
+ ]
329
+ };
330
+
331
+ /**
332
+ * Utility functions for test assertions
333
+ */
334
+ const testUtils = {
335
+ /**
336
+ * Wait for a specified amount of time
337
+ */
338
+ wait: (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)),
339
+
340
+ /**
341
+ * Create a promise that resolves when a node receives a message
342
+ */
343
+ waitForMessage: (node, timeout = 1000) => {
344
+ return new Promise((resolve, reject) => {
345
+ const timer = setTimeout(() => {
346
+ reject(new Error('Timeout waiting for message'));
347
+ }, timeout);
348
+
349
+ node.on('input', (msg) => {
350
+ clearTimeout(timer);
351
+ resolve(msg);
352
+ });
353
+ });
354
+ },
355
+
356
+ /**
357
+ * Verify that a message has expected properties
358
+ */
359
+ verifyMessage: (msg, expectedProps = {}) => {
360
+ const should = require('should');
361
+ should.exist(msg);
362
+
363
+ Object.keys(expectedProps).forEach(prop => {
364
+ if (expectedProps[prop] !== undefined) {
365
+ msg.should.have.property(prop, expectedProps[prop]);
366
+ }
367
+ });
368
+ }
369
+ };
370
+
371
+ module.exports = {
372
+ createMockImap,
373
+ createMockMailparser,
374
+ createMockNodeRED,
375
+ setupModuleMocks,
376
+ testConfigs,
377
+ testFlows,
378
+ testUtils
379
+ };
@@ -0,0 +1,492 @@
1
+ const should = require('should');
2
+ const helper = require('node-red-node-test-helper');
3
+
4
+ describe('Email Receiver Node - Integration Tests', function() {
5
+ // Set a reasonable timeout for integration tests
6
+ this.timeout(10000);
7
+
8
+ let emailReceiverNode;
9
+ let originalLoad;
10
+
11
+ before(function(done) {
12
+ // Set up mocks for dependencies before loading the node
13
+ setupMocks();
14
+
15
+ // Load the node with mocked dependencies
16
+ emailReceiverNode = require('../../email-receiver/email-receiver.js');
17
+
18
+ // CRITICAL: Initialize the helper with Node-RED
19
+ helper.init(require.resolve('node-red'));
20
+ done();
21
+ });
22
+
23
+ after(function() {
24
+ // Restore original module loading
25
+ if (originalLoad) {
26
+ const Module = require('module');
27
+ Module._load = originalLoad;
28
+ }
29
+ });
30
+
31
+ beforeEach(function(done) {
32
+ helper.startServer(done);
33
+ });
34
+
35
+ afterEach(function(done) {
36
+ helper.unload();
37
+ helper.stopServer(done);
38
+ });
39
+
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
+ describe('Node Loading', function() {
108
+ 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
+ ];
131
+
132
+ // ACT: Load the node in the test helper environment
133
+ helper.load(emailReceiverNode, flow, function() {
134
+ try {
135
+ // ASSERT: Verify the node loaded correctly
136
+ const n1 = helper.getNode("n1");
137
+ should.exist(n1);
138
+ n1.should.have.property('name', 'test node');
139
+ n1.should.have.property('type', 'email-receiver');
140
+ done();
141
+ } catch (err) {
142
+ done(err);
143
+ }
144
+ });
145
+ });
146
+
147
+ 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
+ ];
165
+
166
+ // ACT: Load the node
167
+ helper.load(emailReceiverNode, flow, function() {
168
+ try {
169
+ // ASSERT: Verify the node loaded with minimal config
170
+ const n1 = helper.getNode("n1");
171
+ should.exist(n1);
172
+ n1.should.have.property('type', 'email-receiver');
173
+ done();
174
+ } catch (err) {
175
+ done(err);
176
+ }
177
+ });
178
+ });
179
+ });
180
+
181
+ describe('Node Connections', function() {
182
+ 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
+ ];
207
+
208
+ // ACT: Load nodes and verify connections
209
+ helper.load(emailReceiverNode, flow, function() {
210
+ try {
211
+ const n1 = helper.getNode("n1");
212
+ const n2 = helper.getNode("n2");
213
+
214
+ // ASSERT: Both nodes should exist and be connected
215
+ should.exist(n1);
216
+ should.exist(n2);
217
+ n1.should.have.property('name', 'test node');
218
+ n2.should.have.property('type', 'helper');
219
+
220
+ done();
221
+ } catch (err) {
222
+ done(err);
223
+ }
224
+ });
225
+ });
226
+
227
+ 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
+ ];
251
+
252
+ // ACT: Load nodes
253
+ helper.load(emailReceiverNode, flow, function() {
254
+ try {
255
+ const n1 = helper.getNode("n1");
256
+ const n2 = helper.getNode("n2");
257
+ const n3 = helper.getNode("n3");
258
+
259
+ // ASSERT: All nodes should exist
260
+ should.exist(n1);
261
+ should.exist(n2);
262
+ should.exist(n3);
263
+ n1.should.have.property('name', 'multi-output node');
264
+
265
+ done();
266
+ } catch (err) {
267
+ done(err);
268
+ }
269
+ });
270
+ });
271
+ });
272
+
273
+ 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
+ }
315
+ });
316
+ });
317
+
318
+ 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
+ ];
341
+
342
+ // ACT: Load nodes and set up message listener
343
+ helper.load(emailReceiverNode, flow, function() {
344
+ try {
345
+ const n1 = helper.getNode("n1");
346
+ const n2 = helper.getNode("n2");
347
+
348
+ // Set up listener for messages from email receiver
349
+ n2.on("input", function(msg) {
350
+ try {
351
+ // ASSERT: Should receive a message with expected properties
352
+ should.exist(msg);
353
+ should.exist(msg.payload);
354
+ msg.payload.should.equal('test integration body');
355
+ done();
356
+ } catch (err) {
357
+ done(err);
358
+ }
359
+ });
360
+
361
+ // Trigger the email receiver
362
+ n1.receive({ payload: "trigger" });
363
+
364
+ } catch (err) {
365
+ done(err);
366
+ }
367
+ });
368
+ });
369
+ });
370
+
371
+ describe('Configuration Validation', function() {
372
+ 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
+ ];
391
+
392
+ // ACT: Load node with invalid config
393
+ helper.load(emailReceiverNode, flow, function() {
394
+ try {
395
+ const n1 = helper.getNode("n1");
396
+ should.exist(n1);
397
+
398
+ // ASSERT: Node should exist but handle invalid config appropriately
399
+ // Send input to trigger validation
400
+ n1.receive({ payload: "test" });
401
+
402
+ // If we get here without crashing, the validation worked
403
+ setTimeout(() => {
404
+ done();
405
+ }, 300);
406
+
407
+ } catch (err) {
408
+ done(err);
409
+ }
410
+ });
411
+ });
412
+
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
+ }
433
+ ];
434
+
435
+ // ACT: Load node with array folder config
436
+ helper.load(emailReceiverNode, flow, function() {
437
+ try {
438
+ const n1 = helper.getNode("n1");
439
+
440
+ // ASSERT: Node should load successfully with array config
441
+ should.exist(n1);
442
+ n1.should.have.property('name', 'array folder node');
443
+ done();
444
+
445
+ } catch (err) {
446
+ done(err);
447
+ }
448
+ });
449
+ });
450
+ });
451
+
452
+ 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
+ ];
472
+
473
+ // ACT: Load and then unload the node
474
+ helper.load(emailReceiverNode, flow, function() {
475
+ try {
476
+ const n1 = helper.getNode("n1");
477
+ should.exist(n1);
478
+
479
+ // Simulate some activity
480
+ n1.receive({ payload: "test" });
481
+
482
+ // ASSERT: Unloading should not throw errors
483
+ helper.unload();
484
+ done();
485
+
486
+ } catch (err) {
487
+ done(err);
488
+ }
489
+ });
490
+ });
491
+ });
492
+ });
@@ -1,6 +1,6 @@
1
1
  const should = require('should');
2
2
 
3
- describe('Email Receiver Node', function() {
3
+ describe('Email Receiver Node - Unit Tests', function() {
4
4
  // Set a reasonable timeout
5
5
  this.timeout(10000);
6
6
 
@@ -72,7 +72,7 @@ describe('Email Receiver Node', function() {
72
72
  };
73
73
 
74
74
  // Load the node with mocked dependencies
75
- emailReceiverNode = require('../email-receiver/email-receiver.js');
75
+ emailReceiverNode = require('../../email-receiver/email-receiver.js');
76
76
  });
77
77
 
78
78
  after(function() {
@@ -83,7 +83,7 @@ describe('Email Receiver Node', function() {
83
83
  }
84
84
  });
85
85
 
86
- describe('Unit Tests', function() {
86
+ describe('Module Export', function() {
87
87
  it('should export a function', function() {
88
88
  // ARRANGE: Node module is already loaded
89
89
 
@@ -92,7 +92,9 @@ describe('Email Receiver Node', function() {
92
92
  // ASSERT: Should be a function
93
93
  emailReceiverNode.should.be.type('function');
94
94
  });
95
+ });
95
96
 
97
+ describe('Node Registration', function() {
96
98
  it('should register node type without errors', function() {
97
99
  // ARRANGE: Set up mock RED object and capture registration calls
98
100
  let registeredType;
@@ -132,8 +134,10 @@ describe('Email Receiver Node', function() {
132
134
  registeredType.should.equal('email-receiver');
133
135
  registeredConstructor.should.be.type('function');
134
136
  });
137
+ });
135
138
 
136
- it('should handle node instantiation', function() {
139
+ describe('Node Instantiation', function() {
140
+ it('should handle node instantiation with valid config', function() {
137
141
  // ARRANGE: Set up mock RED object and node instance tracking
138
142
  let nodeInstance;
139
143
 
@@ -190,46 +194,11 @@ describe('Email Receiver Node', function() {
190
194
  should.exist(nodeInstance);
191
195
  nodeInstance.should.have.property('name', 'Test Email Receiver');
192
196
  });
197
+ });
193
198
 
194
- it('should handle comma-separated folder string', function(done) {
195
- // ARRANGE: Mock the Node-RED and IMAP environment
196
- let nodeInstance;
197
- let inputCallback;
198
- const mockRED = {
199
- nodes: {
200
- createNode: function(node, config) {
201
- nodeInstance = node;
202
- node.on = (event, callback) => { if (event === 'input') inputCallback = callback; };
203
- node.status = () => {};
204
- node.error = () => {};
205
- node.send = (msg) => {
206
- should.exist(msg);
207
- msg.payload.should.equal('test body');
208
- done();
209
- };
210
- return node;
211
- },
212
- registerType: (type, constructor) => {
213
- new constructor({
214
- host: "imap.test.com", hostType: "str",
215
- port: 993, portType: "num",
216
- user: "test@test.com", userType: "str",
217
- password: "testpass", passwordType: "str",
218
- folder: "INBOX, Spam, Sent", folderType: 'str',
219
- markseen: true, markseenType: 'bool'
220
- });
221
- }
222
- },
223
- util: { evaluateNodeProperty: (value) => value },
224
- };
225
-
226
- // ACT: Register the node, then simulate input
227
- emailReceiverNode(mockRED);
228
- inputCallback({});
229
- });
230
-
199
+ describe('Folder Configuration', function() {
231
200
  it('should handle an array of folders', function(done) {
232
- // ARRANGE: Mock the Node-RED and IMAP environment
201
+ // ARRANGE: Mock the Node-RED environment
233
202
  let nodeInstance;
234
203
  let inputCallback;
235
204
  const mockRED = {
@@ -264,7 +233,9 @@ describe('Email Receiver Node', function() {
264
233
  emailReceiverNode(mockRED);
265
234
  inputCallback({});
266
235
  });
236
+ });
267
237
 
238
+ describe('Error Handling', function() {
268
239
  it('should call node.error for invalid folder type', function(done) {
269
240
  // ARRANGE: Mock the node instance to capture errors
270
241
  let errorCalled = false;
@@ -301,7 +272,7 @@ describe('Email Receiver Node', function() {
301
272
  host: "imap.test.com", hostType: "str",
302
273
  port: 993, portType: "num",
303
274
  user: "test@test.com", userType: "str",
304
- password: "", passwordType: "str",
275
+ password: "", passwordType: "str", // Empty password should trigger error
305
276
  folder: "INBOX", folderType: "str"
306
277
  },
307
278
  on: (event, callback) => { if (event === 'input') nodeInstance.inputCallback = callback; },
@@ -326,152 +297,4 @@ describe('Email Receiver Node', function() {
326
297
  nodeInstance.inputCallback({});
327
298
  });
328
299
  });
329
-
330
- describe('Integration Tests with Node-RED Helper', function() {
331
- const helper = require('node-red-node-test-helper');
332
-
333
- // CRITICAL: Initialize the helper with Node-RED
334
- before(function(done) {
335
- // This is the missing piece that was causing the clearRegistry error
336
- helper.init(require.resolve('node-red'));
337
- done();
338
- });
339
-
340
- beforeEach(function(done) {
341
- helper.startServer(done);
342
- });
343
-
344
- afterEach(function(done) {
345
- helper.unload();
346
- helper.stopServer(done);
347
- });
348
-
349
- it('should load in Node-RED test environment', function(done) {
350
- // ARRANGE: Set up Node-RED flow with proper configuration
351
- const flow = [
352
- {
353
- id: "n1",
354
- type: "email-receiver",
355
- name: "test node",
356
- host: "imap.test.com",
357
- hostType: "str",
358
- port: "993",
359
- portType: "str",
360
- tls: true,
361
- tlsType: "bool",
362
- user: "test@example.com",
363
- userType: "str",
364
- password: "testpass",
365
- passwordType: "str",
366
- folder: "INBOX",
367
- folderType: "str",
368
- markseen: true,
369
- markseenType: "bool"
370
- }
371
- ];
372
-
373
- // ACT: Load the node in the test helper environment
374
- helper.load(emailReceiverNode, flow, function() {
375
- try {
376
- // ASSERT: Verify the node loaded correctly
377
- const n1 = helper.getNode("n1");
378
- should.exist(n1);
379
- n1.should.have.property('name', 'test node');
380
- n1.should.have.property('type', 'email-receiver');
381
- done();
382
- } catch (err) {
383
- done(err);
384
- }
385
- });
386
- });
387
-
388
- it('should create wired connections correctly', function(done) {
389
- // ARRANGE: Set up flow with helper node to catch output
390
- const flow = [
391
- {
392
- id: "n1",
393
- type: "email-receiver",
394
- name: "test node",
395
- host: "imap.test.com",
396
- hostType: "str",
397
- port: "993",
398
- portType: "str",
399
- tls: true,
400
- tlsType: "bool",
401
- user: "test@example.com",
402
- userType: "str",
403
- password: "testpass",
404
- passwordType: "str",
405
- folder: "INBOX",
406
- folderType: "str",
407
- markseen: true,
408
- markseenType: "bool",
409
- wires: [["n2"]]
410
- },
411
- { id: "n2", type: "helper" }
412
- ];
413
-
414
- // ACT: Load nodes and verify connections
415
- helper.load(emailReceiverNode, flow, function() {
416
- try {
417
- const n1 = helper.getNode("n1");
418
- const n2 = helper.getNode("n2");
419
-
420
- // ASSERT: Both nodes should exist and be connected
421
- should.exist(n1);
422
- should.exist(n2);
423
- n1.should.have.property('name', 'test node');
424
- n2.should.have.property('type', 'helper');
425
-
426
- done();
427
- } catch (err) {
428
- done(err);
429
- }
430
- });
431
- });
432
-
433
- it('should handle input without crashing', function(done) {
434
- // ARRANGE: Set up minimal flow
435
- const flow = [
436
- {
437
- id: "n1",
438
- type: "email-receiver",
439
- name: "test node",
440
- host: "imap.test.com",
441
- hostType: "str",
442
- port: "993",
443
- portType: "str",
444
- tls: true,
445
- tlsType: "bool",
446
- user: "test@example.com",
447
- userType: "str",
448
- password: "testpass",
449
- passwordType: "str",
450
- folder: "INBOX",
451
- folderType: "str",
452
- markseen: true,
453
- markseenType: "bool"
454
- }
455
- ];
456
-
457
- // ACT: Load node and send input
458
- helper.load(emailReceiverNode, flow, function() {
459
- try {
460
- const n1 = helper.getNode("n1");
461
- should.exist(n1);
462
-
463
- // Send input - this should not crash due to mocked IMAP
464
- n1.receive({ payload: "test input" });
465
-
466
- // ASSERT: If we reach here, the node handled input gracefully
467
- setTimeout(() => {
468
- done(); // Success if no errors thrown
469
- }, 500);
470
-
471
- } catch (err) {
472
- done(err);
473
- }
474
- });
475
- });
476
- });
477
- });
300
+ });