dorfteich/apps/backup/src/config.test.ts
Claude Fable 5 8dbff86537
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m9s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m47s
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 9s
CI / Auth e2e pack (push) Successful in 5m25s
CI / Import/export fidelity gate (push) Successful in 45s
Add backup sidecar: nightly dump, volume archive, prune, status, restore (#83)
New apps/backup service (ADR 0015): nightly pg_dump -Fc plus one tar of the
uploads/plugins volumes as a consistent restore set on a new backups volume,
retention prune that never removes the newest complete set, atomic
status.json for the readiness/admin consumers (#85/#86), and a failure mail
sent directly via nodemailer (the api may be the broken part) with de/en
texts in the shared mails catalog. BACKUP_RUN_ONCE=1 gives the on-demand
path; deploy/backup/restore.sh automates the documented restore runbook.
The pure secret-store helpers moved to @dorfteich/shared so the sidecar
resolves the wizard-written SMTP relay exactly like the api.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 17:29:15 +02:00

57 lines
1.7 KiB
TypeScript

import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { loadBackupEnv } from './config.js';
const DATABASE_URL = 'postgresql://dorfteich:pw@db:5432/dorfteich';
let dir: string;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), 'dorfteich-backup-env-'));
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
describe('loadBackupEnv', () => {
it('applies defaults and drops compose empty strings', () => {
// Compose passes optional variables as "" — they must fall through to
// the schema defaults, not fail enum/number parsing (issue #80 lesson).
const env = loadBackupEnv({
DATABASE_URL,
BACKUP_TIME: '',
BACKUP_RETENTION_DAYS: '',
BACKUP_MAIL_LOCALE: '',
SECRETS_FILE: join(dir, 'missing.env'),
});
expect(env.BACKUP_TIME).toBe('03:00');
expect(env.BACKUP_RETENTION_DAYS).toBe(30);
expect(env.BACKUP_MAIL_LOCALE).toBe('en');
});
it('fills SMTP settings from the wizard-written secret store', () => {
const secretsFile = join(dir, 'secrets.env');
return writeFile(secretsFile, 'SMTP_HOST="relay.example.com"\nSMTP_PORT="2525"\n').then(() => {
const env = loadBackupEnv({
DATABASE_URL,
SECRETS_FILE: secretsFile,
// Explicit container env must win over the store.
SMTP_PORT: '465',
});
expect(env.SMTP_HOST).toBe('relay.example.com');
expect(env.SMTP_PORT).toBe(465);
});
});
it('rejects a malformed BACKUP_TIME', () => {
expect(() => loadBackupEnv({ DATABASE_URL, BACKUP_TIME: '25:99' })).toThrow(
/BACKUP_TIME.*HH:MM/s,
);
});
});