@5minds/node-red-contrib-processcube 1.16.0-feature-671c49-mfaxerba → 1.16.0-feature-320605-mfchyhti

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.
@@ -0,0 +1,166 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('email-receiver', {
3
+ category: 'ProcessCube Tools',
4
+ color: '#02AFD6',
5
+ defaults: {
6
+ name: { value: "" },
7
+ host: { value: "", required: true, validate: RED.validators.typedInput("hostType") },
8
+ hostType: { value: "str" },
9
+ port: { value: "", required: true, validate: RED.validators.typedInput("portType") },
10
+ portType: { value: "num" },
11
+ tls: { value: true, required: true, validate: RED.validators.typedInput("tlsType") },
12
+ tlsType: { value: "bool" },
13
+ user: { value: "", required: true, validate: RED.validators.typedInput("userType") },
14
+ userType: { value: "str" },
15
+ password: { value: "", required: true, type: "password" },
16
+ passwordType: { value: "cred", required: true },
17
+ folder: { value: "", required: true, validate: RED.validators.typedInput("folderType") },
18
+ folderType: { value: "json" },
19
+ markseen: { value: true, validate: RED.validators.typedInput("markseenType") },
20
+ markseenType: { value: "bool" }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ icon: "font-awesome/fa-inbox",
25
+ label: function() {
26
+ return this.name || "E-Mail Receiver";
27
+ },
28
+ oneditprepare: function() {
29
+ $('#node-input-host').typedInput({
30
+ default: 'str',
31
+ types: ['str', 'msg', 'flow', 'global'],
32
+ typeField: '#node-input-hostType'
33
+ });
34
+
35
+ $('#node-input-port').typedInput({
36
+ default: 'num',
37
+ types: ['num', 'msg', 'flow', 'global'],
38
+ typeField: '#node-input-portType'
39
+ });
40
+
41
+ $('#node-input-tls').typedInput({
42
+ default: 'bool',
43
+ types: ['bool', 'msg', 'flow', 'global'],
44
+ typeField: '#node-input-tlsType'
45
+ });
46
+
47
+ $('#node-input-user').typedInput({
48
+ default: 'str',
49
+ types: ['str', 'msg', 'flow', 'global'],
50
+ typeField: '#node-input-userType'
51
+ });
52
+
53
+ $('#node-input-password').typedInput({
54
+ default: 'cred',
55
+ types: ['cred', 'msg', 'flow', 'global'],
56
+ typeField: '#node-input-passwordType'
57
+ });
58
+
59
+ $('#node-input-folder').typedInput({
60
+ default: 'json',
61
+ types: ['msg', 'flow', 'global', 'json', 'jsonata'],
62
+ typeField: '#node-input-folderType'
63
+ });
64
+
65
+ $('#node-input-markseen').typedInput({
66
+ default: 'bool',
67
+ types: ['bool', 'msg', 'flow', 'global'],
68
+ typeField: '#node-input-markseenType'
69
+ });
70
+ }
71
+ });
72
+ </script>
73
+
74
+ <script type="text/html" data-template-name="email-receiver">
75
+ <div class="form-row">
76
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
77
+ <input type="text" id="node-input-name" placeholder="Name">
78
+ </div>
79
+
80
+ <div class="form-row">
81
+ <label for="node-input-host"><i class="fa fa-server"></i> IMAP Host</label>
82
+ <input type="text" id="node-input-host" placeholder="imap.gmail.com">
83
+ <input type="hidden" id="node-input-hostType">
84
+ </div>
85
+
86
+ <div class="form-row">
87
+ <label for="node-input-port"><i class="fa fa-terminal"></i> Port</label>
88
+ <input type="text" id="node-input-port" placeholder="993">
89
+ <input type="hidden" id="node-input-portType">
90
+ </div>
91
+
92
+ <div class="form-row">
93
+ <label for="node-input-tls"><i class="fa fa-lock"></i> Use TLS</label>
94
+ <input type="text" id="node-input-tls">
95
+ <input type="hidden" id="node-input-tlsType">
96
+ </div>
97
+
98
+ <div class="form-row">
99
+ <label for="node-input-user"><i class="fa fa-user"></i> User</label>
100
+ <input type="text" id="node-input-user">
101
+ <input type="hidden" id="node-input-userType">
102
+ </div>
103
+
104
+ <div class="form-row">
105
+ <label for="node-input-password"><i class="fa fa-key"></i> Password</label>
106
+ <input type="text" id="node-input-password">
107
+ <input type="hidden" id="node-input-passwordType">
108
+ </div>
109
+
110
+ <div class="form-row">
111
+ <label for="node-input-folder"><i class="fa fa-folder-open"></i> Folder(s)</label>
112
+ <input type="text" id="node-input-folder" placeholder="[INBOX]">
113
+ <input type="hidden" id="node-input-folderType">
114
+ </div>
115
+
116
+ <div class="form-row">
117
+ <label for="node-input-markseen"><i class="fa fa-eye"></i> Mark as seen</label>
118
+ <input type="text" id="node-input-markseen">
119
+ <input type="hidden" id="node-input-markseenType">
120
+ </div>
121
+ </script>
122
+
123
+ <script type="text/html" data-help-name="email-receiver">
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
+ <h3>Configuration</h3>
126
+ <p>All fields can be configured as a **string**, from a **message property (msg)**, **flow** or **global** context, or an **environment variable**.</p>
127
+ <h3>Inputs</h3>
128
+ <dl class="message-properties">
129
+ <dt>payload</dt>
130
+ <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>
131
+ </dl>
132
+ <h3>Outputs</h3>
133
+ <dl class="message-properties">
134
+ <dt>payload
135
+ <span class="property-type">string</span>
136
+ </dt>
137
+ <dd>The text body of the email.</dd>
138
+ </dl>
139
+
140
+ <hr/>
141
+
142
+ <h3>Optional Message Properties</h3>
143
+ <p>You can override default settings by passing the following properties in the incoming <code>msg</code> object:</p>
144
+ <dl class="message-properties">
145
+ <dt>msg.imap_connTimeout
146
+ <span class="property-type">number</span>
147
+ </dt>
148
+ <dd>The connection timeout in milliseconds (default: 10000).</dd>
149
+ <dt>msg.imap_authTimeout
150
+ <span class="property-type">number</span>
151
+ </dt>
152
+ <dd>The authentication timeout in milliseconds (default: 5000).</dd>
153
+ <dt>msg.imap_keepalive
154
+ <span class="property-type">boolean</span>
155
+ </dt>
156
+ <dd>If set to `true`, a periodic NOOP command is sent to keep the connection alive (default: `true`).</dd>
157
+ <dt>msg.imap_autotls
158
+ <span class="property-type">string</span>
159
+ </dt>
160
+ <dd>Controls STARTTLS behavior. Set to `never` to disable it (default: `never`).</dd>
161
+ <dt>msg.imap_tlsOptions
162
+ <span class="property-type">object</span>
163
+ </dt>
164
+ <dd>An object containing TLS options for the connection.</dd>
165
+ </dl>
166
+ </script>
@@ -0,0 +1,231 @@
1
+ module.exports = function(RED) {
2
+ const Imap = require('node-imap');
3
+ const mailparser = require('mailparser');
4
+
5
+ function EmailReceiverNode(config) {
6
+ RED.nodes.createNode(this, config);
7
+ const node = this;
8
+
9
+ node.on('input', function(msg) {
10
+ // Retrieve and validate configuration values
11
+ const imap_host = RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg);
12
+ const imap_port = RED.util.evaluateNodeProperty(config.port, config.portType, node, msg);
13
+ const imap_tls = RED.util.evaluateNodeProperty(config.tls, config.tlsType, node, msg);
14
+ const imap_user = RED.util.evaluateNodeProperty(config.user, config.userType, node, msg);
15
+ const imap_password = RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg);
16
+
17
+ // Check if the folder is actually an array
18
+ const imap_folder = RED.util.evaluateNodeProperty(config.folder, config.folderType, node, msg);
19
+ let folders;
20
+ if (Array.isArray(imap_folder)) {
21
+ folders = imap_folder;
22
+ } else if (typeof imap_folder === 'string') {
23
+ folders = imap_folder.split(',').map(f => f.trim()).filter(f => f.length > 0);
24
+ } else {
25
+ const errorMsg = "The 'folders' property must be an array of strings or a comma-separated string.";
26
+ node.status({ fill: 'red', shape: 'ring', text: errorMsg });
27
+ node.error(errorMsg, msg);
28
+ return;
29
+ }
30
+ const imap_markSeen = RED.util.evaluateNodeProperty(config.markseen, config.markseenType, node, msg);
31
+
32
+ const finalConfig = {
33
+ host: imap_host,
34
+ port: (typeof imap_port === 'string') ? parseInt(imap_port, 10) : imap_port,
35
+ tls: imap_tls,
36
+ user: imap_user,
37
+ password: imap_password,
38
+ folders: (Array.isArray(imap_folder)) ? imap_folder : imap_folder.split(',').map(f => f.trim()).filter(f => f.length > 0),
39
+ markSeen: imap_markSeen,
40
+ connTimeout: msg.imap_connTimeout || 10000,
41
+ authTimeout: msg.imap_authTimeout || 5000,
42
+ keepalive: msg.imap_keepalive !== undefined ? msg.imap_keepalive : true,
43
+ autotls: msg.imap_autotls || 'never',
44
+ tlsOptions: msg.imap_tlsOptions || { rejectUnauthorized: false }
45
+ };
46
+
47
+ if (!finalConfig.user || !finalConfig.password || !finalConfig.port || !finalConfig.host || !finalConfig.folders) {
48
+ const errorMessage = 'Missing required IMAP config (user, password, port, host, or folders missing). Aborting.';
49
+ node.status({ fill: 'red', shape: 'ring', text: 'missing config' });
50
+ node.error(errorMessage);
51
+ return;
52
+ }
53
+
54
+ const fetchEmails = ({
55
+ host,
56
+ port,
57
+ tls,
58
+ user,
59
+ password,
60
+ folders,
61
+ markSeen = true,
62
+ connTimeout = 10000,
63
+ authTimeout = 5000,
64
+ keepalive = true,
65
+ autotls = 'never',
66
+ tlsOptions = { rejectUnauthorized: false }
67
+ }, onMail) => {
68
+ const imap = new Imap({
69
+ user,
70
+ password,
71
+ host,
72
+ port,
73
+ tls,
74
+ connTimeout,
75
+ authTimeout,
76
+ keepalive,
77
+ autotls,
78
+ tlsOptions
79
+ });
80
+
81
+ const state = {
82
+ totalFolders: folders.length,
83
+ processedFolders: 0,
84
+ successes: 0,
85
+ failures: 0,
86
+ totalMails: 0,
87
+ errors: [],
88
+ };
89
+
90
+ // Helper to update Node-RED status
91
+ const updateStatus = (color, text) => {
92
+ node.status({ fill: color, shape: 'dot', text });
93
+ };
94
+
95
+ // Helper to finalize status and clean up
96
+ const finalizeSession = (error = null) => {
97
+ if (error) {
98
+ node.error('IMAP session terminated: ' + error.message);
99
+ node.status({ fill: 'red', shape: 'ring', text: 'connection error' });
100
+ } else if (state.failures > 0) {
101
+ node.status({
102
+ fill: 'red',
103
+ shape: 'dot',
104
+ text: `Done, ${state.totalMails} mails from ${state.successes}/${state.totalFolders} folders. ${state.failures} failed.`
105
+ });
106
+ } else {
107
+ node.status({
108
+ fill: 'green',
109
+ shape: 'dot',
110
+ text: `Done, fetched ${state.totalMails} mails from ${folders.join(', ')}.`
111
+ });
112
+ }
113
+ if (imap && imap.state !== 'disconnected') {
114
+ imap.end();
115
+ }
116
+ };
117
+
118
+ const fetchFromFolder = (folder) => {
119
+ updateStatus('yellow', `Fetching from "${folder}"...`);
120
+
121
+ imap.openBox(folder, false, (err, box) => {
122
+ if (err) {
123
+ node.error(`Could not open folder "${folder}": ${err.message}`);
124
+ state.failures++;
125
+ state.processedFolders++;
126
+ return startNextFolder();
127
+ }
128
+
129
+ imap.search(['UNSEEN'], (err, results) => {
130
+ if (err) {
131
+ node.error(`Search failed in folder "${folder}": ${err.message}`);
132
+ state.failures++;
133
+ state.processedFolders++;
134
+ return startNextFolder();
135
+ }
136
+
137
+ if (!results || !results.length) {
138
+ state.successes++;
139
+ state.processedFolders++;
140
+ return startNextFolder();
141
+ }
142
+
143
+ state.totalMails += results.length;
144
+
145
+ const fetch = imap.fetch(results, { bodies: '' });
146
+
147
+ fetch.on('message', msg => {
148
+ msg.on('body', stream => {
149
+ mailparser.simpleParser(stream, (err, parsed) => {
150
+ if (err) {
151
+ node.error(`Parse error for email from folder "${folder}": ${err.message}`);
152
+ return;
153
+ }
154
+
155
+ const outMsg = {
156
+ topic: parsed.subject,
157
+ payload: parsed.text,
158
+ html: parsed.html,
159
+ from: parsed.replyTo?.text || parsed.from?.text,
160
+ date: parsed.date,
161
+ folder,
162
+ header: parsed.headers,
163
+ attachments: parsed.attachments.map(att => ({
164
+ contentType: att.contentType,
165
+ fileName: att.filename,
166
+ transferEncoding: att.transferEncoding,
167
+ contentDisposition: att.contentDisposition,
168
+ generatedFileName: att.cid || att.checksum,
169
+ contentId: att.cid,
170
+ checksum: att.checksum,
171
+ length: att.size,
172
+ content: att.content
173
+ }))
174
+ };
175
+ onMail(outMsg);
176
+ });
177
+ });
178
+ });
179
+
180
+ fetch.once('error', err => {
181
+ node.error(`Fetch error in folder "${folder}": ${err.message}`);
182
+ });
183
+
184
+ fetch.once('end', () => {
185
+ state.successes++;
186
+ state.processedFolders++;
187
+ updateStatus('green', `Fetched ${results.length} from "${folder}".`);
188
+ startNextFolder();
189
+ });
190
+ });
191
+ });
192
+ };
193
+
194
+ const startNextFolder = () => {
195
+ if (state.processedFolders >= state.totalFolders) {
196
+ finalizeSession();
197
+ } else {
198
+ fetchFromFolder(folders[state.processedFolders]);
199
+ }
200
+ };
201
+
202
+ // Centralized event listeners for the IMAP connection
203
+ imap.once('ready', () => {
204
+ node.status({ fill: 'green', shape: 'dot', text: 'connected' });
205
+ startNextFolder();
206
+ });
207
+
208
+ imap.once('error', err => {
209
+ finalizeSession(err);
210
+ });
211
+
212
+ imap.once('end', () => {
213
+ node.log('IMAP connection ended.');
214
+ });
215
+
216
+ updateStatus('yellow', 'Connecting to IMAP...');
217
+ imap.connect();
218
+ };
219
+
220
+ fetchEmails(finalConfig, mail => {
221
+ node.send(mail);
222
+ });
223
+ });
224
+ }
225
+
226
+ RED.nodes.registerType("email-receiver", EmailReceiverNode, {
227
+ credentials: {
228
+ password: { type: "password" }
229
+ }
230
+ });
231
+ };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube",
3
- "version": "1.16.0-feature-671c49-mfaxerba",
3
+ "version": "1.16.0-feature-320605-mfchyhti",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED nodes for ProcessCube",
6
6
  "scripts": {
7
- "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\""
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
10
  },
