import { RoleSyncService } from '../src/modules/auth/role-sync.service'; describe('RoleSyncService - transaction rollback', () => { let service: 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') }; beforeEach(() => { mockPrisma.user = { findUnique: jest.fn() }; mockPrisma.authGroupRoleMapping = { findMany: jest.fn() }; mockPrisma.userRole = { findMany: jest.fn().mockResolvedValue([]) }; mockPrisma.user = mockPrisma.user; // simulate transaction throwing mockPrisma.$transaction = jest.fn(async (cb: any) => { throw new Error('tx failed'); }); service = new RoleSyncService(mockPrisma as any, mockGroupHash as any, mockPermissionCache as any, mockEvents as any); }); test('does not invalidate cache or publish event when transaction fails', async () => { mockPrisma.user.findUnique.mockResolvedValue({ id: 'uid', lastGroupHash: 'old' }); mockPrisma.authGroupRoleMapping.findMany.mockResolvedValue([{ roleId: 'r1' }]); await expect(service.syncUserRolesFromAuthentik('uid', ['RL-Owner'])).rejects.toThrow('tx failed'); expect(mockPermissionCache.invalidate).not.toHaveBeenCalled(); expect(mockEvents.publish).not.toHaveBeenCalled(); }); });