import "dotenv/config"; import assert from "node:assert/strict"; import { randomBytes } from "node:crypto"; import { spawnSync } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { Pool } from "pg"; import { verifyPassword } from "../src/lib/password"; test("Instanz-Reset: Schutzabfragen, vollstaendige Leerung, Demodaten und erneuter Reset", async () => { assert.ok(process.env.DATABASE_URL, "DATABASE_URL fehlt."); const target = new URL(process.env.DATABASE_URL); assert.ok(["localhost", "127.0.0.1", "[::1]"].includes(target.hostname), "Nur lokal testen."); const name = `kibu_reset_test_${randomBytes(8).toString("hex")}`; const admin = new Pool({ connectionString: target.toString() }); target.pathname = `/${name}`; const pool = new Pool({ connectionString: target.toString() }); const tsx = createRequire(import.meta.url).resolve("tsx/cli"); const password = "test-only-reset-password"; const env = { ...process.env, DATABASE_URL: target.toString(), BOOTSTRAP_EMAIL: "reset@example.invalid", BOOTSTRAP_PASSWORD: password }; const run = (script: string, args: string[] = [], overrides: Partial = {}) => spawnSync(process.execPath, [tsx, fileURLToPath(new URL(`../scripts/${script}`, import.meta.url)), ...args], { cwd: fileURLToPath(new URL("../", import.meta.url)), env: { ...env, ...overrides }, encoding: "utf8", timeout: 30_000, }); const succeed = (result: ReturnType) => { assert.ifError(result.error); assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); }; const counts = async () => (await pool.query(`SELECT (SELECT count(*)::int FROM children) AS children, (SELECT count(*)::int FROM staff) AS staff, (SELECT count(*)::int FROM attendance) AS attendance, (SELECT count(*)::int FROM audit_log) AS audit, (SELECT count(*)::int FROM schema_migrations) AS migrations`)).rows[0]; let created = false; try { await admin.query(`CREATE DATABASE "${name}"`); created = true; succeed(run("setup.ts")); succeed(run("seed-demo.ts")); const before = await counts(); assert.equal(before.children, 12); assert.ok(before.attendance > 0 && before.audit > 0); const missingConfirmation = run("reset-instance.ts", ["clear"]); assert.notEqual(missingConfirmation.status, 0); assert.match(missingConfirmation.stderr, /confirm-reset/); const remote = run("reset-instance.ts", ["clear", "--confirm-reset"], { DATABASE_URL: `postgresql://localhost.example.invalid/${name}`, }); assert.notEqual(remote.status, 0); assert.match(remote.stderr, /lokale Anwendungsdatenbank/); const invalidPassword = run("reset-instance.ts", ["demo", "--confirm-reset"], { BOOTSTRAP_PASSWORD: "short" }); assert.notEqual(invalidPassword.status, 0); assert.match(invalidPassword.stderr, /nichts geloescht/); assert.deepEqual(await counts(), before); await pool.query(`INSERT INTO sessions(token_hash, staff_id, expires_at) SELECT 'reset-test-session', id, now() + interval '1 hour' FROM staff`); await pool.query("INSERT INTO pickup_slots(starts_at, ends_at) VALUES ('13:00', '13:15')"); await pool.query(`INSERT INTO admission_rounds(name, code_hash, expires_on, created_by) SELECT 'Reset-Test', 'reset-test-hash', current_date, id FROM staff`); await pool.query(`INSERT INTO applications(round_id, first_name, last_name, class_name, parent_name, email) SELECT id, 'Demo', 'Test', '1a', 'Kontakt', 'test@example.invalid' FROM admission_rounds`); await pool.query("INSERT INTO application_limits VALUES ('test', 1, now())"); await pool.query("INSERT INTO login_attempts VALUES ('test@example.invalid', 1, now())"); succeed(run("reset-instance.ts", ["clear", "--confirm-reset"])); assert.deepEqual(await counts(), { children: 0, staff: 0, attendance: 0, audit: 0, migrations: before.migrations }); for (const table of ["sessions", "login_attempts", "weekly_schedules", "day_overrides", "pickup_contacts", "alone_rules", "applications", "admission_rounds", "application_limits"]) { assert.equal((await pool.query(`SELECT count(*)::int AS count FROM ${table}`)).rows[0].count, 0, table); } const slots = await pool.query("SELECT starts_at::text AS start, ends_at::text AS end, active FROM pickup_slots ORDER BY starts_at"); assert.deepEqual(slots.rows, [14, 15, 16, 17].map((hour) => ({ start: `${hour}:00:00`, end: `${hour}:15:00`, active: true }))); await assert.rejects(pool.query("DELETE FROM audit_log"), /unveraenderlich/); for (let iteration = 0; iteration < 2; iteration++) { succeed(run("reset-instance.ts", ["demo", "--confirm-reset"])); const after = await counts(); assert.equal(after.children, 12); assert.equal(after.staff, 1); assert.ok(after.attendance > 0 && after.audit > 0); assert.equal(after.migrations, before.migrations); const account = (await pool.query("SELECT email, password_hash FROM staff")).rows[0]; assert.equal(account.email, env.BOOTSTRAP_EMAIL); assert.ok(await verifyPassword(password, account.password_hash)); await assert.rejects(pool.query("DELETE FROM audit_log"), /unveraenderlich/); } } finally { await pool.end(); try { if (created) await admin.query(`DROP DATABASE "${name}"`); } finally { await admin.end(); } } });