node-red-contrib-aedes 0.15.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,777 @@
1
+ /* eslint-env mocha */
2
+ const helper = require('node-red-node-test-helper');
3
+ const aedesNode = require('../aedes.js');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const should = require('should');
8
+
9
+ let tmpDir;
10
+
11
+ helper.init(require.resolve('node-red'));
12
+
13
+ describe('Aedes Broker Persistence - Group 1: Happy Path', function () {
14
+ beforeEach(function (done) {
15
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aedes-test-'));
16
+ helper.init(require.resolve('node-red'), { userDir: tmpDir });
17
+ helper.startServer(done);
18
+ });
19
+
20
+ afterEach(function (done) {
21
+ helper.unload().then(function () {
22
+ helper.stopServer(function () {
23
+ fs.rmSync(tmpDir, { recursive: true, force: true });
24
+ done();
25
+ });
26
+ });
27
+ });
28
+
29
+ it('should not create snapshot file when persist_to_file is false', function (done) {
30
+ this.timeout(10000);
31
+ const flow = [{
32
+ id: 'n1',
33
+ type: 'aedes broker',
34
+ mqtt_port: '1890',
35
+ persist_to_file: false,
36
+ name: 'Aedes Persist Test',
37
+ wires: [[], []]
38
+ }];
39
+ helper.load(aedesNode, flow, function () {
40
+ const n1 = helper.getNode('n1');
41
+ n1._initPromise.then(function () {
42
+ // After bug fix 11, properties are initialized (not undefined)
43
+ n1._persistEnabled.should.equal(false);
44
+ should.not.exist(n1._snapshotInterval);
45
+ const files = fs.readdirSync(tmpDir).filter(function (f) {
46
+ return f.startsWith('aedes-persist-');
47
+ });
48
+ files.length.should.equal(0);
49
+ done();
50
+ });
51
+ });
52
+ });
53
+
54
+ it('should write snapshot file on close when persist_to_file is true', function (done) {
55
+ this.timeout(10000);
56
+ const flow = [{
57
+ id: 'n1',
58
+ type: 'aedes broker',
59
+ mqtt_port: '1890',
60
+ persist_to_file: true,
61
+ name: 'Aedes Persist Test',
62
+ wires: [[], []]
63
+ }];
64
+ helper.load(aedesNode, flow, function () {
65
+ const n1 = helper.getNode('n1');
66
+ n1._initPromise.then(function () {
67
+ const persistFile = n1._persistFile;
68
+ should.exist(persistFile);
69
+ helper.unload().then(function () {
70
+ fs.existsSync(persistFile).should.be.true();
71
+ done();
72
+ });
73
+ });
74
+ });
75
+ });
76
+
77
+ it('should restore retained message from snapshot on start', function (done) {
78
+ this.timeout(10000);
79
+ const snapshotData = {
80
+ retained: {
81
+ 'test/restore': {
82
+ topic: 'test/restore',
83
+ payload: Buffer.from('hello').toString('base64'),
84
+ qos: 0,
85
+ retain: true,
86
+ cmd: 'publish'
87
+ }
88
+ },
89
+ subscriptions: {}
90
+ };
91
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
92
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshotData), 'utf8');
93
+
94
+ const flow = [{
95
+ id: 'n1',
96
+ type: 'aedes broker',
97
+ mqtt_port: '1890',
98
+ persist_to_file: true,
99
+ name: 'Aedes Persist Test',
100
+ wires: [[], []]
101
+ }];
102
+ helper.load(aedesNode, flow, function () {
103
+ const n1 = helper.getNode('n1');
104
+ n1._initPromise.then(function () {
105
+ const stream = n1._broker.persistence.createRetainedStream('#');
106
+ console.log('Created retained stream, waiting for data...');
107
+ const packets = [];
108
+ stream.on('data', function (packet) {
109
+ packets.push(packet);
110
+ });
111
+ stream.on('end', function () {
112
+ packets.length.should.equal(1);
113
+ packets[0].topic.should.equal('test/restore');
114
+ packets[0].payload.toString().should.equal('hello');
115
+ done();
116
+ });
117
+ });
118
+ });
119
+ });
120
+
121
+ it('should write empty retained for a fresh broker', function (done) {
122
+ this.timeout(10000);
123
+ const flow = [{
124
+ id: 'n1',
125
+ type: 'aedes broker',
126
+ mqtt_port: '1890',
127
+ persist_to_file: true,
128
+ name: 'Aedes Persist Test',
129
+ wires: [[], []]
130
+ }];
131
+ helper.load(aedesNode, flow, function () {
132
+ const n1 = helper.getNode('n1');
133
+ n1._initPromise.then(function () {
134
+ const persistFile = n1._persistFile;
135
+ should.exist(persistFile);
136
+ helper.unload().then(function () {
137
+ const stat = fs.statSync(persistFile);
138
+ stat.isFile().should.be.true();
139
+ const data = JSON.parse(fs.readFileSync(persistFile, 'utf8'));
140
+ data.should.have.property('retained');
141
+ Object.keys(data.retained).length.should.equal(0);
142
+ done();
143
+ }).catch(done);
144
+ });
145
+ });
146
+ });
147
+
148
+ it('should set up periodic save interval and persist retained messages', function (done) {
149
+ this.timeout(10000);
150
+ const flow = [{
151
+ id: 'n1',
152
+ type: 'aedes broker',
153
+ mqtt_port: '1890',
154
+ persist_to_file: true,
155
+ name: 'Aedes Persist Test',
156
+ wires: [[], []]
157
+ }];
158
+ helper.load(aedesNode, flow, function () {
159
+ const n1 = helper.getNode('n1');
160
+ n1._initPromise.then(function () {
161
+ // Verify periodic save interval is set up
162
+ should.exist(n1._snapshotInterval);
163
+
164
+ // Store a retained message via public API
165
+ const filePath = n1._persistFile;
166
+ n1._broker.persistence
167
+ .storeRetained({
168
+ topic: 'test/periodic',
169
+ payload: Buffer.from('periodic-data'),
170
+ qos: 0,
171
+ retain: true,
172
+ cmd: 'publish'
173
+ })
174
+ .then(function () {
175
+ return helper.unload();
176
+ })
177
+ .then(function () {
178
+ const stat = fs.statSync(filePath);
179
+ stat.isFile().should.be.true();
180
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
181
+ data.retained.should.have.property('test/periodic');
182
+ done();
183
+ })
184
+ .catch(done);
185
+ });
186
+ });
187
+ });
188
+ });
189
+
190
+ describe('Aedes Broker Persistence - Group 2: Snapshot Integrity', function () {
191
+ beforeEach(function (done) {
192
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aedes-test-'));
193
+ helper.init(require.resolve('node-red'), { userDir: tmpDir });
194
+ helper.startServer(done);
195
+ });
196
+
197
+ afterEach(function (done) {
198
+ helper.unload().then(function () {
199
+ helper.stopServer(function () {
200
+ fs.rmSync(tmpDir, { recursive: true, force: true });
201
+ done();
202
+ });
203
+ });
204
+ });
205
+
206
+ it('should survive base64 encode/decode round-trip for binary payloads', function (done) {
207
+ this.timeout(10000);
208
+ // Create a binary buffer with all byte values 0x00-0xFF
209
+ const binaryPayload = Buffer.alloc(256);
210
+ for (let i = 0; i < 256; i++) {
211
+ binaryPayload[i] = i;
212
+ }
213
+ const snapshotData = {
214
+ retained: {
215
+ 'test/binary': {
216
+ topic: 'test/binary',
217
+ payload: binaryPayload.toString('base64'),
218
+ qos: 1,
219
+ retain: true,
220
+ cmd: 'publish'
221
+ }
222
+ },
223
+ subscriptions: {}
224
+ };
225
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
226
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshotData), 'utf8');
227
+
228
+ const flow = [{
229
+ id: 'n1',
230
+ type: 'aedes broker',
231
+ mqtt_port: '1891',
232
+ persist_to_file: true,
233
+ name: 'Aedes Persist Test',
234
+ wires: [[], []]
235
+ }];
236
+ helper.load(aedesNode, flow, function () {
237
+ const n1 = helper.getNode('n1');
238
+ n1._initPromise.then(function () {
239
+ // Verify the binary payload was restored correctly
240
+ const stream = n1._broker.persistence.createRetainedStream('#');
241
+ const packets = [];
242
+ stream.on('data', function (packet) {
243
+ packets.push(packet);
244
+ });
245
+ stream.on('end', function () {
246
+ packets.length.should.equal(1);
247
+ packets[0].topic.should.equal('test/binary');
248
+ const restoredPayload = Buffer.from(packets[0].payload);
249
+ restoredPayload.length.should.equal(256);
250
+ // Verify every byte survived the round-trip
251
+ for (let i = 0; i < 256; i++) {
252
+ restoredPayload[i].should.equal(i);
253
+ }
254
+ // Now save and verify the base64 re-encoding is identical
255
+ const persistFile = n1._persistFile;
256
+ helper.unload().then(function () {
257
+ const saved = JSON.parse(fs.readFileSync(persistFile, 'utf8'));
258
+ saved.retained['test/binary'].payload.should.equal(binaryPayload.toString('base64'));
259
+ done();
260
+ }).catch(done);
261
+ });
262
+ });
263
+ });
264
+ });
265
+
266
+ it('should save and restore multiple retained messages', function (done) {
267
+ this.timeout(10000);
268
+ const snapshotData = {
269
+ retained: {
270
+ 'home/temperature': {
271
+ topic: 'home/temperature',
272
+ payload: Buffer.from('22.5').toString('base64'),
273
+ qos: 0,
274
+ retain: true,
275
+ cmd: 'publish'
276
+ },
277
+ 'home/humidity': {
278
+ topic: 'home/humidity',
279
+ payload: Buffer.from('65').toString('base64'),
280
+ qos: 1,
281
+ retain: true,
282
+ cmd: 'publish'
283
+ },
284
+ 'home/status': {
285
+ topic: 'home/status',
286
+ payload: Buffer.from('online').toString('base64'),
287
+ qos: 2,
288
+ retain: true,
289
+ cmd: 'publish'
290
+ }
291
+ },
292
+ subscriptions: {}
293
+ };
294
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
295
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshotData), 'utf8');
296
+
297
+ const flow = [{
298
+ id: 'n1',
299
+ type: 'aedes broker',
300
+ mqtt_port: '1891',
301
+ persist_to_file: true,
302
+ name: 'Aedes Persist Test',
303
+ wires: [[], []]
304
+ }];
305
+ helper.load(aedesNode, flow, function () {
306
+ const n1 = helper.getNode('n1');
307
+ n1._initPromise.then(function () {
308
+ const stream = n1._broker.persistence.createRetainedStream('#');
309
+ const packets = [];
310
+ stream.on('data', function (packet) {
311
+ packets.push(packet);
312
+ });
313
+ stream.on('end', function () {
314
+ packets.length.should.equal(3);
315
+ const byTopic = {};
316
+ packets.forEach(function (p) { byTopic[p.topic] = p; });
317
+ byTopic['home/temperature'].payload.toString().should.equal('22.5');
318
+ byTopic['home/humidity'].payload.toString().should.equal('65');
319
+ byTopic['home/humidity'].qos.should.equal(1);
320
+ byTopic['home/status'].payload.toString().should.equal('online');
321
+ byTopic['home/status'].qos.should.equal(2);
322
+ done();
323
+ });
324
+ });
325
+ });
326
+ });
327
+
328
+ it('should skip empty-payload retained messages (deletion signal)', function (done) {
329
+ this.timeout(10000);
330
+ const flow = [{
331
+ id: 'n1',
332
+ type: 'aedes broker',
333
+ mqtt_port: '1891',
334
+ persist_to_file: true,
335
+ name: 'Aedes Persist Test',
336
+ wires: [[], []]
337
+ }];
338
+ helper.load(aedesNode, flow, function () {
339
+ const n1 = helper.getNode('n1');
340
+ n1._initPromise.then(function () {
341
+ // Store a retained message, then delete it with empty payload
342
+ n1._broker.persistence.storeRetained({
343
+ topic: 'test/delete-me',
344
+ payload: Buffer.from('will-be-deleted'),
345
+ qos: 0,
346
+ retain: true,
347
+ cmd: 'publish'
348
+ }).then(function () {
349
+ // MQTT delete: retain with empty payload
350
+ return n1._broker.persistence.storeRetained({
351
+ topic: 'test/delete-me',
352
+ payload: Buffer.alloc(0),
353
+ qos: 0,
354
+ retain: true,
355
+ cmd: 'publish'
356
+ });
357
+ }).then(function () {
358
+ // Also store one that should survive
359
+ return n1._broker.persistence.storeRetained({
360
+ topic: 'test/keep-me',
361
+ payload: Buffer.from('keeper'),
362
+ qos: 0,
363
+ retain: true,
364
+ cmd: 'publish'
365
+ });
366
+ }).then(function () {
367
+ const persistFile = n1._persistFile;
368
+ return helper.unload().then(function () {
369
+ const stat = fs.statSync(persistFile);
370
+ stat.isFile().should.be.true();
371
+ const data = JSON.parse(fs.readFileSync(persistFile, 'utf8'));
372
+ // The deleted topic should not appear in the snapshot
373
+ data.retained.should.not.have.property('test/delete-me');
374
+ // The kept topic should be present
375
+ data.retained.should.have.property('test/keep-me');
376
+ done();
377
+ });
378
+ }).catch(done);
379
+ });
380
+ });
381
+ });
382
+
383
+ it('should warn and start with empty state on corrupt JSON file', function (done) {
384
+ this.timeout(10000);
385
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
386
+ fs.writeFileSync(snapshotPath, '{ this is not valid JSON!!!', 'utf8');
387
+
388
+ const flow = [{
389
+ id: 'n1',
390
+ type: 'aedes broker',
391
+ mqtt_port: '1891',
392
+ persist_to_file: true,
393
+ name: 'Aedes Persist Test',
394
+ wires: [[], []]
395
+ }];
396
+ helper.load(aedesNode, flow, function () {
397
+ const n1 = helper.getNode('n1');
398
+ n1._initPromise.then(function () {
399
+ // Broker should still be running with persistence enabled
400
+ n1._persistEnabled.should.be.true();
401
+ // Verify no retained messages were loaded
402
+ const stream = n1._broker.persistence.createRetainedStream('#');
403
+ const packets = [];
404
+ stream.on('data', function (packet) {
405
+ packets.push(packet);
406
+ });
407
+ stream.on('end', function () {
408
+ packets.length.should.equal(0);
409
+ done();
410
+ });
411
+ });
412
+ });
413
+ });
414
+
415
+ it('should warn and start with empty state on wrong schema (array instead of object)', function (done) {
416
+ this.timeout(10000);
417
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
418
+ // Valid JSON but wrong schema — array instead of object
419
+ fs.writeFileSync(snapshotPath, JSON.stringify([1, 2, 3]), 'utf8');
420
+
421
+ const flow = [{
422
+ id: 'n1',
423
+ type: 'aedes broker',
424
+ mqtt_port: '1891',
425
+ persist_to_file: true,
426
+ name: 'Aedes Persist Test',
427
+ wires: [[], []]
428
+ }];
429
+ helper.load(aedesNode, flow, function () {
430
+ const n1 = helper.getNode('n1');
431
+ n1._initPromise.then(function () {
432
+ // Broker should still be running with persistence enabled
433
+ n1._persistEnabled.should.be.true();
434
+ // Verify no retained messages were loaded
435
+ const stream = n1._broker.persistence.createRetainedStream('#');
436
+ const packets = [];
437
+ stream.on('data', function (packet) {
438
+ packets.push(packet);
439
+ });
440
+ stream.on('end', function () {
441
+ packets.length.should.equal(0);
442
+ done();
443
+ });
444
+ });
445
+ });
446
+ });
447
+ });
448
+ describe('Aedes Broker Persistence - Group 3: Filesystem Errors', function () {
449
+ beforeEach(function (done) {
450
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aedes-test-'));
451
+ helper.init(require.resolve('node-red'), { userDir: tmpDir });
452
+ helper.startServer(done);
453
+ });
454
+
455
+ afterEach(function (done) {
456
+ helper.unload().then(function () {
457
+ helper.stopServer(function () {
458
+ fs.rmSync(tmpDir, { recursive: true, force: true });
459
+ done();
460
+ });
461
+ });
462
+ });
463
+
464
+ it('should warn and fall back to pure in-memory when userDir is not writable', function (done) {
465
+ this.timeout(10000);
466
+ // Make tmpDir read-only
467
+ fs.chmodSync(tmpDir, 0o444);
468
+
469
+ const flow = [{
470
+ id: 'n1',
471
+ type: 'aedes broker',
472
+ mqtt_port: '1892',
473
+ persist_to_file: true,
474
+ name: 'Aedes Persist Test',
475
+ wires: [[], []]
476
+ }];
477
+ helper.load(aedesNode, flow, function () {
478
+ const n1 = helper.getNode('n1');
479
+ n1._initPromise.then(function () {
480
+ // Verify persistence was disabled due to write check failure
481
+ n1._persistEnabled.should.equal(false);
482
+ should.not.exist(n1._snapshotInterval);
483
+ // Broker should still be running
484
+ should.exist(n1._broker);
485
+ // Restore write permission for cleanup
486
+ fs.chmodSync(tmpDir, 0o755);
487
+ done();
488
+ }).catch(function (err) {
489
+ fs.chmodSync(tmpDir, 0o755);
490
+ done(err);
491
+ });
492
+ });
493
+ });
494
+
495
+ it('should warn and start with empty state when snapshot file is not readable', function (done) {
496
+ this.timeout(10000);
497
+ // Create a snapshot file and make it unreadable
498
+ const snapshotData = {
499
+ retained: {
500
+ 'test/should-not-load': {
501
+ topic: 'test/should-not-load',
502
+ payload: Buffer.from('should-not-load').toString('base64'),
503
+ qos: 0,
504
+ retain: true,
505
+ cmd: 'publish'
506
+ }
507
+ },
508
+ subscriptions: {}
509
+ };
510
+ const snapshotPath = path.join(tmpDir, 'aedes-persist-n1.json');
511
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshotData), 'utf8');
512
+ fs.chmodSync(snapshotPath, 0o000); // Make unreadable
513
+
514
+ const flow = [{
515
+ id: 'n1',
516
+ type: 'aedes broker',
517
+ mqtt_port: '1893',
518
+ persist_to_file: true,
519
+ name: 'Aedes Persist Test',
520
+ wires: [[], []]
521
+ }];
522
+ helper.load(aedesNode, flow, function () {
523
+ const n1 = helper.getNode('n1');
524
+ n1._initPromise.then(function () {
525
+ // Persistence should be enabled (writable dir check passed)
526
+ n1._persistEnabled.should.equal(true);
527
+ // But the snapshot should not have been loaded due to read permission issue
528
+ const stream = n1._broker.persistence.createRetainedStream('#');
529
+ const packets = [];
530
+ stream.on('data', function (packet) {
531
+ packets.push(packet);
532
+ });
533
+ stream.on('end', function () {
534
+ // No retained messages should be loaded
535
+ packets.length.should.equal(0);
536
+ // Restore read permission for cleanup
537
+ fs.chmodSync(snapshotPath, 0o644);
538
+ done();
539
+ });
540
+ }).catch(function (err) {
541
+ fs.chmodSync(snapshotPath, 0o644);
542
+ done(err);
543
+ });
544
+ });
545
+ });
546
+
547
+ it('should warn and continue when snapshot write fails (e.g., permission denied on close)', function (done) {
548
+ this.timeout(10000);
549
+ const flow = [{
550
+ id: 'n1',
551
+ type: 'aedes broker',
552
+ mqtt_port: '1894',
553
+ persist_to_file: true,
554
+ name: 'Aedes Persist Test',
555
+ wires: [[], []]
556
+ }];
557
+ helper.load(aedesNode, flow, function () {
558
+ const n1 = helper.getNode('n1');
559
+ n1._initPromise.then(function () {
560
+ n1._persistEnabled.should.equal(true);
561
+ should.exist(n1._persistFile);
562
+
563
+ // Make userDir read-only before unload to simulate write failure on close
564
+ fs.chmodSync(tmpDir, 0o555);
565
+
566
+ helper.unload().then(function () {
567
+ // Node should unload cleanly despite write failure
568
+ // Restore permissions for cleanup
569
+ fs.chmodSync(tmpDir, 0o755);
570
+ done();
571
+ }).catch(function (err) {
572
+ fs.chmodSync(tmpDir, 0o755);
573
+ done(err);
574
+ });
575
+ });
576
+ });
577
+ });
578
+
579
+ it('should gracefully handle missing snapshot file and start with empty state', function (done) {
580
+ this.timeout(10000);
581
+ // Explicitly do NOT create a snapshot file
582
+ const flow = [{
583
+ id: 'n1',
584
+ type: 'aedes broker',
585
+ mqtt_port: '1895',
586
+ persist_to_file: true,
587
+ name: 'Aedes Persist Test',
588
+ wires: [[], []]
589
+ }];
590
+ helper.load(aedesNode, flow, function () {
591
+ const n1 = helper.getNode('n1');
592
+ n1._initPromise.then(function () {
593
+ n1._persistEnabled.should.equal(true);
594
+ should.exist(n1._persistFile);
595
+ // Verify no retained messages are present
596
+ const stream = n1._broker.persistence.createRetainedStream('#');
597
+ const packets = [];
598
+ stream.on('data', function (packet) {
599
+ packets.push(packet);
600
+ });
601
+ stream.on('end', function () {
602
+ packets.length.should.equal(0);
603
+ done();
604
+ });
605
+ });
606
+ });
607
+ });
608
+ });
609
+
610
+ describe('Aedes Broker Persistence - Group 4: Configuration', function () {
611
+ beforeEach(function (done) {
612
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aedes-test-'));
613
+ helper.init(require.resolve('node-red'), { userDir: tmpDir });
614
+ helper.startServer(done);
615
+ });
616
+
617
+ afterEach(function (done) {
618
+ helper.unload().then(function () {
619
+ helper.stopServer(function () {
620
+ fs.rmSync(tmpDir, { recursive: true, force: true });
621
+ done();
622
+ });
623
+ });
624
+ });
625
+
626
+ it('should not create snapshot file when persist_to_file is false', function (done) {
627
+ this.timeout(10000);
628
+ const flow = [{
629
+ id: 'n1',
630
+ type: 'aedes broker',
631
+ mqtt_port: '1896',
632
+ persist_to_file: false,
633
+ name: 'Aedes No Persist Test',
634
+ wires: [[], []]
635
+ }];
636
+ helper.load(aedesNode, flow, function () {
637
+ const n1 = helper.getNode('n1');
638
+ n1._initPromise.then(function () {
639
+ // Verify persistence is disabled
640
+ n1._persistEnabled.should.equal(false);
641
+ should.not.exist(n1._snapshotInterval);
642
+ should.not.exist(n1._persistFile);
643
+ // Store a retained message
644
+ n1._broker.persistence.storeRetained({
645
+ topic: 'test/no-persist',
646
+ payload: Buffer.from('should-not-persist'),
647
+ qos: 0,
648
+ retain: true,
649
+ cmd: 'publish'
650
+ }).then(function () {
651
+ return helper.unload();
652
+ }).then(function () {
653
+ // Verify no snapshot file was created
654
+ const files = fs.readdirSync(tmpDir).filter(function (f) {
655
+ return f.startsWith('aedes-persist-');
656
+ });
657
+ files.length.should.equal(0);
658
+ done();
659
+ }).catch(done);
660
+ });
661
+ });
662
+ });
663
+
664
+ it('should assign separate snapshot files to different broker instances', function (done) {
665
+ this.timeout(10000);
666
+ const flow = [
667
+ {
668
+ id: 'n1',
669
+ type: 'aedes broker',
670
+ mqtt_port: '1897',
671
+ persist_to_file: true,
672
+ name: 'Broker 1',
673
+ wires: [[], []]
674
+ },
675
+ {
676
+ id: 'n2',
677
+ type: 'aedes broker',
678
+ mqtt_port: '1898',
679
+ persist_to_file: true,
680
+ name: 'Broker 2',
681
+ wires: [[], []]
682
+ }
683
+ ];
684
+ helper.load(aedesNode, flow, function () {
685
+ const n1 = helper.getNode('n1');
686
+ const n2 = helper.getNode('n2');
687
+
688
+ Promise.all([n1._initPromise, n2._initPromise]).then(function () {
689
+ // Both should have persistence enabled
690
+ n1._persistEnabled.should.equal(true);
691
+ n2._persistEnabled.should.equal(true);
692
+
693
+ // Files should be different (based on node.id)
694
+ const file1 = n1._persistFile;
695
+ const file2 = n2._persistFile;
696
+ should.exist(file1);
697
+ should.exist(file2);
698
+ file1.should.not.equal(file2);
699
+
700
+ // Store different retained messages in each broker
701
+ return Promise.all([
702
+ n1._broker.persistence.storeRetained({
703
+ topic: 'broker1/test',
704
+ payload: Buffer.from('broker1-data'),
705
+ qos: 0,
706
+ retain: true,
707
+ cmd: 'publish'
708
+ }),
709
+ n2._broker.persistence.storeRetained({
710
+ topic: 'broker2/test',
711
+ payload: Buffer.from('broker2-data'),
712
+ qos: 0,
713
+ retain: true,
714
+ cmd: 'publish'
715
+ })
716
+ ]);
717
+ }).then(function () {
718
+ return helper.unload();
719
+ }).then(function () {
720
+ // Verify both files exist and contain their own data
721
+ const file1 = path.join(tmpDir, 'aedes-persist-n1.json');
722
+ const file2 = path.join(tmpDir, 'aedes-persist-n2.json');
723
+ fs.existsSync(file1).should.be.true();
724
+ fs.existsSync(file2).should.be.true();
725
+
726
+ const data1 = JSON.parse(fs.readFileSync(file1, 'utf8'));
727
+ const data2 = JSON.parse(fs.readFileSync(file2, 'utf8'));
728
+
729
+ // Each should have only its own retained message
730
+ data1.retained.should.have.property('broker1/test');
731
+ data1.retained.should.not.have.property('broker2/test');
732
+ data2.retained.should.have.property('broker2/test');
733
+ data2.retained.should.not.have.property('broker1/test');
734
+
735
+ done();
736
+ }).catch(done);
737
+ });
738
+ });
739
+
740
+ it('should clear interval and save final snapshot on close', function (done) {
741
+ this.timeout(10000);
742
+ const flow = [{
743
+ id: 'n1',
744
+ type: 'aedes broker',
745
+ mqtt_port: '1899',
746
+ persist_to_file: true,
747
+ name: 'Aedes Persist Test',
748
+ wires: [[], []]
749
+ }];
750
+ helper.load(aedesNode, flow, function () {
751
+ const n1 = helper.getNode('n1');
752
+ n1._initPromise.then(function () {
753
+ // Verify interval is set
754
+ should.exist(n1._snapshotInterval);
755
+
756
+ // Store a retained message
757
+ return n1._broker.persistence.storeRetained({
758
+ topic: 'test/close',
759
+ payload: Buffer.from('close-test'),
760
+ qos: 0,
761
+ retain: true,
762
+ cmd: 'publish'
763
+ }).then(function () {
764
+ return helper.unload();
765
+ });
766
+ }).then(function () {
767
+ // Verify the snapshot file was created with the retained message
768
+ const persistFile = path.join(tmpDir, 'aedes-persist-n1.json');
769
+ fs.existsSync(persistFile).should.be.true();
770
+ const data = JSON.parse(fs.readFileSync(persistFile, 'utf8'));
771
+ data.retained.should.have.property('test/close');
772
+ data.retained['test/close'].payload.should.equal(Buffer.from('close-test').toString('base64'));
773
+ done();
774
+ }).catch(done);
775
+ });
776
+ });
777
+ });