fix(quick-260910-krx): Konfliktklasse ueber den generierten Client messen statt sie zu behaupten
This commit is contained in:
@@ -2451,7 +2451,9 @@ async function runDashboardAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
id text PRIMARY KEY,
|
id text PRIMARY KEY,
|
||||||
"userId" text NOT NULL UNIQUE,
|
"userId" text NOT NULL UNIQUE,
|
||||||
"tenantId" text NOT NULL,
|
"tenantId" text NOT NULL,
|
||||||
layouts jsonb NOT NULL DEFAULT '{}'::jsonb
|
layouts jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
await db.$executeRawUnsafe(`
|
await db.$executeRawUnsafe(`
|
||||||
@@ -2604,6 +2606,52 @@ async function runDashboardAreaChecks(adminUrl, scratchRoleUrl, results) {
|
|||||||
conflictDetail,
|
conflictDetail,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 5b: dashboardlayout-gebundenes-upsert-auf-unsichtbare-zeile-wirft-unknown
|
||||||
|
//
|
||||||
|
// Pruefung 5 misst mit Roh-SQL, WAS die Datenbank tut (42501). Der
|
||||||
|
// Anwendungscode in `saveLayout` faengt aber nicht SQLSTATEs, sondern die
|
||||||
|
// Fehlerklasse, die der GENERIERTE Prisma-Client daraus macht — und die
|
||||||
|
// ist nicht dieselbe wie beim P2002-Fall der Bereiche `tenders`/`user`
|
||||||
|
// (`PrismaClientKnownRequestError`), sondern
|
||||||
|
// `PrismaClientUnknownRequestError`, weil die Regel den Schreibzugriff
|
||||||
|
// abweist, bevor eine Eindeutigkeit ueberhaupt geprueft wird. Genau
|
||||||
|
// DIESE Klasse muss `saveLayout` uebersetzen; eine Uebersetzung der
|
||||||
|
// falschen Klasse spraenge nie an.
|
||||||
|
//
|
||||||
|
// Diese Messung fehlte in der ersten Lieferung von 260910-krx — die
|
||||||
|
// Zusammenfassung berief sich auf eine nicht committete Ad-hoc-Messung.
|
||||||
|
// Vom Verifizierer gefunden, hier nachgereicht: derselbe Vorgang wie in
|
||||||
|
// Pruefung 5, aber ueber `bound.dashboardLayout.upsert(...)` auf dem
|
||||||
|
// gebundenen generierten Client, und gepruegt wird der KONSTRUKTORNAME
|
||||||
|
// des geworfenen Fehlers.
|
||||||
|
let upsertThrewUnknown = false;
|
||||||
|
let upsertDetail = '';
|
||||||
|
try {
|
||||||
|
const bound = buildInlineExtendedClient(prisma, 'TENANT-A');
|
||||||
|
await bound.dashboardLayout.upsert({
|
||||||
|
where: { userId: 'user-conflict' },
|
||||||
|
update: { layouts: {} },
|
||||||
|
create: {
|
||||||
|
id: 'layout-conflict-attempt-upsert',
|
||||||
|
userId: 'user-conflict',
|
||||||
|
tenantId: 'TENANT-A',
|
||||||
|
layouts: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
upsertDetail =
|
||||||
|
'gebundenes dashboardLayout.upsert unter TENANT-A auf die unter TENANT-B unsichtbare Zeile (user-conflict) ist NICHT fehlgeschlagen';
|
||||||
|
} catch (err) {
|
||||||
|
const ctor = err?.constructor?.name ?? 'unbekannt';
|
||||||
|
upsertThrewUnknown = ctor === 'PrismaClientUnknownRequestError';
|
||||||
|
upsertDetail = `gebundenes dashboardLayout.upsert unter TENANT-A auf die unter TENANT-B unsichtbare Zeile wirft ${ctor}${err?.code ? ` (code ${err.code})` : ''} — saveLayout uebersetzt genau diese Klasse; ${upsertThrewUnknown ? 'stimmt mit dem Anwendungscode ueberein' : 'STIMMT NICHT mit dem Anwendungscode ueberein, die Uebersetzung in saveLayout spraenge nie an'}`;
|
||||||
|
}
|
||||||
|
report(
|
||||||
|
results,
|
||||||
|
'dashboardlayout-gebundenes-upsert-auf-unsichtbare-zeile-wirft-unknown',
|
||||||
|
upsertThrewUnknown,
|
||||||
|
upsertDetail,
|
||||||
|
);
|
||||||
|
|
||||||
// 6: widgetinstance-gebundenes-einfuegen-fremder-mandant-abgelehnt
|
// 6: widgetinstance-gebundenes-einfuegen-fremder-mandant-abgelehnt
|
||||||
let foreignWidgetInsertRejected = false;
|
let foreignWidgetInsertRejected = false;
|
||||||
let foreignWidgetInsertDetail = '';
|
let foreignWidgetInsertDetail = '';
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ vi.mock('../prisma/prisma-tenant.extension', () => ({
|
|||||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
import { ConflictException } from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
import { DashboardService } from './dashboard.service';
|
import { DashboardService } from './dashboard.service';
|
||||||
|
|
||||||
@@ -408,6 +410,56 @@ describe('DashboardService — Anordnung und Widgets gebunden an forTenant() (26
|
|||||||
expectBoundCall(prisma, 'tenant-1', 'dashboardLayout', 'upsert');
|
expectBoundCall(prisma, 'tenant-1', 'dashboardLayout', 'upsert');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gegenrichtung der Bindung (w4, 260910-krx). `DashboardLayout.userId` ist
|
||||||
|
* plattformweit eindeutig, ohne Mandantenanteil. Ist die vorhandene Zeile
|
||||||
|
* unter dem gebundenen Kontext unsichtbar, laeuft das `upsert` in einen
|
||||||
|
* Konflikt — und der aeussert sich hier NICHT als der bekannte P2002-Fehler
|
||||||
|
* (`PrismaClientKnownRequestError`), sondern als
|
||||||
|
* `PrismaClientUnknownRequestError`, weil die Zeilenschutz-Regel den
|
||||||
|
* Schreibzugriff mit SQLSTATE 42501 abweist, bevor die Eindeutigkeit
|
||||||
|
* ueberhaupt geprueft wird. Gemessen in `rls-scratch-check.mjs`
|
||||||
|
* (`dashboardlayout-gebundenes-upsert-auf-unsichtbare-zeile-wirft-unknown`).
|
||||||
|
*
|
||||||
|
* Dieser Test fehlte in der ersten Lieferung — die Zusammenfassung berief
|
||||||
|
* sich auf eine nicht committete Ad-hoc-Messung. Vom Verifizierer gefunden.
|
||||||
|
*/
|
||||||
|
describe('saveLayout: Konflikt auf unsichtbare Zeile (w4)', () => {
|
||||||
|
it('uebersetzt PrismaClientUnknownRequestError in eine ConflictException statt in einen 500', async () => {
|
||||||
|
const prisma = makeFakePrisma({});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set());
|
||||||
|
const unknown = new Prisma.PrismaClientUnknownRequestError(
|
||||||
|
'Error occurred during query execution: ConnectorError(... new row violates row-level security policy ...)',
|
||||||
|
{ clientVersion: 'test' },
|
||||||
|
);
|
||||||
|
prisma.dashboardLayout.upsert = vi.fn(async () => {
|
||||||
|
throw unknown;
|
||||||
|
});
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.saveLayout('user-1', 'tenant-1', { layouts: { lg: [] } } as any),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reicht einen bekannten Prisma-Fehler (z.B. P2002) unveraendert durch — der ist hier NICHT der gemessene Fall', async () => {
|
||||||
|
const prisma = makeFakePrisma({});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set());
|
||||||
|
const known = new Prisma.PrismaClientKnownRequestError('Unique constraint failed', {
|
||||||
|
code: 'P2002',
|
||||||
|
clientVersion: 'test',
|
||||||
|
});
|
||||||
|
prisma.dashboardLayout.upsert = vi.fn(async () => {
|
||||||
|
throw known;
|
||||||
|
});
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.saveLayout('user-1', 'tenant-1', { layouts: { lg: [] } } as any),
|
||||||
|
).rejects.toBe(known);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Anordnung lesen und speichern sind GEMEINSAM gebunden: beide laufen über denselben gebundenen Klienten und dieselbe Mandantenkennung', async () => {
|
it('Anordnung lesen und speichern sind GEMEINSAM gebunden: beide laufen über denselben gebundenen Klienten und dieselbe Mandantenkennung', async () => {
|
||||||
const prisma = makeFakePrisma({
|
const prisma = makeFakePrisma({
|
||||||
layout: { userId: 'user-1', tenantId: 'tenant-1', layouts: {} },
|
layout: { userId: 'user-1', tenantId: 'tenant-1', layouts: {} },
|
||||||
|
|||||||
Reference in New Issue
Block a user