mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
828 lines
21 KiB
TypeScript
828 lines
21 KiB
TypeScript
import { testDb, linkUserToProject, createTeamProject } from '@n8n/backend-test-utils';
|
|
import { GlobalConfig } from '@n8n/config';
|
|
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 for non-SQLite legacy databases', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const { type: dbType, sqlite: sqliteConfig } = Container.get(GlobalConfig).database;
|
|
// Skip this test for legacy SQLite
|
|
if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) {
|
|
return;
|
|
}
|
|
|
|
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();
|
|
});
|
|
|
|
it('should use direct manager for SQLite legacy databases', async () => {
|
|
//
|
|
// ARRANGE
|
|
//
|
|
const { type: dbType, sqlite: sqliteConfig } = Container.get(GlobalConfig).database;
|
|
// Only run this test for legacy SQLite
|
|
if (dbType !== 'sqlite' || sqliteConfig.poolSize !== 0) {
|
|
return;
|
|
}
|
|
|
|
await createRole({
|
|
slug: 'role-for-legacy-test',
|
|
displayName: 'Original Name',
|
|
description: 'Original Description',
|
|
});
|
|
|
|
// Spy on transaction method to verify it's NOT called
|
|
const transactionSpy = jest.spyOn(roleRepository.manager, 'transaction');
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const updatedRole = await roleRepository.updateRole('role-for-legacy-test', {
|
|
displayName: 'Updated Name',
|
|
description: 'Updated Description',
|
|
});
|
|
|
|
//
|
|
// ASSERT
|
|
//
|
|
expect(transactionSpy).not.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);
|
|
});
|
|
});
|
|
});
|
|
});
|