import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import test from 'node:test'; import { openHarborStorage } from '../../dist/server/services/harborStorage.js'; import { migrateStoredState } from '../../dist/server/services/stateStore.js'; import { migrateDeviceInventoryState } from '../../dist/server/services/deviceInventoryService.js'; import { createActivityJournalService } from '../../dist/server/services/activityJournalService.js'; function fixture(t) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-sqlite-')); const stores = []; t.after(() => { for (const store of stores) if (store.db.isOpen) store.close(); fs.rmSync(directory, { recursive: true, force: true }); }); return { directory, open: () => { const store = openHarborStorage(directory); stores.push(store); return store; } }; } test('atomic import preserves revisions, rules, device checkpoints and journal; JSON becomes backup only', (t) => { const f = fixture(t); const state = migrateStoredState({ revision: 41, routeRules: [{ type: 'domain_suffix', value: 'example.org', enabled: true }] }); const mac = 'aa:bb:cc:dd:ee:ff'; const devices = migrateDeviceInventoryState({ revision: 17, traffic: { baselinesByMac: { [mac]: { epoch: 'kernel-1', uploadBytes: '9007199254740993', downloadBytes: '123' } }, totalsByMac: { [mac]: { uploadBytes: '9007199254740993', downloadBytes: '987', observedAt: '2026-09-10T10:00:00.000Z' } }, } }); const event = { id: '00000000-0000-4000-8000-000000000001', occurredAt: new Date().toISOString(), type: 'connection.stopped', severity: 'info', source: 'connection', dedupeKey: null, data: {} }; for (const [name, value] of [['state.json', state], ['devices.json', devices], ['activity-journal.json', { schemaVersion: 1, events: [event] }]]) { fs.writeFileSync(path.join(f.directory, name), JSON.stringify(value)); } const originals = ['state.json', 'devices.json', 'activity-journal.json'].map((name) => fs.readFileSync(path.join(f.directory, name), 'utf8')); let store = f.open(); assert.equal(store.imported, true); assert.deepEqual(store.state.read(), state); assert.deepEqual(store.devices.read(), devices); assert.deepEqual(createActivityJournalService({ db: store.db }).page().events, [event]); store.state.update((value) => ({ ...value, revision: 42 })); store.devices.update((value) => ({ ...value, revision: 18 })); store.close(); // Even broken obsolete files cannot override or break the canonical SQL state. fs.writeFileSync(path.join(f.directory, 'state.json'), '{obsolete'); store = f.open(); assert.equal(store.imported, false); assert.equal(store.state.read().revision, 42); assert.equal(store.devices.read().revision, 18); assert.equal(store.devices.read().traffic.baselinesByMac[mac].uploadBytes, '9007199254740993'); assert.equal(fs.readFileSync(path.join(f.directory, 'devices.json'), 'utf8'), originals[1]); assert.equal(fs.readFileSync(path.join(f.directory, 'activity-journal.json'), 'utf8'), originals[2]); }); test('failed import leaves no partial documents and can be retried after fixing the original', (t) => { const f = fixture(t); fs.writeFileSync(path.join(f.directory, 'state.json'), JSON.stringify({ revision: 9 })); fs.writeFileSync(path.join(f.directory, 'devices.json'), '{broken'); assert.throws(f.open, /Cannot migrate devices.json/); assert.equal(fs.readFileSync(path.join(f.directory, 'devices.json'), 'utf8'), '{broken'); const inspect = new DatabaseSync(path.join(f.directory, 'harbor.sqlite')); assert.equal(inspect.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE name = 'documents'").get().n, 0); inspect.close(); fs.writeFileSync(path.join(f.directory, 'devices.json'), '{}'); assert.equal(f.open().state.read().revision, 9); }); test('an optional legacy null subscription cache migrates without changing the backup', (t) => { const f = fixture(t); fs.writeFileSync(path.join(f.directory, 'state.json'), JSON.stringify({ schemaVersion: 4, revision: 7 })); fs.writeFileSync(path.join(f.directory, 'subscription-cache.json'), 'null'); assert.equal(f.open().state.read().revision, 7); assert.equal(fs.readFileSync(path.join(f.directory, 'subscription-cache.json'), 'utf8'), 'null'); }); test('a database constraint failure rolls back the entire import including journal and marker', (t) => { const f = fixture(t); const event = { id: '00000000-0000-4000-8000-000000000001', occurredAt: new Date().toISOString(), type: 'connection.stopped', severity: 'info', source: 'connection', dedupeKey: null, data: {} }; fs.writeFileSync(path.join(f.directory, 'activity-journal.json'), JSON.stringify({ schemaVersion: 1, events: [event, event] })); assert.throws(f.open, /UNIQUE/); const db = new DatabaseSync(path.join(f.directory, 'harbor.sqlite')); assert.equal(db.prepare('PRAGMA user_version').get().user_version, 0); assert.equal(db.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table'").get().n, 0); db.close(); }); test('durable DB corruption, future versions and invalid synchronous mutations never fall back to old JSON', (t) => { const f = fixture(t); const store = f.open(); assert.throws(() => store.state.update(async (state) => state), /synchronous/); assert.equal(store.state.read().revision, 0); store.db.exec('PRAGMA user_version = 99'); store.close(); fs.writeFileSync(path.join(f.directory, 'state.json'), '{}'); assert.throws(f.open, /Unsupported Harbor database/); });