kundencenter/apps/api/test/backup-api.test.ts
2026-09-27 00:51:32 +02:00

52 lines
4.7 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { mkdtempSync, existsSync, rmSync, writeFileSync, mkdirSync, readFileSync, unlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildApp } from '../src/server.js';
import { query } from '../src/core/db.js';
import { call, code, login, makeUser } from './helpers.js';
let app: FastifyInstance; const root = mkdtempSync(join(tmpdir(), 'kc-bkapi-')); const statusFile = join(root, 'status.json'); const reqDir = join(root, 'requests');
beforeAll(async () => { process.env.BACKUP_STATUS_FILE = statusFile; process.env.BACKUP_REQUEST_DIR = reqDir; process.env.BACKUP_REMOTES = 'nas:kundencenter'; app = await buildApp(); await app.ready(); });
afterAll(() => { rmSync(root, { recursive: true, force: true }); delete process.env.BACKUP_STATUS_FILE; delete process.env.BACKUP_REQUEST_DIR; delete process.env.BACKUP_REMOTES; });
async function staff(email: string, role: string) {
await makeUser({ email, kind: 'staff', staffRole: role });
const { client } = await login(app, email); const s = (await call(app, client, 'POST', '/auth/mfa/setup')).json();
await call(app, client, 'POST', '/auth/mfa/confirm', { code: code(s.secret) }); return client;
}
describe('Backup-Seite (API)', () => {
it('zeigt Zustand nur Berechtigten, ohne Statusdatei als "noch nie gelaufen"', async () => {
const admin = await staff('bk-adm@x.test', 'admin'); const sup = await staff('bk-sup@x.test', 'support');
expect((await call(app, sup, 'GET', '/admin/backup')).statusCode).toBe(403);
expect((await app.inject({ method: 'GET', url: '/v1/admin/backup' })).statusCode).toBe(401);
const s0 = (await call(app, admin, 'GET', '/admin/backup')).json();
expect(s0.lastRun).toBeNull(); expect(s0.stale).toBe(true); expect(s0.hasExternalTarget).toBe(true); expect(s0.remotes).toEqual(['nas:kundencenter']); expect(s0.retention).toEqual({ daily: 14, weekly: 8, monthly: 12 });
mkdirSync(join(root), { recursive: true });
writeFileSync(statusFile, JSON.stringify({ lastRun: { at: new Date().toISOString(), ok: true, file: 'x.age', sizeBytes: 1000, durationMs: 900, targets: [{ name: 'lokal', ok: true }] }, lastRestoreTest: { at: new Date().toISOString(), ok: true, checks: { auditKette: true } }, history: [{ at: new Date().toISOString(), ok: true }] }));
const s1 = (await call(app, admin, 'GET', '/admin/backup')).json(); expect(s1.stale).toBe(false); expect(s1.restoreStale).toBe(false); expect(s1.history).toHaveLength(1);
// veraltet, wenn der letzte Erfolg zu alt ist
writeFileSync(statusFile, JSON.stringify({ lastRun: { at: new Date(Date.now() - 30 * 3600000).toISOString(), ok: true, targets: [] } }));
expect((await call(app, admin, 'GET', '/admin/backup')).json().stale).toBe(true);
});
it('legt Anfragen nur für feste Aktionen ab, nur einmal gleichzeitig, mit Audit', async () => {
const admin = await staff('bk-adm2@x.test', 'admin'); const sup = await staff('bk-sup2@x.test', 'support');
expect((await call(app, sup, 'POST', '/admin/backup/run', { action: 'backup' })).statusCode).toBe(403);
expect((await call(app, admin, 'POST', '/admin/backup/run', { action: 'rm -rf /' })).statusCode).toBe(400); // nur feste Aktionen
expect(existsSync(reqDir) ? readdirSyncSafe(reqDir) : []).toEqual([]);
const ok = await call(app, admin, 'POST', '/admin/backup/run', { action: 'backup' }); expect(ok.statusCode).toBe(202);
expect(readdirSyncSafe(reqDir)).toEqual(['backup-run']);
expect((await call(app, admin, 'POST', '/admin/backup/run', { action: 'restore-test' })).json().error.code).toBe('BACKUP_PENDING'); // eine Anfrage nach der anderen
expect((await call(app, admin, 'GET', '/admin/backup')).json().pendingRequests).toEqual(['backup']);
unlinkSync(join(reqDir, 'backup-run'));
writeFileSync(statusFile, JSON.stringify({ running: { action: 'backup', since: new Date().toISOString() } }));
expect((await call(app, admin, 'POST', '/admin/backup/run', { action: 'restore-test' })).json().error.code).toBe('BACKUP_RUNNING');
writeFileSync(statusFile, JSON.stringify({}));
expect((await call(app, admin, 'POST', '/admin/backup/run', { action: 'restore-test' })).statusCode).toBe(202);
expect(readdirSyncSafe(reqDir)).toEqual(['backup-restore-test']);
expect((await query("SELECT action FROM audit_events WHERE action LIKE 'backup.request.%' ORDER BY id")).map((r) => r.action)).toEqual(['backup.request.backup', 'backup.request.restore-test']);
});
});
import { readdirSync } from 'node:fs';
function readdirSyncSafe(d: string): string[] { try { return readdirSync(d); } catch { return []; } }