test(260805-d0r): add failing tests for groups in getUserAccess

- Regression: user in a group without any module grant stays visible
- Cross-tenant: membership in a foreign tenant's group is excluded
- Origin (MANUAL/LDAP), empty-modules case, stable alpha sort
This commit is contained in:
2026-08-05 09:33:22 +02:00
parent be1183a61c
commit b6d4e4acb6
@@ -15,6 +15,7 @@ function makeFakePrisma() {
const groups = new Map<string, any>();
const users = new Map<string, any>();
const memberships = new Map<string, Set<string>>(); // groupId -> Set<userId>
const membershipSources = new Map<string, string>(); // `${groupId}::${userId}` -> source
const activations = new Map<string, any>(); // key: tenantId::moduleId
const grants = new Map<string, any>();
let grantCounter = 0;
@@ -47,10 +48,11 @@ function makeFakePrisma() {
__seedUser(user: { id: string; tenantId: string }) {
users.set(user.id, user);
},
__seedMembership(groupId: string, userId: string) {
__seedMembership(groupId: string, userId: string, source: string = 'MANUAL') {
const set = memberships.get(groupId) ?? new Set<string>();
set.add(userId);
memberships.set(groupId, set);
membershipSources.set(`${groupId}::${userId}`, source);
},
__seedActivation(a: {
tenantId: string;
@@ -145,6 +147,25 @@ function makeFakePrisma() {
return { count };
},
},
groupMembership: {
findMany: async ({ where }: any) => {
const userId = where.userId;
const tenantId = where.group.tenantId;
const rows: any[] = [];
for (const [groupId, memberSet] of memberships.entries()) {
if (!memberSet.has(userId)) continue;
const group = groups.get(groupId);
if (!group || group.tenantId !== tenantId) continue;
rows.push({
groupId,
userId,
source: membershipSources.get(`${groupId}::${userId}`) ?? 'MANUAL',
group: { id: group.id, name: group.name },
});
}
return rows;
},
},
};
}
@@ -383,13 +404,14 @@ describe('ModuleGrantsService.getUserAccess', () => {
const result = await service.getUserAccess('t1', 'u1');
expect(result).toEqual([
expect(result.modules).toEqual([
{
module: { id: 'mod-1', category: 'ops', name: 'Modul Eins' },
viaGroups: ['Gruppe A'],
direct: false,
},
]);
expect(result.groups).toEqual([{ id: 'g1', name: 'Gruppe A', source: 'MANUAL' }]);
});
it('adjacency: ein Direkt-Grant UND ein Gruppen-Grant auf dasselbe Modul erscheinen gleichzeitig, keiner verdrängt den anderen', async () => {
@@ -402,8 +424,8 @@ describe('ModuleGrantsService.getUserAccess', () => {
const result = await service.getUserAccess('t1', 'u1');
expect(result[0].viaGroups).toEqual(['Gruppe A']);
expect(result[0].direct).toBe(true);
expect(result.modules[0].viaGroups).toEqual(['Gruppe A']);
expect(result.modules[0].direct).toBe(true);
});
it('wirft NotFoundException für eine userId aus einem anderen Mandanten', async () => {
@@ -416,6 +438,71 @@ describe('ModuleGrantsService.getUserAccess', () => {
NotFoundException,
);
});
it('REGRESSION: Mitglied einer Gruppe ohne Modul-Freigabe bleibt sichtbar', async () => {
const prisma = makeFakePrisma();
seedBase(prisma);
prisma.__seedMembership('g1', 'u1');
// Bewusst KEIN Grant für g1 auf mod-1.
const service = new ModuleGrantsService(prisma as any);
const result = await service.getUserAccess('t1', 'u1');
expect(result.groups).toEqual([{ id: 'g1', name: 'Gruppe A', source: 'MANUAL' }]);
expect(result.modules.every((m: any) => m.viaGroups.length === 0)).toBe(true);
});
it('Cross-Tenant: eine Mitgliedschaft in einer Gruppe eines fremden Mandanten erscheint nicht in groups', async () => {
const prisma = makeFakePrisma();
seedBase(prisma);
prisma.__seedGroup({ id: 'g-foreign', tenantId: 't2', name: 'Fremde Gruppe' });
prisma.__seedMembership('g-foreign', 'u1');
const service = new ModuleGrantsService(prisma as any);
const result = await service.getUserAccess('t1', 'u1');
expect(result.groups).toEqual([]);
});
it('Herkunft: eine mit source LDAP geseedete Mitgliedschaft kommt mit source LDAP zurück', async () => {
const prisma = makeFakePrisma();
seedBase(prisma);
prisma.__seedMembership('g1', 'u1', 'LDAP');
const service = new ModuleGrantsService(prisma as any);
const result = await service.getUserAccess('t1', 'u1');
expect(result.groups).toEqual([{ id: 'g1', name: 'Gruppe A', source: 'LDAP' }]);
});
it('ohne aktives Modul im Mandanten: modules ist leer, groups trotzdem befüllt', async () => {
const prisma = makeFakePrisma();
prisma.__seedGroup({ id: 'g1', tenantId: 't1', name: 'Gruppe A' });
prisma.__seedUser({ id: 'u1', tenantId: 't1' });
// keine Activation geseedet
prisma.__seedMembership('g1', 'u1');
const service = new ModuleGrantsService(prisma as any);
const result = await service.getUserAccess('t1', 'u1');
expect(result.modules).toEqual([]);
expect(result.groups).toEqual([{ id: 'g1', name: 'Gruppe A', source: 'MANUAL' }]);
});
it('Sortierung: groups ist alphabetisch nach name stabil über wiederholte Aufrufe', async () => {
const prisma = makeFakePrisma();
seedBase(prisma);
prisma.__seedGroup({ id: 'g2', tenantId: 't1', name: 'Alpha' });
prisma.__seedMembership('g1', 'u1');
prisma.__seedMembership('g2', 'u1');
const service = new ModuleGrantsService(prisma as any);
const first = await service.getUserAccess('t1', 'u1');
const second = await service.getUserAccess('t1', 'u1');
expect(first.groups.map((g: any) => g.name)).toEqual(['Alpha', 'Gruppe A']);
expect(second.groups.map((g: any) => g.name)).toEqual(['Alpha', 'Gruppe A']);
});
});
describe('ModuleGrantsService — Logging (D-23)', () => {