feat(jts-02): groups.service.ts binden, Transaktionen tragfaehig machen, Absicherung sehend machen
prisma-tenant.extension.ts bekommt withTenantTransaction(prisma, tenantId, fn) — die interaktive Transaktion auf dem UNgebundenen Client mit set_config als erster Anweisung direkt auf tx, die in Aufgabe 1 als einzige der drei gemessenen Formen sowohl die Einzelmessung als auch eine Lastprobe unter echter Nebenlaeufigkeit bestand (die Array-Form auf dem gebundenen Client verteilt jede Operation auf eine eigene Teiltransaktion; die interaktive Form auf dem gebundenen Client brach unter 40 parallelen Aufrufen mit P2028 ab). rls-access-inventory.spec.ts bekommt eine dritte Erkennung fuer Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion (zwei Formen: direkter Empfaenger.$transaction(async...) und das neue Hilfsmittel withTenantTransaction(...)) — macht das Paar (groups.service.ts, tenantModuleActivation) erstmals sichtbar, das bislang keine Pruefung dieses Projekts je gesehen hat. groups.service.ts: alle zwoelf Methoden inklusive der drei Transaktionen (update() isDefault:true, reassignDefaultBeforeDelete(), ensureDefaultGroup()) laufen jetzt ueber den Mandantenkontext. Zaehler und Transaktion in ensureDefaultGroup() sind gemeinsam gebunden (T-JTS-05). addUserToDefaultGroup() prueft neu, dass der Zielbenutzer zum Mandanten gehoert (Befund E, T-JTS-02) — die Regel auf GroupMembership prueft nachweislich nur die Gruppenseite. groups.service.spec.ts bekommt zwei unterscheidbare Clients ueber demselben Speicher-Fake (Muster aus 260909-ipc, auf die interaktive Form uebertragen) und 13 neue Bindungsnachweise; alle 42 Bestandstests bleiben gruen. Klassifikationsdokument nachgezogen. 737 Tests und die Typpruefung gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { forTenant } from './prisma-tenant.extension';
|
||||
import { forTenant, withTenantTransaction } from './prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* Prueft ohne laufende Datenbank die FORM des Aufrufs, nicht seinen mit
|
||||
@@ -30,6 +30,23 @@ function stripComments(source: string): string {
|
||||
.replace(/^\s*\/\/.*$/gm, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schneidet den Quelltext einer einzelnen Top-Level-`export function`
|
||||
* heraus (bis zur naechsten `export function` oder zum Dateiende). Seit
|
||||
* 260909-jts (Aufgabe 2) enthaelt diese Datei ZWEI Funktionen mit
|
||||
* unterschiedlichem, jeweils gemessen begruendetem Transaktionsmuster —
|
||||
* die Array-Form-Garantie unten gilt nachweislich nur fuer `forTenant()`
|
||||
* selbst, nicht mehr fuer die gesamte Datei.
|
||||
*/
|
||||
function extractFunctionSource(source: string, functionName: string): string {
|
||||
const startMarker = `export function ${functionName}`;
|
||||
const startIdx = source.indexOf(startMarker);
|
||||
if (startIdx === -1) return '';
|
||||
const rest = source.slice(startIdx);
|
||||
const nextExportIdx = rest.indexOf('\nexport function ', startMarker.length);
|
||||
return nextExportIdx === -1 ? rest : rest.slice(0, nextExportIdx);
|
||||
}
|
||||
|
||||
describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
|
||||
it('ruft $transaction mit einem Feld aus genau zwei Eintraegen auf', async () => {
|
||||
const transactionCalls: unknown[] = [];
|
||||
@@ -108,9 +125,87 @@ describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
|
||||
expect(source).not.toContain('$executeRawUnsafe');
|
||||
});
|
||||
|
||||
it('nutzt im tatsaechlichen Code die Array-Form von $transaction (kein interaktiver async-Callback)', () => {
|
||||
it('nutzt im tatsaechlichen Code die Array-Form von $transaction (kein interaktiver async-Callback) — innerhalb von forTenant() selbst', () => {
|
||||
const source = stripComments(readFileSync(EXTENSION_SOURCE_PATH, 'utf-8'));
|
||||
expect(source).toMatch(/\$transaction\(\s*\[/);
|
||||
expect(source).not.toMatch(/\$transaction\(\s*async/);
|
||||
const forTenantSource = extractFunctionSource(source, 'forTenant');
|
||||
expect(forTenantSource).not.toBe('');
|
||||
expect(forTenantSource).toMatch(/\$transaction\(\s*\[/);
|
||||
expect(forTenantSource).not.toMatch(/\$transaction\(\s*async/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withTenantTransaction() — interaktive Callback-Form auf dem UNgebundenen Client (260909-jts, Aufgabe 1)', () => {
|
||||
it('nutzt im tatsaechlichen Code die interaktive Callback-Form auf tx, nicht die Array-Form (gemessen: einzige Form, die die Lastprobe bestand)', () => {
|
||||
const source = stripComments(readFileSync(EXTENSION_SOURCE_PATH, 'utf-8'));
|
||||
const withTenantTransactionSource = extractFunctionSource(source, 'withTenantTransaction');
|
||||
expect(withTenantTransactionSource).not.toBe('');
|
||||
expect(withTenantTransactionSource).toMatch(/\$transaction\(async/);
|
||||
});
|
||||
|
||||
it('setzt den Mandantenkontext als erste Anweisung DIREKT AUF tx, nicht auf dem aeusseren Client', async () => {
|
||||
const setConfigCalls: unknown[] = [];
|
||||
const fakeTx: any = {
|
||||
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
|
||||
setConfigCalls.push(values);
|
||||
return Promise.resolve(1);
|
||||
}),
|
||||
};
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((fn: (tx: unknown) => unknown) => fn(fakeTx)),
|
||||
// Der aeussere Client darf NIE direkt fuer set_config oder die
|
||||
// eigentliche Abfrage herangezogen werden — nur $transaction selbst.
|
||||
$executeRaw: vi.fn(() => {
|
||||
throw new Error('set_config darf nicht auf dem aeusseren Client laufen');
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await withTenantTransaction(fakePrisma, 'tenant-a', async (tx) => {
|
||||
expect(tx).toBe(fakeTx);
|
||||
return 'fn-result';
|
||||
});
|
||||
|
||||
expect(fakePrisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
expect(fakeTx.$executeRaw).toHaveBeenCalledTimes(1);
|
||||
expect(setConfigCalls).toEqual([['tenant-a']]);
|
||||
expect(result).toBe('fn-result');
|
||||
});
|
||||
|
||||
it('reicht denselben tx-Parameter an fn weiter, sodass mehrere Schritte auf derselben Verbindung laufen', async () => {
|
||||
const touchedByFn: unknown[] = [];
|
||||
const fakeTx: any = {
|
||||
$executeRaw: vi.fn(() => Promise.resolve(1)),
|
||||
group: { create: vi.fn(() => Promise.resolve({ id: 'g1' })) },
|
||||
user: { findMany: vi.fn(() => Promise.resolve([])) },
|
||||
};
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((fn: (tx: unknown) => unknown) => fn(fakeTx)),
|
||||
};
|
||||
|
||||
await withTenantTransaction(fakePrisma, 'tenant-a', async (tx) => {
|
||||
touchedByFn.push(await tx.group.create({ data: {} }));
|
||||
touchedByFn.push(await tx.user.findMany({ where: {} }));
|
||||
return null;
|
||||
});
|
||||
|
||||
expect(fakeTx.group.create).toHaveBeenCalledTimes(1);
|
||||
expect(fakeTx.user.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(touchedByFn).toEqual([{ id: 'g1' }, []]);
|
||||
});
|
||||
|
||||
it('setzt den Mandantenkontext ueber ein getaggtes Template, nicht ueber zusammengebauten Text (T-02-05)', async () => {
|
||||
const fakeTx: any = {
|
||||
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
|
||||
expect(Array.isArray(strings)).toBe(true);
|
||||
expect(values).toContain("tenant-with-quote-' OR 1=1");
|
||||
return Promise.resolve(1);
|
||||
}),
|
||||
};
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((fn: (tx: unknown) => unknown) => fn(fakeTx)),
|
||||
};
|
||||
|
||||
await withTenantTransaction(fakePrisma, "tenant-with-quote-' OR 1=1", async () => 'ok');
|
||||
|
||||
expect(fakeTx.$executeRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user