feat: implement database reset and demo population scripts with tests

This commit is contained in:
Paul Schoeneck committed 2026-11-03 08:17:53 +01:00
1 parent 4df14584f2
commit a2e5184b3e
4 files changed
+214 -1

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
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<NodeJS.ProcessEnv> = {}) =>
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<typeof run>) => {
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();
}
}
});