Files
harbor-net/test/server/state-store.test.js
Dmitriy Petrov a0c66edb02
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 17s
Build and Deploy Gateway / deploy (push) Successful in 6s
Add local routing rules to Harbor
2026-07-11 21:52:03 +03:00

85 lines
2.8 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
atomicWriteJson,
createStateStore,
STATE_SCHEMA_VERSION,
} from '../../src/server/services/stateStore.js';
const fixture = (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-state-store-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
return path.join(directory, 'state.json');
};
test('a failure before rename preserves the last successful file', (t) => {
const filePath = fixture(t);
atomicWriteJson(filePath, { revision: 1 });
assert.throws(
() => atomicWriteJson(filePath, { revision: 2 }, {
beforeRename: () => { throw new Error('injected failure'); },
}),
/injected failure/,
);
assert.deepEqual(JSON.parse(fs.readFileSync(filePath, 'utf8')), { revision: 1 });
assert.equal(
fs.readdirSync(path.dirname(filePath)).some((name) => name.endsWith('.tmp')),
false,
);
});
test('schema v1 state migrates to the current schema and keeps a backup', (t) => {
const filePath = fixture(t);
const legacy = {
schemaVersion: 1,
revision: 7,
selectedTag: 'nl',
servers: [{ tag: 'nl' }],
};
fs.writeFileSync(filePath, JSON.stringify(legacy));
const store = createStateStore(filePath, {
now: () => new Date('2026-07-11T12:00:00.000Z'),
});
const migrated = store.read();
assert.equal(migrated.schemaVersion, STATE_SCHEMA_VERSION);
assert.equal(migrated.appliedTag, 'nl');
assert.equal(store.migration.fromVersion, 1);
assert.deepEqual(JSON.parse(fs.readFileSync(store.migration.backupPath, 'utf8')), legacy);
assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).schemaVersion, STATE_SCHEMA_VERSION);
});
test('corrupt JSON is preserved and replaced with an explicit recovery state', (t) => {
const filePath = fixture(t);
fs.writeFileSync(filePath, '{broken');
const store = createStateStore(filePath, {
now: () => new Date('2026-07-11T12:00:00.000Z'),
});
const recovered = store.read();
assert.equal(recovered.schemaVersion, STATE_SCHEMA_VERSION);
assert.equal(recovered.revision, 0);
assert.equal(store.recovery.kind, 'corrupt-json');
assert.equal(fs.readFileSync(store.recovery.backupPath, 'utf8'), '{broken');
assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).schemaVersion, STATE_SCHEMA_VERSION);
});
test('concurrent updates are serialized without lost values', async (t) => {
const store = createStateStore(fixture(t));
store.read();
await Promise.all(Array.from({ length: 50 }, () => Promise.resolve().then(() => (
store.update((state) => ({ ...state, counter: (state.counter || 0) + 1 }))
))));
assert.equal(store.read().counter, 50);
assert.throws(() => store.update(async (state) => state), /must be synchronous/);
});