// 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 { 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, fn: () => void) { const S3_KEYS = ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET", "S3_REGION", "BACKUP_LOCAL_DIR"]; const saved: Record = {}; 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 /.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(vars: Record, 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); });