import { describe, it, expect, vi } from 'vitest'; import Role from './role'; import Base from './base.js'; import Permission from './permission.js'; import User from './user.js'; import { createRole } from '../../test/factories/role.js'; import { createPermission } from '../../test/factories/permission.js'; import { createUser } from '../../test/factories/user.js'; import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js'; describe('Role model', () => { it('tableName should return correct name', () => { expect(Role.tableName).toBe('roles'); }); it('jsonSchema should have correct validations', () => { expect(Role.jsonSchema).toMatchSnapshot(); }); it('relationMappingsshould return correct associations', () => { const relationMappings = Role.relationMappings(); const expectedRelations = { users: { relation: Base.HasManyRelation, modelClass: User, join: { from: 'roles.id', to: 'users.role_id', }, }, permissions: { relation: Base.HasManyRelation, modelClass: Permission, join: { from: 'roles.id', to: 'permissions.role_id', }, }, }; expect(relationMappings).toStrictEqual(expectedRelations); }); it('virtualAttributes should return correct attributes', () => { expect(Role.virtualAttributes).toStrictEqual(['isAdmin']); }); describe('isAdmin', () => { it('should return true for admin named role', () => { const role = new Role(); role.name = 'Admin'; expect(role.isAdmin).toBe(true); }); it('should return false for not admin named roles', () => { const role = new Role(); role.name = 'User'; expect(role.isAdmin).toBe(false); }); }); it('findAdmin should return admin role', async () => { const createdAdminRole = await createRole({ name: 'Admin' }); const adminRole = await Role.findAdmin(); expect(createdAdminRole).toStrictEqual(adminRole); }); describe('preventAlteringAdmin', () => { it('preventAlteringAdmin should throw an error when altering admin role', async () => { const role = await createRole({ name: 'Admin' }); await expect(() => role.preventAlteringAdmin()).rejects.toThrowError( 'The admin role cannot be altered!' ); }); it('preventAlteringAdmin should not throw an error when altering non-admin roles', async () => { const role = await createRole({ name: 'User' }); expect(await role.preventAlteringAdmin()).toBe(undefined); }); }); it("deletePermissions should delete role's permissions", async () => { const role = await createRole({ name: 'User' }); await createPermission({ roleId: role.id }); await role.deletePermissions(); expect(await role.$relatedQuery('permissions')).toStrictEqual([]); }); describe('createPermissions', () => { it('should create permissions', async () => { const role = await createRole({ name: 'User' }); await role.createPermissions([ { action: 'read', subject: 'Flow', conditions: [] }, ]); expect(await role.$relatedQuery('permissions')).toMatchObject([ { action: 'read', subject: 'Flow', conditions: [], }, ]); }); it('should call Permission.filter', async () => { const role = await createRole({ name: 'User' }); const permissions = [{ action: 'read', subject: 'Flow', conditions: [] }]; const permissionFilterSpy = vi .spyOn(Permission, 'filter') .mockReturnValue(permissions); await role.createPermissions(permissions); expect(permissionFilterSpy).toHaveBeenCalledWith(permissions); }); }); it('updatePermissions should delete existing permissions and create new permissions', async () => { const permissionsData = [ { action: 'read', subject: 'Flow', conditions: [] }, ]; const deletePermissionsSpy = vi .spyOn(Role.prototype, 'deletePermissions') .mockResolvedValueOnce(); const createPermissionsSpy = vi .spyOn(Role.prototype, 'createPermissions') .mockResolvedValueOnce(); const role = await createRole({ name: 'User' }); await role.updatePermissions(permissionsData); expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan( createPermissionsSpy.mock.invocationCallOrder[0] ); expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1); expect(createPermissionsSpy).toHaveBeenNthCalledWith(1, permissionsData); }); describe('updateWithPermissions', () => { it('should update role along with given permissions', async () => { const role = await createRole({ name: 'User' }); await createPermission({ roleId: role.id, subject: 'Flow', action: 'read', conditions: [], }); const newRoleData = { name: 'Updated user', description: 'Updated description', permissions: [ { action: 'update', subject: 'Flow', conditions: [], }, ], }; await role.updateWithPermissions(newRoleData); const roleWithPermissions = await role .$query() .leftJoinRelated({ permissions: true }) .withGraphFetched({ permissions: true }); expect(roleWithPermissions).toMatchObject(newRoleData); }); }); describe('deleteWithPermissions', () => { it('should delete role along with given permissions', async () => { const role = await createRole({ name: 'User' }); await createPermission({ roleId: role.id, subject: 'Flow', action: 'read', conditions: [], }); await role.deleteWithPermissions(); const refetchedRole = await role.$query(); const rolePermissions = await Permission.query().where({ roleId: role.id, }); expect(refetchedRole).toBe(undefined); expect(rolePermissions).toStrictEqual([]); }); }); describe('assertNoRoleUserExists', () => { it('should reject with an error when the role has users', async () => { const role = await createRole({ name: 'User' }); await createUser({ roleId: role.id }); await expect(() => role.assertNoRoleUserExists()).rejects.toThrowError( `All users must be migrated away from the "User" role.` ); }); it('should resolve when the role does not have any users', async () => { const role = await createRole(); expect(await role.assertNoRoleUserExists()).toBe(undefined); }); }); describe('assertNoConfigurationUsage', () => { it('should reject with an error when the role is used in configuration', async () => { const role = await createRole(); await createSamlAuthProvider({ defaultRoleId: role.id }); await expect(() => role.assertNoConfigurationUsage() ).rejects.toThrowError( 'samlAuthProvider: You need to change the default role in the SAML configuration before deleting this role.' ); }); it('should resolve when the role does not have any users', async () => { const role = await createRole(); expect(await role.assertNoConfigurationUsage()).toBe(undefined); }); }); it('assertRoleIsNotUsed should call assertNoRoleUserExists and assertNoConfigurationUsage', async () => { const role = new Role(); const assertNoRoleUserExistsSpy = vi .spyOn(role, 'assertNoRoleUserExists') .mockResolvedValue(); const assertNoConfigurationUsageSpy = vi .spyOn(role, 'assertNoConfigurationUsage') .mockResolvedValue(); await role.assertRoleIsNotUsed(); expect(assertNoRoleUserExistsSpy).toHaveBeenCalledOnce(); expect(assertNoConfigurationUsageSpy).toHaveBeenCalledOnce(); }); describe('$beforeDelete', () => { it('should call preventAlteringAdmin', async () => { const role = await createRole({ name: 'User' }); const preventAlteringAdminSpy = vi .spyOn(role, 'preventAlteringAdmin') .mockResolvedValue(); await role.$query().delete(); expect(preventAlteringAdminSpy).toHaveBeenCalledOnce(); }); it('should call assertRoleIsNotUsed', async () => { const role = await createRole({ name: 'User' }); const assertRoleIsNotUsedSpy = vi .spyOn(role, 'assertRoleIsNotUsed') .mockResolvedValue(); await role.$query().delete(); expect(assertRoleIsNotUsedSpy).toHaveBeenCalledOnce(); }); }); });