node-red-contrib-influxdb3 1.0.7 → 1.0.8

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.
@@ -1,1294 +0,0 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const os = require('os');
4
-
5
- let mockLastClientOptions;
6
- let mockLastClientInstance;
7
- let mockLastPoint;
8
-
9
- jest.mock('@influxdata/influxdb3-client', () => {
10
- class MockInfluxDBClient {
11
- constructor(options) {
12
- mockLastClientOptions = options;
13
- mockLastClientInstance = this;
14
- this.write = jest.fn().mockResolvedValue(undefined);
15
- this.close = jest.fn();
16
- }
17
- }
18
-
19
- class MockPoint {
20
- constructor(measurement) {
21
- this.measurement = measurement;
22
- this.tags = {};
23
- this.integerFields = {};
24
- this.floatFields = {};
25
- this.stringFields = {};
26
- this.booleanFields = {};
27
- this.timestamp = null;
28
- mockLastPoint = this;
29
- }
30
-
31
- setTag(key, value) {
32
- this.tags[key] = value;
33
- }
34
-
35
- setIntegerField(key, value) {
36
- this.integerFields[key] = value;
37
- }
38
-
39
- setFloatField(key, value) {
40
- this.floatFields[key] = value;
41
- }
42
-
43
- setStringField(key, value) {
44
- this.stringFields[key] = value;
45
- }
46
-
47
- setBooleanField(key, value) {
48
- this.booleanFields[key] = value;
49
- }
50
-
51
- setTimestamp(ts) {
52
- this.timestamp = ts;
53
- }
54
-
55
- toLineProtocol() {
56
- return `lp:${this.measurement}`;
57
- }
58
- }
59
-
60
- return {
61
- InfluxDBClient: MockInfluxDBClient,
62
- Point: MockPoint,
63
- __getLastClientOptions: () => mockLastClientOptions,
64
- __getLastClientInstance: () => mockLastClientInstance,
65
- __getLastPoint: () => mockLastPoint
66
- };
67
- });
68
-
69
- function buildRED() {
70
- const types = {};
71
- return {
72
- log: {
73
- info: jest.fn(),
74
- warn: jest.fn(),
75
- error: jest.fn()
76
- },
77
- nodes: {
78
- createNode(node, config) {
79
- node.credentials = config.credentials || {};
80
- node.status = jest.fn();
81
- node.error = jest.fn();
82
- node.warn = jest.fn();
83
- node.send = jest.fn();
84
- node.on = jest.fn((event, handler) => {
85
- node._handlers = node._handlers || {};
86
- node._handlers[event] = handler;
87
- });
88
- },
89
- registerType(name, ctor) {
90
- types[name] = ctor;
91
- },
92
- getNode(id) {
93
- return id;
94
- }
95
- },
96
- _types: types
97
- };
98
- }
99
-
100
- function setup() {
101
- jest.resetModules();
102
- const RED = buildRED();
103
- require('../influxdb3.js')(RED);
104
- const influxModule = require('@influxdata/influxdb3-client');
105
- return { RED, influxModule };
106
- }
107
-
108
- beforeEach(() => {
109
- jest.useFakeTimers();
110
- mockLastClientOptions = undefined;
111
- mockLastClientInstance = undefined;
112
- mockLastPoint = undefined;
113
- });
114
-
115
- afterEach(() => {
116
- jest.runOnlyPendingTimers();
117
- jest.useRealTimers();
118
- });
119
-
120
- describe('InfluxDB v3 config node', () => {
121
- test('normalizes host and uses provided config', () => {
122
- const { RED, influxModule } = setup();
123
- const ConfigCtor = RED._types['influxdb3-config'];
124
-
125
- const configNode = new ConfigCtor({
126
- host: 'https://example.com',
127
- database: 'metrics',
128
- name: 'Test',
129
- credentials: { token: 'token' }
130
- });
131
-
132
- configNode.getClient();
133
-
134
- const options = influxModule.__getLastClientOptions();
135
- expect(options.host).toBe('https://example.com/');
136
- expect(options.database).toBe('metrics');
137
- expect(options.token).toBe('token');
138
- });
139
-
140
- test('passes CA certificate via transportOptions and does not touch global env', () => {
141
- const originalEnv = process.env.NODE_EXTRA_CA_CERTS;
142
- const caPath = path.join(os.tmpdir(), `influx-ca-${Date.now()}.pem`);
143
- const caContents = '-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----\n';
144
- fs.writeFileSync(caPath, caContents);
145
-
146
- try {
147
- const { RED, influxModule } = setup();
148
- const ConfigCtor = RED._types['influxdb3-config'];
149
-
150
- const configNode = new ConfigCtor({
151
- host: 'https://example.com',
152
- database: 'metrics',
153
- name: 'Test',
154
- caCertPath: caPath,
155
- credentials: { token: 'token' }
156
- });
157
-
158
- configNode.getClient();
159
-
160
- const options = influxModule.__getLastClientOptions();
161
- expect(options.transportOptions).toBeDefined();
162
- expect(options.transportOptions.ca.toString()).toBe(caContents);
163
- // Global env must be left untouched
164
- expect(process.env.NODE_EXTRA_CA_CERTS).toBe(originalEnv);
165
- } finally {
166
- fs.unlinkSync(caPath);
167
- }
168
- });
169
-
170
- test('throws a clear error when the CA certificate cannot be read', () => {
171
- const { RED } = setup();
172
- const ConfigCtor = RED._types['influxdb3-config'];
173
-
174
- const configNode = new ConfigCtor({
175
- host: 'https://example.com',
176
- database: 'metrics',
177
- name: 'Test',
178
- caCertPath: path.join(os.tmpdir(), 'does-not-exist-influx-ca.pem'),
179
- credentials: { token: 'token' }
180
- });
181
-
182
- expect(() => configNode.getClient()).toThrow(/Failed to read CA certificate/);
183
- });
184
-
185
- test('disables TLS verification via transportOptions without setting global env', () => {
186
- const originalEnv = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
187
- const { RED, influxModule } = setup();
188
- const ConfigCtor = RED._types['influxdb3-config'];
189
-
190
- const configNode = new ConfigCtor({
191
- host: 'https://example.com',
192
- database: 'metrics',
193
- name: 'Test',
194
- tlsRejectUnauthorized: false,
195
- credentials: { token: 'token' }
196
- });
197
-
198
- configNode.getClient();
199
-
200
- const options = influxModule.__getLastClientOptions();
201
- expect(options.transportOptions).toBeDefined();
202
- expect(options.transportOptions.rejectUnauthorized).toBe(false);
203
- // Global env must be left untouched
204
- expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBe(originalEnv);
205
- });
206
-
207
- test('omits transportOptions entirely when TLS defaults are used', () => {
208
- const { RED, influxModule } = setup();
209
- const ConfigCtor = RED._types['influxdb3-config'];
210
-
211
- const configNode = new ConfigCtor({
212
- host: 'https://example.com',
213
- database: 'metrics',
214
- name: 'Test',
215
- credentials: { token: 'token' }
216
- });
217
-
218
- configNode.getClient();
219
-
220
- const options = influxModule.__getLastClientOptions();
221
- expect(options.transportOptions).toBeUndefined();
222
- });
223
- });
224
-
225
- describe('InfluxDB v3 write node', () => {
226
- test('writes line protocol from object payload', async () => {
227
- const { RED, influxModule } = setup();
228
- const ConfigCtor = RED._types['influxdb3-config'];
229
- const WriteCtor = RED._types['influxdb3-write'];
230
-
231
- const configNode = new ConfigCtor({
232
- host: 'https://example.com',
233
- database: 'metrics',
234
- name: 'Test',
235
- credentials: { token: 'token' }
236
- });
237
-
238
- const writeNode = new WriteCtor({
239
- influxdb: configNode,
240
- measurement: 'cpu',
241
- database: ''
242
- });
243
-
244
- const msg = {
245
- measurement: 'cpu',
246
- payload: {
247
- fields: {
248
- temperature: 21.5,
249
- count: 5
250
- },
251
- tags: { location: 'lab' },
252
- integers: ['count'],
253
- timestamp: 1700000000000
254
- }
255
- };
256
-
257
- const send = jest.fn();
258
- const done = jest.fn();
259
- await writeNode._handlers.input(msg, send, done);
260
-
261
- const point = influxModule.__getLastPoint();
262
- const client = influxModule.__getLastClientInstance();
263
-
264
- expect(point.floatFields.temperature).toBe(21.5);
265
- expect(point.integerFields.count).toBe(5);
266
- expect(point.tags.location).toBe('lab');
267
-
268
- expect(client.write).toHaveBeenCalledWith('lp:cpu', 'metrics');
269
- expect(send).toHaveBeenCalledWith(msg);
270
- expect(done).toHaveBeenCalled();
271
-
272
- if (writeNode._handlers.close) {
273
- writeNode._handlers.close();
274
- }
275
- });
276
-
277
- test('writes raw line protocol string with database override', async () => {
278
- const { RED, influxModule } = setup();
279
- const ConfigCtor = RED._types['influxdb3-config'];
280
- const WriteCtor = RED._types['influxdb3-write'];
281
-
282
- const configNode = new ConfigCtor({
283
- host: 'https://example.com',
284
- database: 'metrics',
285
- name: 'Test',
286
- credentials: { token: 'token' }
287
- });
288
-
289
- const writeNode = new WriteCtor({
290
- influxdb: configNode,
291
- measurement: '',
292
- database: ''
293
- });
294
-
295
- const msg = {
296
- payload: ' weather,location=lab temperature=18.5 ',
297
- database: 'override-db'
298
- };
299
-
300
- const send = jest.fn();
301
- const done = jest.fn();
302
- await writeNode._handlers.input(msg, send, done);
303
-
304
- const client = influxModule.__getLastClientInstance();
305
- expect(client.write).toHaveBeenCalledWith('weather,location=lab temperature=18.5', 'override-db');
306
- expect(send).toHaveBeenCalledWith(msg);
307
- expect(done).toHaveBeenCalled();
308
-
309
- if (writeNode._handlers.close) {
310
- writeNode._handlers.close();
311
- }
312
- });
313
- });
314
-
315
- // Helper to create a write node for addFieldToPoint / buildLineProtocol tests
316
- function createWriteNode() {
317
- const { RED, influxModule } = setup();
318
- const ConfigCtor = RED._types['influxdb3-config'];
319
- const WriteCtor = RED._types['influxdb3-write'];
320
-
321
- const configNode = new ConfigCtor({
322
- host: 'https://example.com',
323
- database: 'metrics',
324
- name: 'Test',
325
- credentials: { token: 'token' }
326
- });
327
-
328
- const writeNode = new WriteCtor({
329
- influxdb: configNode,
330
- measurement: 'test_measurement',
331
- database: ''
332
- });
333
-
334
- return { RED, influxModule, configNode, writeNode };
335
- }
336
-
337
- describe('addFieldToPoint – field type handling', () => {
338
- test('float fields by default for numbers', async () => {
339
- const { influxModule, writeNode } = createWriteNode();
340
- const msg = {
341
- measurement: 'sensor',
342
- payload: { fields: { temperature: 21.5, humidity: 60 } }
343
- };
344
- const send = jest.fn();
345
- const done = jest.fn();
346
- await writeNode._handlers.input(msg, send, done);
347
-
348
- const point = influxModule.__getLastPoint();
349
- expect(point.floatFields.temperature).toBe(21.5);
350
- expect(point.floatFields.humidity).toBe(60);
351
- expect(done).toHaveBeenCalled();
352
- expect(done.mock.calls[0][0]).toBeUndefined();
353
- });
354
-
355
- test('integer fields when listed in msg.payload.integers', async () => {
356
- const { influxModule, writeNode } = createWriteNode();
357
- const msg = {
358
- measurement: 'sensor',
359
- payload: {
360
- fields: { count: 42, temperature: 21.5 },
361
- integers: ['count']
362
- }
363
- };
364
- const send = jest.fn();
365
- const done = jest.fn();
366
- await writeNode._handlers.input(msg, send, done);
367
-
368
- const point = influxModule.__getLastPoint();
369
- expect(point.integerFields.count).toBe(42);
370
- expect(point.floatFields.temperature).toBe(21.5);
371
- });
372
-
373
- test('integer suffix string "42i" is parsed as integer', async () => {
374
- const { influxModule, writeNode } = createWriteNode();
375
- const msg = {
376
- measurement: 'sensor',
377
- payload: { fields: { count: '42i' } }
378
- };
379
- const send = jest.fn();
380
- const done = jest.fn();
381
- await writeNode._handlers.input(msg, send, done);
382
-
383
- const point = influxModule.__getLastPoint();
384
- expect(point.integerFields.count).toBe(42);
385
- });
386
-
387
- test('negative integer suffix string "-7i" is parsed as integer', async () => {
388
- const { influxModule, writeNode } = createWriteNode();
389
- const msg = {
390
- measurement: 'sensor',
391
- payload: { fields: { offset: '-7i' } }
392
- };
393
- const send = jest.fn();
394
- const done = jest.fn();
395
- await writeNode._handlers.input(msg, send, done);
396
-
397
- const point = influxModule.__getLastPoint();
398
- expect(point.integerFields.offset).toBe(-7);
399
- });
400
-
401
- test('regular strings are set as string fields', async () => {
402
- const { influxModule, writeNode } = createWriteNode();
403
- const msg = {
404
- measurement: 'sensor',
405
- payload: { fields: { status: 'ok' } }
406
- };
407
- const send = jest.fn();
408
- const done = jest.fn();
409
- await writeNode._handlers.input(msg, send, done);
410
-
411
- const point = influxModule.__getLastPoint();
412
- expect(point.stringFields.status).toBe('ok');
413
- });
414
-
415
- test('boolean fields are set correctly', async () => {
416
- const { influxModule, writeNode } = createWriteNode();
417
- const msg = {
418
- measurement: 'sensor',
419
- payload: { fields: { active: true, disabled: false } }
420
- };
421
- const send = jest.fn();
422
- const done = jest.fn();
423
- await writeNode._handlers.input(msg, send, done);
424
-
425
- const point = influxModule.__getLastPoint();
426
- expect(point.booleanFields.active).toBe(true);
427
- expect(point.booleanFields.disabled).toBe(false);
428
- });
429
-
430
- test('non-integer float is truncated with warning when marked as integer', async () => {
431
- const { influxModule, writeNode } = createWriteNode();
432
- const msg = {
433
- measurement: 'sensor',
434
- payload: {
435
- fields: { value: 3.7 },
436
- integers: ['value']
437
- }
438
- };
439
- const send = jest.fn();
440
- const done = jest.fn();
441
- await writeNode._handlers.input(msg, send, done);
442
-
443
- const point = influxModule.__getLastPoint();
444
- expect(point.integerFields.value).toBe(3);
445
- expect(writeNode.warn).toHaveBeenCalledWith(
446
- expect.stringContaining("marked as integer but value is 3.7")
447
- );
448
- });
449
-
450
- test('negative non-integer float truncates toward zero (not floor)', async () => {
451
- const { influxModule, writeNode } = createWriteNode();
452
- const msg = {
453
- measurement: 'sensor',
454
- payload: {
455
- fields: { value: -3.7 },
456
- integers: ['value']
457
- }
458
- };
459
- const send = jest.fn();
460
- const done = jest.fn();
461
- await writeNode._handlers.input(msg, send, done);
462
-
463
- const point = influxModule.__getLastPoint();
464
- // Math.trunc(-3.7) === -3 (toward zero), not Math.floor's -4
465
- expect(point.integerFields.value).toBe(-3);
466
- expect(writeNode.warn).toHaveBeenCalledWith(
467
- expect.stringContaining("truncated to -3")
468
- );
469
- });
470
- });
471
-
472
- describe('addFieldToPoint – enhanced error messages (issue #16)', () => {
473
- test('object field value produces detailed warning with type and value', async () => {
474
- const { writeNode } = createWriteNode();
475
- const msg = {
476
- measurement: 'sensor',
477
- payload: {
478
- fields: {
479
- good: 42,
480
- nested: { a: 1, b: 2 }
481
- }
482
- }
483
- };
484
- const send = jest.fn();
485
- const done = jest.fn();
486
- await writeNode._handlers.input(msg, send, done);
487
-
488
- expect(writeNode.warn).toHaveBeenCalledWith(
489
- expect.stringContaining("Skipping field 'nested': unsupported type 'object' (Object)")
490
- );
491
- expect(writeNode.warn).toHaveBeenCalledWith(
492
- expect.stringContaining('Actual value: {"a":1,"b":2}')
493
- );
494
- expect(writeNode.warn).toHaveBeenCalledWith(
495
- expect.stringContaining("must be a number, string, or boolean")
496
- );
497
- // Should still succeed for the valid field
498
- expect(send).toHaveBeenCalled();
499
- expect(done).toHaveBeenCalled();
500
- });
501
-
502
- test('array field value shows Array type name in warning', async () => {
503
- const { writeNode } = createWriteNode();
504
- const msg = {
505
- measurement: 'sensor',
506
- payload: {
507
- fields: {
508
- good: 1,
509
- values: [1, 2, 3]
510
- }
511
- }
512
- };
513
- const send = jest.fn();
514
- const done = jest.fn();
515
- await writeNode._handlers.input(msg, send, done);
516
-
517
- expect(writeNode.warn).toHaveBeenCalledWith(
518
- expect.stringContaining("unsupported type 'object' (Array)")
519
- );
520
- expect(writeNode.warn).toHaveBeenCalledWith(
521
- expect.stringContaining('Actual value: [1,2,3]')
522
- );
523
- });
524
-
525
- test('null field value produces clear warning', async () => {
526
- const { writeNode } = createWriteNode();
527
- const msg = {
528
- measurement: 'sensor',
529
- payload: {
530
- fields: {
531
- good: 1,
532
- broken: null
533
- }
534
- }
535
- };
536
- const send = jest.fn();
537
- const done = jest.fn();
538
- await writeNode._handlers.input(msg, send, done);
539
-
540
- expect(writeNode.warn).toHaveBeenCalledWith(
541
- expect.stringContaining("Skipping field 'broken': value is null")
542
- );
543
- });
544
-
545
- test('undefined field value produces clear warning', async () => {
546
- const { writeNode } = createWriteNode();
547
- const msg = {
548
- measurement: 'sensor',
549
- payload: {
550
- fields: {
551
- good: 1,
552
- missing: undefined
553
- }
554
- }
555
- };
556
- const send = jest.fn();
557
- const done = jest.fn();
558
- await writeNode._handlers.input(msg, send, done);
559
-
560
- expect(writeNode.warn).toHaveBeenCalledWith(
561
- expect.stringContaining("Skipping field 'missing': value is undefined")
562
- );
563
- });
564
-
565
- test('NaN field value is skipped with warning', async () => {
566
- const { writeNode } = createWriteNode();
567
- const msg = {
568
- measurement: 'sensor',
569
- payload: {
570
- fields: {
571
- good: 1,
572
- bad: NaN
573
- }
574
- }
575
- };
576
- const send = jest.fn();
577
- const done = jest.fn();
578
- await writeNode._handlers.input(msg, send, done);
579
-
580
- expect(writeNode.warn).toHaveBeenCalledWith(
581
- expect.stringContaining("Skipping field 'bad': numeric value is NaN (not finite)")
582
- );
583
- });
584
-
585
- test('Infinity field value is skipped with warning', async () => {
586
- const { writeNode } = createWriteNode();
587
- const msg = {
588
- measurement: 'sensor',
589
- payload: {
590
- fields: {
591
- good: 1,
592
- bad: Infinity
593
- }
594
- }
595
- };
596
- const send = jest.fn();
597
- const done = jest.fn();
598
- await writeNode._handlers.input(msg, send, done);
599
-
600
- expect(writeNode.warn).toHaveBeenCalledWith(
601
- expect.stringContaining("Skipping field 'bad': numeric value is Infinity (not finite)")
602
- );
603
- });
604
-
605
- test('warning includes measurement name as context', async () => {
606
- const { writeNode } = createWriteNode();
607
- const msg = {
608
- measurement: 'my_sensor',
609
- payload: {
610
- fields: {
611
- good: 1,
612
- broken: { nested: true }
613
- }
614
- }
615
- };
616
- const send = jest.fn();
617
- const done = jest.fn();
618
- await writeNode._handlers.input(msg, send, done);
619
-
620
- expect(writeNode.warn).toHaveBeenCalledWith(
621
- expect.stringContaining("(measurement: 'my_sensor')")
622
- );
623
- });
624
-
625
- test('all fields skipped produces error with payload dump', async () => {
626
- const { writeNode } = createWriteNode();
627
- const msg = {
628
- measurement: 'sensor',
629
- payload: {
630
- fields: {
631
- bad1: null,
632
- bad2: { nested: true }
633
- }
634
- }
635
- };
636
- const send = jest.fn();
637
- const done = jest.fn();
638
- await writeNode._handlers.input(msg, send, done);
639
-
640
- // done is called with an error when no valid fields remain
641
- expect(done).toHaveBeenCalledWith(expect.any(Error));
642
- expect(done.mock.calls[0][0].message).toContain('No valid fields to write');
643
- });
644
- });
645
-
646
- describe('buildLineProtocol – simplified payload format', () => {
647
- test('non-reserved keys are used as fields', async () => {
648
- const { influxModule, writeNode } = createWriteNode();
649
- const msg = {
650
- measurement: 'sensor',
651
- payload: {
652
- temperature: 21.5,
653
- humidity: 60,
654
- tags: { location: 'lab' }
655
- }
656
- };
657
- const send = jest.fn();
658
- const done = jest.fn();
659
- await writeNode._handlers.input(msg, send, done);
660
-
661
- const point = influxModule.__getLastPoint();
662
- expect(point.floatFields.temperature).toBe(21.5);
663
- expect(point.floatFields.humidity).toBe(60);
664
- expect(point.tags.location).toBe('lab');
665
- // Reserved keys should NOT appear as fields
666
- expect(point.floatFields.tags).toBeUndefined();
667
- expect(point.stringFields.tags).toBeUndefined();
668
- });
669
-
670
- test('reserved keys (tags, timestamp, integers, fields) are excluded from fields', async () => {
671
- const { influxModule, writeNode } = createWriteNode();
672
- const msg = {
673
- measurement: 'sensor',
674
- payload: {
675
- value: 42,
676
- tags: { location: 'lab' },
677
- timestamp: 1700000000000,
678
- integers: ['value']
679
- }
680
- };
681
- const send = jest.fn();
682
- const done = jest.fn();
683
- await writeNode._handlers.input(msg, send, done);
684
-
685
- const point = influxModule.__getLastPoint();
686
- expect(point.integerFields.value).toBe(42);
687
- // None of the reserved keys should appear as field entries
688
- expect(point.floatFields.tags).toBeUndefined();
689
- expect(point.floatFields.timestamp).toBeUndefined();
690
- expect(point.floatFields.integers).toBeUndefined();
691
- expect(point.floatFields.fields).toBeUndefined();
692
- });
693
- });
694
-
695
- describe('buildLineProtocol – tag value handling', () => {
696
- test('string, number and boolean tag values are coerced to strings', async () => {
697
- const { influxModule, writeNode } = createWriteNode();
698
- const msg = {
699
- measurement: 'sensor',
700
- payload: {
701
- fields: { value: 1 },
702
- tags: { location: 'lab', floor: 2, active: true }
703
- }
704
- };
705
- const send = jest.fn();
706
- const done = jest.fn();
707
- await writeNode._handlers.input(msg, send, done);
708
-
709
- const point = influxModule.__getLastPoint();
710
- expect(point.tags.location).toBe('lab');
711
- expect(point.tags.floor).toBe('2');
712
- expect(point.tags.active).toBe('true');
713
- });
714
-
715
- test('object tag value is skipped with a warning instead of "[object Object]"', async () => {
716
- const { influxModule, writeNode } = createWriteNode();
717
- const msg = {
718
- measurement: 'sensor',
719
- payload: {
720
- fields: { value: 1 },
721
- tags: { good: 'ok', broken: { nested: true } }
722
- }
723
- };
724
- const send = jest.fn();
725
- const done = jest.fn();
726
- await writeNode._handlers.input(msg, send, done);
727
-
728
- const point = influxModule.__getLastPoint();
729
- expect(point.tags.good).toBe('ok');
730
- expect(point.tags.broken).toBeUndefined();
731
- expect(writeNode.warn).toHaveBeenCalledWith(
732
- expect.stringContaining("Skipping tag 'broken': unsupported type 'object' (Object)")
733
- );
734
- // The valid field still writes, so the message succeeds
735
- expect(send).toHaveBeenCalled();
736
- expect(done).toHaveBeenCalled();
737
- });
738
-
739
- test('array tag value shows Array type name in warning and is skipped', async () => {
740
- const { influxModule, writeNode } = createWriteNode();
741
- const msg = {
742
- measurement: 'sensor',
743
- payload: {
744
- fields: { value: 1 },
745
- tags: { list: [1, 2, 3] }
746
- }
747
- };
748
- const send = jest.fn();
749
- const done = jest.fn();
750
- await writeNode._handlers.input(msg, send, done);
751
-
752
- const point = influxModule.__getLastPoint();
753
- expect(point.tags.list).toBeUndefined();
754
- expect(writeNode.warn).toHaveBeenCalledWith(
755
- expect.stringContaining("Skipping tag 'list': unsupported type 'object' (Array)")
756
- );
757
- });
758
-
759
- test('null and undefined tag values are skipped silently', async () => {
760
- const { influxModule, writeNode } = createWriteNode();
761
- const msg = {
762
- measurement: 'sensor',
763
- payload: {
764
- fields: { value: 1 },
765
- tags: { keep: 'yes', gone: null, missing: undefined }
766
- }
767
- };
768
- const send = jest.fn();
769
- const done = jest.fn();
770
- await writeNode._handlers.input(msg, send, done);
771
-
772
- const point = influxModule.__getLastPoint();
773
- expect(point.tags.keep).toBe('yes');
774
- expect(point.tags.gone).toBeUndefined();
775
- expect(point.tags.missing).toBeUndefined();
776
- expect(writeNode.warn).not.toHaveBeenCalled();
777
- });
778
- });
779
-
780
- describe('buildLineProtocol – timestamp handling', () => {
781
- test('numeric timestamp from msg.payload.timestamp', async () => {
782
- const { influxModule, writeNode } = createWriteNode();
783
- const msg = {
784
- measurement: 'sensor',
785
- payload: {
786
- fields: { value: 1 },
787
- timestamp: 1700000000000
788
- }
789
- };
790
- const send = jest.fn();
791
- const done = jest.fn();
792
- await writeNode._handlers.input(msg, send, done);
793
-
794
- const point = influxModule.__getLastPoint();
795
- expect(point.timestamp).toEqual(new Date(1700000000000));
796
- });
797
-
798
- test('fallback to msg.timestamp when payload.timestamp is absent', async () => {
799
- const { influxModule, writeNode } = createWriteNode();
800
- const msg = {
801
- measurement: 'sensor',
802
- timestamp: 1700000000000,
803
- payload: {
804
- fields: { value: 1 }
805
- }
806
- };
807
- const send = jest.fn();
808
- const done = jest.fn();
809
- await writeNode._handlers.input(msg, send, done);
810
-
811
- const point = influxModule.__getLastPoint();
812
- expect(point.timestamp).toEqual(new Date(1700000000000));
813
- });
814
-
815
- test('Date object timestamp is used directly', async () => {
816
- const { influxModule, writeNode } = createWriteNode();
817
- const date = new Date('2025-01-01T00:00:00Z');
818
- const msg = {
819
- measurement: 'sensor',
820
- payload: {
821
- fields: { value: 1 },
822
- timestamp: date
823
- }
824
- };
825
- const send = jest.fn();
826
- const done = jest.fn();
827
- await writeNode._handlers.input(msg, send, done);
828
-
829
- const point = influxModule.__getLastPoint();
830
- expect(point.timestamp).toEqual(date);
831
- });
832
-
833
- test('invalid timestamp string produces warning', async () => {
834
- const { writeNode } = createWriteNode();
835
- const msg = {
836
- measurement: 'sensor',
837
- payload: {
838
- fields: { value: 1 },
839
- timestamp: 'not-a-date'
840
- }
841
- };
842
- const send = jest.fn();
843
- const done = jest.fn();
844
- await writeNode._handlers.input(msg, send, done);
845
-
846
- expect(writeNode.warn).toHaveBeenCalledWith(
847
- expect.stringContaining("Invalid timestamp string: 'not-a-date'")
848
- );
849
- });
850
- });
851
-
852
- describe('buildLineProtocol – error cases', () => {
853
- test('missing measurement produces error', async () => {
854
- const { RED } = setup();
855
- const ConfigCtor = RED._types['influxdb3-config'];
856
- const WriteCtor = RED._types['influxdb3-write'];
857
-
858
- const configNode = new ConfigCtor({
859
- host: 'https://example.com',
860
- database: 'metrics',
861
- name: 'Test',
862
- credentials: { token: 'token' }
863
- });
864
-
865
- // No measurement on node
866
- const writeNode = new WriteCtor({
867
- influxdb: configNode,
868
- measurement: '',
869
- database: ''
870
- });
871
-
872
- const msg = {
873
- // No measurement on msg either
874
- payload: { fields: { value: 1 } }
875
- };
876
- const send = jest.fn();
877
- const done = jest.fn();
878
- await writeNode._handlers.input(msg, send, done);
879
-
880
- expect(done).toHaveBeenCalledWith(expect.any(Error));
881
- expect(done.mock.calls[0][0].message).toContain('Measurement not specified');
882
- });
883
-
884
- test('empty line protocol string produces error', async () => {
885
- const { RED } = setup();
886
- const ConfigCtor = RED._types['influxdb3-config'];
887
- const WriteCtor = RED._types['influxdb3-write'];
888
-
889
- const configNode = new ConfigCtor({
890
- host: 'https://example.com',
891
- database: 'metrics',
892
- name: 'Test',
893
- credentials: { token: 'token' }
894
- });
895
-
896
- const writeNode = new WriteCtor({
897
- influxdb: configNode,
898
- measurement: '',
899
- database: ''
900
- });
901
-
902
- const msg = {
903
- payload: ' '
904
- };
905
- const send = jest.fn();
906
- const done = jest.fn();
907
- await writeNode._handlers.input(msg, send, done);
908
-
909
- expect(done).toHaveBeenCalledWith(expect.any(Error));
910
- expect(done.mock.calls[0][0].message).toContain('Line protocol string is empty');
911
- });
912
-
913
- test('empty array produces error', async () => {
914
- const { writeNode } = createWriteNode();
915
- const msg = {
916
- payload: []
917
- };
918
- const send = jest.fn();
919
- const done = jest.fn();
920
- await writeNode._handlers.input(msg, send, done);
921
-
922
- expect(done).toHaveBeenCalledWith(expect.any(Error));
923
- expect(done.mock.calls[0][0].message).toContain('Payload array is empty');
924
- });
925
-
926
- test('array with invalid item type produces error', async () => {
927
- const { writeNode } = createWriteNode();
928
- const msg = {
929
- payload: [1, 2, 3]
930
- };
931
- const send = jest.fn();
932
- const done = jest.fn();
933
- await writeNode._handlers.input(msg, send, done);
934
-
935
- expect(done).toHaveBeenCalledWith(expect.any(Error));
936
- expect(done.mock.calls[0][0].message).toContain('Array item 0 has invalid format');
937
- });
938
- });
939
-
940
- describe('Array payload support', () => {
941
- test('writes array of line protocol strings', async () => {
942
- const { writeNode, influxModule } = createWriteNode();
943
- const msg = {
944
- payload: [
945
- 'temperature,location=room1 value=21.5',
946
- 'temperature,location=room2 value=19.8',
947
- 'humidity,location=room1 value=65'
948
- ]
949
- };
950
- const send = jest.fn();
951
- const done = jest.fn();
952
- await writeNode._handlers.input(msg, send, done);
953
-
954
- const client = influxModule.__getLastClientInstance();
955
- expect(client.write).toHaveBeenCalledWith(
956
- 'temperature,location=room1 value=21.5\ntemperature,location=room2 value=19.8\nhumidity,location=room1 value=65',
957
- 'metrics'
958
- );
959
- expect(send).toHaveBeenCalledWith(msg);
960
- expect(done).toHaveBeenCalled();
961
- });
962
-
963
- test('writes array of object payloads', async () => {
964
- const { writeNode, influxModule } = createWriteNode();
965
- const msg = {
966
- payload: [
967
- {
968
- measurement: 'temperature',
969
- fields: { value: 21.5 },
970
- tags: { location: 'room1' }
971
- },
972
- {
973
- measurement: 'temperature',
974
- fields: { value: 19.8 },
975
- tags: { location: 'room2' }
976
- },
977
- {
978
- measurement: 'humidity',
979
- fields: { value: 65 },
980
- tags: { location: 'room1' }
981
- }
982
- ]
983
- };
984
- const send = jest.fn();
985
- const done = jest.fn();
986
- await writeNode._handlers.input(msg, send, done);
987
-
988
- const client = influxModule.__getLastClientInstance();
989
- expect(client.write).toHaveBeenCalled();
990
- expect(send).toHaveBeenCalledWith(msg);
991
- expect(done).toHaveBeenCalled();
992
- });
993
-
994
- test('writes mixed array of objects and line protocol strings', async () => {
995
- const { writeNode, influxModule } = createWriteNode();
996
- const msg = {
997
- payload: [
998
- 'temperature,location=room1 value=21.5',
999
- {
1000
- measurement: 'humidity',
1001
- fields: { value: 65 },
1002
- tags: { location: 'room1' }
1003
- },
1004
- 'pressure,location=room1 value=1013.25'
1005
- ]
1006
- };
1007
- const send = jest.fn();
1008
- const done = jest.fn();
1009
- await writeNode._handlers.input(msg, send, done);
1010
-
1011
- const client = influxModule.__getLastClientInstance();
1012
- expect(client.write).toHaveBeenCalled();
1013
- expect(send).toHaveBeenCalledWith(msg);
1014
- expect(done).toHaveBeenCalled();
1015
- });
1016
-
1017
- test('array objects can use msg.measurement as fallback', async () => {
1018
- const { writeNode, influxModule } = createWriteNode();
1019
- const msg = {
1020
- measurement: 'temperature',
1021
- payload: [
1022
- {
1023
- fields: { value: 21.5 },
1024
- tags: { location: 'room1' }
1025
- },
1026
- {
1027
- fields: { value: 19.8 },
1028
- tags: { location: 'room2' }
1029
- }
1030
- ]
1031
- };
1032
- const send = jest.fn();
1033
- const done = jest.fn();
1034
- await writeNode._handlers.input(msg, send, done);
1035
-
1036
- const client = influxModule.__getLastClientInstance();
1037
- expect(client.write).toHaveBeenCalled();
1038
- expect(send).toHaveBeenCalledWith(msg);
1039
- expect(done).toHaveBeenCalled();
1040
- });
1041
-
1042
- test('array item error includes item index', async () => {
1043
- const { RED } = setup();
1044
- const ConfigCtor = RED._types['influxdb3-config'];
1045
- const WriteCtor = RED._types['influxdb3-write'];
1046
-
1047
- const configNode = new ConfigCtor({
1048
- host: 'https://example.com',
1049
- database: 'metrics',
1050
- name: 'Test',
1051
- credentials: { token: 'token' }
1052
- });
1053
-
1054
- // Create node without default measurement
1055
- const writeNode = new WriteCtor({
1056
- influxdb: configNode,
1057
- measurement: '', // No default measurement
1058
- database: ''
1059
- });
1060
-
1061
- const msg = {
1062
- payload: [
1063
- 'temperature,location=room1 value=21.5',
1064
- {
1065
- // Missing measurement and no fallback
1066
- fields: { value: 19.8 }
1067
- }
1068
- ]
1069
- };
1070
- const send = jest.fn();
1071
- const done = jest.fn();
1072
- await writeNode._handlers.input(msg, send, done);
1073
-
1074
- expect(done).toHaveBeenCalledWith(expect.any(Error));
1075
- expect(done.mock.calls[0][0].message).toContain('Array item 1');
1076
- });
1077
-
1078
- test('array with empty string produces error', async () => {
1079
- const { writeNode } = createWriteNode();
1080
- const msg = {
1081
- payload: [
1082
- 'temperature,location=room1 value=21.5',
1083
- ' ',
1084
- 'humidity,location=room1 value=65'
1085
- ]
1086
- };
1087
- const send = jest.fn();
1088
- const done = jest.fn();
1089
- await writeNode._handlers.input(msg, send, done);
1090
-
1091
- expect(done).toHaveBeenCalledWith(expect.any(Error));
1092
- expect(done.mock.calls[0][0].message).toContain('Array item 1 is an empty string');
1093
- });
1094
-
1095
- test('array with invalid line protocol produces error with index', async () => {
1096
- const { writeNode } = createWriteNode();
1097
- const msg = {
1098
- payload: [
1099
- 'temperature,location=room1 value=21.5',
1100
- 'invalid line protocol no equals',
1101
- 'humidity,location=room1 value=65'
1102
- ]
1103
- };
1104
- const send = jest.fn();
1105
- const done = jest.fn();
1106
- await writeNode._handlers.input(msg, send, done);
1107
-
1108
- expect(done).toHaveBeenCalledWith(expect.any(Error));
1109
- expect(done.mock.calls[0][0].message).toContain('Array item 1');
1110
- });
1111
- });
1112
-
1113
- describe('write node – client.write failure', () => {
1114
- test('rejected write sets red status and calls done with the error', async () => {
1115
- const { configNode, writeNode } = createWriteNode();
1116
-
1117
- // Pre-create the cached client and make its write reject
1118
- const client = configNode.getClient();
1119
- client.write = jest.fn().mockRejectedValue(new Error('connection refused'));
1120
-
1121
- const msg = {
1122
- measurement: 'sensor',
1123
- payload: { fields: { value: 1 } }
1124
- };
1125
- const send = jest.fn();
1126
- const done = jest.fn();
1127
- await writeNode._handlers.input(msg, send, done);
1128
-
1129
- expect(client.write).toHaveBeenCalled();
1130
- expect(done).toHaveBeenCalledWith(expect.any(Error));
1131
- expect(done.mock.calls[0][0].message).toBe('connection refused');
1132
- // Error status is red and the message is NOT forwarded
1133
- expect(writeNode.status).toHaveBeenCalledWith(
1134
- expect.objectContaining({ fill: 'red', text: 'connection refused' })
1135
- );
1136
- expect(send).not.toHaveBeenCalled();
1137
- });
1138
-
1139
- test('long error messages are truncated in the node status', async () => {
1140
- const { configNode, writeNode } = createWriteNode();
1141
- const longMessage = 'x'.repeat(200);
1142
-
1143
- const client = configNode.getClient();
1144
- client.write = jest.fn().mockRejectedValue(new Error(longMessage));
1145
-
1146
- const msg = { measurement: 'sensor', payload: { fields: { value: 1 } } };
1147
- const send = jest.fn();
1148
- const done = jest.fn();
1149
- await writeNode._handlers.input(msg, send, done);
1150
-
1151
- const statusArg = writeNode.status.mock.calls.find(c => c[0] && c[0].fill === 'red')[0];
1152
- expect(statusArg.text.length).toBeLessThanOrEqual(83); // 80 chars + '...'
1153
- expect(statusArg.text.endsWith('...')).toBe(true);
1154
- // done still receives the full, untruncated error
1155
- expect(done.mock.calls[0][0].message).toBe(longMessage);
1156
- });
1157
- });
1158
-
1159
- describe('config node – close()', () => {
1160
- test('close() closes the cached client and clears the reference', () => {
1161
- const { configNode } = createWriteNode();
1162
-
1163
- const client = configNode.getClient();
1164
- expect(configNode.client).toBe(client);
1165
-
1166
- configNode._handlers.close();
1167
-
1168
- expect(client.close).toHaveBeenCalled();
1169
- expect(configNode.client).toBeNull();
1170
- });
1171
-
1172
- test('close() is a no-op when no client was ever created', () => {
1173
- const { configNode } = createWriteNode();
1174
- expect(configNode.client).toBeNull();
1175
- expect(() => configNode._handlers.close()).not.toThrow();
1176
- });
1177
- });
1178
-
1179
- describe('write node – measurement resolution', () => {
1180
- test('empty msg.measurement falls back to the node default', async () => {
1181
- const { influxModule, writeNode } = createWriteNode(); // node default: 'test_measurement'
1182
- const msg = {
1183
- measurement: '',
1184
- payload: { fields: { value: 1 } }
1185
- };
1186
- const send = jest.fn();
1187
- const done = jest.fn();
1188
- await writeNode._handlers.input(msg, send, done);
1189
-
1190
- const point = influxModule.__getLastPoint();
1191
- expect(point.measurement).toBe('test_measurement');
1192
- expect(done).toHaveBeenCalled();
1193
- expect(done.mock.calls[0][0]).toBeUndefined();
1194
- });
1195
-
1196
- test('msg.measurement overrides the node default', async () => {
1197
- const { influxModule, writeNode } = createWriteNode();
1198
- const msg = {
1199
- measurement: 'override',
1200
- payload: { fields: { value: 1 } }
1201
- };
1202
- const send = jest.fn();
1203
- const done = jest.fn();
1204
- await writeNode._handlers.input(msg, send, done);
1205
-
1206
- const point = influxModule.__getLastPoint();
1207
- expect(point.measurement).toBe('override');
1208
- });
1209
-
1210
- test('whitespace-only msg.measurement falls back to the node default', async () => {
1211
- const { influxModule, writeNode } = createWriteNode(); // node default: 'test_measurement'
1212
- const msg = {
1213
- measurement: ' ',
1214
- payload: { fields: { value: 1 } }
1215
- };
1216
- const send = jest.fn();
1217
- const done = jest.fn();
1218
- await writeNode._handlers.input(msg, send, done);
1219
-
1220
- const point = influxModule.__getLastPoint();
1221
- expect(point.measurement).toBe('test_measurement');
1222
- expect(done.mock.calls[0][0]).toBeUndefined();
1223
- });
1224
-
1225
- test('a provided measurement is trimmed before use', async () => {
1226
- const { influxModule, writeNode } = createWriteNode();
1227
- const msg = {
1228
- measurement: ' spaced ',
1229
- payload: { fields: { value: 1 } }
1230
- };
1231
- const send = jest.fn();
1232
- const done = jest.fn();
1233
- await writeNode._handlers.input(msg, send, done);
1234
-
1235
- const point = influxModule.__getLastPoint();
1236
- expect(point.measurement).toBe('spaced');
1237
- });
1238
-
1239
- test('whitespace-only measurement with no node default errors', async () => {
1240
- const { RED } = setup();
1241
- const ConfigCtor = RED._types['influxdb3-config'];
1242
- const WriteCtor = RED._types['influxdb3-write'];
1243
-
1244
- const configNode = new ConfigCtor({
1245
- host: 'https://example.com',
1246
- database: 'metrics',
1247
- name: 'Test',
1248
- credentials: { token: 'token' }
1249
- });
1250
- const writeNode = new WriteCtor({ influxdb: configNode, measurement: '', database: '' });
1251
-
1252
- const msg = { measurement: ' ', payload: { fields: { value: 1 } } };
1253
- const send = jest.fn();
1254
- const done = jest.fn();
1255
- await writeNode._handlers.input(msg, send, done);
1256
-
1257
- expect(done).toHaveBeenCalledWith(expect.any(Error));
1258
- expect(done.mock.calls[0][0].message).toContain('Measurement not specified');
1259
- });
1260
- });
1261
-
1262
- describe('InfluxDB v3 config node – credentials warning', () => {
1263
- test('logs warning when credentials object is undefined', () => {
1264
- const RED = buildRED();
1265
-
1266
- // Override createNode to NOT set credentials
1267
- RED.nodes.createNode = function(node, _config) {
1268
- node.status = jest.fn();
1269
- node.error = jest.fn();
1270
- node.warn = jest.fn();
1271
- node.send = jest.fn();
1272
- node.on = jest.fn((event, handler) => {
1273
- node._handlers = node._handlers || {};
1274
- node._handlers[event] = handler;
1275
- });
1276
- // Deliberately NOT setting node.credentials
1277
- };
1278
-
1279
- require('../influxdb3.js')(RED);
1280
- const ConfigCtor = RED._types['influxdb3-config'];
1281
-
1282
- const configNode = new ConfigCtor({
1283
- host: 'https://example.com',
1284
- database: 'metrics',
1285
- name: 'Test'
1286
- });
1287
-
1288
- expect(RED.log.warn).toHaveBeenCalledWith(
1289
- 'InfluxDB v3 config: credentials object is undefined'
1290
- );
1291
- expect(configNode.token).toBeUndefined();
1292
- });
1293
- });
1294
-