b9d87be360
- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000 - API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten - Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“ - MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
127 lines
5.4 KiB
TypeScript
127 lines
5.4 KiB
TypeScript
import { NotFoundException } from '@nestjs/common';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
// `forTenant` reicht den Klienten durch — Mandantenbindung selbst prueft
|
|
// rls-access-inventory.spec.ts; hier zaehlt, dass je Methode (prisma, tenantId)
|
|
// uebergeben wird.
|
|
vi.mock('../prisma/prisma-tenant.extension', () => ({
|
|
forTenant: vi.fn((p: unknown) => p),
|
|
}));
|
|
|
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
|
import { CustomModulesService } from './custom-modules.service';
|
|
|
|
function makeFakePrisma() {
|
|
const rows = new Map<string, any>();
|
|
let seq = 0;
|
|
const customModule = {
|
|
create: vi.fn(async ({ data }: { data: any }) => {
|
|
const id = `cm-${++seq}`;
|
|
const row = { id, createdAt: new Date(), updatedAt: new Date(), ...data };
|
|
rows.set(id, row);
|
|
return row;
|
|
}),
|
|
findMany: vi.fn(async ({ where, orderBy }: { where?: any; orderBy?: any } = {}) => {
|
|
let list = [...rows.values()];
|
|
if (where?.tenantId) list = list.filter((r) => r.tenantId === where.tenantId);
|
|
if (orderBy?.name === 'asc') list.sort((a, b) => a.name.localeCompare(b.name));
|
|
return list;
|
|
}),
|
|
findUnique: vi.fn(async ({ where }: { where: { id: string } }) => rows.get(where.id) ?? null),
|
|
update: vi.fn(async ({ where, data }: { where: { id: string }; data: any }) => {
|
|
const row = { ...rows.get(where.id), ...data };
|
|
rows.set(where.id, row);
|
|
return row;
|
|
}),
|
|
delete: vi.fn(async ({ where }: { where: { id: string } }) => {
|
|
rows.delete(where.id);
|
|
}),
|
|
};
|
|
return { customModule, rows };
|
|
}
|
|
|
|
const dto = { name: 'Wiki', url: 'https://example.com', category: 'infrastructure' as const };
|
|
|
|
describe('CustomModulesService', () => {
|
|
it('create speichert tenantId aus dem Argument, nie aus dem DTO', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
await service.create('t1', { ...dto, tenantId: 'evil' } as any);
|
|
expect(prisma.customModule.create).toHaveBeenCalledTimes(1);
|
|
expect(prisma.customModule.create.mock.calls[0][0].data.tenantId).toBe('t1');
|
|
});
|
|
|
|
it('list liefert nur Zeilen des Mandanten, nach Name sortiert', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
await service.create('t1', { ...dto, name: 'Zebra' });
|
|
await service.create('t1', { ...dto, name: 'Anker' });
|
|
await service.create('t2', { ...dto, name: 'Fremd' });
|
|
const result = await service.list('t1');
|
|
expect(result.map((r: any) => r.name)).toEqual(['Anker', 'Zebra']);
|
|
expect(prisma.customModule.findMany.mock.calls[0]?.[0]?.where).toEqual({ tenantId: 't1' });
|
|
});
|
|
|
|
it('getOne liefert die Zeile ohne tenantId', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
const created: any = await service.create('t1', dto);
|
|
const row: any = await service.getOne('t1', created.id);
|
|
expect(row.name).toBe('Wiki');
|
|
expect(row).not.toHaveProperty('tenantId');
|
|
});
|
|
|
|
it('getOne/update/remove mit unbekannter id -> NotFoundException', async () => {
|
|
const service = new CustomModulesService(makeFakePrisma() as any);
|
|
await expect(service.getOne('t1', 'nope')).rejects.toBeInstanceOf(NotFoundException);
|
|
await expect(service.update('t1', 'nope', { name: 'x' })).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
await expect(service.remove('t1', 'nope')).rejects.toBeInstanceOf(NotFoundException);
|
|
});
|
|
|
|
it('getOne/update/remove mit Zeile eines anderen Mandanten -> NotFoundException', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
const created: any = await service.create('t2', dto);
|
|
await expect(service.getOne('t1', created.id)).rejects.toBeInstanceOf(NotFoundException);
|
|
await expect(service.update('t1', created.id, { name: 'x' })).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
await expect(service.remove('t1', created.id)).rejects.toBeInstanceOf(NotFoundException);
|
|
expect(prisma.customModule.update).not.toHaveBeenCalled();
|
|
expect(prisma.customModule.delete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('update aendert nur gesetzte Felder', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
const created: any = await service.create('t1', dto);
|
|
await service.update('t1', created.id, { name: 'Neu' });
|
|
expect(prisma.customModule.update.mock.calls[0][0].data).toEqual({ name: 'Neu' });
|
|
});
|
|
|
|
it('remove loescht und liefert { deleted: true }', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
const created: any = await service.create('t1', dto);
|
|
await expect(service.remove('t1', created.id)).resolves.toEqual({ deleted: true });
|
|
expect(prisma.rows.size).toBe(0);
|
|
});
|
|
|
|
it('ruft forTenant je Methode mit (prisma, tenantId) auf', async () => {
|
|
const prisma = makeFakePrisma();
|
|
const service = new CustomModulesService(prisma as any);
|
|
vi.mocked(forTenant).mockClear();
|
|
const created: any = await service.create('t1', dto);
|
|
await service.list('t1');
|
|
await service.getOne('t1', created.id);
|
|
await service.update('t1', created.id, { name: 'a' });
|
|
await service.remove('t1', created.id);
|
|
expect(forTenant).toHaveBeenCalledTimes(5);
|
|
for (const call of vi.mocked(forTenant).mock.calls) {
|
|
expect(call).toEqual([prisma, 't1']);
|
|
}
|
|
});
|
|
});
|