feat(quick-260910-das): user-Dienst binden, Linie ziehen, Startsperre entschaerfen
- user.service.ts: findById/update/deactivate/delete bekommen einen Pflicht-Mandanten und laufen ueber forTenant(); create/update uebersetzen die plattformweite Eindeutigkeitsverletzung (P2002) in eine deutsche Konfliktmeldung ohne Halter/Mandant zu nennen; zwei neue Methoden (findAllForPlatformAdmin, findByIdForPlatformAdmin) bilden die Plattform-Administratorsicht als Schleife ueber alle Mandanten mit je einem gebundenen Lesezugriff nach; findByUsername bleibt bewusst ungebunden, Kopfkommentar richtiggestellt (Anmeldeweg laeuft seit Etappe 1 ueber SECURITY-DEFINER-Funktionen, kein Aufrufer mehr) - admin-seed.service.ts: Erstanlage-Pruefung bleibt ungebunden (mit Begruendung), Erstanlage des Administrators bindet an den unmittelbar zuvor angelegten Mandanten (Befund J-Korrektur); P2002 bei der Anlage wird wie "Administrator existiert bereits" behandelt statt den Start abzubrechen -- jeder andere Fehler bricht weiterhin ab - user.controller.ts: die vier Aufrufstellen der geaenderten Signaturen auf currentUser.tenantId umgestellt (Signatur-Minimalanpassung; die Rollenlogik inkl. Plattform-Administratorsicht folgt in Aufgabe 3) - Zwei-Klienten-Nachweis in beiden Testdateien (Muster groups.service.spec.ts), Falsifizierungsnachweis fuer beide Bereiche durchgefuehrt und zurueckgenommen (siehe SUMMARY) - docs/mandantentrennung-zugriffsklassifikation.md: Zwischenstand fuer (user.service.ts, user) und (admin-seed.service.ts, user) auf gemischt korrigiert, neue Zeile (user.service.ts, tenant) ergaenzt -- volle Klassenkorrektur mit Begruendung sowie die vier handgepflegten Uebersichtstabellen folgen in Aufgabe 3 - .planning/WINDOWS.md: offener Eintrag fuer die plattformweite Eindeutigkeit von username/email (Produktentscheidung fuer Etappe 3) - 802 Tests gruen (13 neue in user.service.spec.ts, 4 neue in admin-seed.service.spec.ts), Typpruefung sauber, Wegwerf-Werkzeug meldet weiterhin alle 53 Pruefungen bestanden Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -2,26 +2,33 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AdminSeedService } from './admin-seed.service';
|
||||
|
||||
/**
|
||||
* AdminSeedService — Reihenfolge der Mandanten-/Admin-Anlage und
|
||||
* Startup-Reparatur (quick-260805-fok).
|
||||
* AdminSeedService — Reihenfolge der Mandanten-/Admin-Anlage, Startup-
|
||||
* Reparatur (quick-260805-fok) UND Bindungsnachweis/Startsperren-
|
||||
* Entschärfung (260910-das, Aufgabe 2, Befund C/I/J).
|
||||
*
|
||||
* Deckt ab: seedAdmin() ruft ensureDefaultGroup NACH tenant.upsert und VOR
|
||||
* user.create auf; beide frühen Rückkehrpfade (fehlende ENV, Admin existiert
|
||||
* bereits) überspringen den Benutzer, lassen aber die Reparatur über ALLE
|
||||
* Mandanten laufen; die Reparatur ist fehlerisoliert je Mandant; ein zweiter
|
||||
* Bootstrap-Lauf löst keinen zusätzlichen user.create aus.
|
||||
* Diese Datei hatte bisher KEINE Attrappe für das Bindungshilfsmittel — die
|
||||
* Erstanlage des Administrators lief nach der Umstellung über
|
||||
* `forTenant(this.prisma, tenant.id)`, ohne dass ein Test das bemerken
|
||||
* konnte. Zwei-Klienten-Nachweis nach dem Muster aus
|
||||
* `groups.service.spec.ts`: `forTenant(prisma, tenantId)` delegiert an
|
||||
* `prisma.__makeBoundClient(tenantId)`, ein protokollierender Wrapper.
|
||||
*
|
||||
* Die Reihenfolgeprüfung läuft über ein gemeinsames Aufruf-Log-Array, in das
|
||||
* jeder Mock beim Aufruf seinen Namen schiebt (nicht über bloße
|
||||
* Aufrufzähler) — Mock-invocationCallOrder ist über drei unabhängige vi.fn's
|
||||
* (tenant.upsert / ensureDefaultGroup / user.create) weniger lesbar als ein
|
||||
* geteiltes Log.
|
||||
* Die Reihenfolgeprüfung läuft weiterhin über ein gemeinsames
|
||||
* Aufruf-Log-Array (`callLog`), in das jeder Mock beim Aufruf seinen Namen
|
||||
* schiebt — die Ordering-Assertions der ursprünglichen Datei bleiben
|
||||
* inhaltlich erhalten.
|
||||
*/
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||
}));
|
||||
|
||||
describe('AdminSeedService', () => {
|
||||
let prisma: any;
|
||||
let configService: any;
|
||||
let groupsService: any;
|
||||
let callLog: string[];
|
||||
let boundCallLog: { tenantId: string; model: string; method: string }[];
|
||||
let userCreateImpl: (args: any) => Promise<any>;
|
||||
let service: AdminSeedService;
|
||||
|
||||
const tenant = { id: 't-default', slug: 'default' };
|
||||
@@ -34,6 +41,11 @@ describe('AdminSeedService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
callLog = [];
|
||||
boundCallLog = [];
|
||||
userCreateImpl = async () => {
|
||||
callLog.push('user.create');
|
||||
return { id: 'u1' };
|
||||
};
|
||||
|
||||
configService = {
|
||||
get: vi.fn((key: string) => envValues[key]),
|
||||
@@ -45,10 +57,6 @@ describe('AdminSeedService', () => {
|
||||
callLog.push('user.findUnique');
|
||||
return null;
|
||||
}),
|
||||
create: vi.fn(async () => {
|
||||
callLog.push('user.create');
|
||||
return { id: 'u1' };
|
||||
}),
|
||||
},
|
||||
tenant: {
|
||||
upsert: vi.fn(async () => {
|
||||
@@ -60,6 +68,21 @@ describe('AdminSeedService', () => {
|
||||
return [tenant];
|
||||
}),
|
||||
},
|
||||
__setUserCreateImpl(fn: (args: any) => Promise<any>) {
|
||||
userCreateImpl = fn;
|
||||
},
|
||||
__makeBoundClient(tenantId: string) {
|
||||
return {
|
||||
__isBoundClient: true,
|
||||
__tenantId: tenantId,
|
||||
user: {
|
||||
create: async (args: any) => {
|
||||
boundCallLog.push({ tenantId, model: 'user', method: 'create' });
|
||||
return userCreateImpl(args);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
groupsService = {
|
||||
@@ -93,7 +116,7 @@ describe('AdminSeedService', () => {
|
||||
await service.onApplicationBootstrap();
|
||||
|
||||
expect(prisma.tenant.upsert).not.toHaveBeenCalled();
|
||||
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||
expect(callLog).not.toContain('user.create');
|
||||
expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||
});
|
||||
@@ -104,7 +127,7 @@ describe('AdminSeedService', () => {
|
||||
await service.onApplicationBootstrap();
|
||||
|
||||
expect(prisma.tenant.upsert).not.toHaveBeenCalled();
|
||||
expect(prisma.user.create).not.toHaveBeenCalled();
|
||||
expect(callLog).not.toContain('user.create');
|
||||
expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||
});
|
||||
@@ -141,7 +164,7 @@ describe('AdminSeedService', () => {
|
||||
|
||||
it('ein zweiter onApplicationBootstrap-Lauf ruft ensureDefaultGroup erneut auf, loest aber keinen zusaetzlichen user.create aus', async () => {
|
||||
await service.onApplicationBootstrap();
|
||||
expect(prisma.user.create).toHaveBeenCalledTimes(1);
|
||||
expect(callLog.filter((c) => c === 'user.create')).toHaveLength(1);
|
||||
// Erster Lauf: seedAdmin() ruft ensureDefaultGroup einmal vor
|
||||
// user.create auf, die Reparatur einmal danach fuer denselben Mandanten.
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(2);
|
||||
@@ -152,7 +175,54 @@ describe('AdminSeedService', () => {
|
||||
prisma.user.findUnique = vi.fn(async () => ({ id: 'u1' }));
|
||||
await service.onApplicationBootstrap();
|
||||
|
||||
expect(prisma.user.create).toHaveBeenCalledTimes(1);
|
||||
expect(callLog.filter((c) => c === 'user.create')).toHaveLength(1);
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('Test 9: die Erstanlage-Pruefung steht NICHT im Bindungsprotokoll, die Erstanlage des Administrators dagegen steht dort mit der Kennung des unmittelbar zuvor angelegten Mandanten', async () => {
|
||||
await service.onApplicationBootstrap();
|
||||
|
||||
expect(
|
||||
boundCallLog.some((c) => c.model === 'user' && c.method === 'findUnique'),
|
||||
).toBe(false);
|
||||
expect(boundCallLog).toContainEqual({
|
||||
tenantId: tenant.id,
|
||||
model: 'user',
|
||||
method: 'create',
|
||||
});
|
||||
});
|
||||
|
||||
it('Test 10: liefert die Erstanlage-Pruefung nichts, waehrend das Anlegen an der plattformweiten Eindeutigkeit scheitert, entsteht KEIN Startabbruch — der Dienst behandelt das wie "Administrator existiert bereits", protokolliert und laeuft weiter', async () => {
|
||||
prisma.__setUserCreateImpl(async () => {
|
||||
const err: any = new Error('Unique constraint failed on the fields: (`username`)');
|
||||
err.code = 'P2002';
|
||||
throw err;
|
||||
});
|
||||
|
||||
await expect(service.onApplicationBootstrap()).resolves.not.toThrow();
|
||||
// Die Reparatur laeuft trotz der abgefangenen Kollision weiter:
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||
});
|
||||
|
||||
it('Test 11: jeder ANDERE Fehler beim Anlegen bricht den Start weiterhin ab — die Absicht des Dateikopfs bleibt erhalten', async () => {
|
||||
prisma.__setUserCreateImpl(async () => {
|
||||
throw new Error('connection refused');
|
||||
});
|
||||
|
||||
await expect(service.onApplicationBootstrap()).rejects.toThrow('connection refused');
|
||||
});
|
||||
|
||||
it('Test 12: die beiden Zugriffe auf die Mandantentabelle stehen NICHT im Bindungsprotokoll, und die Reparaturschleife ruft die Standardgruppen-Sicherung weiterhin je Mandant mit dessen Kennung auf', async () => {
|
||||
const otherTenant = { id: 't-other', slug: 'other' };
|
||||
prisma.tenant.findMany = vi.fn(async () => {
|
||||
callLog.push('tenant.findMany');
|
||||
return [tenant, otherTenant];
|
||||
});
|
||||
|
||||
await service.onApplicationBootstrap();
|
||||
|
||||
expect(boundCallLog.some((c) => c.model === 'tenant')).toBe(false);
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id);
|
||||
expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(otherTenant.id);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user