- Removed orphaned __trace2.js (unnecessary escape error) - Fixed empty block statement in config-migrations.test.js busy-wait - Fixed empty block statement in metrics.test.js busy-wait - Auto-fixed 5 fixable warnings via eslint --fix - Remaining 547 warnings (require-await, no-unused-vars) are non-blocking code quality - 0 errors, 1633 tests pass
217 lines
7.8 KiB
JavaScript
217 lines
7.8 KiB
JavaScript
/**
|
|
* Config migration tests
|
|
*
|
|
* These tests verify that a config file from any older version of DashCaddy
|
|
* gets correctly migrated to the current version. Migration MUST be:
|
|
* - Deterministic (same input always produces same output)
|
|
* - Idempotent (running migration on already-migrated config is a no-op)
|
|
* - Safe (no data loss; only adds fields, never removes user values)
|
|
* - Silent (no exceptions thrown for any version from 0 to CURRENT)
|
|
*/
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const {
|
|
CURRENT_VERSION,
|
|
migrations,
|
|
migrate,
|
|
loadAndMigrate
|
|
} = require('../src/config/migrations');
|
|
|
|
describe('config/migrations', () => {
|
|
let tmpDir;
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mig-test-'));
|
|
});
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('migrate()', () => {
|
|
test('null/empty config returns fresh v_current', () => {
|
|
const result = migrate(null);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
});
|
|
|
|
test('undefined config returns fresh v_current', () => {
|
|
const result = migrate(undefined);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
});
|
|
|
|
test('v0 (no _version) migrates all the way to current', () => {
|
|
const v0 = { tld: '.home', customValue: 'preserved' };
|
|
const result = migrate(v0);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
// User data must be preserved
|
|
expect(result.tld).toBe('.home');
|
|
expect(result.customValue).toBe('preserved');
|
|
});
|
|
|
|
test('each intermediate version migrates forward to current', () => {
|
|
for (let v = 0; v < CURRENT_VERSION; v++) {
|
|
const config = { _version: v, tld: '.test' };
|
|
const result = migrate(config);
|
|
// Final version is always CURRENT_VERSION after running all migrations
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
// User data preserved
|
|
expect(result.tld).toBe('.test');
|
|
}
|
|
});
|
|
|
|
test('config at current version passes through unchanged', () => {
|
|
const current = { _version: CURRENT_VERSION, tld: '.home', customField: 'kept' };
|
|
const result = migrate(current);
|
|
expect(result).toEqual(current);
|
|
});
|
|
|
|
test('config from FUTURE version is left alone (forward compat)', () => {
|
|
const future = { _version: 999, tld: '.home', newField: 'unknown' };
|
|
const result = migrate(future);
|
|
// We don't touch future configs — let validation catch issues
|
|
expect(result._version).toBe(999);
|
|
expect(result.newField).toBe('unknown');
|
|
});
|
|
});
|
|
|
|
describe('v0 → v1 migration: dns normalization', () => {
|
|
test('string dns gets converted to object', () => {
|
|
const result = migrations[1]({ dns: '192.168.1.1' });
|
|
expect(result.dns).toEqual({ ip: '192.168.1.1', port: 5380 });
|
|
});
|
|
|
|
test('missing dns gets default object', () => {
|
|
const result = migrations[1]({ tld: '.home' });
|
|
expect(result.dns).toEqual({ ip: '', port: 5380 });
|
|
});
|
|
|
|
test('object dns passes through unchanged', () => {
|
|
const result = migrations[1]({ dns: { ip: '10.0.0.1', port: 5380, custom: 'kept' } });
|
|
expect(result.dns.ip).toBe('10.0.0.1');
|
|
expect(result.dns.custom).toBe('kept');
|
|
});
|
|
|
|
test('_version is set to 1', () => {
|
|
const result = migrations[1]({ tld: '.home' });
|
|
expect(result._version).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('v1 → v2 migration: dns.provider field', () => {
|
|
test('adds provider: technitium default', () => {
|
|
const result = migrations[2]({ dns: { ip: '10.0.0.1', port: 5380 }, _version: 1 });
|
|
expect(result.dns.provider).toBe('technitium');
|
|
expect(result.dns.ip).toBe('10.0.0.1');
|
|
expect(result.dns.port).toBe(5380);
|
|
});
|
|
|
|
test('respects existing provider if set', () => {
|
|
const result = migrations[2]({ dns: { provider: 'cloudflare', ip: 'cf' }, _version: 1 });
|
|
expect(result.dns.provider).toBe('cloudflare');
|
|
});
|
|
|
|
test('_version is set to 2', () => {
|
|
const result = migrations[2]({ _version: 1 });
|
|
expect(result._version).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('loadAndMigrate()', () => {
|
|
test('creates fresh config when file does not exist', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
const result = loadAndMigrate(configFile, null);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
// Should NOT write a file when there was nothing to migrate
|
|
expect(fs.existsSync(configFile)).toBe(false);
|
|
});
|
|
|
|
test('migrates old config and writes back to disk', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
// Write an unversioned config (v0)
|
|
fs.writeFileSync(configFile, JSON.stringify({ tld: '.sami', customField: 'preserve-me' }));
|
|
|
|
const result = loadAndMigrate(configFile, null);
|
|
|
|
// Returned value is migrated
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
expect(result.tld).toBe('.sami');
|
|
expect(result.customField).toBe('preserve-me');
|
|
|
|
// File on disk is updated
|
|
const written = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
expect(written._version).toBe(CURRENT_VERSION);
|
|
expect(written.tld).toBe('.sami');
|
|
});
|
|
|
|
test('does not rewrite file when already at current version', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
const original = JSON.stringify({ _version: CURRENT_VERSION, tld: '.home' }, null, 2);
|
|
fs.writeFileSync(configFile, original);
|
|
|
|
// Record mtime before
|
|
const mtimeBefore = fs.statSync(configFile).mtimeMs;
|
|
// Wait a tick
|
|
const start = Date.now();
|
|
let spin = start;
|
|
while (Date.now() - spin < 50) { spin = Date.now(); } // 50ms busy-wait
|
|
|
|
loadAndMigrate(configFile, null);
|
|
|
|
// File should not have been rewritten (mtime unchanged)
|
|
const mtimeAfter = fs.statSync(configFile).mtimeMs;
|
|
expect(mtimeAfter).toBe(mtimeBefore);
|
|
});
|
|
|
|
test('handles corrupt JSON gracefully (returns defaults, no crash)', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
fs.writeFileSync(configFile, '{ this is not valid json');
|
|
|
|
// Should not throw
|
|
const result = loadAndMigrate(configFile, null);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
});
|
|
|
|
test('creates parent directory if missing', () => {
|
|
const nested = path.join(tmpDir, 'nested', 'subdir', 'config.json');
|
|
// Pre-create parent dirs (test setup)
|
|
fs.mkdirSync(path.dirname(nested), { recursive: true });
|
|
fs.writeFileSync(nested, JSON.stringify({ tld: '.home' }));
|
|
|
|
const result = loadAndMigrate(nested, null);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
});
|
|
|
|
test('full chain: v0 file with string dns becomes v2 with provider', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
fs.writeFileSync(configFile, JSON.stringify({
|
|
tld: '.sami',
|
|
dns: '10.0.0.1'
|
|
}));
|
|
|
|
const result = loadAndMigrate(configFile, null);
|
|
expect(result._version).toBe(CURRENT_VERSION);
|
|
// After full chain, dns is normalized to object AND has provider
|
|
expect(result.dns.ip).toBe('10.0.0.1');
|
|
expect(result.dns.port).toBe(5380);
|
|
expect(result.dns.provider).toBe('technitium');
|
|
});
|
|
});
|
|
|
|
describe('idempotency', () => {
|
|
test('running migration twice produces same result', () => {
|
|
const v0 = { tld: '.home', customField: 'x' };
|
|
const first = migrate(v0);
|
|
const second = migrate(first);
|
|
expect(second).toEqual(first);
|
|
});
|
|
|
|
test('loadAndMigrate is idempotent across reloads', () => {
|
|
const configFile = path.join(tmpDir, 'config.json');
|
|
fs.writeFileSync(configFile, JSON.stringify({ tld: '.home' }));
|
|
|
|
const first = loadAndMigrate(configFile, null);
|
|
const second = loadAndMigrate(configFile, null);
|
|
expect(second).toEqual(first);
|
|
});
|
|
});
|
|
});
|