import { ForbiddenException } from '@nestjs/common'; import { TeamPermissionsService } from './team-permissions.service'; describe('TeamPermissionsService', () => { const teamRepository = { findOne: jest.fn() }; const teamSettingRepository = { save: jest.fn((s) => Promise.resolve(s)), create: jest.fn((s) => s), }; const access = { assertMember: jest.fn(), assertManager: jest.fn() }; const logger = { info: jest.fn() }; let service: TeamPermissionsService; beforeEach(() => { jest.resetAllMocks(); teamSettingRepository.save.mockImplementation((s) => Promise.resolve(s)); teamSettingRepository.create.mockImplementation((s) => s); service = new TeamPermissionsService( teamRepository as any, teamSettingRepository as any, access as any, logger as any, ); }); const teamWithSettings = (overrides: Record = {}) => ({ id: 9, settings: [ { key: 'transaction_create_min_role', value: overrides.transaction_create_min_role ?? '2' }, { key: 'transaction_reverse_min_role', value: overrides.transaction_reverse_min_role ?? '2' }, { key: 'invite_min_role', value: overrides.invite_min_role ?? '3' }, { key: 'member_manage_min_role', value: overrides.member_manage_min_role ?? '3' }, { key: 'penalty_manage_min_role', value: overrides.penalty_manage_min_role ?? '3' }, { key: 'public_access_manage_min_role', value: overrides.public_access_manage_min_role ?? '3' }, ], }); it('requires at least team membership to read permissions', async () => { access.assertMember.mockRejectedValue(new ForbiddenException()); await expect(service.getPermissions(2, 9)).rejects.toBeInstanceOf( ForbiddenException, ); expect(teamRepository.findOne).not.toHaveBeenCalled(); }); it('maps the stored settings to the permissions DTO', async () => { access.assertMember.mockResolvedValue(undefined); teamRepository.findOne.mockResolvedValue(teamWithSettings()); await expect(service.getPermissions(2, 9)).resolves.toEqual({ transactionCreateMinRole: 2, transactionReverseMinRole: 2, inviteMinRole: 3, memberManageMinRole: 3, penaltyManageMinRole: 3, publicAccessManageMinRole: 3, }); }); it('falls back to defaults for missing settings', async () => { access.assertMember.mockResolvedValue(undefined); teamRepository.findOne.mockResolvedValue({ id: 9, settings: [] }); await expect(service.getPermissions(2, 9)).resolves.toEqual({ transactionCreateMinRole: 2, transactionReverseMinRole: 2, inviteMinRole: 3, memberManageMinRole: 3, penaltyManageMinRole: 3, publicAccessManageMinRole: 3, }); }); it('requires manager rights to update permissions', async () => { access.assertManager.mockRejectedValue(new ForbiddenException()); await expect( service.updatePermissions(2, 9, { inviteMinRole: 4 }), ).rejects.toBeInstanceOf(ForbiddenException); expect(teamSettingRepository.save).not.toHaveBeenCalled(); }); it('updates only the fields present in the DTO and leaves the rest untouched', async () => { access.assertManager.mockResolvedValue(undefined); const team = teamWithSettings(); teamRepository.findOne.mockResolvedValue(team); await service.updatePermissions(2, 9, { inviteMinRole: 4 }); expect(teamSettingRepository.save).toHaveBeenCalledTimes(1); const saved = teamSettingRepository.save.mock.calls[0][0]; expect(saved).toMatchObject({ key: 'invite_min_role', value: '4' }); expect(logger.info).toHaveBeenCalledWith( expect.objectContaining({ event: 'team_permissions_update', userId: 2 }), ); }); it('creates a new setting row if none exists yet for that key', async () => { access.assertManager.mockResolvedValue(undefined); teamRepository.findOne.mockResolvedValue({ id: 9, settings: [] }); await service.updatePermissions(2, 9, { memberManageMinRole: 4 }); expect(teamSettingRepository.create).toHaveBeenCalledWith( expect.objectContaining({ key: 'member_manage_min_role', value: '4' }), ); expect(teamSettingRepository.save).toHaveBeenCalledTimes(1); }); });