feat(quick-260909-dgj): Pruefwerkzeug fuer den Nachweis plus Betriebsanleitung (Task 3/4)

- apps/api/scripts/rls-preflight.mjs misst fuenf benannte Eigenschaften
  (rollenrechte, kontext-setzbar, ohne-kontext-leer, mit-kontext-sichtbar,
  schreibrechte) jeweils in einer eigenen Transaktion gegen eine per
  TESSERA_PREFLIGHT_DATABASE_URL angegebene Verbindung; --print-plan
  verbindet nicht, das Werkzeug schreibt in keiner Betriebsart
- 5 Tests in rls-preflight.spec.ts (rot vor dem Werkzeug, jetzt gruen)
- docs/mandantentrennung-datenbankrolle.md: Befund, Sperrgrund (182
  unskalierte Zugriffe, Anmeldeweg), Handgriffe des Betreibers samt
  Kennwortsetzung, Freigabebedingung und Rueckweg; docs/README.md verweist
  darauf

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
2026-09-09 10:03:25 +02:00
parent a5f99e52e3
commit 44a90e5bfd
4 changed files with 431 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
/**
* Prueft apps/api/scripts/rls-preflight.mjs ausschliesslich in der
* Pruefplan-Betriebsart (--print-plan) — es wird keine Datenbankverbindung
* aufgebaut (Task 3, WINDOWS #18).
*/
const SCRIPT_PATH = join(__dirname, '../../scripts/rls-preflight.mjs');
describe('rls-preflight.mjs --print-plan', () => {
it('Test 1: endet mit Ende-Code 0, obwohl keine Verbindungsangabe in der Umgebung steht', () => {
const stdout = execFileSync(process.execPath, [SCRIPT_PATH, '--print-plan'], {
env: { PATH: process.env.PATH ?? '' },
encoding: 'utf-8',
});
expect(stdout).toBeTruthy();
});
it('Test 2: nennt alle fuenf Pruefkennungen', () => {
const stdout = execFileSync(process.execPath, [SCRIPT_PATH, '--print-plan'], {
env: { PATH: process.env.PATH ?? '' },
encoding: 'utf-8',
});
for (const kennung of [
'rollenrechte',
'kontext-setzbar',
'ohne-kontext-leer',
'mit-kontext-sichtbar',
'schreibrechte',
]) {
expect(stdout).toContain(kennung);
}
});
it('Test 3: nennt TESSERA_PREFLIGHT_DATABASE_URL als Quelle der zu pruefenden Verbindung', () => {
const stdout = execFileSync(process.execPath, [SCRIPT_PATH, '--print-plan'], {
env: { PATH: process.env.PATH ?? '' },
encoding: 'utf-8',
});
expect(stdout).toContain('TESSERA_PREFLIGHT_DATABASE_URL');
});
it('Test 4: jede Pruefung laeuft innerhalb einer Transaktion ($transaction) — ausserhalb kann der Verbindungspool die Folgeabfrage auf eine andere Verbindung legen als die, auf der set_config gesetzt wurde, und die Messung waere wertlos', () => {
const source = readFileSync(SCRIPT_PATH, 'utf-8');
expect(source).toContain('$transaction');
});
it('Test 5: das Werkzeug schreibt nicht — keines der Schluesselwoerter INSERT, UPDATE, DELETE, DROP, ALTER in einer SQL-Zeichenkette', () => {
const source = readFileSync(SCRIPT_PATH, 'utf-8');
const sqlStrings = source.match(/`[^`]*`/g) ?? [];
for (const str of sqlStrings) {
for (const verb of ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER']) {
expect(str.toUpperCase()).not.toContain(verb);
}
}
});
});