98 lines
5.4 KiB
TypeScript
98 lines
5.4 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
});
|