25c8db746a
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
263 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|