feat(quick-260910-exd): ModuleRegistryService binden, Klassifikation abschliessen
- module-registry.service.ts: findActiveForTenant, activateForTenant, isModuleActive binden je einen Aktivierungszugriff, deactivateForTenant bindet beide (Lesen+Schreiben) ueber EINEN Klienten unter tenantPrisma; alle sechs Katalogzugriffe (findAll/findBySlug/beide Existenzpruefungen/isModuleActive-Katalogsuche/seedModule) bleiben bewusst ungebunden, mit Kommentar der Messung von Bedingung trennt - isModuleActive-Kopfkommentar richtiggestellt: der Waechter ruft sie nicht auf (0 Aufrufer, TEIL 3 von Aufgabe 1) — Waechter nimmt findBySlug + ModuleAccessService.getAccessibleModuleIds - module-registry.service.spec.ts: NEU, Zwei-Klienten-Nachweis, deckt die bislang ungetestete Datei mit elf der siebzehn Zugriffe des Bereichs ab, inkl. der lauten (deactivate ohne Aktivierung) und stillen (isModuleActive ohne Aktivierung) Richtung und dem Katalog-Wachhund - tender-scheduler.service.spec.ts: forTenant() auf Identitaet gemockt (dieselbe Konvention wie ldap.service.spec.ts) — cross-area Bruch durch die Umstellung von activateForTenant behoben (Rule 1/3) - docs/mandantentrennung-zugriffsklassifikation.md: alle fuenf handgepflegten Stellen nachgezogen (Bestandsaufnahme, Uebersichtszeile 7/10, Summenzeile 108/134, Klassen-Verteilung unveraendert bei 63 Paaren, Hintergrunddienst-Abschnitt haelt die Abwesenheit eines sechsten Falls fest) — alle gemessen, nicht abgeschrieben, Befund K haelt exakt - docs/mandantentrennung-etappe2-fehlerrichtung.md: Nachtrag mit tatsaechlich umgesetzten Pfaden, beiden Falsifizierungsnachweisen (Testname+Meldung), und der Feststellung zum unveraenderten Controller-Kommentar - .planning/WINDOWS.md: neuer offener Eintrag #23 (deviation) — kein Signal unterscheidet "keine Freigabe" von "Abfrage fand nichts", mit Vorabpruefung fuer Etappe 4 und begruendeter Verwerfung einer Laufzeitwarnung - 833 Tests gruen (56 Dateien), Typpruefung sauber, Wegwerf-Werkzeug 66/66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ModuleRegistryService } from './module-registry.service';
|
||||
|
||||
/**
|
||||
* ModuleRegistryService — bisher OHNE Testdatei (260910-exd, Aufgabe 3,
|
||||
* Befund C, zweite Form). Diese Datei haelt elf der siebzehn Zugriffe des
|
||||
* Bereichs `module-registry`, darunter JEDEN Schreibpfad.
|
||||
*
|
||||
* Bindung an forTenant() (dasselbe Muster wie
|
||||
* `module-grants.service.spec.ts` und `module-access.service.spec.ts`): der
|
||||
* gebundene Klient ist ein ZWEITES, von `prisma` unterscheidbares Objekt
|
||||
* ueber DEMSELBEN Speicher, das protokolliert, welche Aufrufe ueber ihn
|
||||
* liefen. `module` wird NICHT gewrappt — der Katalogzugriff bleibt bewusst
|
||||
* ungebunden (Aufgabe 1, Befund E).
|
||||
*/
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
|
||||
}));
|
||||
|
||||
const BOUND_MODEL_NAMES = ['tenantModuleActivation'];
|
||||
|
||||
function makeFakePrisma() {
|
||||
const modules = new Map<string, any>();
|
||||
const activations = new Map<string, any>(); // key: tenantId::moduleId
|
||||
const boundCallLog: { tenantId: string; model: string; method: string }[] = [];
|
||||
|
||||
const fake: any = {
|
||||
__seedModule(m: { id: string; slug: string; name: string; version: string; category: string; description: any; icon?: string | null; isSystem?: boolean }) {
|
||||
modules.set(m.id, { icon: null, isSystem: false, ...m });
|
||||
},
|
||||
__seedActivation(a: { tenantId: string; moduleId: string; isActive: boolean }) {
|
||||
activations.set(`${a.tenantId}::${a.moduleId}`, { ...a, activatedAt: new Date() });
|
||||
},
|
||||
module: {
|
||||
findMany: vi.fn(async () => Array.from(modules.values()).sort((a, b) => a.name.localeCompare(b.name))),
|
||||
findUnique: vi.fn(async ({ where }: any) => {
|
||||
if (where.id) return modules.get(where.id) ?? null;
|
||||
if (where.slug) return Array.from(modules.values()).find((m) => m.slug === where.slug) ?? null;
|
||||
return null;
|
||||
}),
|
||||
upsert: vi.fn(async ({ where, update, create }: any) => {
|
||||
const existing = Array.from(modules.values()).find((m) => m.slug === where.slug);
|
||||
if (existing) {
|
||||
const updated = { ...existing, ...update };
|
||||
modules.set(existing.id, updated);
|
||||
return updated;
|
||||
}
|
||||
const record = { id: `mod-${modules.size + 1}`, ...create };
|
||||
modules.set(record.id, record);
|
||||
return record;
|
||||
}),
|
||||
},
|
||||
tenantModuleActivation: {
|
||||
findMany: vi.fn(async ({ where }: any) => {
|
||||
return Array.from(activations.values())
|
||||
.filter((a) => a.tenantId === where.tenantId && (where.isActive === undefined || a.isActive === where.isActive))
|
||||
.map((a) => ({ ...a, module: modules.get(a.moduleId) ?? null }));
|
||||
}),
|
||||
findUnique: vi.fn(async ({ where }: any) => {
|
||||
const { tenantId, moduleId } = where.tenantId_moduleId;
|
||||
return activations.get(`${tenantId}::${moduleId}`) ?? null;
|
||||
}),
|
||||
upsert: vi.fn(async ({ where, update, create }: any) => {
|
||||
const key = `${where.tenantId_moduleId.tenantId}::${where.tenantId_moduleId.moduleId}`;
|
||||
const existing = activations.get(key);
|
||||
const record = existing
|
||||
? { ...existing, ...update }
|
||||
: { ...create, activatedAt: new Date() };
|
||||
activations.set(key, record);
|
||||
return { ...record, module: modules.get(record.moduleId) ?? null };
|
||||
}),
|
||||
update: vi.fn(async ({ where, data }: any) => {
|
||||
const { tenantId, moduleId } = where.tenantId_moduleId;
|
||||
const key = `${tenantId}::${moduleId}`;
|
||||
const existing = activations.get(key);
|
||||
const record = { ...existing, ...data };
|
||||
activations.set(key, record);
|
||||
return { ...record, module: modules.get(record.moduleId) ?? null };
|
||||
}),
|
||||
},
|
||||
// --- Bindungsnachweis (260910-exd, Befund C uebertragen) ---------------
|
||||
__boundCallLog: boundCallLog,
|
||||
__makeBoundClient(tenantId: string) {
|
||||
const bound: any = { __isBoundClient: true, __tenantId: tenantId };
|
||||
for (const modelName of BOUND_MODEL_NAMES) {
|
||||
const model = fake[modelName];
|
||||
const wrapped: any = {};
|
||||
for (const method of Object.keys(model)) {
|
||||
wrapped[method] = async (...args: any[]) => {
|
||||
boundCallLog.push({ tenantId, model: modelName, method });
|
||||
return model[method](...args);
|
||||
};
|
||||
}
|
||||
bound[modelName] = wrapped;
|
||||
}
|
||||
return bound;
|
||||
},
|
||||
};
|
||||
|
||||
return fake;
|
||||
}
|
||||
|
||||
function expectBoundCall(prisma: any, tenantId: string, model: string, method: string) {
|
||||
const found = prisma.__boundCallLog.some(
|
||||
(c: any) => c.tenantId === tenantId && c.model === model && c.method === method,
|
||||
);
|
||||
expect(
|
||||
found,
|
||||
`erwarteter gebundener Aufruf ${model}.${method}(tenant=${tenantId}) fehlt im Protokoll: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
function expectNeverBound(prisma: any, model: string) {
|
||||
const found = prisma.__boundCallLog.some((c: any) => c.model === model);
|
||||
expect(
|
||||
found,
|
||||
`Modell "${model}" darf nie im Bindungsprotokoll auftauchen (Katalog bleibt ungebunden): ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||
).toBe(false);
|
||||
}
|
||||
|
||||
describe('ModuleRegistryService.findAll/findBySlug — Katalog (bewusst UNGEBUNDEN)', () => {
|
||||
it('findAll liefert alle registrierten Module, sortiert nach Namen', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-b', slug: 'b', name: 'B-Modul', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedModule({ id: 'mod-a', slug: 'a', name: 'A-Modul', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.findAll();
|
||||
|
||||
expect(result.map((m: any) => m.id)).toEqual(['mod-a', 'mod-b']);
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
|
||||
it('findBySlug findet ein Modul über seinen Slug', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.findBySlug('domaincheck');
|
||||
|
||||
expect(result?.id).toBe('mod-1');
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
|
||||
it('findBySlug liefert null für einen unbekannten Slug, ohne zu werfen', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.findBySlug('unknown');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService.findActiveForTenant', () => {
|
||||
it('bindet den Lesezugriff an den Mandanten UND liefert die Katalogseite mit (die verbundene Modellform überlebt die Bindung)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.findActiveForTenant('t1');
|
||||
|
||||
expect(result).toEqual([expect.objectContaining({ id: 'mod-1', name: 'Domaincheck' })]);
|
||||
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findMany');
|
||||
});
|
||||
|
||||
it('die Aktivierungsliste eines zweiten Mandanten liefert keine Zeile des ersten', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.findActiveForTenant('t2');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expectBoundCall(prisma, 't2', 'tenantModuleActivation', 'findMany');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService.activateForTenant', () => {
|
||||
it('erreicht die Katalog-Existenzprüfung UNGEBUNDEN und schreibt die Aktivierung GEBUNDEN, an den übergebenen Mandanten', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.activateForTenant('t1', 'mod-1');
|
||||
|
||||
expect(result.isActive).toBe(true);
|
||||
expect(prisma.module.findUnique).toHaveBeenCalled();
|
||||
expectNeverBound(prisma, 'module');
|
||||
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert');
|
||||
});
|
||||
|
||||
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung, BEVOR irgendein gebundener Schreibzugriff im Protokoll steht', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
await expect(service.activateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
|
||||
|
||||
expect(
|
||||
prisma.__boundCallLog,
|
||||
`kein gebundener Aufruf erwartet, wenn die Katalog-Existenzpruefung bereits scheitert: ${JSON.stringify(prisma.__boundCallLog)}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService.deactivateForTenant', () => {
|
||||
it('bindet beide Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen Klienten', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.deactivateForTenant('t1', 'mod-1');
|
||||
|
||||
expect(result.isActive).toBe(false);
|
||||
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
|
||||
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'update');
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
|
||||
/**
|
||||
* Die LAUTE, harmlose Richtung dieses Bereichs (260910-exd, m3) — hier
|
||||
* ausdruecklich festgenagelt, damit sie bei einem spaeteren Umbau nicht
|
||||
* versehentlich in ein stilles `false` verwandelt wird.
|
||||
*/
|
||||
it('wirft die Nicht-gefunden-Ausnahme, wenn keine Aktivierung vorliegt (LAUT, nicht still)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
await expect(service.deactivateForTenant('t1', 'mod-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('wirft die Nicht-gefunden-Ausnahme für eine unbekannte Modulkennung', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
await expect(service.deactivateForTenant('t1', 'mod-unknown')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService.isModuleActive', () => {
|
||||
it('bindet ihren Aktivierungs-Lesezugriff und erreicht den Katalog ungebunden', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.isModuleActive('t1', 'domaincheck');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'findUnique');
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
|
||||
/**
|
||||
* Die STILLE Richtung dieses Bereichs (260910-exd, m3) — ebenfalls
|
||||
* festgenagelt, mit diesem Kommentar als Markierung: heute ohne Aufrufer
|
||||
* (Aufgabe 1, TEIL 3), aber die Falle für morgen, sollte diese Methode
|
||||
* verdrahtet werden.
|
||||
*/
|
||||
it('liefert `false`, ohne Aktivierung (STILL, keine Ausnahme)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.isModuleActive('t1', 'domaincheck');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('liefert `false` für einen unbekannten Slug, ohne die Aktivierungstabelle überhaupt zu befragen', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.isModuleActive('t1', 'unknown-slug');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(prisma.__boundCallLog).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService.seedModule — Katalogpflege beim Start (bewusst UNGEBUNDEN)', () => {
|
||||
it('legt ein neues Modul an, wenn der Slug noch nicht existiert', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.seedModule({
|
||||
slug: 'domaincheck',
|
||||
name: 'Domaincheck',
|
||||
version: '1.0.0',
|
||||
category: 'ops',
|
||||
description: { de: 'Test' },
|
||||
});
|
||||
|
||||
expect(result.slug).toBe('domaincheck');
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
|
||||
it('aktualisiert ein bestehendes Modul über denselben Slug (Upsert)', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Alt', version: '1', category: 'ops', description: {} });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
const result = await service.seedModule({
|
||||
slug: 'domaincheck',
|
||||
name: 'Neu',
|
||||
version: '2.0.0',
|
||||
category: 'ops',
|
||||
description: { de: 'Test' },
|
||||
});
|
||||
|
||||
expect(result.name).toBe('Neu');
|
||||
expect(result.version).toBe('2.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleRegistryService — Katalogzugriffe tauchen nie im Bindungsprotokoll auf (Wachhund, 260910-exd)', () => {
|
||||
it('kein Katalogzugriff des Dienstes — weder Gesamtliste, noch Kennzeichen-Suche, noch Existenzprüfungen, noch Katalogpflege — taucht im Bindungsprotokoll auf', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedModule({ id: 'mod-1', slug: 'domaincheck', name: 'Domaincheck', version: '1', category: 'ops', description: {} });
|
||||
prisma.__seedActivation({ tenantId: 't1', moduleId: 'mod-1', isActive: true });
|
||||
const service = new ModuleRegistryService(prisma as any);
|
||||
|
||||
await service.findAll();
|
||||
await service.findBySlug('domaincheck');
|
||||
await service.activateForTenant('t1', 'mod-1');
|
||||
await service.deactivateForTenant('t1', 'mod-1');
|
||||
await service.isModuleActive('t1', 'domaincheck');
|
||||
await service.seedModule({
|
||||
slug: 'domaincheck',
|
||||
name: 'Domaincheck',
|
||||
version: '1.0.0',
|
||||
category: 'ops',
|
||||
description: {},
|
||||
});
|
||||
|
||||
expectNeverBound(prisma, 'module');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
|
||||
/**
|
||||
* Service managing the module registry and per-tenant activations.
|
||||
@@ -13,6 +14,13 @@ export class ModuleRegistryService {
|
||||
|
||||
/**
|
||||
* Returns all registered modules.
|
||||
*
|
||||
* Bewusst UNGEBUNDEN (260910-exd, Aufgabe 1, Befund E): "Module" traegt
|
||||
* heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht
|
||||
* katastrophal. Katastrophal wuerde sie erst, WENN Etappe 3 dieser
|
||||
* Tabelle eine Regel gibt — dann verschwaende der gesamte Katalog fuer
|
||||
* jeden Mandanten. Diese Bedingung steht hier als Bedingung, nicht als
|
||||
* heute beobachtbare Tatsache.
|
||||
*/
|
||||
async findAll() {
|
||||
return this.prisma.module.findMany({
|
||||
@@ -22,6 +30,12 @@ export class ModuleRegistryService {
|
||||
|
||||
/**
|
||||
* Finds a module by its unique slug.
|
||||
*
|
||||
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben. Diese
|
||||
* Methode ist zusaetzlich die Stelle, die `ModuleGuard` bei JEDER
|
||||
* Modulanfrage aufruft — eine Bindung wuerde jede Modulanfrage mit einer
|
||||
* Meldung abweisen, die faelschlich von einer fehlenden Aktivierung
|
||||
* spricht.
|
||||
*/
|
||||
async findBySlug(slug: string) {
|
||||
return this.prisma.module.findUnique({
|
||||
@@ -33,7 +47,8 @@ export class ModuleRegistryService {
|
||||
* Returns all active modules for a given tenant.
|
||||
*/
|
||||
async findActiveForTenant(tenantId: string) {
|
||||
const activations = await this.prisma.tenantModuleActivation.findMany({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const activations = await tenantPrisma.tenantModuleActivation.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
isActive: true,
|
||||
@@ -43,7 +58,7 @@ export class ModuleRegistryService {
|
||||
},
|
||||
});
|
||||
|
||||
return activations.map((a) => a.module);
|
||||
return activations.map((a: { module: unknown }) => a.module);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +66,10 @@ export class ModuleRegistryService {
|
||||
* Per D-08: dynamic activation without restart.
|
||||
*/
|
||||
async activateForTenant(tenantId: string, moduleId: string) {
|
||||
// Verify module exists
|
||||
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
|
||||
// `findAll` oben (Aufgabe 1, Befund E). Laeuft VOR jedem gebundenen
|
||||
// Schreibzugriff: eine unbekannte moduleId wirft, bevor der gebundene
|
||||
// Klient ueberhaupt erzeugt wird.
|
||||
const moduleExists = await this.prisma.module.findUnique({
|
||||
where: { id: moduleId },
|
||||
});
|
||||
@@ -59,7 +77,8 @@ export class ModuleRegistryService {
|
||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||
}
|
||||
|
||||
return this.prisma.tenantModuleActivation.upsert({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
return tenantPrisma.tenantModuleActivation.upsert({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
@@ -86,7 +105,8 @@ export class ModuleRegistryService {
|
||||
* Does not remove the activation record, preserving audit trail.
|
||||
*/
|
||||
async deactivateForTenant(tenantId: string, moduleId: string) {
|
||||
// Verify module exists
|
||||
// Verify module exists — bewusst UNGEBUNDEN, dieselbe Begruendung wie
|
||||
// `findAll` oben.
|
||||
const moduleExists = await this.prisma.module.findUnique({
|
||||
where: { id: moduleId },
|
||||
});
|
||||
@@ -94,8 +114,13 @@ export class ModuleRegistryService {
|
||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||
}
|
||||
|
||||
// EIN gebundener Klient fuer beide Aktivierungszugriffe dieser Methode
|
||||
// (Lesen, Schreiben) — nicht ein Klient je Zugriff (260910-exd,
|
||||
// Aufgabe 3, dieselbe Konvention wie `module-access.service.ts`).
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
|
||||
// Check if activation record exists
|
||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
||||
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
@@ -110,7 +135,7 @@ export class ModuleRegistryService {
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.tenantModuleActivation.update({
|
||||
return tenantPrisma.tenantModuleActivation.update({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
@@ -128,7 +153,15 @@ export class ModuleRegistryService {
|
||||
|
||||
/**
|
||||
* Checks whether a module (by slug) is active for a given tenant.
|
||||
* Used by ModuleGuard to gate access to module-specific endpoints.
|
||||
*
|
||||
* Richtiggestellt (260910-exd, Aufgabe 1, Befund G): der vorherige
|
||||
* Kommentar behauptete, `ModuleGuard` benutze diese Methode — er tut es
|
||||
* NICHT. Gemessen (Aufgabe 1, TEIL 3, `grep -rn "isModuleActive"
|
||||
* apps/api/src apps/web/src packages`): genau EIN Treffer, die Definition
|
||||
* selbst, kein Aufrufer. Der Waechter nimmt stattdessen `findBySlug` plus
|
||||
* `ModuleAccessService.getAccessibleModuleIds`. Diese Methode bleibt
|
||||
* TROTZDEM umgestellt: heute toter, ungebunden gelassener Code ist die
|
||||
* Falle fuer den, der ihn morgen verdrahtet.
|
||||
*/
|
||||
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
|
||||
const module = await this.prisma.module.findUnique({
|
||||
@@ -139,7 +172,8 @@ export class ModuleRegistryService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const activation = await tenantPrisma.tenantModuleActivation.findUnique({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
@@ -154,6 +188,11 @@ export class ModuleRegistryService {
|
||||
/**
|
||||
* Registers or updates a module in the registry by slug (upsert).
|
||||
* Used during application startup to seed built-in modules.
|
||||
*
|
||||
* Bewusst UNGEBUNDEN, dieselbe Begruendung wie `findAll` oben — mit einem
|
||||
* zusaetzlichen Grund, den nur diese Methode hat: sie laeuft beim
|
||||
* Anwendungsstart aus vier Seed-Dateien, ohne Anfrage und ohne Mandanten
|
||||
* — ein gebundener Aufruf haette dort strukturell keinen Kontext.
|
||||
*/
|
||||
async seedModule(manifest: {
|
||||
slug: string;
|
||||
|
||||
@@ -15,7 +15,17 @@ import { TenderSchedulerService } from './tender-scheduler.service';
|
||||
* tender-ingestion.service.spec.ts / ldap.service.spec.ts, driving the REAL
|
||||
* ModuleRegistryService (unmocked) so the activation call path is genuine,
|
||||
* not a stand-in.
|
||||
*
|
||||
* forTenant() just returns the same client in these tests (identical
|
||||
* convention to ldap.service.spec.ts) — tenant scoping/RLS binding is not
|
||||
* what this file tests, only ModuleRegistryService.activateForTenant's
|
||||
* poll-once-fan-out-many behavior. Needed since 260910-exd (Aufgabe 3)
|
||||
* converted ModuleRegistryService.activateForTenant to forTenant(), and the
|
||||
* hand-rolled fake below does not implement `$extends`.
|
||||
*/
|
||||
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
||||
forTenant: vi.fn((p: unknown) => p),
|
||||
}));
|
||||
|
||||
function makeFakePrisma() {
|
||||
const modules = new Map<string, any>();
|
||||
|
||||
Reference in New Issue
Block a user