All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m54s
CI / Auth e2e pack (pull_request) Successful in 8m4s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Build and push images (push) Successful in 19s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 5m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m41s
CI / Import/export fidelity gate (push) Successful in 56s
BACKUP_ALLOWED_TARGETS (comma-separated destination hosts) constrains where backups may go, enforced twice: the api rejects settings writes and connection tests towards non-allowlisted hosts with admin-visible error codes and resolves a non-allowlisted configured target to null, and the sidecar enforces the same policy at the point of egress for the WebDAV upload and the rsync mirror alike (shared policy helpers in packages/shared/src/backup-target-policy.ts). BREAKING: the empty default disables every remote target - backups stay local only, the VS-NfD reference configuration (ADR 0026). Existing deployments with a remote target must list its host or uploads and mirror stop. The admin UI distinguishes unavailable-by-policy from unconfigured (i18n de+en) and shows the permitted hosts. Refs #192 (ADR 0026) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { parseCommand } from './commands.js';
|
|
import { resolveRemoteTarget } from './remote.js';
|
|
import { parseBackupSettings } from './settings.js';
|
|
|
|
describe('parseBackupSettings', () => {
|
|
it('returns pure defaults for a database without the keys', () => {
|
|
expect(parseBackupSettings([])).toEqual({
|
|
localRetentionDays: null,
|
|
remoteRetentionDays: 30,
|
|
nextcloud: {
|
|
enabled: false,
|
|
baseUrl: '',
|
|
username: '',
|
|
folder: 'dorfteich-backups',
|
|
uploadSchedule: 'daily',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('applies stored rows and falls back per key on invalid values', () => {
|
|
const settings = parseBackupSettings([
|
|
{ key: 'backup.localRetentionDays', value: 14 },
|
|
{ key: 'backup.remoteRetentionDays', value: 'not-a-number' },
|
|
{ key: 'backup.nextcloud.enabled', value: true },
|
|
{ key: 'backup.nextcloud.baseUrl', value: 'https://cloud.example.com' },
|
|
{ key: 'backup.nextcloud.username', value: 'backupuser' },
|
|
{ key: 'backup.nextcloud.uploadSchedule', value: 'weekly' },
|
|
]);
|
|
expect(settings.localRetentionDays).toBe(14);
|
|
expect(settings.remoteRetentionDays).toBe(30);
|
|
expect(settings.nextcloud).toMatchObject({
|
|
enabled: true,
|
|
baseUrl: 'https://cloud.example.com',
|
|
username: 'backupuser',
|
|
uploadSchedule: 'weekly',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('resolveRemoteTarget', () => {
|
|
const settings = parseBackupSettings([
|
|
{ key: 'backup.nextcloud.enabled', value: true },
|
|
{ key: 'backup.nextcloud.baseUrl', value: 'https://cloud.example.com' },
|
|
{ key: 'backup.nextcloud.username', value: 'backupuser' },
|
|
]);
|
|
|
|
const allowlist = ['cloud.example.com'];
|
|
|
|
it('combines settings with the secret-store app password', () => {
|
|
expect(
|
|
resolveRemoteTarget(settings, { BACKUP_NEXTCLOUD_PASSWORD: 'app-pass' }, allowlist),
|
|
).toEqual({
|
|
baseUrl: 'https://cloud.example.com',
|
|
username: 'backupuser',
|
|
password: 'app-pass',
|
|
folder: 'dorfteich-backups',
|
|
});
|
|
});
|
|
|
|
it('is null when disabled or incompletely configured', () => {
|
|
expect(resolveRemoteTarget(settings, {}, allowlist)).toBeNull();
|
|
expect(
|
|
resolveRemoteTarget(
|
|
parseBackupSettings([]),
|
|
{ BACKUP_NEXTCLOUD_PASSWORD: 'app-pass' },
|
|
allowlist,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('is null when the configured host is outside the deploy allowlist (issue #192)', () => {
|
|
const secrets = { BACKUP_NEXTCLOUD_PASSWORD: 'app-pass' };
|
|
expect(resolveRemoteTarget(settings, secrets, ['other.host'])).toBeNull();
|
|
// The empty allowlist disables the WebDAV target outright.
|
|
expect(resolveRemoteTarget(settings, secrets, [])).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseCommand', () => {
|
|
it('parses run and restore commands', () => {
|
|
expect(parseCommand('{"kind":"run","requestedBy":"admin"}')).toEqual({
|
|
kind: 'run',
|
|
requestedBy: 'admin',
|
|
});
|
|
expect(
|
|
parseCommand('{"kind":"restore","source":"remote","backupId":"20260712-030000"}'),
|
|
).toEqual({
|
|
kind: 'restore',
|
|
source: 'remote',
|
|
backupId: '20260712-030000',
|
|
requestedBy: null,
|
|
});
|
|
});
|
|
|
|
it('rejects malformed payloads and bad backup ids', () => {
|
|
expect(parseCommand('not json')).toBeNull();
|
|
expect(parseCommand('{"kind":"nuke"}')).toBeNull();
|
|
expect(
|
|
parseCommand('{"kind":"restore","source":"remote","backupId":"../../etc/passwd"}'),
|
|
).toBeNull();
|
|
expect(
|
|
parseCommand('{"kind":"restore","source":"elsewhere","backupId":"20260712-030000"}'),
|
|
).toBeNull();
|
|
});
|
|
});
|