Files
tessera-ctl/apps/api/src/user/admin-seed.service.spec.ts
T
2026-09-25 08:49:21 +02:00

263 lines
10 KiB
TypeScript

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<any>;
let service: AdminSeedService;
const tenant = { id: 't-default', slug: 'default' };
const envValues: Record<string, string> = {
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<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 = {
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);
});
});