import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AdminSeedService } from './admin-seed.service'; /** * AdminSeedService — Reihenfolge der Mandanten-/Admin-Anlage, Startup- * Reparatur (quick-260805-fok) UND Bindungsnachweis/Startsperren- * Entschärfung (260910-das, Aufgabe 2, Befund C/I/J). * * 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 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; let service: AdminSeedService; const tenant = { id: 't-default', slug: 'default' }; const envValues: Record = { TESSERA_ADMIN_USER: 'admin', TESSERA_ADMIN_EMAIL: 'admin@example.com', TESSERA_ADMIN_PASSWORD: 'secret', }; beforeEach(() => { vi.clearAllMocks(); callLog = []; boundCallLog = []; userCreateImpl = async () => { callLog.push('user.create'); return { id: 'u1' }; }; configService = { get: vi.fn((key: string) => envValues[key]), }; prisma = { user: { findUnique: vi.fn(async () => { callLog.push('user.findUnique'); return null; }), }, tenant: { upsert: vi.fn(async () => { callLog.push('tenant.upsert'); return tenant; }), findMany: vi.fn(async () => { callLog.push('tenant.findMany'); return [tenant]; }), }, __setUserCreateImpl(fn: (args: any) => Promise) { 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 = { ensureDefaultGroup: vi.fn(async (tenantId: string) => { callLog.push(`ensureDefaultGroup:${tenantId}`); return { id: 'g1' }; }), }; service = new AdminSeedService(prisma, configService, groupsService); }); afterEach(() => { vi.unstubAllEnvs(); }); // quick-260925-bow (D-04): der Erst-Administrator bekommt die laufende // freigegebene Version eingetragen und sieht kein "Was ist neu"-Fenster // mit Altlasten; auf dev-Staenden null. it('legt den Erst-Administrator mit lastSeenReleaseVersion der laufenden Version an', async () => { vi.stubEnv('APP_VERSION', 'v10.2.3-3-gabc1234'); let createdData: any; prisma.__setUserCreateImpl(async (args: any) => { createdData = args.data; return { id: 'u1' }; }); await service.onApplicationBootstrap(); expect(createdData.lastSeenReleaseVersion).toBe('10.2.3'); expect(createdData.role).toBe('SUPER_ADMIN'); }); it('legt den Erst-Administrator auf dev-Staenden mit lastSeenReleaseVersion null an', async () => { vi.stubEnv('APP_VERSION', 'dev'); let createdData: any; prisma.__setUserCreateImpl(async (args: any) => { createdData = args.data; return { id: 'u1' }; }); await service.onApplicationBootstrap(); expect(createdData).toHaveProperty('lastSeenReleaseVersion', null); }); it('ruft auf einer frischen Installation in genau dieser Reihenfolge auf: tenant.upsert, ensureDefaultGroup, dann user.create', async () => { await service.onApplicationBootstrap(); const seedOrder = callLog.filter( (c) => c === 'tenant.upsert' || c === 'user.create', ); expect(seedOrder).toEqual(['tenant.upsert', 'user.create']); const upsertIdx = callLog.indexOf('tenant.upsert'); const ensureIdx = callLog.indexOf(`ensureDefaultGroup:${tenant.id}`); const createIdx = callLog.indexOf('user.create'); expect(upsertIdx).toBeLessThan(ensureIdx); expect(ensureIdx).toBeLessThan(createIdx); }); it('fehlen die ENV-Variablen, wird kein Benutzer angelegt, die Reparatur ueber alle Mandanten laeuft aber trotzdem', async () => { configService.get = vi.fn(() => undefined); await service.onApplicationBootstrap(); expect(prisma.tenant.upsert).not.toHaveBeenCalled(); expect(callLog).not.toContain('user.create'); expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id); }); it('existiert der Admin-Benutzer bereits, wird kein Benutzer angelegt, die Reparatur ueber alle Mandanten laeuft aber trotzdem', async () => { prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' })); await service.onApplicationBootstrap(); expect(prisma.tenant.upsert).not.toHaveBeenCalled(); expect(callLog).not.toContain('user.create'); expect(prisma.tenant.findMany).toHaveBeenCalledTimes(1); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id); }); it('die Reparatur ruft ensureDefaultGroup fuer JEDEN von tenant.findMany gelieferten Mandanten auf, auch fuer nicht-default-Mandanten', async () => { prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' })); const otherTenant = { id: 't-other', slug: 'other' }; prisma.tenant.findMany = vi.fn(async () => [tenant, otherTenant]); await service.onApplicationBootstrap(); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(tenant.id); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(otherTenant.id); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledTimes(2); }); it('wirft ensureDefaultGroup fuer einen Mandanten, werden die uebrigen Mandanten trotzdem abgearbeitet und onApplicationBootstrap wirft nicht', async () => { prisma.user.findUnique = vi.fn(async () => ({ id: 'existing-admin' })); const failing = { id: 't-fail', slug: 'fail' }; const ok = { id: 't-ok', slug: 'ok' }; prisma.tenant.findMany = vi.fn(async () => [failing, ok]); groupsService.ensureDefaultGroup = vi.fn(async (tenantId: string) => { if (tenantId === failing.id) { throw new Error('boom'); } return { id: 'g-ok' }; }); await expect(service.onApplicationBootstrap()).resolves.not.toThrow(); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(failing.id); expect(groupsService.ensureDefaultGroup).toHaveBeenCalledWith(ok.id); }); it('ein zweiter onApplicationBootstrap-Lauf ruft ensureDefaultGroup erneut auf, loest aber keinen zusaetzlichen user.create aus', async () => { await service.onApplicationBootstrap(); 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); // Zweiter Lauf: Admin existiert jetzt bereits (Neustart-Semantik), also // ruft nur noch die Reparatur ensureDefaultGroup auf — kein weiterer // user.create. prisma.user.findUnique = vi.fn(async () => ({ id: 'u1' })); await service.onApplicationBootstrap(); 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); }); });