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>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user