mirror of
https://github.com/abort-retry-ignore/joplock.git
synced 2026-05-22 11:38:29 +00:00
197 lines
7.8 KiB
JavaScript
197 lines
7.8 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { EventEmitter } = require('events');
|
|
const { PassThrough } = require('stream');
|
|
|
|
const { createBackupService, ensureWithinDir, timestampForFile } = require('../app/backupService');
|
|
|
|
const makeChild = ({ exitCode = 0, stdoutText = '', stderrText = '' } = {}) => {
|
|
const child = new EventEmitter();
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
process.nextTick(() => {
|
|
if (stdoutText) child.stdout.write(stdoutText);
|
|
child.stdout.end();
|
|
if (stderrText) child.stderr.write(stderrText);
|
|
child.stderr.end();
|
|
child.emit('close', exitCode);
|
|
});
|
|
return child;
|
|
};
|
|
|
|
test('ensureWithinDir rejects invalid backup names', () => {
|
|
assert.throws(() => ensureWithinDir('/tmp/backups', '../evil.dump'));
|
|
assert.throws(() => ensureWithinDir('/tmp/backups', 'evil.sql'));
|
|
});
|
|
|
|
test('timestampForFile produces filesystem-safe timestamp', () => {
|
|
assert.equal(timestampForFile(Date.UTC(2026, 4, 18, 14, 22, 31)), '2026-05-18T14-22-31Z');
|
|
});
|
|
|
|
test('backup service lists backups newest first', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
const oldFile = path.join(dir, 'joplock-backup-older.dump');
|
|
const newFile = path.join(dir, 'joplock-backup-newer.dump');
|
|
fs.writeFileSync(oldFile, 'one');
|
|
fs.writeFileSync(newFile, 'two');
|
|
const older = new Date('2026-05-18T14:00:00Z');
|
|
const newer = new Date('2026-05-18T15:00:00Z');
|
|
fs.utimesSync(oldFile, older, older);
|
|
fs.utimesSync(newFile, newer, newer);
|
|
const service = createBackupService({ backupDir: dir, postgresConfig: {} });
|
|
const backups = await service.listBackups();
|
|
assert.equal(backups[0].name, 'joplock-backup-newer.dump');
|
|
assert.equal(backups[1].name, 'joplock-backup-older.dump');
|
|
});
|
|
|
|
test('backup service starts backup job via pg_dump and completes in background', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
now: () => Date.UTC(2026, 4, 18, 14, 22, 31),
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
const backup = await service.startBackupJob();
|
|
assert.equal(spawnArgs.cmd, 'pg_dump');
|
|
assert.deepEqual(spawnArgs.args, ['--format=custom', '--compress=zstd:19', '--no-owner', '--no-privileges', '--dbname', 'joplin']);
|
|
assert.equal(backup.fileName, 'joplock-backup-2026-05-18T14-22-31Z.dump');
|
|
await service.waitForIdle();
|
|
assert.ok(fs.existsSync(path.join(dir, backup.fileName)));
|
|
assert.equal(service.currentStatus().state, 'completed');
|
|
});
|
|
|
|
test('backup service clamps compression level for gzip fallback', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: '',
|
|
compressionLevel: 42,
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob();
|
|
assert.ok(spawnArgs.args.includes('--compress=gzip:9'));
|
|
});
|
|
|
|
test('backup service uses explicit compression spec when provided', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: 'gzip:1',
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob();
|
|
assert.ok(spawnArgs.args.includes('--compress=gzip:1'));
|
|
});
|
|
|
|
test('backup service can create uncompressed backups per run', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: 'zstd:19',
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob({ mode: 'uncompressed' });
|
|
assert.ok(spawnArgs.args.includes('--compress=none'));
|
|
});
|
|
|
|
test('backup service supports explicit zstd compression mode', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: 'gzip:9',
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob({ mode: 'zstd' });
|
|
assert.ok(spawnArgs.args.includes('--compress=zstd:3'));
|
|
});
|
|
|
|
test('backup service supports balanced gzip compression mode', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: 'zstd:19',
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob({ mode: 'balanced' });
|
|
assert.ok(spawnArgs.args.includes('--compress=zstd:3'));
|
|
});
|
|
|
|
test('backup service supports fast gzip compression mode', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
compression: 'zstd:19',
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild({ stdoutText: 'dump-bytes' });
|
|
},
|
|
});
|
|
await service.startBackupJob({ mode: 'fast' });
|
|
assert.ok(spawnArgs.args.includes('--compress=gzip:1'));
|
|
});
|
|
|
|
test('backup service starts restore job via pg_restore and completes in background', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
fs.writeFileSync(path.join(dir, 'joplock-backup-2026.dump'), 'x');
|
|
let spawnArgs = null;
|
|
const service = createBackupService({
|
|
backupDir: dir,
|
|
postgresConfig: { database: 'joplin', host: 'db', port: 5432, user: 'joplin', password: 'secret' },
|
|
spawnImpl: (cmd, args, options) => {
|
|
spawnArgs = { cmd, args, options };
|
|
return makeChild();
|
|
},
|
|
});
|
|
await service.startRestoreJob('joplock-backup-2026.dump');
|
|
await service.waitForIdle();
|
|
assert.equal(spawnArgs.cmd, 'pg_restore');
|
|
assert.ok(spawnArgs.args.includes('--clean'));
|
|
assert.ok(spawnArgs.args.includes('--single-transaction'));
|
|
assert.ok(spawnArgs.args.includes(path.join(dir, 'joplock-backup-2026.dump')));
|
|
assert.equal(service.currentStatus().state, 'completed');
|
|
});
|
|
|
|
test('backup service deletes an existing backup file', async () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'joplock-backups-'));
|
|
const file = path.join(dir, 'joplock-backup-2026.dump');
|
|
fs.writeFileSync(file, 'x');
|
|
const service = createBackupService({ backupDir: dir, postgresConfig: {} });
|
|
const deleted = await service.deleteBackup('joplock-backup-2026.dump');
|
|
assert.equal(deleted.name, 'joplock-backup-2026.dump');
|
|
assert.equal(fs.existsSync(file), false);
|
|
});
|