9
11
  "authors": [
10
12
  {
@@ -18,6 +20,10 @@
18
20
  {
19
21
  "name": "André Siebelist",
20
22
  "email": "Andre.Siebelist@5Minds.de"
23
+ },
24
+ {
25
+ "name": "Diana Stefan",
26
+ "email": "Diana.Stefan@5Minds.de"
21
27
  }
22
28
  ],
23
29
  "repository": {
@@ -37,6 +43,7 @@
37
43
  "nodes": {
38
44
  "checkAuthorization": "check-authorization.js",
39
45
  "DataobjectInstanceQuery": "dataobject-instance-query.js",
46
+ "emailReceiver": "email-receiver.js",
40
47
  "EndEventFinishedListener": "endevent-finished-listener.js",
41
48
  "externaltaskInput": "externaltask-input.js",
42
49
  "externaltaskOutput": "externaltask-output.js",
@@ -61,11 +68,24 @@
61
68
  },
62
69
  "examples": "examples"
63
70
  },
71
+ "devDependencies": {
72
+ "chai": "^4.3.4",
73
+ "mocha": "^11.7.2",
74
+ "node-red": "^4.1.0",
75
+ "node-red-node-test-helper": "^0.3.5",
76
+ "should": "^13.2.3",
77
+ "sinon": "^11.1.2"
78
+ },
64
79
  "dependencies": {
65
80
  "@5minds/processcube_engine_client": "^6.1.4",
66
81
  "adm-zip": "^0.5.16",
82
+ "chai": "^4.3.4",
67
83
  "jwt-decode": "^4.0.0",
68
- "openid-client": "^5.5.0"
84
+ "mailparser": "^3.6.8",
85
+ "node-imap": "^0.9.6"
86
+ },
87
+ "overrides": {
88
+ "semver": ">=7.0.0"
69
89
  },
70
90
  "keywords": [
71
91
  "node-red",
@@ -0,0 +1,317 @@
1
+ const should = require('should');
2
+
3
+ describe('Email Receiver Node', function() {
4
+ // Set a reasonable timeout
5
+ this.timeout(10000);
6
+
7
+ // Module and mocking setup
8
+ let emailReceiverNode;
9
+ let originalLoad;
10
+
11
+ before(function() {
12
+ // Create mock modules
13
+ const mockModules = {
14
+ 'node-imap': function(config) {
15
+ this.config = config;
16
+ this.connect = function() {};
17
+ this.openBox = function() {};
18
+ this.search = function() {};
19
+ this.fetch = function() { return { on: function() {}, once: function() {} }; };
20
+ this.end = function() {};
21
+ this.once = function() {};
22
+ return this;
23
+ },
24
+ 'mailparser': {
25
+ simpleParser: function() {
26
+ return Promise.resolve({
27
+ subject: 'test',
28
+ text: 'test body',
29
+ html: '<p>test</p>',
30
+ from: { text: 'test@test.com' },
31
+ date: new Date(),
32
+ headers: new Map(),
33
+ attachments: []
34
+ });
35
+ }
36
+ }
37
+ };
38
+
39
+ // Override require
40
+ const Module = require('module');
41
+ originalLoad = Module._load;
42
+ Module._load = function(request, parent) {
43
+ if (mockModules[request]) {
44
+ return mockModules[request];
45
+ }
46
+ return originalLoad.apply(this, arguments);
47
+ };
48
+
49
+ // Load the node with mocked dependencies
50
+ emailReceiverNode = require('../email-receiver.js');
51
+ });
52
+
53
+ after(function() {
54
+ // Restore original module loading
55
+ if (originalLoad) {
56
+ const Module = require('module');
57
+ Module._load = originalLoad;
58
+ }
59
+ });
60
+
61
+ describe('Unit Tests', function() {
62
+ it('should export a function', function() {
63
+ // ARRANGE: Node module is already loaded
64
+
65
+ // ACT: Check the type of the exported module
66
+
67
+ // ASSERT: Should be a function
68
+ emailReceiverNode.should.be.type('function');
69
+ });
70
+
71
+ it('should register node type without errors', function() {
72
+ // ARRANGE: Set up mock RED object and capture registration calls
73
+ let registeredType;
74
+ let registeredConstructor;
75
+
76
+ const mockRED = {
77
+ nodes: {
78
+ createNode: function(node, config) {
79
+ node.id = config.id;
80
+ node.type = config.type;
81
+ node.name = config.name;
82
+ node.on = function() {};
83
+ node.status = function() {};
84
+ node.error = function() {};
85
+ node.send = function() {};
86
+ return node;
87
+ },
88
+ registerType: function(type, constructor) {
89
+ registeredType = type;
90
+ registeredConstructor = constructor;
91
+ }
92
+ },
93
+ util: {
94
+ evaluateNodeProperty: function(value, type) {
95
+ return value;
96
+ },
97
+ encrypt: function(value) {
98
+ return 'encrypted:' + value;
99
+ }
100
+ }
101
+ };
102
+
103
+ // ACT: Call the node registration function
104
+ emailReceiverNode(mockRED);
105
+
106
+ // ASSERT: Verify registration was called correctly
107
+ registeredType.should.equal('email-receiver');
108
+ registeredConstructor.should.be.type('function');
109
+ });
110
+
111
+ it('should handle node instantiation', function() {
112
+ // ARRANGE: Set up mock RED object and node instance tracking
113
+ let nodeInstance;
114
+
115
+ const mockRED = {
116
+ nodes: {
117
+ createNode: function(node, config) {
118
+ nodeInstance = node;
119
+ node.id = config.id;
120
+ node.type = config.type;
121
+ node.name = config.name;
122
+ node.on = function() {};
123
+ node.status = function() {};
124
+ node.error = function() {};
125
+ node.send = function() {};
126
+ return node;
127
+ },
128
+ registerType: function(type, NodeConstructor) {
129
+ // Simulate creating a node instance with valid config
130
+ const config = {
131
+ id: 'test-node',
132
+ type: 'email-receiver',
133
+ name: 'Test Email Receiver',
134
+ host: 'imap.test.com',
135
+ hostType: 'str',
136
+ port: 993,
137
+ portType: 'num',
138
+ user: 'test@test.com',
139
+ userType: 'str',
140
+ password: 'testpass',
141
+ passwordType: 'str',
142
+ folder: 'INBOX',
143
+ folderType: 'str',
144
+ markseen: true,
145
+ markseenType: 'bool'
146
+ };
147
+
148
+ new NodeConstructor(config);
149
+ }
150
+ },
151
+ util: {
152
+ evaluateNodeProperty: function(value, type) {
153
+ return value;
154
+ },
155
+ encrypt: function(value) {
156
+ return 'encrypted:' + value;
157
+ }
158
+ }
159
+ };
160
+
161
+ // ACT: Register the node and create an instance
162
+ emailReceiverNode(mockRED);
163
+
164
+ // ASSERT: Verify the node instance was created with correct properties
165
+ should.exist(nodeInstance);
166
+ nodeInstance.should.have.property('name', 'Test Email Receiver');
167
+ });
168
+ });
169
+
170
+ describe('Integration Tests with Node-RED Helper', function() {
171
+ const helper = require('node-red-node-test-helper');
172
+
173
+ // CRITICAL: Initialize the helper with Node-RED
174
+ before(function(done) {
175
+ // This is the missing piece that was causing the clearRegistry error
176
+ helper.init(require.resolve('node-red'));
177
+ done();
178
+ });
179
+
180
+ beforeEach(function(done) {
181
+ helper.startServer(done);
182
+ });
183
+
184
+ afterEach(function(done) {
185
+ helper.unload();
186
+ helper.stopServer(done);
187
+ });
188
+
189
+ it('should load in Node-RED test environment', function(done) {
190
+ // ARRANGE: Set up Node-RED flow with proper configuration
191
+ const flow = [
192
+ {
193
+ id: "n1",
194
+ type: "email-receiver",
195
+ name: "test node",
196
+ host: "imap.test.com",
197
+ hostType: "str",
198
+ port: "993",
199
+ portType: "str",
200
+ tls: true,
201
+ tlsType: "bool",
202
+ user: "test@example.com",
203
+ userType: "str",
204
+ password: "testpass",
205
+ passwordType: "str",
206
+ folder: "INBOX",
207
+ folderType: "str",
208
+ markseen: true,
209
+ markseenType: "bool"
210
+ }
211
+ ];
212
+
213
+ // ACT: Load the node in the test helper environment
214
+ helper.load(emailReceiverNode, flow, function() {
215
+ try {
216
+ // ASSERT: Verify the node loaded correctly
217
+ const n1 = helper.getNode("n1");
218
+ should.exist(n1);
219
+ n1.should.have.property('name', 'test node');
220
+ n1.should.have.property('type', 'email-receiver');
221
+ done();
222
+ } catch (err) {
223
+ done(err);
224
+ }
225
+ });
226
+ });
227
+
228
+ it('should create wired connections correctly', function(done) {
229
+ // ARRANGE: Set up flow with helper node to catch output
230
+ const flow = [
231
+ {
232
+ id: "n1",
233
+ type: "email-receiver",
234
+ name: "test node",
235
+ host: "imap.test.com",
236
+ hostType: "str",
237
+ port: "993",
238
+ portType: "str",
239
+ tls: true,
240
+ tlsType: "bool",
241
+ user: "test@example.com",
242
+ userType: "str",
243
+ password: "testpass",
244
+ passwordType: "str",
245
+ folder: "INBOX",
246
+ folderType: "str",
247
+ markseen: true,
248
+ markseenType: "bool",
249
+ wires: [["n2"]]
250
+ },
251
+ { id: "n2", type: "helper" }
252
+ ];
253
+
254
+ // ACT: Load nodes and verify connections
255
+ helper.load(emailReceiverNode, flow, function() {
256
+ try {
257
+ const n1 = helper.getNode("n1");
258
+ const n2 = helper.getNode("n2");
259
+
260
+ // ASSERT: Both nodes should exist and be connected
261
+ should.exist(n1);
262
+ should.exist(n2);
263
+ n1.should.have.property('name', 'test node');
264
+ n2.should.have.property('type', 'helper');
265
+
266
+ done();
267
+ } catch (err) {
268
+ done(err);
269
+ }
270
+ });
271
+ });
272
+
273
+ it('should handle input without crashing', function(done) {
274
+ // ARRANGE: Set up minimal flow
275
+ const flow = [
276
+ {
277
+ id: "n1",
278
+ type: "email-receiver",
279
+ name: "test node",
280
+ host: "imap.test.com",
281
+ hostType: "str",
282
+ port: "993",
283
+ portType: "str",
284
+ tls: true,
285
+ tlsType: "bool",
286
+ user: "test@example.com",
287
+ userType: "str",
288
+ password: "testpass",
289
+ passwordType: "str",
290
+ folder: "INBOX",
291
+ folderType: "str",
292
+ markseen: true,
293
+ markseenType: "bool"
294
+ }
295
+ ];
296
+
297
+ // ACT: Load node and send input
298
+ helper.load(emailReceiverNode, flow, function() {
299
+ try {
300
+ const n1 = helper.getNode("n1");
301
+ should.exist(n1);
302
+
303
+ // Send input - this should not crash due to mocked IMAP
304
+ n1.receive({ payload: "test input" });
305
+
306
+ // ASSERT: If we reach here, the node handled input gracefully
307
+ setTimeout(() => {
308
+ done(); // Success if no errors thrown
309
+ }, 500);
310
+
311
+ } catch (err) {
312
+ done(err);
313
+ }
314
+ });
315
+ });
316
+ });
317
+ });