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:
@@ -15,6 +15,7 @@ function makeFakePrisma() {
|
|||||||
const groups = new Map<string, any>();
|
const groups = new Map<string, any>();
|
||||||
const users = new Map<string, any>();
|
const users = new Map<string, any>();
|
||||||
const memberships = new Map<string, Set<string>>(); // groupId -> Set<userId>
|
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 activations = new Map<string, any>(); // key: tenantId::moduleId
|
||||||
const grants = new Map<string, any>();
|
const grants = new Map<string, any>();
|
||||||
let grantCounter = 0;
|
let grantCounter = 0;
|
||||||
@@ -47,10 +48,11 @@ function makeFakePrisma() {
|
|||||||
__seedUser(user: { id: string; tenantId: string }) {
|
__seedUser(user: { id: string; tenantId: string }) {
|
||||||
users.set(user.id, user);
|
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>();
|
const set = memberships.get(groupId) ?? new Set<string>();
|
||||||
set.add(userId);
|
set.add(userId);
|
||||||
memberships.set(groupId, set);
|
memberships.set(groupId, set);
|
||||||
|
membershipSources.set(`${groupId}::${userId}`, source);
|
||||||
},
|
},
|
||||||
__seedActivation(a: {
|
__seedActivation(a: {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
@@ -145,6 +147,25 @@ function makeFakePrisma() {
|
|||||||
return { count };
|
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');
|
const result = await service.getUserAccess('t1', 'u1');
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result.modules).toEqual([
|
||||||
{
|
{
|
||||||
module: { id: 'mod-1', category: 'ops', name: 'Modul Eins' },
|
module: { id: 'mod-1', category: 'ops', name: 'Modul Eins' },
|
||||||
viaGroups: ['Gruppe A'],
|
viaGroups: ['Gruppe A'],
|
||||||
direct: false,
|
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 () => {
|
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');
|
const result = await service.getUserAccess('t1', 'u1');
|
||||||
|
|
||||||
expect(result[0].viaGroups).toEqual(['Gruppe A']);
|
expect(result.modules[0].viaGroups).toEqual(['Gruppe A']);
|
||||||
expect(result[0].direct).toBe(true);
|
expect(result.modules[0].direct).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('wirft NotFoundException für eine userId aus einem anderen Mandanten', async () => {
|
it('wirft NotFoundException für eine userId aus einem anderen Mandanten', async () => {
|
||||||
@@ -416,6 +438,71 @@ describe('ModuleGrantsService.getUserAccess', () => {
|
|||||||
NotFoundException,
|
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)', () => {
|
describe('ModuleGrantsService — Logging (D-23)', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user