node-red-contrib-influxdb3 1.0.6 → 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,972 +0,0 @@
1
- const path = require('path');
2
-
3
- let mockLastClientOptions;
4
- let mockLastClientInstance;
5
- let mockLastPoint;
6
-
7
- jest.mock('@influxdata/influxdb3-client', () => {
8
- class MockInfluxDBClient {
9
- constructor(options) {
10
- mockLastClientOptions = options;
11
- mockLastClientInstance = this;
12
- this.write = jest.fn().mockResolvedValue(undefined);
13
- this.close = jest.fn();
14
- }
15
- }
16
-
17
- class MockPoint {
18
- constructor(measurement) {
19
- this.measurement = measurement;
20
- this.tags = {};
21
- this.integerFields = {};
22
- this.floatFields = {};
23
- this.stringFields = {};
24
- this.booleanFields = {};
25
- this.timestamp = null;
26
- mockLastPoint = this;
27
- }
28
-
29
- setTag(key, value) {
30
- this.tags[key] = value;
31
- }
32
-
33
- setIntegerField(key, value) {
34
- this.integerFields[key] = value;
35
- }
36
-
37
- setFloatField(key, value) {
38
- this.floatFields[key] = value;
39
- }
40
-
41
- setStringField(key, value) {
42
- this.stringFields[key] = value;
43
- }
44
-
45
- setBooleanField(key, value) {
46
- this.booleanFields[key] = value;
47
- }
48
-
49
- setTimestamp(ts) {
50
- this.timestamp = ts;
51
- }
52
-
53
- toLineProtocol() {
54
- return `lp:${this.measurement}`;
55
- }
56
- }
57
-
58
- return {
59
- InfluxDBClient: MockInfluxDBClient,
60
- Point: MockPoint,
61
- __getLastClientOptions: () => mockLastClientOptions,
62
- __getLastClientInstance: () => mockLastClientInstance,
63
- __getLastPoint: () => mockLastPoint
64
- };
65
- });
66
-
67
- function buildRED() {
68
- const types = {};
69
- return {
70
- log: {
71
- info: jest.fn(),
72
- warn: jest.fn(),
73
- error: jest.fn()
74
- },
75
- nodes: {
76
- createNode(node, config) {
77
- node.credentials = config.credentials || {};
78
- node.status = jest.fn();
79
- node.error = jest.fn();
80
- node.warn = jest.fn();
81
- node.send = jest.fn();
82
- node.on = jest.fn((event, handler) => {
83
- node._handlers = node._handlers || {};
84
- node._handlers[event] = handler;
85
- });
86
- },
87
- registerType(name, ctor) {
88
- types[name] = ctor;
89
- },
90
- getNode(id) {
91
- return id;
92
- }
93
- },
94
- _types: types
95
- };
96
- }
97
-
98
- function setup() {
99
- jest.resetModules();
100
- const RED = buildRED();
101
- require('../influxdb3.js')(RED);
102
- const influxModule = require('@influxdata/influxdb3-client');
103
- return { RED, influxModule };
104
- }
105
-
106
- beforeEach(() => {
107
- jest.useFakeTimers();
108
- mockLastClientOptions = undefined;
109
- mockLastClientInstance = undefined;
110
- mockLastPoint = undefined;
111
- });
112
-
113
- afterEach(() => {
114
- jest.runOnlyPendingTimers();
115
- jest.useRealTimers();
116
- });
117
-
118
- describe('InfluxDB v3 config node', () => {
119
- test('normalizes host and uses provided config', () => {
120
- const { RED, influxModule } = setup();
121
- const ConfigCtor = RED._types['influxdb3-config'];
122
-
123
- const configNode = new ConfigCtor({
124
- host: 'https://example.com',
125
- database: 'metrics',
126
- name: 'Test',
127
- credentials: { token: 'token' }
128
- });
129
-
130
- configNode.getClient();
131
-
132
- const options = influxModule.__getLastClientOptions();
133
- expect(options.host).toBe('https://example.com/');
134
- expect(options.database).toBe('metrics');
135
- expect(options.token).toBe('token');
136
- });
137
-
138
- test('sets extra CA certificate path when configured', () => {
139
- const original = process.env.NODE_EXTRA_CA_CERTS;
140
- const { RED } = setup();
141
- const ConfigCtor = RED._types['influxdb3-config'];
142
-
143
- const configNode = new ConfigCtor({
144
- host: 'https://example.com',
145
- database: 'metrics',
146
- name: 'Test',
147
- caCertPath: path.join('C:', 'certs', 'root.pem'),
148
- credentials: { token: 'token' }
149
- });
150
-
151
- configNode.getClient();
152
-
153
- expect(process.env.NODE_EXTRA_CA_CERTS).toBe(path.join('C:', 'certs', 'root.pem'));
154
- process.env.NODE_EXTRA_CA_CERTS = original;
155
- });
156
- });
157
-
158
- describe('InfluxDB v3 write node', () => {
159
- test('writes line protocol from object payload', async () => {
160
- const { RED, influxModule } = setup();
161
- const ConfigCtor = RED._types['influxdb3-config'];
162
- const WriteCtor = RED._types['influxdb3-write'];
163
-
164
- const configNode = new ConfigCtor({
165
- host: 'https://example.com',
166
- database: 'metrics',
167
- name: 'Test',
168
- credentials: { token: 'token' }
169
- });
170
-
171
- const writeNode = new WriteCtor({
172
- influxdb: configNode,
173
- measurement: 'cpu',
174
- database: ''
175
- });
176
-
177
- const msg = {
178
- measurement: 'cpu',
179
- payload: {
180
- fields: {
181
- temperature: 21.5,
182
- count: 5
183
- },
184
- tags: { location: 'lab' },
185
- integers: ['count'],
186
- timestamp: 1700000000000
187
- }
188
- };
189
-
190
- const send = jest.fn();
191
- const done = jest.fn();
192
- await writeNode._handlers.input(msg, send, done);
193
-
194
- const point = influxModule.__getLastPoint();
195
- const client = influxModule.__getLastClientInstance();
196
-
197
- expect(point.floatFields.temperature).toBe(21.5);
198
- expect(point.integerFields.count).toBe(5);
199
- expect(point.tags.location).toBe('lab');
200
-
201
- expect(client.write).toHaveBeenCalledWith('lp:cpu', 'metrics');
202
- expect(send).toHaveBeenCalledWith(msg);
203
- expect(done).toHaveBeenCalled();
204
-
205
- if (writeNode._handlers.close) {
206
- writeNode._handlers.close();
207
- }
208
- });
209
-
210
- test('writes raw line protocol string with database override', async () => {
211
- const { RED, influxModule } = setup();
212
- const ConfigCtor = RED._types['influxdb3-config'];
213
- const WriteCtor = RED._types['influxdb3-write'];
214
-
215
- const configNode = new ConfigCtor({
216
- host: 'https://example.com',
217
- database: 'metrics',
218
- name: 'Test',
219
- credentials: { token: 'token' }
220
- });
221
-
222
- const writeNode = new WriteCtor({
223
- influxdb: configNode,
224
- measurement: '',
225
- database: ''
226
- });
227
-
228
- const msg = {
229
- payload: ' weather,location=lab temperature=18.5 ',
230
- database: 'override-db'
231
- };
232
-
233
- const send = jest.fn();
234
- const done = jest.fn();
235
- await writeNode._handlers.input(msg, send, done);
236
-
237
- const client = influxModule.__getLastClientInstance();
238
- expect(client.write).toHaveBeenCalledWith('weather,location=lab temperature=18.5', 'override-db');
239
- expect(send).toHaveBeenCalledWith(msg);
240
- expect(done).toHaveBeenCalled();
241
-
242
- if (writeNode._handlers.close) {
243
- writeNode._handlers.close();
244
- }
245
- });
246
- });
247
-
248
- // Helper to create a write node for addFieldToPoint / buildLineProtocol tests
249
- function createWriteNode() {
250
- const { RED, influxModule } = setup();
251
- const ConfigCtor = RED._types['influxdb3-config'];
252
- const WriteCtor = RED._types['influxdb3-write'];
253
-
254
- const configNode = new ConfigCtor({
255
- host: 'https://example.com',
256
- database: 'metrics',
257
- name: 'Test',
258
- credentials: { token: 'token' }
259
- });
260
-
261
- const writeNode = new WriteCtor({
262
- influxdb: configNode,
263
- measurement: 'test_measurement',
264
- database: ''
265
- });
266
-
267
- return { RED, influxModule, configNode, writeNode };
268
- }
269
-
270
- describe('addFieldToPoint – field type handling', () => {
271
- test('float fields by default for numbers', async () => {
272
- const { influxModule, writeNode } = createWriteNode();
273
- const msg = {
274
- measurement: 'sensor',
275
- payload: { fields: { temperature: 21.5, humidity: 60 } }
276
- };
277
- const send = jest.fn();
278
- const done = jest.fn();
279
- await writeNode._handlers.input(msg, send, done);
280
-
281
- const point = influxModule.__getLastPoint();
282
- expect(point.floatFields.temperature).toBe(21.5);
283
- expect(point.floatFields.humidity).toBe(60);
284
- expect(done).toHaveBeenCalled();
285
- expect(done.mock.calls[0][0]).toBeUndefined();
286
- });
287
-
288
- test('integer fields when listed in msg.payload.integers', async () => {
289
- const { influxModule, writeNode } = createWriteNode();
290
- const msg = {
291
- measurement: 'sensor',
292
- payload: {
293
- fields: { count: 42, temperature: 21.5 },
294
- integers: ['count']
295
- }
296
- };
297
- const send = jest.fn();
298
- const done = jest.fn();
299
- await writeNode._handlers.input(msg, send, done);
300
-
301
- const point = influxModule.__getLastPoint();
302
- expect(point.integerFields.count).toBe(42);
303
- expect(point.floatFields.temperature).toBe(21.5);
304
- });
305
-
306
- test('integer suffix string "42i" is parsed as integer', async () => {
307
- const { influxModule, writeNode } = createWriteNode();
308
- const msg = {
309
- measurement: 'sensor',
310
- payload: { fields: { count: '42i' } }
311
- };
312
- const send = jest.fn();
313
- const done = jest.fn();
314
- await writeNode._handlers.input(msg, send, done);
315
-
316
- const point = influxModule.__getLastPoint();
317
- expect(point.integerFields.count).toBe(42);
318
- });
319
-
320
- test('negative integer suffix string "-7i" is parsed as integer', async () => {
321
- const { influxModule, writeNode } = createWriteNode();
322
- const msg = {
323
- measurement: 'sensor',
324
- payload: { fields: { offset: '-7i' } }
325
- };
326
- const send = jest.fn();
327
- const done = jest.fn();
328
- await writeNode._handlers.input(msg, send, done);
329
-
330
- const point = influxModule.__getLastPoint();
331
- expect(point.integerFields.offset).toBe(-7);
332
- });
333
-
334
- test('regular strings are set as string fields', async () => {
335
- const { influxModule, writeNode } = createWriteNode();
336
- const msg = {
337
- measurement: 'sensor',
338
- payload: { fields: { status: 'ok' } }
339
- };
340
- const send = jest.fn();
341
- const done = jest.fn();
342
- await writeNode._handlers.input(msg, send, done);
343
-
344
- const point = influxModule.__getLastPoint();
345
- expect(point.stringFields.status).toBe('ok');
346
- });
347
-
348
- test('boolean fields are set correctly', async () => {
349
- const { influxModule, writeNode } = createWriteNode();
350
- const msg = {
351
- measurement: 'sensor',
352
- payload: { fields: { active: true, disabled: false } }
353
- };
354
- const send = jest.fn();
355
- const done = jest.fn();
356
- await writeNode._handlers.input(msg, send, done);
357
-
358
- const point = influxModule.__getLastPoint();
359
- expect(point.booleanFields.active).toBe(true);
360
- expect(point.booleanFields.disabled).toBe(false);
361
- });
362
-
363
- test('non-integer float is truncated with warning when marked as integer', async () => {
364
- const { influxModule, writeNode } = createWriteNode();
365
- const msg = {
366
- measurement: 'sensor',
367
- payload: {
368
- fields: { value: 3.7 },
369
- integers: ['value']
370
- }
371
- };
372
- const send = jest.fn();
373
- const done = jest.fn();
374
- await writeNode._handlers.input(msg, send, done);
375
-
376
- const point = influxModule.__getLastPoint();
377
- expect(point.integerFields.value).toBe(3);
378
- expect(writeNode.warn).toHaveBeenCalledWith(
379
- expect.stringContaining("marked as integer but value is 3.7")
380
- );
381
- });
382
- });
383
-
384
- describe('addFieldToPoint – enhanced error messages (issue #16)', () => {
385
- test('object field value produces detailed warning with type and value', async () => {
386
- const { writeNode } = createWriteNode();
387
- const msg = {
388
- measurement: 'sensor',
389
- payload: {
390
- fields: {
391
- good: 42,
392
- nested: { a: 1, b: 2 }
393
- }
394
- }
395
- };
396
- const send = jest.fn();
397
- const done = jest.fn();
398
- await writeNode._handlers.input(msg, send, done);
399
-
400
- expect(writeNode.warn).toHaveBeenCalledWith(
401
- expect.stringContaining("Skipping field 'nested': unsupported type 'object' (Object)")
402
- );
403
- expect(writeNode.warn).toHaveBeenCalledWith(
404
- expect.stringContaining('Actual value: {"a":1,"b":2}')
405
- );
406
- expect(writeNode.warn).toHaveBeenCalledWith(
407
- expect.stringContaining("must be a number, string, or boolean")
408
- );
409
- // Should still succeed for the valid field
410
- expect(send).toHaveBeenCalled();
411
- expect(done).toHaveBeenCalled();
412
- });
413
-
414
- test('array field value shows Array type name in warning', async () => {
415
- const { writeNode } = createWriteNode();
416
- const msg = {
417
- measurement: 'sensor',
418
- payload: {
419
- fields: {
420
- good: 1,
421
- values: [1, 2, 3]
422
- }
423
- }
424
- };
425
- const send = jest.fn();
426
- const done = jest.fn();
427
- await writeNode._handlers.input(msg, send, done);
428
-
429
- expect(writeNode.warn).toHaveBeenCalledWith(
430
- expect.stringContaining("unsupported type 'object' (Array)")
431
- );
432
- expect(writeNode.warn).toHaveBeenCalledWith(
433
- expect.stringContaining('Actual value: [1,2,3]')
434
- );
435
- });
436
-
437
- test('null field value produces clear warning', async () => {
438
- const { writeNode } = createWriteNode();
439
- const msg = {
440
- measurement: 'sensor',
441
- payload: {
442
- fields: {
443
- good: 1,
444
- broken: null
445
- }
446
- }
447
- };
448
- const send = jest.fn();
449
- const done = jest.fn();
450
- await writeNode._handlers.input(msg, send, done);
451
-
452
- expect(writeNode.warn).toHaveBeenCalledWith(
453
- expect.stringContaining("Skipping field 'broken': value is null")
454
- );
455
- });
456
-
457
- test('undefined field value produces clear warning', async () => {
458
- const { writeNode } = createWriteNode();
459
- const msg = {
460
- measurement: 'sensor',
461
- payload: {
462
- fields: {
463
- good: 1,
464
- missing: undefined
465
- }
466
- }
467
- };
468
- const send = jest.fn();
469
- const done = jest.fn();
470
- await writeNode._handlers.input(msg, send, done);
471
-
472
- expect(writeNode.warn).toHaveBeenCalledWith(
473
- expect.stringContaining("Skipping field 'missing': value is undefined")
474
- );
475
- });
476
-
477
- test('NaN field value is skipped with warning', async () => {
478
- const { writeNode } = createWriteNode();
479
- const msg = {
480
- measurement: 'sensor',
481
- payload: {
482
- fields: {
483
- good: 1,
484
- bad: NaN
485
- }
486
- }
487
- };
488
- const send = jest.fn();
489
- const done = jest.fn();
490
- await writeNode._handlers.input(msg, send, done);
491
-
492
- expect(writeNode.warn).toHaveBeenCalledWith(
493
- expect.stringContaining("Skipping field 'bad': numeric value is NaN (not finite)")
494
- );
495
- });
496
-
497
- test('Infinity field value is skipped with warning', async () => {
498
- const { writeNode } = createWriteNode();
499
- const msg = {
500
- measurement: 'sensor',
501
- payload: {
502
- fields: {
503
- good: 1,
504
- bad: Infinity
505
- }
506
- }
507
- };
508
- const send = jest.fn();
509
- const done = jest.fn();
510
- await writeNode._handlers.input(msg, send, done);
511
-
512
- expect(writeNode.warn).toHaveBeenCalledWith(
513
- expect.stringContaining("Skipping field 'bad': numeric value is Infinity (not finite)")
514
- );
515
- });
516
-
517
- test('warning includes measurement name as context', async () => {
518
- const { writeNode } = createWriteNode();
519
- const msg = {
520
- measurement: 'my_sensor',
521
- payload: {
522
- fields: {
523
- good: 1,
524
- broken: { nested: true }
525
- }
526
- }
527
- };
528
- const send = jest.fn();
529
- const done = jest.fn();
530
- await writeNode._handlers.input(msg, send, done);
531
-
532
- expect(writeNode.warn).toHaveBeenCalledWith(
533
- expect.stringContaining("(measurement: 'my_sensor')")
534
- );
535
- });
536
-
537
- test('all fields skipped produces error with payload dump', async () => {
538
- const { writeNode } = createWriteNode();
539
- const msg = {
540
- measurement: 'sensor',
541
- payload: {
542
- fields: {
543
- bad1: null,
544
- bad2: { nested: true }
545
- }
546
- }
547
- };
548
- const send = jest.fn();
549
- const done = jest.fn();
550
- await writeNode._handlers.input(msg, send, done);
551
-
552
- // done is called with an error when no valid fields remain
553
- expect(done).toHaveBeenCalledWith(expect.any(Error));
554
- expect(done.mock.calls[0][0].message).toContain('No valid fields to write');
555
- });
556
- });
557
-
558
- describe('buildLineProtocol – simplified payload format', () => {
559
- test('non-reserved keys are used as fields', async () => {
560
- const { influxModule, writeNode } = createWriteNode();
561
- const msg = {
562
- measurement: 'sensor',
563
- payload: {
564
- temperature: 21.5,
565
- humidity: 60,
566
- tags: { location: 'lab' }
567
- }
568
- };
569
- const send = jest.fn();
570
- const done = jest.fn();
571
- await writeNode._handlers.input(msg, send, done);
572
-
573
- const point = influxModule.__getLastPoint();
574
- expect(point.floatFields.temperature).toBe(21.5);
575
- expect(point.floatFields.humidity).toBe(60);
576
- expect(point.tags.location).toBe('lab');
577
- // Reserved keys should NOT appear as fields
578
- expect(point.floatFields.tags).toBeUndefined();
579
- expect(point.stringFields.tags).toBeUndefined();
580
- });
581
-
582
- test('reserved keys (tags, timestamp, integers, fields) are excluded from fields', async () => {
583
- const { influxModule, writeNode } = createWriteNode();
584
- const msg = {
585
- measurement: 'sensor',
586
- payload: {
587
- value: 42,
588
- tags: { location: 'lab' },
589
- timestamp: 1700000000000,
590
- integers: ['value']
591
- }
592
- };
593
- const send = jest.fn();
594
- const done = jest.fn();
595
- await writeNode._handlers.input(msg, send, done);
596
-
597
- const point = influxModule.__getLastPoint();
598
- expect(point.integerFields.value).toBe(42);
599
- // None of the reserved keys should appear as field entries
600
- expect(point.floatFields.tags).toBeUndefined();
601
- expect(point.floatFields.timestamp).toBeUndefined();
602
- expect(point.floatFields.integers).toBeUndefined();
603
- expect(point.floatFields.fields).toBeUndefined();
604
- });
605
- });
606
-
607
- describe('buildLineProtocol – timestamp handling', () => {
608
- test('numeric timestamp from msg.payload.timestamp', async () => {
609
- const { influxModule, writeNode } = createWriteNode();
610
- const msg = {
611
- measurement: 'sensor',
612
- payload: {
613
- fields: { value: 1 },
614
- timestamp: 1700000000000
615
- }
616
- };
617
- const send = jest.fn();
618
- const done = jest.fn();
619
- await writeNode._handlers.input(msg, send, done);
620
-
621
- const point = influxModule.__getLastPoint();
622
- expect(point.timestamp).toEqual(new Date(1700000000000));
623
- });
624
-
625
- test('fallback to msg.timestamp when payload.timestamp is absent', async () => {
626
- const { influxModule, writeNode } = createWriteNode();
627
- const msg = {
628
- measurement: 'sensor',
629
- timestamp: 1700000000000,
630
- payload: {
631
- fields: { value: 1 }
632
- }
633
- };
634
- const send = jest.fn();
635
- const done = jest.fn();
636
- await writeNode._handlers.input(msg, send, done);
637
-
638
- const point = influxModule.__getLastPoint();
639
- expect(point.timestamp).toEqual(new Date(1700000000000));
640
- });
641
-
642
- test('Date object timestamp is used directly', async () => {
643
- const { influxModule, writeNode } = createWriteNode();
644
- const date = new Date('2025-01-01T00:00:00Z');
645
- const msg = {
646
- measurement: 'sensor',
647
- payload: {
648
- fields: { value: 1 },
649
- timestamp: date
650
- }
651
- };
652
- const send = jest.fn();
653
- const done = jest.fn();
654
- await writeNode._handlers.input(msg, send, done);
655
-
656
- const point = influxModule.__getLastPoint();
657
- expect(point.timestamp).toEqual(date);
658
- });
659
-
660
- test('invalid timestamp string produces warning', async () => {
661
- const { writeNode } = createWriteNode();
662
- const msg = {
663
- measurement: 'sensor',
664
- payload: {
665
- fields: { value: 1 },
666
- timestamp: 'not-a-date'
667
- }
668
- };
669
- const send = jest.fn();
670
- const done = jest.fn();
671
- await writeNode._handlers.input(msg, send, done);
672
-
673
- expect(writeNode.warn).toHaveBeenCalledWith(
674
- expect.stringContaining("Invalid timestamp string: 'not-a-date'")
675
- );
676
- });
677
- });
678
-
679
- describe('buildLineProtocol – error cases', () => {
680
- test('missing measurement produces error', async () => {
681
- const { RED } = setup();
682
- const ConfigCtor = RED._types['influxdb3-config'];
683
- const WriteCtor = RED._types['influxdb3-write'];
684
-
685
- const configNode = new ConfigCtor({
686
- host: 'https://example.com',
687
- database: 'metrics',
688
- name: 'Test',
689
- credentials: { token: 'token' }
690
- });
691
-
692
- // No measurement on node
693
- const writeNode = new WriteCtor({
694
- influxdb: configNode,
695
- measurement: '',
696
- database: ''
697
- });
698
-
699
- const msg = {
700
- // No measurement on msg either
701
- payload: { fields: { value: 1 } }
702
- };
703
- const send = jest.fn();
704
- const done = jest.fn();
705
- await writeNode._handlers.input(msg, send, done);
706
-
707
- expect(done).toHaveBeenCalledWith(expect.any(Error));
708
- expect(done.mock.calls[0][0].message).toContain('Measurement not specified');
709
- });
710
-
711
- test('empty line protocol string produces error', async () => {
712
- const { RED } = setup();
713
- const ConfigCtor = RED._types['influxdb3-config'];
714
- const WriteCtor = RED._types['influxdb3-write'];
715
-
716
- const configNode = new ConfigCtor({
717
- host: 'https://example.com',
718
- database: 'metrics',
719
- name: 'Test',
720
- credentials: { token: 'token' }
721
- });
722
-
723
- const writeNode = new WriteCtor({
724
- influxdb: configNode,
725
- measurement: '',
726
- database: ''
727
- });
728
-
729
- const msg = {
730
- payload: ' '
731
- };
732
- const send = jest.fn();
733
- const done = jest.fn();
734
- await writeNode._handlers.input(msg, send, done);
735
-
736
- expect(done).toHaveBeenCalledWith(expect.any(Error));
737
- expect(done.mock.calls[0][0].message).toContain('Line protocol string is empty');
738
- });
739
-
740
- test('empty array produces error', async () => {
741
- const { writeNode } = createWriteNode();
742
- const msg = {
743
- payload: []
744
- };
745
- const send = jest.fn();
746
- const done = jest.fn();
747
- await writeNode._handlers.input(msg, send, done);
748
-
749
- expect(done).toHaveBeenCalledWith(expect.any(Error));
750
- expect(done.mock.calls[0][0].message).toContain('Payload array is empty');
751
- });
752
-
753
- test('array with invalid item type produces error', async () => {
754
- const { writeNode } = createWriteNode();
755
- const msg = {
756
- payload: [1, 2, 3]
757
- };
758
- const send = jest.fn();
759
- const done = jest.fn();
760
- await writeNode._handlers.input(msg, send, done);
761
-
762
- expect(done).toHaveBeenCalledWith(expect.any(Error));
763
- expect(done.mock.calls[0][0].message).toContain('Array item 0 has invalid format');
764
- });
765
- });
766
-
767
- describe('Array payload support', () => {
768
- test('writes array of line protocol strings', async () => {
769
- const { writeNode, influxModule } = createWriteNode();
770
- const msg = {
771
- payload: [
772
- 'temperature,location=room1 value=21.5',
773
- 'temperature,location=room2 value=19.8',
774
- 'humidity,location=room1 value=65'
775
- ]
776
- };
777
- const send = jest.fn();
778
- const done = jest.fn();
779
- await writeNode._handlers.input(msg, send, done);
780
-
781
- const client = influxModule.__getLastClientInstance();
782
- expect(client.write).toHaveBeenCalledWith(
783
- 'temperature,location=room1 value=21.5\ntemperature,location=room2 value=19.8\nhumidity,location=room1 value=65',
784
- 'metrics'
785
- );
786
- expect(send).toHaveBeenCalledWith(msg);
787
- expect(done).toHaveBeenCalled();
788
- });
789
-
790
- test('writes array of object payloads', async () => {
791
- const { writeNode, influxModule } = createWriteNode();
792
- const msg = {
793
- payload: [
794
- {
795
- measurement: 'temperature',
796
- fields: { value: 21.5 },
797
- tags: { location: 'room1' }
798
- },
799
- {
800
- measurement: 'temperature',
801
- fields: { value: 19.8 },
802
- tags: { location: 'room2' }
803
- },
804
- {
805
- measurement: 'humidity',
806
- fields: { value: 65 },
807
- tags: { location: 'room1' }
808
- }
809
- ]
810
- };
811
- const send = jest.fn();
812
- const done = jest.fn();
813
- await writeNode._handlers.input(msg, send, done);
814
-
815
- const client = influxModule.__getLastClientInstance();
816
- expect(client.write).toHaveBeenCalled();
817
- expect(send).toHaveBeenCalledWith(msg);
818
- expect(done).toHaveBeenCalled();
819
- });
820
-
821
- test('writes mixed array of objects and line protocol strings', async () => {
822
- const { writeNode, influxModule } = createWriteNode();
823
- const msg = {
824
- payload: [
825
- 'temperature,location=room1 value=21.5',
826
- {
827
- measurement: 'humidity',
828
- fields: { value: 65 },
829
- tags: { location: 'room1' }
830
- },
831
- 'pressure,location=room1 value=1013.25'
832
- ]
833
- };
834
- const send = jest.fn();
835
- const done = jest.fn();
836
- await writeNode._handlers.input(msg, send, done);
837
-
838
- const client = influxModule.__getLastClientInstance();
839
- expect(client.write).toHaveBeenCalled();
840
- expect(send).toHaveBeenCalledWith(msg);
841
- expect(done).toHaveBeenCalled();
842
- });
843
-
844
- test('array objects can use msg.measurement as fallback', async () => {
845
- const { writeNode, influxModule } = createWriteNode();
846
- const msg = {
847
- measurement: 'temperature',
848
- payload: [
849
- {
850
- fields: { value: 21.5 },
851
- tags: { location: 'room1' }
852
- },
853
- {
854
- fields: { value: 19.8 },
855
- tags: { location: 'room2' }
856
- }
857
- ]
858
- };
859
- const send = jest.fn();
860
- const done = jest.fn();
861
- await writeNode._handlers.input(msg, send, done);
862
-
863
- const client = influxModule.__getLastClientInstance();
864
- expect(client.write).toHaveBeenCalled();
865
- expect(send).toHaveBeenCalledWith(msg);
866
- expect(done).toHaveBeenCalled();
867
- });
868
-
869
- test('array item error includes item index', async () => {
870
- const { RED, influxModule } = setup();
871
- const ConfigCtor = RED._types['influxdb3-config'];
872
- const WriteCtor = RED._types['influxdb3-write'];
873
-
874
- const configNode = new ConfigCtor({
875
- host: 'https://example.com',
876
- database: 'metrics',
877
- name: 'Test',
878
- credentials: { token: 'token' }
879
- });
880
-
881
- // Create node without default measurement
882
- const writeNode = new WriteCtor({
883
- influxdb: configNode,
884
- measurement: '', // No default measurement
885
- database: ''
886
- });
887
-
888
- const msg = {
889
- payload: [
890
- 'temperature,location=room1 value=21.5',
891
- {
892
- // Missing measurement and no fallback
893
- fields: { value: 19.8 }
894
- }
895
- ]
896
- };
897
- const send = jest.fn();
898
- const done = jest.fn();
899
- await writeNode._handlers.input(msg, send, done);
900
-
901
- expect(done).toHaveBeenCalledWith(expect.any(Error));
902
- expect(done.mock.calls[0][0].message).toContain('Array item 1');
903
- });
904
-
905
- test('array with empty string produces error', async () => {
906
- const { writeNode } = createWriteNode();
907
- const msg = {
908
- payload: [
909
- 'temperature,location=room1 value=21.5',
910
- ' ',
911
- 'humidity,location=room1 value=65'
912
- ]
913
- };
914
- const send = jest.fn();
915
- const done = jest.fn();
916
- await writeNode._handlers.input(msg, send, done);
917
-
918
- expect(done).toHaveBeenCalledWith(expect.any(Error));
919
- expect(done.mock.calls[0][0].message).toContain('Array item 1 is an empty string');
920
- });
921
-
922
- test('array with invalid line protocol produces error with index', async () => {
923
- const { writeNode } = createWriteNode();
924
- const msg = {
925
- payload: [
926
- 'temperature,location=room1 value=21.5',
927
- 'invalid line protocol no equals',
928
- 'humidity,location=room1 value=65'
929
- ]
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 1');
937
- });
938
- });
939
-
940
- describe('InfluxDB v3 config node – credentials warning', () => {
941
- test('logs warning when credentials object is undefined', () => {
942
- const RED = buildRED();
943
-
944
- // Override createNode to NOT set credentials
945
- RED.nodes.createNode = function(node, _config) {
946
- node.status = jest.fn();
947
- node.error = jest.fn();
948
- node.warn = jest.fn();
949
- node.send = jest.fn();
950
- node.on = jest.fn((event, handler) => {
951
- node._handlers = node._handlers || {};
952
- node._handlers[event] = handler;
953
- });
954
- // Deliberately NOT setting node.credentials
955
- };
956
-
957
- require('../influxdb3.js')(RED);
958
- const ConfigCtor = RED._types['influxdb3-config'];
959
-
960
- const configNode = new ConfigCtor({
961
- host: 'https://example.com',
962
- database: 'metrics',
963
- name: 'Test'
964
- });
965
-
966
- expect(RED.log.warn).toHaveBeenCalledWith(
967
- 'InfluxDB v3 config: credentials object is undefined'
968
- );
969
- expect(configNode.token).toBeUndefined();
970
- });
971
- });
972
-