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
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
57 lines
1.7 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|