Files
craftvia/scripts/test-backup-target.ts
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

169 lines
7.6 KiB
TypeScript

// Lane „Konfigurierbarer Backup-Zielspeicher": Store-Auflösung + Präzedenz + fail-secure.
//
// Deckt OHNE DB/Redis ab (reine Auflösungslogik `resolveBackupStore` + echte
// Byte-Persistenz des LocalBackupStore):
// (1) DB-Config lokal (expliziter Pfad) → LocalBackupStore auf genau diesem Pfad.
// (2) DB-Config S3 (vollständig) → S3BackupStore (kein still-lokaler Fallback).
// (3) Fail-secure: DB-Config S3 unvollständig → klarer Fehler (kein Local-Fallback).
// (4) Präzedenz DB → Env → Default (inkl. „unberührter Default-Datensatz fällt auf Env").
// (5) „Verbindung testen"-Pfad: put→get→remove eines winzigen Test-Keys (Local, echt).
//
// Lauf: npx tsx scripts/test-backup-target.ts (braucht KEINE DB und KEIN Redis)
import "dotenv/config";
import { mkdtempSync, rmSync, existsSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveBackupStore, type BackupTargetConfig } from "../src/server/storage/backup-store";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
/** Minimal-Config-Fabrik (nur die relevanten Felder überschreiben). */
function cfg(partial: Partial<BackupTargetConfig>): BackupTargetConfig {
return {
backupTarget: "local",
backupLocalDir: null,
backupS3Endpoint: null,
backupS3Bucket: null,
backupS3Region: null,
backupS3AccessKey: null,
backupS3SecretKey: null,
...partial,
};
}
const storeKind = (s: unknown) => (s as { constructor: { name: string } }).constructor.name;
/** Env-Variablen für einen Testblock setzen/entfernen und danach wiederherstellen. */
function withEnv(vars: Record<string, string | undefined>, fn: () => void) {
const S3_KEYS = ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET", "S3_REGION", "BACKUP_LOCAL_DIR"];
const saved: Record<string, string | undefined> = {};
for (const k of S3_KEYS) saved[k] = process.env[k];
try {
// Erst alle relevanten Keys leeren, dann die gewünschten setzen (deterministisch).
for (const k of S3_KEYS) delete process.env[k];
for (const [k, v] of Object.entries(vars)) if (v !== undefined) process.env[k] = v;
fn();
} finally {
for (const k of S3_KEYS) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
}
}
async function main() {
const tmp = mkdtempSync(join(tmpdir(), "cvbktarget-"));
// ── (1) DB-Config lokal (expliziter Pfad) ──────────────────────────────────
console.log("\n(1) DB-Config lokal (expliziter Pfad):");
const localDir = join(tmp, "db-local");
withEnv({}, () => {
const store = resolveBackupStore(cfg({ backupTarget: "local", backupLocalDir: localDir }));
ok(storeKind(store) === "LocalBackupStore", "explizite lokale DB-Config → LocalBackupStore");
});
{
// Byte-Persistenz auf genau diesem Pfad nachweisen.
const store = resolveBackupStore(cfg({ backupTarget: "local", backupLocalDir: localDir }));
await store.put("t/probe.txt", Buffer.from("hi", "utf8"));
ok(existsSync(join(localDir, "t/probe.txt")), "Bytes landen unter dem konfigurierten Pfad");
ok(readFileSync(join(localDir, "t/probe.txt"), "utf8") === "hi", "Inhalt korrekt persistiert");
}
// ── (2) DB-Config S3 vollständig ────────────────────────────────────────────
console.log("\n(2) DB-Config S3 (vollständig):");
withEnv({}, () => {
const store = resolveBackupStore(
cfg({
backupTarget: "s3",
backupS3Endpoint: "http://minio:9000",
backupS3Bucket: "certvia-backups",
backupS3AccessKey: "AK",
backupS3SecretKey: "SK",
}),
);
ok(storeKind(store) === "S3BackupStore", "vollständige S3-DB-Config → S3BackupStore (kein Local-Fallback)");
});
// ── (3) Fail-secure: S3 unvollständig ───────────────────────────────────────
console.log("\n(3) Fail-secure (S3 unvollständig):");
withEnv({ S3_ENDPOINT: "http://env-minio:9000", S3_ACCESS_KEY: "E", S3_SECRET_KEY: "E", S3_BUCKET: "env" }, () => {
let threw = false;
try {
resolveBackupStore(cfg({ backupTarget: "s3", backupS3Endpoint: "http://minio:9000", backupS3Bucket: "b" }));
} catch {
threw = true;
}
ok(threw, "S3-DB-Config ohne Secret/Access-Key wirft (fällt NICHT still auf lokal/Env)");
});
// ── (4) Präzedenz DB → Env → Default ────────────────────────────────────────
console.log("\n(4) Präzedenz DB → Env → Default:");
// (4a) Keine DB-Config, aber Env-S3 vollständig → S3 (Rückwärtskompatibilität).
withEnv({ S3_ENDPOINT: "http://env-minio:9000", S3_ACCESS_KEY: "E", S3_SECRET_KEY: "E", S3_BUCKET: "env" }, () => {
ok(storeKind(resolveBackupStore(null)) === "S3BackupStore", "cfg=null + Env-S3 → S3 (Env-Fallback)");
});
// (4b) „Unberührter Default-Datensatz" (local/NULL) fällt ebenfalls auf Env zurück.
withEnv({ S3_ENDPOINT: "http://env-minio:9000", S3_ACCESS_KEY: "E", S3_SECRET_KEY: "E", S3_BUCKET: "env" }, () => {
const store = resolveBackupStore(cfg({ backupTarget: "local", backupLocalDir: null }));
ok(storeKind(store) === "S3BackupStore", "DB local/NULL (unkonfiguriert) → Env-S3 gewinnt (Rückwärtskompatibilität)");
});
// (4c) Keine DB-Config, kein Env-S3, aber BACKUP_LOCAL_DIR gesetzt → Local dort.
const envLocalDir = join(tmp, "env-local");
withEnv({ BACKUP_LOCAL_DIR: envLocalDir }, async () => {
const store = resolveBackupStore(null);
ok(storeKind(store) === "LocalBackupStore", "cfg=null + kein Env-S3, aber BACKUP_LOCAL_DIR → Local");
});
{
const store = withEnvReturn({ BACKUP_LOCAL_DIR: envLocalDir }, () => resolveBackupStore(null));
await store.put("p.txt", Buffer.from("x", "utf8"));
ok(existsSync(join(envLocalDir, "p.txt")), "Env-BACKUP_LOCAL_DIR steuert den Ablagepfad");
}
// (4d) Nichts gesetzt → lokaler Default <cwd>/.backups.
withEnv({}, () => {
const store = resolveBackupStore(null);
ok(storeKind(store) === "LocalBackupStore", "cfg=null + keinerlei Env → lokaler Default (.backups)");
});
// ── (5) „Verbindung testen"-Pfad (put→get→remove) ───────────────────────────
console.log("\n(5) „Verbindung testen\"-Pfad (Local, put→get→remove):");
{
const store = resolveBackupStore(cfg({ backupTarget: "local", backupLocalDir: join(tmp, "conn") }));
const key = "__connectivity-test__/probe.txt";
const payload = Buffer.from("certvia connectivity", "utf8");
await store.put(key, payload);
const back = await store.get(key);
ok(!!back && back.equals(payload), "put→get liefert identische Bytes");
await store.remove(key);
ok((await store.get(key)) === null, "remove entfernt den Test-Key wieder (kein Rückstand)");
}
rmSync(tmp, { recursive: true, force: true });
}
/** Wie withEnv, aber gibt den Rückgabewert der Funktion durch (für synchrone Fälle). */
function withEnvReturn<T>(vars: Record<string, string | undefined>, fn: () => T): T {
let out!: T;
withEnv(vars, () => {
out = fn();
});
return out;
}
main()
.then(() => {
if (failures > 0) {
console.error(`\n✗ ${failures} Testfall/-fälle fehlgeschlagen.`);
process.exit(1);
}
console.log("\nOK");
})
.catch((e) => {
console.error(e);
process.exit(1);
});