import { RoleSyncService } from '../src/modules/auth/role-sync.service'; describe('RoleSyncService', () => { const mockPrisma: any = {}; const mockPermissionCache: any = { invalidate: jest.fn() }; const mockEvents: any = { publish: jest.fn() }; const mockGroupHash: any = { compute: (g: any) => require('crypto').createHash('sha256').update((g||[]).slice().sort().join(','), 'utf8').digest('hex') }; let service: RoleSyncService; beforeEach(() => { mockPrisma.user = { findUnique: jest.fn() }; mockPrisma.authGroupRoleMapping = { findMany: jest.fn() }; mockPrisma.userRole = { findMany: jest.fn() }; mockPrisma.auditLog = { create: jest.fn() }; mockPrisma.user = mockPrisma.user; mockPrisma.$transaction = jest.fn(async (cb: any) => { // simulate transaction by calling provided callback with tx = mockPrisma await cb(mockPrisma); }); service = new RoleSyncService(mockPrisma as any, mockGroupHash as any, mockPermissionCache as any, mockEvents as any); }); test('computeGroupHash consistent and order independent', () => { const a = ['b', 'a', 'c']; const h1 = service.computeGroupHash(a); const h2 = service.computeGroupHash(['a', 'b', 'c']); expect(h1).toBe(h2); }); test('skips sync when group hash unchanged', async () => { const groups = ['RL-Owner']; const hash = service.computeGroupHash(groups); mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: hash }); const res = await service.syncUserRolesFromAuthentik('uid', groups); expect(res.skipped).toBe(true); expect(mockPrisma.$transaction).not.toHaveBeenCalled(); }); test('performs transaction and writes audit when changed', async () => { const groups = ['RL-Owner']; mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: 'old' }); mockPrisma.authGroupRoleMapping.findMany.mockResolvedValue([{ roleId: 'r1' }]); mockPrisma.userRole.findMany.mockResolvedValue([{ roleId: 'r_old' }]); // spy on tx ops mockPrisma.userRole.deleteMany = jest.fn(); mockPrisma.userRole.createMany = jest.fn(); mockPrisma.user.update = jest.fn(); mockPrisma.auditLog.create = jest.fn(); const res = await service.syncUserRolesFromAuthentik('uid', groups); expect(mockPrisma.$transaction).toHaveBeenCalled(); expect(mockPrisma.userRole.deleteMany).toHaveBeenCalledWith({ where: { userId: 'uid', source: 'AUTHENTIK' } }); expect(mockPrisma.userRole.createMany).toHaveBeenCalled(); expect(mockPrisma.user.update).toHaveBeenCalled(); expect(mockPermissionCache.invalidate).toHaveBeenCalledWith('uid'); expect(mockEvents.publish).toHaveBeenCalledWith(expect.objectContaining({ type: 'RolesSynchronized' })); expect(res.skipped).toBe(false); expect(res.assignedRoleIds).toEqual(['r1']); }); });