import { testDb, linkUserToProject, createTeamProject } from '@n8n/backend-test-utils'; import { AuthRolesService, RoleRepository, ScopeRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { createRole, createSystemRole, createCustomRoleWithScopes, createTestScopes, } from '../../shared/db/roles'; import { createUser } from '../../shared/db/users'; describe('RoleRepository', () => { let roleRepository: RoleRepository; let scopeRepository: ScopeRepository; beforeAll(async () => { await testDb.init(); roleRepository = Container.get(RoleRepository); scopeRepository = Container.get(ScopeRepository); }); beforeEach(async () => { // Truncate in the correct order to respect foreign key constraints // user table references role via roleSlug // ProjectRelation references role await testDb.truncate(['User', 'ProjectRelation', 'Project', 'Role', 'Scope']); }); afterAll(async () => { await testDb.terminate(); }); describe('findAll()', () => { it('should return empty array when no roles exist', async () => { // // ARRANGE & ACT // const roles = await roleRepository.findAll(); // // ASSERT // expect(roles).toEqual([]); }); it('should return all roles when roles exist', async () => { // // ARRANGE // const role1 = await createRole({ slug: 'test-role-1', displayName: 'Role 1' }); const role2 = await createRole({ slug: 'test-role-2', displayName: 'Role 2' }); const role3 = await createSystemRole({ slug: 'system-role-1', displayName: 'System Role' }); // // ACT // const roles = await roleRepository.findAll(); // // ASSERT // expect(roles).toHaveLength(3); expect(roles).toEqual( expect.arrayContaining([ expect.objectContaining({ slug: role1.slug, displayName: role1.displayName }), expect.objectContaining({ slug: role2.slug, displayName: role2.displayName }), expect.objectContaining({ slug: role3.slug, displayName: role3.displayName }), ]), ); }); it('should return roles with their eager-loaded scopes', async () => { // // ARRANGE // const { readScope, writeScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope, writeScope], { slug: 'test-role-with-scopes', displayName: 'Role With Scopes', }); // // ACT // const roles = await roleRepository.findAll(); // // ASSERT // expect(roles).toHaveLength(1); expect(roles[0].scopes).toHaveLength(2); expect(roles[0].scopes).toEqual( expect.arrayContaining([ expect.objectContaining({ slug: readScope.slug }), expect.objectContaining({ slug: writeScope.slug }), ]), ); }); }); describe('findBySlug()', () => { it('should return null when role does not exist', async () => { // // ARRANGE & ACT // const role = await roleRepository.findBySlug('non-existent-role'); // // ASSERT // expect(role).toBeNull(); }); it('should return role when it exists', async () => { // // ARRANGE // const createdRole = await createRole({ slug: 'test-find-role', displayName: 'Test Find Role', description: 'A role for testing findBySlug', roleType: 'project', }); // // ACT // const foundRole = await roleRepository.findBySlug('test-find-role'); // // ASSERT // expect(foundRole).not.toBeNull(); expect(foundRole!.slug).toBe(createdRole.slug); expect(foundRole!.displayName).toBe(createdRole.displayName); expect(foundRole!.description).toBe(createdRole.description); expect(foundRole!.roleType).toBe(createdRole.roleType); }); it('should return role with eager-loaded scopes', async () => { // // ARRANGE // const { readScope, writeScope, adminScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope, writeScope, adminScope], { slug: 'test-role-with-all-scopes', displayName: 'Role With All Scopes', }); // // ACT // const foundRole = await roleRepository.findBySlug('test-role-with-all-scopes'); // // ASSERT // expect(foundRole).not.toBeNull(); expect(foundRole!.scopes).toHaveLength(3); expect(foundRole!.scopes).toEqual( expect.arrayContaining([ expect.objectContaining({ slug: readScope.slug }), expect.objectContaining({ slug: writeScope.slug }), expect.objectContaining({ slug: adminScope.slug }), ]), ); }); it('should find system roles correctly', async () => { // // ARRANGE // const systemRole = await createSystemRole({ slug: 'system-test-role', displayName: 'System Test Role', }); // // ACT // const foundRole = await roleRepository.findBySlug('system-test-role'); // // ASSERT // expect(foundRole).not.toBeNull(); expect(foundRole!.systemRole).toBe(true); expect(foundRole!.slug).toBe(systemRole.slug); }); }); describe('removeBySlug()', () => { it('should successfully remove existing role', async () => { // // ARRANGE // await createRole({ slug: 'role-to-delete', displayName: 'Role To Delete', }); // Verify role exists let foundRole = await roleRepository.findBySlug('role-to-delete'); expect(foundRole).not.toBeNull(); // // ACT // await roleRepository.removeBySlug('role-to-delete'); // // ASSERT // foundRole = await roleRepository.findBySlug('role-to-delete'); expect(foundRole).toBeNull(); // Verify it's removed from database const allRoles = await roleRepository.findAll(); expect(allRoles.find((r) => r.slug === 'role-to-delete')).toBeUndefined(); }); it('should throw error when trying to remove non-existent role', async () => { // // ARRANGE & ACT & ASSERT // await expect(roleRepository.removeBySlug('non-existent-role')).rejects.toThrow( 'Failed to delete role "non-existent-role"', ); }); it('should remove role with associated scopes (many-to-many relationship)', async () => { // // ARRANGE // const { readScope, writeScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope, writeScope], { slug: 'role-with-scopes-to-delete', displayName: 'Role With Scopes To Delete', }); // Verify role and scopes exist let foundRole = await roleRepository.findBySlug('role-with-scopes-to-delete'); expect(foundRole).not.toBeNull(); expect(foundRole!.scopes).toHaveLength(2); // // ACT // await roleRepository.removeBySlug('role-with-scopes-to-delete'); // // ASSERT // foundRole = await roleRepository.findBySlug('role-with-scopes-to-delete'); expect(foundRole).toBeNull(); // Verify scopes still exist (should not cascade delete) const foundScopes = await scopeRepository.findByList([readScope.slug, writeScope.slug]); expect(foundScopes).toHaveLength(2); }); }); describe('updateRole()', () => { describe('transaction handling', () => { it('should use transactions', async () => { // // ARRANGE // await createRole({ slug: 'role-for-transaction-test', displayName: 'Original Name', description: 'Original Description', }); // Spy on transaction method to verify it's called const transactionSpy = jest.spyOn(roleRepository.manager, 'transaction'); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-transaction-test', { displayName: 'Updated Name', description: 'Updated Description', }); // // ASSERT // expect(transactionSpy).toHaveBeenCalled(); expect(updatedRole.displayName).toBe('Updated Name'); expect(updatedRole.description).toBe('Updated Description'); transactionSpy.mockRestore(); }); }); describe('successful updates', () => { it('should update role displayName', async () => { // // ARRANGE // await createRole({ slug: 'role-for-name-update', displayName: 'Original Name', description: 'Original Description', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-name-update', { displayName: 'New Display Name', }); // // ASSERT // expect(updatedRole.displayName).toBe('New Display Name'); expect(updatedRole.description).toBe('Original Description'); // Should remain unchanged expect(updatedRole.slug).toBe('role-for-name-update'); // Should remain unchanged // Verify in database const foundRole = await roleRepository.findBySlug('role-for-name-update'); expect(foundRole!.displayName).toBe('New Display Name'); }); it('should update role description', async () => { // // ARRANGE // await createRole({ slug: 'role-for-desc-update', displayName: 'Test Role', description: 'Original Description', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-desc-update', { description: 'New Description', }); // // ASSERT // expect(updatedRole.description).toBe('New Description'); expect(updatedRole.displayName).toBe('Test Role'); // Should remain unchanged // Verify in database const foundRole = await roleRepository.findBySlug('role-for-desc-update'); expect(foundRole!.description).toBe('New Description'); }); it('should update role scopes', async () => { // // ARRANGE // const { readScope, writeScope, deleteScope, adminScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope, writeScope], { slug: 'role-for-scope-update', displayName: 'Role For Scope Update', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-scope-update', { scopes: [deleteScope, adminScope], }); // // ASSERT // expect(updatedRole.scopes).toHaveLength(2); expect(updatedRole.scopes).toEqual( expect.arrayContaining([ expect.objectContaining({ slug: deleteScope.slug }), expect.objectContaining({ slug: adminScope.slug }), ]), ); // Verify in database const foundRole = await roleRepository.findBySlug('role-for-scope-update'); expect(foundRole!.scopes).toHaveLength(2); expect(foundRole!.scopes.map((s) => s.slug)).toEqual( expect.arrayContaining([deleteScope.slug, adminScope.slug]), ); }); it('should update multiple fields simultaneously', async () => { // // ARRANGE // const { readScope } = await createTestScopes(); await createRole({ slug: 'role-for-multi-update', displayName: 'Original Name', description: 'Original Description', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-multi-update', { displayName: 'Updated Name', description: 'Updated Description', scopes: [readScope], }); // // ASSERT // expect(updatedRole.displayName).toBe('Updated Name'); expect(updatedRole.description).toBe('Updated Description'); expect(updatedRole.scopes).toHaveLength(1); expect(updatedRole.scopes[0].slug).toBe(readScope.slug); // Verify in database const foundRole = await roleRepository.findBySlug('role-for-multi-update'); expect(foundRole!.displayName).toBe('Updated Name'); expect(foundRole!.description).toBe('Updated Description'); expect(foundRole!.scopes).toHaveLength(1); }); it('should set scopes to empty array', async () => { // // ARRANGE // const { readScope, writeScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope, writeScope], { slug: 'role-for-empty-scopes', displayName: 'Role With Scopes', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-empty-scopes', { scopes: [], }); // // ASSERT // expect(updatedRole.scopes).toEqual([]); // Verify in database const foundRole = await roleRepository.findBySlug('role-for-empty-scopes'); expect(foundRole!.scopes).toEqual([]); }); }); describe('system role protection', () => { it('should throw error when trying to update system role', async () => { // // ARRANGE // await createSystemRole({ slug: 'system-role-protected', displayName: 'Protected System Role', }); // // ACT & ASSERT // await expect( roleRepository.updateRole('system-role-protected', { displayName: 'Attempt To Change System Role', }), ).rejects.toThrow('Cannot update system roles'); }); it('should not modify system role in database when update fails', async () => { // // ARRANGE // await createSystemRole({ slug: 'system-role-immutable', displayName: 'Immutable System Role', description: 'Original Description', }); // // ACT // try { await roleRepository.updateRole('system-role-immutable', { displayName: 'Malicious Change', description: 'Malicious Description', }); } catch (error) { // Expected to throw } // // ASSERT // const foundRole = await roleRepository.findBySlug('system-role-immutable'); expect(foundRole!.displayName).toBe('Immutable System Role'); expect(foundRole!.description).toBe('Original Description'); expect(foundRole!.systemRole).toBe(true); }); }); describe('error scenarios', () => { it('should throw error when role does not exist', async () => { // // ARRANGE & ACT & ASSERT // await expect( roleRepository.updateRole('non-existent-role', { displayName: 'New Name', }), ).rejects.toThrow('Role not found'); }); }); describe('edge cases', () => { it('should handle null description update', async () => { // // ARRANGE // await createRole({ slug: 'role-for-null-desc', displayName: 'Role With Description', description: 'Original Description', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-null-desc', { description: null, }); // // ASSERT // expect(updatedRole.description).toBeNull(); // Verify in database const foundRole = await roleRepository.findBySlug('role-for-null-desc'); expect(foundRole!.description).toBeNull(); }); it('should handle update with no changes', async () => { // // ARRANGE // await createRole({ slug: 'role-for-no-change', displayName: 'Unchanged Role', description: 'Unchanged Description', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-no-change', {}); // // ASSERT // expect(updatedRole.displayName).toBe('Unchanged Role'); expect(updatedRole.description).toBe('Unchanged Description'); }); it('should handle undefined scope update (no change to scopes)', async () => { // // ARRANGE // const { readScope } = await createTestScopes(); await createCustomRoleWithScopes([readScope], { slug: 'role-for-undefined-scopes', displayName: 'Role With Scope', }); // // ACT // const updatedRole = await roleRepository.updateRole('role-for-undefined-scopes', { displayName: 'Updated Name', scopes: undefined, }); // // ASSERT // expect(updatedRole.displayName).toBe('Updated Name'); // When scopes is undefined, it should not modify scopes, and the returned role should have scopes loaded // However, the updateRole method may not have eager loaded scopes, so let's verify with a fresh fetch const foundRole = await roleRepository.findBySlug('role-for-undefined-scopes'); expect(foundRole!.scopes).toHaveLength(1); expect(foundRole!.scopes[0].slug).toBe(readScope.slug); }); }); }); describe('countUsersWithRole()', () => { beforeEach(async () => { // make sure to initalize the default roles for user creation await Container.get(AuthRolesService).init(); }); describe('global roles', () => { it('should return 0 when no users have the global role', async () => { // // ARRANGE // const globalRole = await createRole({ slug: 'global-empty-role', displayName: 'Global Empty Role', roleType: 'global', }); // // ACT // const count = await roleRepository.countUsersWithRole(globalRole); // // ASSERT // expect(count).toBe(0); }); it('should return correct count when multiple users have the global role', async () => { // // ARRANGE // const globalRole = await createRole({ slug: 'global-multi-role', displayName: 'Global Multi Role', roleType: 'global', }); await createUser({ role: globalRole }); await createUser({ role: globalRole }); await createUser({ role: globalRole }); // Create user with different role to ensure isolation const otherRole = await createRole({ slug: 'other-global-role', displayName: 'Other Global Role', roleType: 'global', }); await createUser({ role: otherRole }); // // ACT // const count = await roleRepository.countUsersWithRole(globalRole); // // ASSERT // expect(count).toBe(3); }); }); describe('project roles', () => { it('should return 0 when no project relations exist for the project role', async () => { // // ARRANGE // const projectRole = await createRole({ slug: 'project-empty-role', displayName: 'Project Empty Role', roleType: 'project', }); // // ACT // const count = await roleRepository.countUsersWithRole(projectRole); // // ASSERT // expect(count).toBe(0); }); it('should return correct count when multiple users have the project role', async () => { // // ARRANGE // const projectRole = await createRole({ slug: 'project-multi-role', displayName: 'Project Multi Role', roleType: 'project', }); // Create users and projects const user1 = await createUser(); const user2 = await createUser(); const user3 = await createUser(); const project1 = await createTeamProject('Test Project 1'); const project2 = await createTeamProject('Test Project 2'); // Link users to projects with the target role await linkUserToProject(user1, project1, projectRole.slug); await linkUserToProject(user2, project1, projectRole.slug); await linkUserToProject(user3, project2, projectRole.slug); // // ACT // const count = await roleRepository.countUsersWithRole(projectRole); // // ASSERT // expect(count).toBe(3); }); it('should only count users with the specific project role slug', async () => { // // ARRANGE // const targetRole = await createRole({ slug: 'project-target-role', displayName: 'Project Target Role', roleType: 'project', }); const otherRole = await createRole({ slug: 'project-other-role', displayName: 'Project Other Role', roleType: 'project', }); const user1 = await createUser(); const user2 = await createUser(); const user3 = await createUser(); const project = await createTeamProject('Test Project'); // Link users with different roles await linkUserToProject(user1, project, targetRole.slug as any); await linkUserToProject(user2, project, targetRole.slug as any); await linkUserToProject(user3, project, otherRole.slug as any); // // ACT // const count = await roleRepository.countUsersWithRole(targetRole); // // ASSERT // expect(count).toBe(2); }); }); describe('edge cases', () => { it('should handle project roles when query returns null count', async () => { // // ARRANGE // const projectRole = await createRole({ slug: 'project-null-count-role', displayName: 'Project Null Count Role', roleType: 'project', }); // Create a project role but don't link any users to it // This ensures the query returns a row but with null/0 count // // ACT // const count = await roleRepository.countUsersWithRole(projectRole); // // ASSERT // expect(count).toBe(0); }); }); }); });