@5minds/node-red-contrib-processcube-tools 1.2.0-develop-d19f89-mg68thdf → 1.2.0-develop-59ef22-mg9d9ja5

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.
Files changed (55) hide show
  1. package/.mocharc.json +5 -0
  2. package/package.json +26 -10
  3. package/src/custom-node-template/custom-node-template.html.template +45 -0
  4. package/src/custom-node-template/custom-node-template.ts.template +69 -0
  5. package/src/email-receiver/email-receiver.ts +439 -0
  6. package/src/email-sender/email-sender.ts +210 -0
  7. package/{processcube-html-to-text/processcube-html-to-text.html → src/html-to-text/html-to-text.html} +3 -3
  8. package/src/html-to-text/html-to-text.ts +53 -0
  9. package/src/index.ts +12 -0
  10. package/src/interfaces/EmailReceiverMessage.ts +22 -0
  11. package/src/interfaces/EmailSenderNodeProperties.ts +37 -0
  12. package/src/interfaces/FetchState.ts +9 -0
  13. package/src/interfaces/ImapConnectionConfig.ts +14 -0
  14. package/src/test/framework/advanced-test-patterns.ts +224 -0
  15. package/src/test/framework/generic-node-test-suite.ts +58 -0
  16. package/src/test/framework/index.ts +17 -0
  17. package/src/test/framework/integration-assertions.ts +67 -0
  18. package/src/test/framework/integration-scenario-builder.ts +77 -0
  19. package/src/test/framework/integration-test-runner.ts +101 -0
  20. package/src/test/framework/node-assertions.ts +63 -0
  21. package/src/test/framework/node-test-runner.ts +260 -0
  22. package/src/test/framework/test-scenario-builder.ts +74 -0
  23. package/src/test/framework/types.ts +61 -0
  24. package/src/test/helpers/email-receiver-test-configs.ts +67 -0
  25. package/src/test/helpers/email-receiver-test-flows.ts +16 -0
  26. package/src/test/helpers/email-sender-test-configs.ts +123 -0
  27. package/src/test/helpers/email-sender-test-flows.ts +16 -0
  28. package/src/test/integration/email-receiver.integration.test.ts +41 -0
  29. package/src/test/integration/email-sender.integration.test.ts +129 -0
  30. package/src/test/interfaces/email-data.ts +10 -0
  31. package/src/test/interfaces/email-receiver-config.ts +12 -0
  32. package/src/test/interfaces/email-sender-config.ts +26 -0
  33. package/src/test/interfaces/imap-config.ts +9 -0
  34. package/src/test/interfaces/imap-mailbox.ts +5 -0
  35. package/src/test/interfaces/mail-options.ts +20 -0
  36. package/src/test/interfaces/parsed-email.ts +11 -0
  37. package/src/test/interfaces/send-mail-result.ts +7 -0
  38. package/src/test/mocks/imap-mock.ts +147 -0
  39. package/src/test/mocks/mailparser-mock.ts +82 -0
  40. package/src/test/mocks/nodemailer-mock.ts +118 -0
  41. package/src/test/unit/email-receiver.unit.test.ts +471 -0
  42. package/src/test/unit/email-sender.unit.test.ts +550 -0
  43. package/tsconfig.json +23 -0
  44. package/email-receiver/email-receiver.js +0 -304
  45. package/email-sender/email-sender.js +0 -178
  46. package/examples/.gitkeep +0 -0
  47. package/processcube-html-to-text/processcube-html-to-text.js +0 -22
  48. package/test/helpers/email-receiver.mocks.js +0 -447
  49. package/test/helpers/email-sender.mocks.js +0 -368
  50. package/test/integration/email-receiver.integration.test.js +0 -515
  51. package/test/integration/email-sender.integration.test.js +0 -239
  52. package/test/unit/email-receiver.unit.test.js +0 -304
  53. package/test/unit/email-sender.unit.test.js +0 -570
  54. /package/{email-receiver → src/email-receiver}/email-receiver.html +0 -0
  55. /package/{email-sender → src/email-sender}/email-sender.html +0 -0
