Dienstplan-Pro/server/auth-schema.test.js
Kenearos b2dbd31b41 feat(auth): Story 1.1 — Auth-Schema (users/tokens/sessions/audit) + foreign_keys + audit-Helper
Fuegt users/login_tokens/sessions/audit_log Tabellen + FK-Indizes zu
server/db.js hinzu (foreign_keys=ON fuer CASCADE), plus server/audit.js
(append-only, keine PII). Bestehende documents/history-Tabellen und
exports unveraendert. TDD: server/auth-schema.test.js zuerst rot,
dann gruen; volle Suite (24 Tests) regressionsfrei.
2026-07-07 22:21:59 +02:00

77 lines
3.1 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
// DATA_DIR auf Temp umbiegen, BEVOR db.js geladen wird
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dp-auth-schema-'));
const { db } = require('./db');
const { audit } = require('./audit');
function tableNames() {
return db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
}
function indexNames() {
return db.prepare("SELECT name FROM sqlite_master WHERE type='index'").all().map((r) => r.name);
}
test('Auth-Tabellen existieren', () => {
const names = tableNames();
assert.ok(names.includes('users'), 'users fehlt');
assert.ok(names.includes('login_tokens'), 'login_tokens fehlt');
assert.ok(names.includes('sessions'), 'sessions fehlt');
assert.ok(names.includes('audit_log'), 'audit_log fehlt');
});
test('foreign_keys ist aktiv', () => {
assert.strictEqual(db.pragma('foreign_keys', { simple: true }), 1);
});
test('Indizes auf login_tokens.user_id und sessions.user_id existieren', () => {
const names = indexNames();
assert.ok(names.includes('idx_login_tokens_user'), 'idx_login_tokens_user fehlt');
assert.ok(names.includes('idx_sessions_user'), 'idx_sessions_user fehlt');
});
test('foreign_keys CASCADE: Nutzer löschen räumt tokens+sessions', () => {
const now = '2026-07-07T10:00:00.000Z';
const userId = db.prepare('INSERT INTO users (email, is_admin, created_at) VALUES (?, 0, ?)')
.run('cascade-test@example.com', now).lastInsertRowid;
db.prepare('INSERT INTO login_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)')
.run('hash-cascade', userId, now);
db.prepare('INSERT INTO sessions (id_hash, user_id, expires_at, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)')
.run('sess-cascade', userId, now, now, now);
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
assert.strictEqual(db.prepare('SELECT * FROM login_tokens WHERE user_id = ?').get(userId), undefined);
assert.strictEqual(db.prepare('SELECT * FROM sessions WHERE user_id = ?').get(userId), undefined);
});
test('audit() schreibt append-only Zeile ohne PII', () => {
const before = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get().n;
audit('login_ok', 1, 'abc');
const after = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get().n;
assert.strictEqual(after, before + 1);
const row = db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT 1').get();
assert.strictEqual(row.event, 'login_ok');
assert.strictEqual(row.user_id, 1);
assert.strictEqual(row.ip_hash, 'abc');
assert.ok(row.ts, 'ts fehlt');
// keine PII-Spalten (email/name) vorhanden
assert.deepStrictEqual(
Object.keys(row).sort(),
['event', 'id', 'ip_hash', 'ts', 'user_id'].sort()
);
});
test('audit() ohne userId/ipHash (defaults null)', () => {
audit('server_start');
const row = db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT 1').get();
assert.strictEqual(row.event, 'server_start');
assert.strictEqual(row.user_id, null);
assert.strictEqual(row.ip_hash, null);
});