diff --git a/apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx b/apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx index b04aea1..a905a49 100644 --- a/apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx +++ b/apps/web/src/app/(portal)/admin/users/user-access-modal.test.tsx @@ -23,6 +23,10 @@ const messages: Record> = { '{module} direkt für {user} {granted, select, true {freigeben} other {entziehen}}', saveError: 'Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.', }, + 'admin.groups.members': { + sourceManual: 'Manuell', + sourceLdap: 'LDAP', + }, common: { loading: 'Laden...', close: 'Schliessen', @@ -58,18 +62,21 @@ vi.mock('next-intl', () => ({ import { UserAccessModal } from './components/UserAccessModal'; -const mockAccessRows = [ - { - module: { id: 'm1', name: 'Ausschreibungs-Radar', category: 'procurement' }, - viaGroups: ['Alle Benutzer'], - direct: false, - }, - { - module: { id: 'm2', name: 'DKV Flotte', category: 'fleet' }, - viaGroups: [], - direct: true, - }, -]; +const mockAccess = { + groups: [{ id: 'g1', name: 'Alle Benutzer', source: 'MANUAL' as const }], + modules: [ + { + module: { id: 'm1', name: 'Ausschreibungs-Radar', category: 'procurement' }, + viaGroups: ['Alle Benutzer'], + direct: false, + }, + { + module: { id: 'm2', name: 'DKV Flotte', category: 'fleet' }, + viaGroups: [], + direct: true, + }, + ], +}; afterEach(() => { cleanup(); @@ -80,7 +87,7 @@ describe('UserAccessModal', () => { it('renders the group-membership chip list, and the empty state when the user has none', async () => { vi.stubGlobal( 'fetch', - vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccessRows) })), + vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccess) })), ); render(); @@ -96,10 +103,7 @@ describe('UserAccessModal', () => { vi.fn(() => Promise.resolve({ ok: true, - json: () => - Promise.resolve( - mockAccessRows.map((row) => ({ ...row, viaGroups: [] as string[] })), - ), + json: () => Promise.resolve({ groups: [], modules: mockAccess.modules }), }), ), ); @@ -113,10 +117,44 @@ describe('UserAccessModal', () => { }); }); + it('REGRESSION: a group without any module grant stays visible as a chip', async () => { + // Deliberately no module row references the group name -- viaGroups is + // empty everywhere, so any found text is unambiguously the chip and not + // the "Über Gruppe(n)" column. + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + groups: [{ id: 'g1', name: 'Gruppe A', source: 'MANUAL' as const }], + modules: [ + { + module: { id: 'm1', name: 'Ausschreibungs-Radar', category: 'procurement' }, + viaGroups: [], + direct: false, + }, + ], + }), + }), + ), + ); + + render(); + + await waitFor(() => { + expect(screen.getByText('Gruppe A')).toBeInTheDocument(); + }); + expect( + screen.queryByText('Dieser Benutzer ist keiner Gruppe zugeordnet.'), + ).not.toBeInTheDocument(); + }); + it('renders the module access table with one inherited and one non-inherited module', async () => { vi.stubGlobal( 'fetch', - vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccessRows) })), + vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccess) })), ); render(); @@ -125,21 +163,43 @@ describe('UserAccessModal', () => { expect(screen.getByText('Ausschreibungs-Radar')).toBeInTheDocument(); }); expect(screen.getByText('DKV Flotte')).toBeInTheDocument(); - // m1 is inherited via 'Alle Benutzer' -- shown as a chip in the via-groups column. + // m1 is inherited via 'Alle Benutzer' -- shown as a chip in the via-groups column + // AND as a group-membership chip. expect(screen.getAllByText('Alle Benutzer').length).toBeGreaterThan(0); // m2 has no inheriting groups -- renders the em-dash placeholder. expect(screen.getByText('–')).toBeInTheDocument(); }); + it('shows both the group chips and the no-active-modules hint when modules is empty but groups is not', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ groups: mockAccess.groups, modules: [] }), + }), + ), + ); + + render(); + + await waitFor(() => { + expect(screen.getAllByText('Alle Benutzer').length).toBeGreaterThan(0); + }); + expect( + screen.getByText('Für diesen Mandanten sind keine Module aktiviert.'), + ).toBeInTheDocument(); + }); + it('rolls back the direct checkbox and shows a visible error when the request fails', async () => { const fetchMock = vi.fn((url: string, init?: RequestInit) => { if (typeof url === 'string' && url.endsWith('/module-grants/users/u1')) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccessRows) }); + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccess) }); } if (typeof url === 'string' && url.endsWith('/module-grants') && init?.method === 'POST') { return Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve('boom') }); } - return Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccessRows) }); + return Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccess) }); }); vi.stubGlobal('fetch', fetchMock); @@ -164,7 +224,9 @@ describe('UserAccessModal', () => { it('shows the hint text when the tenant has no active modules', async () => { vi.stubGlobal( 'fetch', - vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve([]) })), + vi.fn(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({ groups: [], modules: [] }) }), + ), ); render(); @@ -179,7 +241,7 @@ describe('UserAccessModal', () => { it('gives every direct checkbox an aria-label', async () => { vi.stubGlobal( 'fetch', - vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccessRows) })), + vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockAccess) })), ); render(); @@ -194,4 +256,32 @@ describe('UserAccessModal', () => { expect(box).toHaveAccessibleName(); } }); + + it('shows the LDAP origin badge on a chip with source LDAP and the Manuell badge on source MANUAL', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + groups: [ + { id: 'g1', name: 'Manuelle Gruppe', source: 'MANUAL' as const }, + { id: 'g2', name: 'LDAP-Gruppe', source: 'LDAP' as const }, + ], + modules: [], + }), + }), + ), + ); + + render(); + + await waitFor(() => { + expect(screen.getByText('Manuelle Gruppe')).toBeInTheDocument(); + }); + expect(screen.getByText('LDAP-Gruppe')).toBeInTheDocument(); + expect(screen.getByText('Manuell')).toBeInTheDocument(); + expect(screen.getByText('LDAP')).toBeInTheDocument(); + }); });