fix(quick-260909-eor): forTenant() bindet Mandantenkontext auf dieselbe Verbindung
WINDOWS #20: set_config() lief auf einer anderen Postgres-Verbindung als die eigentliche Abfrage, weil die interaktive Callback-Form von $transaction verwendet wurde. Ersetzt durch die Array-Form, die set_config und Abfrage als eine Transaktion auf einer Verbindung ausfuehrt (Prismas empfohlenes Muster fuer RLS-ueber-Extensions). Injektionsfestigkeit (T-02-05) bleibt ueber ein getaggtes $executeRaw-Template statt $executeRawUnsafe erhalten. - prisma-tenant.extension.spec.ts: prueft die Form des Aufrufs (Array mit zwei Eintraegen, Rueckgabewert ist der zweite Eintrag) ohne laufende Datenbank - rls-scratch-check.mjs: neues Werkzeug, das eine Wegwerf-Datenbank anlegt und live misst — gleiche Backend-Verbindung, gesetzter Kontext, keine Fremdmandanten-Zeilen, keine Zeilen ohne Kontext. Alle 5 Pruefungen bestehen gegen die lokale Datenbank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { forTenant } from './prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* Prueft ohne laufende Datenbank die FORM des Aufrufs, nicht seinen mit
|
||||
* Produktionscode erzeugten Inhalt (WINDOWS #20, Lehre aus dem
|
||||
* tautologischen Test in STATE.md — Pitfall "Tautologischer Test"): ein
|
||||
* vorgetaeuschter Client zeichnet auf, dass `$transaction` mit einem Feld
|
||||
* aus zwei Eintraegen aufgerufen wird, dass der Rueckgabewert der zweite
|
||||
* Eintrag ist, und dass `query` waehrend des Aufbaus dieses Feldes genau
|
||||
* einmal beruehrt wird.
|
||||
*/
|
||||
|
||||
const EXTENSION_SOURCE_PATH = join(__dirname, 'prisma-tenant.extension.ts');
|
||||
|
||||
/**
|
||||
* Der Kopfkommentar der Datei erklaert absichtlich das ALTE, defekte
|
||||
* Verhalten (inklusive $executeRawUnsafe und der interaktiven
|
||||
* $transaction(async ...)-Form) als Beleg fuer die Reparatur. Eine
|
||||
* Volltextpruefung auf den Quelltext wuerde deshalb faelschlich fehlschlagen
|
||||
* — Block- und Zeilenkommentare muessen vor der Pruefung des tatsaechlichen
|
||||
* Codes herausgefiltert werden (gleiches Vorgehen wie in
|
||||
* auth-lookup-functions.spec.ts fuer die Migrations-SQL).
|
||||
*/
|
||||
function stripComments(source: string): string {
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/^\s*\/\/.*$/gm, '');
|
||||
}
|
||||
|
||||
describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
|
||||
it('ruft $transaction mit einem Feld aus genau zwei Eintraegen auf', async () => {
|
||||
const transactionCalls: unknown[] = [];
|
||||
const fakeQueryResult = { id: 'row-1' };
|
||||
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((arg: unknown) => {
|
||||
transactionCalls.push(arg);
|
||||
expect(Array.isArray(arg)).toBe(true);
|
||||
expect((arg as unknown[]).length).toBe(2);
|
||||
return Promise.resolve(['set_config-result', fakeQueryResult]);
|
||||
}),
|
||||
$extends: (config: any) => {
|
||||
// Reproduziert nur den Teil der echten $extends-API, den
|
||||
// $allOperations braucht — kein echter Prisma-Client noetig.
|
||||
return {
|
||||
async __invoke(args: unknown, query: (args: unknown) => unknown) {
|
||||
return config.query.$allOperations({ args, query });
|
||||
},
|
||||
};
|
||||
},
|
||||
$executeRaw: vi.fn((_strings: TemplateStringsArray, ..._values: unknown[]) => {
|
||||
return 'set-config-promise';
|
||||
}),
|
||||
};
|
||||
|
||||
const scoped = forTenant(fakePrisma, 'tenant-a') as any;
|
||||
|
||||
let queryCallCount = 0;
|
||||
const query = (args: unknown) => {
|
||||
queryCallCount += 1;
|
||||
return fakeQueryResult;
|
||||
};
|
||||
|
||||
const result = await scoped.__invoke({ where: { id: 'row-1' } }, query);
|
||||
|
||||
expect(fakePrisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(transactionCalls).toHaveLength(1);
|
||||
expect((transactionCalls[0] as unknown[]).length).toBe(2);
|
||||
|
||||
// Rueckgabewert ist der ZWEITE Eintrag des Felds (der Query-Aufruf),
|
||||
// nicht das Ergebnis von set_config.
|
||||
expect(result).toBe(fakeQueryResult);
|
||||
|
||||
// query() wurde beim Aufbau des Felds genau einmal beruehrt.
|
||||
expect(queryCallCount).toBe(1);
|
||||
});
|
||||
|
||||
it('setzt den Mandantenkontext ueber ein getaggtes $executeRaw-Template, nicht ueber zusammengebauten Text', async () => {
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((arg: unknown) => Promise.resolve(['set-config-result', 'query-result'])),
|
||||
$extends: (config: any) => ({
|
||||
async __invoke(args: unknown, query: (args: unknown) => unknown) {
|
||||
return config.query.$allOperations({ args, query });
|
||||
},
|
||||
}),
|
||||
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
|
||||
// Ein getaggtes Template liefert ein Array von Textstuecken plus die
|
||||
// interpolierten Werte getrennt — genau das ist der Beleg dafuer,
|
||||
// dass der Mandantenwert parametrisiert und nicht in den SQL-Text
|
||||
// eingebaut wird.
|
||||
expect(Array.isArray(strings)).toBe(true);
|
||||
expect(values).toContain('tenant-with-quote-\' OR 1=1');
|
||||
return 'set-config-promise';
|
||||
}),
|
||||
};
|
||||
|
||||
const scoped = forTenant(fakePrisma, "tenant-with-quote-' OR 1=1") as any;
|
||||
await scoped.__invoke({}, () => 'query-result');
|
||||
|
||||
expect(fakePrisma.$executeRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('verwendet im tatsaechlichen Code (ohne Kommentare) kein $executeRawUnsafe mehr', () => {
|
||||
const source = stripComments(readFileSync(EXTENSION_SOURCE_PATH, 'utf-8'));
|
||||
expect(source).not.toContain('$executeRawUnsafe');
|
||||
});
|
||||
|
||||
it('nutzt im tatsaechlichen Code die Array-Form von $transaction (kein interaktiver async-Callback)', () => {
|
||||
const source = stripComments(readFileSync(EXTENSION_SOURCE_PATH, 'utf-8'));
|
||||
expect(source).toMatch(/\$transaction\(\s*\[/);
|
||||
expect(source).not.toMatch(/\$transaction\(\s*async/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user