53 lines
4.7 KiB
TypeScript
53 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 []; } }
|