@@ -0,0 +1,471 @@
1
+ import { expect } from 'chai';
2
+ import emailReceiverNode from '../../email-receiver/email-receiver';
3
+ import { EmailReceiverTestConfigs } from '../helpers/email-receiver-test-configs';
4
+
5
+ import { MockImap } from '../mocks/imap-mock';
6
+ import { createMockMailparser } from '../mocks/mailparser-mock';
7
+
8
+ import {
9
+ TestScenarioBuilder,
10
+ NodeTestRunner,
11
+ NodeAssertions,
12
+ createNodeTestSuite,
13
+ type TestScenario,
14
+ type MockNodeREDOptions,
15
+ SecurityTestBuilder,
16
+ EdgeCaseTestBuilder,
17
+ ErrorResilienceTestBuilder,
18
+ } from '../framework';
19
+
20
+ describe('E-Mail Receiver Node - Unit Tests', function () {
21
+ // ========================================================================
22
+ // USE GENERIC TEST SUITE FOR BASIC FUNCTIONALITY
23
+ // ========================================================================
24
+
25
+ createNodeTestSuite('Email Receiver', emailReceiverNode, EmailReceiverTestConfigs);
26
+
27
+ // ========================================================================
28
+ // SPECIFIC EMAIL RECEIVER TESTS
29
+ // ========================================================================
30
+
31
+ describe('Email Receiver Specific Tests', function () {
32
+ describe('Configuration Validation', function () {
33
+ const configTests = new TestScenarioBuilder()
34
+ .addValidScenario('valid configuration', EmailReceiverTestConfigs.valid)
35
+ .addValidScenario('minimal configuration', EmailReceiverTestConfigs.minimal)
36
+ .addValidScenario('array folders configuration', EmailReceiverTestConfigs.arrayFolders)
37
+ .addErrorScenario(
38
+ 'invalid folder type',
39
+ EmailReceiverTestConfigs.invalidFolderType,
40
+ "The 'folders' property must be an array of strings.",
41
+ )
42
+ .addErrorScenario(
43
+ 'missing required config',
44
+ EmailReceiverTestConfigs.invalidConfig,
45
+ 'Missing required IMAP config',
46
+ );
47
+
48
+ configTests.getScenarios().forEach((scenario) => {
49
+ it(`should handle ${scenario.name}`, async function () {
50
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
51
+
52
+ // Verify node was created
53
+ expect(context.nodeInstance).to.exist;
54
+
55
+ // Check specific expectations
56
+ if (scenario.expectedError) {
57
+ NodeAssertions.expectError(context, scenario.expectedError);
58
+ } else {
59
+ NodeAssertions.expectNoErrors(context);
60
+ }
61
+
62
+ // Verify node properties
63
+ if (scenario.config.name) {
64
+ NodeAssertions.expectNodeProperty(context, 'name', scenario.config.name);
65
+ }
66
+
67
+ if (scenario.config.id) {
68
+ NodeAssertions.expectNodeProperty(context, 'id', scenario.config.id);
69
+ }
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('IMAP Connection Handling', function () {
75
+ it('should establish connection successfully till the end', async function () {
76
+ this.timeout(15000);
77
+ const mockDependencies = {
78
+ ImapClient: MockImap,
79
+ mailParser: createMockMailparser(),
80
+ };
81
+
82
+ const mockOptions: MockNodeREDOptions = {
83
+ dependencies: mockDependencies,
84
+ statusHandler: function (status: any) {
85
+ console.log('📊 Status received:', JSON.stringify(status, null, 2));
86
+ },
87
+ errorHandler: function (err: any) {
88
+ console.log('❌ Error received:', err);
89
+ },
90
+ };
91
+
92
+ const scenario: TestScenario = {
93
+ name: 'successful connection',
94
+ config: EmailReceiverTestConfigs.valid,
95
+ input: { payload: 'test' },
96
+ expectedStatus: { fill: 'green', shape: 'dot', text: 'IMAP connection ended.' },
97
+ timeout: 10000,
98
+ };
99
+
100
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario, mockOptions);
101
+
102
+ // Should have received a green status
103
+ const finalStatus = context.statuses.pop(); // Get the last status update
104
+ expect(finalStatus.fill).to.equal('green', 'IMAP connection ended.');
105
+ expect(finalStatus.text).to.include(
106
+ 'IMAP connection ended.',
107
+ 'Final status text should indicate completion',
108
+ );
109
+ });
110
+
111
+ it('should establish connection successfully to show mails received', async function () {
112
+ this.timeout(15000);
113
+ const mockDependencies = {
114
+ ImapClient: MockImap,
115
+ mailParser: createMockMailparser(),
116
+ };
117
+
118
+ const mockOptions: MockNodeREDOptions = {
119
+ dependencies: mockDependencies,
120
+ statusHandler: function (status: any) {
121
+ console.log('📊 Status received:', JSON.stringify(status, null, 2));
122
+ },
123
+ errorHandler: function (err: any) {
124
+ console.log('❌ Error received:', err);
125
+ },
126
+ };
127
+
128
+ const scenario: TestScenario = {
129
+ name: 'successful connection',
130
+ config: EmailReceiverTestConfigs.valid,
131
+ input: { payload: 'test' },
132
+ expectedStatus: { fill: 'green', shape: 'dot', text: 'Done, fetched 5 mails from INBOX.' },
133
+ timeout: 10000,
134
+ };
135
+
136
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario, mockOptions);
137
+ const doneStatus = context.statuses.find((s) => s.text?.includes('Done, fetched'));
138
+
139
+ // Should have received a green status
140
+ expect(doneStatus).to.exist;
141
+ expect(doneStatus.fill).to.equal('green', 'Done, fetched 5 mails from INBOX.');
142
+ expect(doneStatus.text).to.include(
143
+ 'Done, fetched',
144
+ 'Final status text should indicate completion and fetched mails',
145
+ );
146
+ });
147
+
148
+ it('should handle connection failures gracefully', async function () {
149
+ const mockDependencies = {
150
+ ImapClient: MockImap,
151
+ mailParser: createMockMailparser(),
152
+ };
153
+
154
+ const mockOptions: MockNodeREDOptions = {
155
+ dependencies: mockDependencies,
156
+ statusHandler: function (status: any) {
157
+ console.log('📊 Status received:', JSON.stringify(status, null, 2));
158
+ },
159
+ errorHandler: function (err: any) {
160
+ console.log('❌ Error received:', err);
161
+ },
162
+ };
163
+
164
+ const scenario: TestScenario = {
165
+ name: 'connection failure',
166
+ config: EmailReceiverTestConfigs.invalidConfig,
167
+ input: { payload: 'test' },
168
+ expectedStatus: { fill: 'red', shape: 'ring', text: 'config error' },
169
+ expectedError: 'Missing required IMAP config: host, password. Aborting.',
170
+ timeout: 5000,
171
+ };
172
+
173
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario, mockOptions);
174
+
175
+ // Should have either error or red status (or both)
176
+ const hasError = context.errors.length > 0;
177
+ const hasRedStatus = context.statuses.some((s) => s.fill === 'red');
178
+
179
+ expect(hasError || hasRedStatus, 'Should have error or red status for connection failure').to.be.true;
180
+
181
+ if (hasRedStatus) {
182
+ const redStatus = context.statuses.find((s) => s.fill === 'red');
183
+ expect(redStatus!.text).to.include('error');
184
+ }
185
+ });
186
+ });
187
+
188
+ describe('Email Processing', function () {
189
+ const processingTests = new TestScenarioBuilder()
190
+ .addCustomScenario({
191
+ name: 'single email fetch',
192
+ config: EmailReceiverTestConfigs.valid,
193
+ input: { payload: 'fetch' },
194
+ timeout: 5000,
195
+ })
196
+ .addCustomScenario({
197
+ name: 'multiple folders processing',
198
+ config: EmailReceiverTestConfigs.arrayFolders,
199
+ input: { payload: 'fetch' },
200
+ timeout: 5000,
201
+ });
202
+
203
+ processingTests.getScenarios().forEach((scenario) => {
204
+ it(`should handle ${scenario.name}`, async function () {
205
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
206
+
207
+ // Node should be created without errors
208
+ expect(context.nodeInstance).to.exist;
209
+ NodeAssertions.expectNoErrors(context);
210
+
211
+ // For now, just verify the node processes input without crashing
212
+ // You can add more specific email processing assertions here
213
+ });
214
+ });
215
+ });
216
+
217
+ describe('Error Recovery', function () {
218
+ it('should recover from temporary connection issues', async function () {
219
+ // This test would verify that the node can recover from network issues
220
+ // Implementation depends on your specific error recovery logic
221
+ const scenario: TestScenario = {
222
+ name: 'error recovery',
223
+ config: EmailReceiverTestConfigs.valid,
224
+ input: { payload: 'test' },
225
+ timeout: 3000,
226
+ };
227
+
228
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
229
+ expect(context.nodeInstance).to.exist;
230
+ });
231
+ });
232
+ });
233
+
234
+ // ========================================================================
235
+ // EMAIL-SPECIFIC ERROR RESILIENCE TESTS
236
+ // ========================================================================
237
+
238
+ describe('Email Error Resilience', function () {
239
+ const resilience = new ErrorResilienceTestBuilder()
240
+ .addNetworkErrorScenario('IMAP connection', EmailReceiverTestConfigs.valid)
241
+ .addMalformedInputScenario('email message processing', EmailReceiverTestConfigs.valid)
242
+ .addRapidFireScenario('email burst handling', EmailReceiverTestConfigs.valid, 50);
243
+
244
+ resilience.getScenarios().forEach((scenario) => {
245
+ it(`should handle ${scenario.name}`, async function () {
246
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
247
+
248
+ // Node should exist and handle errors gracefully
249
+ expect(context.nodeInstance).to.exist;
250
+
251
+ // Should either process successfully or handle errors appropriately
252
+ const hasGracefulHandling =
253
+ context.errors.length === 0 ||
254
+ context.statuses.some((s) => s.fill === 'red') ||
255
+ context.errors.some((e) => typeof e === 'string');
256
+
257
+ expect(hasGracefulHandling, 'Should handle errors gracefully').to.be.true;
258
+ });
259
+ });
260
+ });
261
+
262
+ // ========================================================================
263
+ // EMAIL-SPECIFIC EDGE CASES
264
+ // ========================================================================
265
+
266
+ describe('Email Edge Cases', function () {
267
+ const edgeCases = new EdgeCaseTestBuilder()
268
+ .addEmptyDataScenarios('empty email data', EmailReceiverTestConfigs.valid)
269
+ .addSpecialCharacterScenarios('special characters in emails', EmailReceiverTestConfigs.valid)
270
+ .addLargeDataScenarios('large email attachments', EmailReceiverTestConfigs.valid);
271
+
272
+ // Add email-specific edge cases
273
+ const emailSpecificCases = new TestScenarioBuilder()
274
+ .addCustomScenario({
275
+ name: 'very long subject line',
276
+ config: EmailReceiverTestConfigs.valid,
277
+ input: {
278
+ payload: 'fetch',
279
+ subject: 'a'.repeat(1000), // Very long subject
280
+ },
281
+ })
282
+ .addCustomScenario({
283
+ name: 'multiple folder processing',
284
+ config: {
285
+ ...EmailReceiverTestConfigs.valid,
286
+ folders: Array.from({ length: 50 }, (_, i) => `FOLDER${i}`),
287
+ },
288
+ input: { payload: 'fetch' },
289
+ })
290
+ .addCustomScenario({
291
+ name: 'special email characters',
292
+ config: EmailReceiverTestConfigs.valid,
293
+ input: {
294
+ payload: 'fetch',
295
+ from: 'tëst@exämple.com',
296
+ subject: '📧 Émails with spéciál chars! 🌟',
297
+ },
298
+ });
299
+
300
+ [...edgeCases.getScenarios(), ...emailSpecificCases.getScenarios()].forEach((scenario) => {
301
+ it(`should handle ${scenario.name}`, async function () {
302
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
303
+ expect(context.nodeInstance).to.exist;
304
+ });
305
+ });
306
+ });
307
+
308
+ // ========================================================================
309
+ // SECURITY TESTS FOR EMAIL PROCESSING
310
+ // ========================================================================
311
+
312
+ describe('Email Security', function () {
313
+ const security = new SecurityTestBuilder()
314
+ .addInjectionTestScenarios('email content injection', EmailReceiverTestConfigs.valid)
315
+ .addOversizedPayloadScenarios('large email payload', EmailReceiverTestConfigs.valid);
316
+
317
+ // Email-specific security tests
318
+ const emailSecurity = new TestScenarioBuilder()
319
+ .addCustomScenario({
320
+ name: 'malicious email headers',
321
+ config: EmailReceiverTestConfigs.valid,
322
+ input: {
323
+ payload: 'fetch',
324
+ headers: {
325
+ 'X-Malicious': '<script>alert("xss")</script>',
326
+ 'X-Injection': "'; DROP TABLE emails; --",
327
+ },
328
+ },
329
+ })
330
+ .addCustomScenario({
331
+ name: 'suspicious attachment handling',
332
+ config: EmailReceiverTestConfigs.valid,
333
+ input: {
334
+ payload: 'fetch',
335
+ attachments: [
336
+ { filename: '../../../../../../etc/passwd' },
337
+ { filename: 'virus.exe.txt' },
338
+ { filename: '<script>evil.js</script>' },
339
+ ],
340
+ },
341
+ });
342
+
343
+ [...security.getScenarios(), ...emailSecurity.getScenarios()].forEach((scenario) => {
344
+ it(`should resist ${scenario.name}`, async function () {
345
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario);
346
+
347
+ // Node should exist and not crash
348
+ expect(context.nodeInstance).to.exist;
349
+
350
+ // Should handle security threats gracefully
351
+ const handledSecurely =
352
+ context.errors.length === 0 || context.errors.some((e) => typeof e === 'string');
353
+
354
+ expect(handledSecurely, 'Should handle security threats gracefully').to.be.true;
355
+ });
356
+ });
357
+ });
358
+
359
+ // ========================================================================
360
+ // DATA-DRIVEN TESTS FOR EMAIL SCENARIOS
361
+ // ========================================================================
362
+
363
+ describe('Email Receiver Data driven tests', function () {
364
+ const mockDependencies = {
365
+ ImapClient: MockImap,
366
+ mailParser: createMockMailparser(),
367
+ };
368
+ const mockOptions: MockNodeREDOptions = {
369
+ dependencies: mockDependencies,
370
+ statusHandler: function (status: any) {
371
+ console.log('📊 Status:', JSON.stringify(status, null, 2));
372
+ },
373
+ errorHandler: function (err: any) {
374
+ console.log('❌ Error:', err);
375
+ console.log('❌ Error stack:', new Error().stack); // See when error occurs
376
+ },
377
+ onHandler: function (event: string, callback: Function) {
378
+ console.log(`🎯 Event registered: ${event}`);
379
+ if (event === 'input') {
380
+ (this as any).inputCallback = callback;
381
+ }
382
+ },
383
+ };
384
+
385
+ const DataDrivenTests = [
386
+ {
387
+ name: 'fetch INBOX emails',
388
+ config: {
389
+ ...EmailReceiverTestConfigs.valid,
390
+ folder: 'INBOX',
391
+ },
392
+ },
393
+ {
394
+ name: 'fetch SENT emails',
395
+ config: {
396
+ ...EmailReceiverTestConfigs.valid,
397
+ folder: 'SENT',
398
+ },
399
+ },
400
+ {
401
+ name: 'invalid email receiver',
402
+ config: {
403
+ ...EmailReceiverTestConfigs.valid,
404
+ folder: 'INBOX',
405
+ host: '',
406
+ },
407
+ expectedStatus: { fill: 'red' },
408
+ expectedError: /invalid|unknown|missing/i,
409
+ },
410
+ {
411
+ name: 'empty folder name',
412
+ config: {
413
+ ...EmailReceiverTestConfigs.valid,
414
+ folder: '',
415
+ },
416
+ expectedStatus: { fill: 'red' },
417
+ expectedError: /folders|empty|invalid/i,
418
+ timeout: 2000,
419
+ },
420
+ {
421
+ name: 'numeric folder name',
422
+ config: {
423
+ ...EmailReceiverTestConfigs.valid,
424
+ folder: 123,
425
+ },
426
+ expectedStatus: { fill: 'red' },
427
+ expectedError: /folder|string|type/i,
428
+ timeout: 2000,
429
+ },
430
+ ];
431
+
432
+ DataDrivenTests.forEach((testCase) => {
433
+ it(`Email Processing Scenarios ${testCase.name}`, async function () {
434
+ const scenario: TestScenario = {
435
+ name: testCase.name,
436
+ config: testCase.config,
437
+ input: { payload: 'fetch' },
438
+ expectedError: testCase.expectedError,
439
+ expectedStatus: testCase.expectedStatus,
440
+ timeout: 3000,
441
+ };
442
+ console.log(`\n🧪 Starting test: ${testCase.name}`);
443
+ console.log('📝 Config:', JSON.stringify(scenario.config));
444
+ console.log('📝 Input:', JSON.stringify(scenario.input));
445
+
446
+ const context = await NodeTestRunner.runScenario(emailReceiverNode, scenario, mockOptions);
447
+
448
+ // 🔍 DEBUG
449
+ console.log('📦 Node constructed');
450
+ console.log('❌ Errors so far:', context.errors);
451
+ console.log('📊 Statuses so far:', context.statuses);
452
+ console.log('🐛 Has configError:', !!(context.nodeInstance as any).configError);
453
+
454
+ setTimeout(() => {
455
+ console.log('⏰ After 100ms:');
456
+ console.log('❌ Errors:', context.errors);
457
+ console.log('📊 Statuses:', context.statuses);
458
+ }, 100);
459
+ expect(context.nodeInstance).to.exist;
460
+
461
+ if (scenario.expectedStatus) {
462
+ NodeAssertions.expectStatus(context, scenario.expectedStatus);
463
+ }
464
+
465
+ if (scenario.expectedError) {
466
+ NodeAssertions.expectError(context, scenario.expectedError);
467
+ }
468
+ });
469
+ });
470
+ });
471
+ });