@flusys/nestjs-iam 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/cjs/controllers/action.controller.js +17 -9
  2. package/cjs/controllers/action.controller.spec.js +79 -0
  3. package/cjs/controllers/company-action-permission.controller.spec.js +65 -0
  4. package/cjs/controllers/my-permission.controller.spec.js +56 -0
  5. package/cjs/controllers/role-permission.controller.spec.js +140 -0
  6. package/cjs/controllers/role.controller.spec.js +86 -0
  7. package/cjs/controllers/user-action-permission.controller.js +0 -3
  8. package/cjs/controllers/user-action-permission.controller.spec.js +68 -0
  9. package/cjs/docs/iam-swagger.config.js +6 -0
  10. package/cjs/docs/iam-swagger.config.spec.js +99 -0
  11. package/cjs/dtos/action.dto.js +17 -0
  12. package/cjs/entities/index.spec.js +56 -0
  13. package/cjs/helpers/company-access.helper.spec.js +68 -0
  14. package/cjs/helpers/permission-mode.helper.spec.js +54 -0
  15. package/cjs/modules/iam.module.js +3 -1
  16. package/cjs/modules/iam.module.spec.js +293 -0
  17. package/cjs/services/action.service.js +90 -7
  18. package/cjs/services/action.service.spec.js +314 -0
  19. package/cjs/services/iam-config.service.spec.js +110 -0
  20. package/cjs/services/iam-datasource.service.spec.js +117 -0
  21. package/cjs/services/permission-cache.service.js +113 -110
  22. package/cjs/services/permission-cache.service.spec.js +252 -0
  23. package/cjs/services/permission.service.js +213 -92
  24. package/cjs/services/permission.service.spec.js +967 -0
  25. package/cjs/services/role.service.js +47 -3
  26. package/cjs/services/role.service.spec.js +312 -0
  27. package/controllers/action.controller.d.ts +5 -3
  28. package/dtos/action.dto.d.ts +3 -0
  29. package/entities/index.d.ts +2 -2
  30. package/fesm/controllers/action.controller.js +18 -10
  31. package/fesm/controllers/action.controller.spec.js +75 -0
  32. package/fesm/controllers/company-action-permission.controller.spec.js +61 -0
  33. package/fesm/controllers/my-permission.controller.spec.js +52 -0
  34. package/fesm/controllers/role-permission.controller.spec.js +136 -0
  35. package/fesm/controllers/role.controller.spec.js +82 -0
  36. package/fesm/controllers/user-action-permission.controller.js +0 -3
  37. package/fesm/controllers/user-action-permission.controller.spec.js +64 -0
  38. package/fesm/docs/iam-swagger.config.js +6 -0
  39. package/fesm/docs/iam-swagger.config.spec.js +95 -0
  40. package/fesm/dtos/action.dto.js +14 -0
  41. package/fesm/entities/index.spec.js +52 -0
  42. package/fesm/helpers/company-access.helper.spec.js +64 -0
  43. package/fesm/helpers/permission-mode.helper.spec.js +50 -0
  44. package/fesm/modules/iam.module.js +4 -2
  45. package/fesm/modules/iam.module.spec.js +289 -0
  46. package/fesm/services/action.service.js +90 -7
  47. package/fesm/services/action.service.spec.js +310 -0
  48. package/fesm/services/iam-config.service.spec.js +106 -0
  49. package/fesm/services/iam-datasource.service.spec.js +113 -0
  50. package/fesm/services/permission-cache.service.js +101 -108
  51. package/fesm/services/permission-cache.service.spec.js +248 -0
  52. package/fesm/services/permission.service.js +213 -92
  53. package/fesm/services/permission.service.spec.js +963 -0
  54. package/fesm/services/role.service.js +48 -4
  55. package/fesm/services/role.service.spec.js +308 -0
  56. package/package.json +3 -3
  57. package/services/action.service.d.ts +7 -3
  58. package/services/permission-cache.service.d.ts +13 -19
  59. package/services/permission.service.d.ts +6 -3
  60. package/services/role.service.d.ts +7 -3
@@ -0,0 +1,52 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { MyPermissionController } from './my-permission.controller';
3
+ describe('MyPermissionController', ()=>{
4
+ let controller;
5
+ let mockPermissionService;
6
+ beforeEach(()=>{
7
+ mockPermissionService = {
8
+ getMyPermissions: jest.fn()
9
+ };
10
+ controller = new MyPermissionController(mockPermissionService);
11
+ });
12
+ describe('getMyPermissions', ()=>{
13
+ it('resolves permissions for the current user, branch, and company', async ()=>{
14
+ const user = buildMockUser({
15
+ id: 'user-1',
16
+ companyId: 'company-1',
17
+ branchId: 'branch-1'
18
+ });
19
+ const permissions = {
20
+ menus: [],
21
+ actions: [
22
+ 'user.create'
23
+ ]
24
+ };
25
+ mockPermissionService.getMyPermissions.mockResolvedValue(permissions);
26
+ const result = await controller.getMyPermissions({
27
+ parentCodes: [
28
+ 'user'
29
+ ]
30
+ }, user);
31
+ expect(mockPermissionService.getMyPermissions).toHaveBeenCalledWith('user-1', 'branch-1', 'company-1', [
32
+ 'user'
33
+ ]);
34
+ expect(result).toEqual({
35
+ success: true,
36
+ message: 'Permissions loaded successfully',
37
+ messageKey: expect.any(String),
38
+ data: permissions
39
+ });
40
+ });
41
+ it('passes null for missing branch and company context', async ()=>{
42
+ const user = buildMockUser({
43
+ id: 'user-1',
44
+ companyId: undefined,
45
+ branchId: undefined
46
+ });
47
+ mockPermissionService.getMyPermissions.mockResolvedValue({});
48
+ await controller.getMyPermissions({}, user);
49
+ expect(mockPermissionService.getMyPermissions).toHaveBeenCalledWith('user-1', null, null, undefined);
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,136 @@
1
+ import { ForbiddenException } from '@nestjs/common';
2
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
3
+ import { RolePermissionController } from './role-permission.controller';
4
+ describe('RolePermissionController', ()=>{
5
+ let controller;
6
+ let mockPermissionService;
7
+ let mockConfig;
8
+ beforeEach(()=>{
9
+ mockPermissionService = {
10
+ assignRoleActions: jest.fn(),
11
+ getRoleActions: jest.fn(),
12
+ assignUserRoles: jest.fn(),
13
+ getUserRoles: jest.fn()
14
+ };
15
+ mockConfig = {
16
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
17
+ };
18
+ controller = new RolePermissionController(mockPermissionService, mockConfig);
19
+ });
20
+ describe('assignRoleActions', ()=>{
21
+ it('assigns actions to a role and returns the operation summary', async ()=>{
22
+ mockPermissionService.assignRoleActions.mockResolvedValue({
23
+ added: 2,
24
+ removed: 1,
25
+ total: 3
26
+ });
27
+ const result = await controller.assignRoleActions({
28
+ roleId: 'role-1',
29
+ actionIds: [
30
+ 'action-1',
31
+ 'action-2'
32
+ ]
33
+ });
34
+ expect(mockPermissionService.assignRoleActions).toHaveBeenCalledWith({
35
+ roleId: 'role-1',
36
+ actionIds: [
37
+ 'action-1',
38
+ 'action-2'
39
+ ]
40
+ });
41
+ expect(result.success).toBe(true);
42
+ expect(result.data).toEqual({
43
+ added: 2,
44
+ removed: 1,
45
+ total: 3
46
+ });
47
+ });
48
+ });
49
+ describe('getRoleActions', ()=>{
50
+ it('returns actions assigned to the role', async ()=>{
51
+ mockPermissionService.getRoleActions.mockResolvedValue([
52
+ {
53
+ actionId: 'action-1'
54
+ }
55
+ ]);
56
+ const result = await controller.getRoleActions({
57
+ roleId: 'role-1'
58
+ });
59
+ expect(mockPermissionService.getRoleActions).toHaveBeenCalledWith('role-1');
60
+ expect(result.data).toEqual([
61
+ {
62
+ actionId: 'action-1'
63
+ }
64
+ ]);
65
+ });
66
+ });
67
+ describe('assignUserRoles', ()=>{
68
+ it('assigns roles to a user when the caller has access to the target company', async ()=>{
69
+ mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
70
+ const user = buildMockUser({
71
+ companyId: 'company-1'
72
+ });
73
+ mockPermissionService.assignUserRoles.mockResolvedValue({
74
+ added: 1,
75
+ removed: 0,
76
+ total: 1
77
+ });
78
+ const result = await controller.assignUserRoles({
79
+ userId: 'user-2',
80
+ roleIds: [
81
+ 'role-1'
82
+ ],
83
+ companyId: 'company-1'
84
+ }, user);
85
+ expect(mockPermissionService.assignUserRoles).toHaveBeenCalledWith({
86
+ userId: 'user-2',
87
+ roleIds: [
88
+ 'role-1'
89
+ ],
90
+ companyId: 'company-1'
91
+ });
92
+ expect(result.data).toEqual({
93
+ added: 1,
94
+ removed: 0,
95
+ total: 1
96
+ });
97
+ });
98
+ it('rejects assignment when the caller has no access to the target company', async ()=>{
99
+ mockConfig.isCompanyFeatureEnabled.mockReturnValue(true);
100
+ const user = buildMockUser({
101
+ companyId: 'company-1'
102
+ });
103
+ await expect(controller.assignUserRoles({
104
+ userId: 'user-2',
105
+ roleIds: [
106
+ 'role-1'
107
+ ],
108
+ companyId: 'company-2'
109
+ }, user)).rejects.toThrow(ForbiddenException);
110
+ expect(mockPermissionService.assignUserRoles).not.toHaveBeenCalled();
111
+ });
112
+ });
113
+ describe('getUserRoles', ()=>{
114
+ it('returns roles assigned to the user scoped by branch and company', async ()=>{
115
+ const user = buildMockUser({
116
+ companyId: 'company-1'
117
+ });
118
+ mockPermissionService.getUserRoles.mockResolvedValue([
119
+ {
120
+ roleId: 'role-1'
121
+ }
122
+ ]);
123
+ const result = await controller.getUserRoles({
124
+ userId: 'user-2',
125
+ branchId: 'branch-1',
126
+ companyId: 'company-1'
127
+ }, user);
128
+ expect(mockPermissionService.getUserRoles).toHaveBeenCalledWith('user-2', 'branch-1', 'company-1');
129
+ expect(result.data).toEqual([
130
+ {
131
+ roleId: 'role-1'
132
+ }
133
+ ]);
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,82 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { RoleController } from './role.controller';
3
+ function buildRole(overrides = {}) {
4
+ return {
5
+ id: 'role-1',
6
+ name: 'Admin',
7
+ description: null,
8
+ isActive: true,
9
+ readOnly: false,
10
+ createdAt: new Date(),
11
+ updatedAt: new Date(),
12
+ ...overrides
13
+ };
14
+ }
15
+ describe('RoleController', ()=>{
16
+ let controller;
17
+ let mockRoleService;
18
+ beforeEach(()=>{
19
+ mockRoleService = {
20
+ insert: jest.fn(),
21
+ findById: jest.fn(),
22
+ getAll: jest.fn(),
23
+ update: jest.fn(),
24
+ delete: jest.fn()
25
+ };
26
+ controller = new RoleController(mockRoleService);
27
+ });
28
+ it('wires the injected service onto the generated controller base', ()=>{
29
+ expect(controller.roleService).toBe(mockRoleService);
30
+ expect(controller.service).toBe(mockRoleService);
31
+ });
32
+ describe('insert', ()=>{
33
+ it('creates a role and returns the mapped response', async ()=>{
34
+ const user = buildMockUser();
35
+ mockRoleService.insert.mockResolvedValue(buildRole());
36
+ const result = await controller.insert({
37
+ name: 'Admin'
38
+ }, user);
39
+ expect(mockRoleService.insert).toHaveBeenCalledWith({
40
+ name: 'Admin'
41
+ }, user);
42
+ expect(result.success).toBe(true);
43
+ expect(result.data).toEqual(expect.objectContaining({
44
+ id: 'role-1',
45
+ name: 'Admin'
46
+ }));
47
+ });
48
+ });
49
+ describe('getAll', ()=>{
50
+ it('paginates roles and maps the response list', async ()=>{
51
+ mockRoleService.getAll.mockResolvedValue({
52
+ data: [
53
+ buildRole()
54
+ ],
55
+ total: 1
56
+ });
57
+ const result = await controller.getAll({}, buildMockUser(), '');
58
+ expect(mockRoleService.getAll).toHaveBeenCalledWith('', {}, buildMockUser());
59
+ expect(result.data).toHaveLength(1);
60
+ });
61
+ });
62
+ describe('delete', ()=>{
63
+ it('delegates soft-delete to the service', async ()=>{
64
+ mockRoleService.delete.mockResolvedValue({
65
+ count: 1
66
+ });
67
+ const result = await controller.delete({
68
+ id: [
69
+ 'role-1'
70
+ ],
71
+ type: 'delete'
72
+ }, buildMockUser());
73
+ expect(mockRoleService.delete).toHaveBeenCalledWith({
74
+ id: [
75
+ 'role-1'
76
+ ],
77
+ type: 'delete'
78
+ }, buildMockUser());
79
+ expect(result.success).toBe(true);
80
+ });
81
+ });
82
+ });
@@ -30,12 +30,10 @@ import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
30
30
  import { ApiBearerAuth, ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
31
31
  import { PERMISSION_OPERATION_MESSAGES, USER_ACTION_PERMISSION_MESSAGES } from '../config';
32
32
  import { AssignUserActionsDto, GetUserActionsDto, PermissionOperationResultDto, UserActionResponseDto } from '../dtos/permission.dto';
33
- import { validateCompanyAccess } from '../helpers';
34
33
  import { IAMConfigService } from '../services/iam-config.service';
35
34
  import { PermissionService } from '../services/permission.service';
36
35
  export class UserActionPermissionController {
37
36
  async assignUserActions(dto, user) {
38
- validateCompanyAccess(this.config, dto.companyId, user);
39
37
  const result = await this.permissionService.assignUserActions(dto);
40
38
  return {
41
39
  success: true,
@@ -50,7 +48,6 @@ export class UserActionPermissionController {
50
48
  };
51
49
  }
52
50
  async getUserActions(dto, user) {
53
- validateCompanyAccess(this.config, dto.companyId, user);
54
51
  const actions = await this.permissionService.getUserActions(dto.userId, dto.branchId, dto.companyId);
55
52
  return {
56
53
  success: true,
@@ -0,0 +1,64 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { UserActionPermissionController } from './user-action-permission.controller';
3
+ describe('UserActionPermissionController', ()=>{
4
+ let controller;
5
+ let mockPermissionService;
6
+ let mockConfig;
7
+ beforeEach(()=>{
8
+ mockPermissionService = {
9
+ assignUserActions: jest.fn(),
10
+ getUserActions: jest.fn()
11
+ };
12
+ mockConfig = {};
13
+ controller = new UserActionPermissionController(mockPermissionService, mockConfig);
14
+ });
15
+ describe('assignUserActions', ()=>{
16
+ it('assigns direct action permissions to a user', async ()=>{
17
+ const user = buildMockUser();
18
+ mockPermissionService.assignUserActions.mockResolvedValue({
19
+ added: 1,
20
+ removed: 0,
21
+ total: 1
22
+ });
23
+ const result = await controller.assignUserActions({
24
+ userId: 'user-2',
25
+ actionIds: [
26
+ 'action-1'
27
+ ]
28
+ }, user);
29
+ expect(mockPermissionService.assignUserActions).toHaveBeenCalledWith({
30
+ userId: 'user-2',
31
+ actionIds: [
32
+ 'action-1'
33
+ ]
34
+ });
35
+ expect(result.success).toBe(true);
36
+ expect(result.data).toEqual({
37
+ added: 1,
38
+ removed: 0,
39
+ total: 1
40
+ });
41
+ });
42
+ });
43
+ describe('getUserActions', ()=>{
44
+ it('returns direct action permissions for a user scoped by branch/company', async ()=>{
45
+ const user = buildMockUser();
46
+ mockPermissionService.getUserActions.mockResolvedValue([
47
+ {
48
+ actionId: 'action-1'
49
+ }
50
+ ]);
51
+ const result = await controller.getUserActions({
52
+ userId: 'user-2',
53
+ branchId: 'branch-1',
54
+ companyId: 'company-1'
55
+ }, user);
56
+ expect(mockPermissionService.getUserActions).toHaveBeenCalledWith('user-2', 'branch-1', 'company-1');
57
+ expect(result.data).toEqual([
58
+ {
59
+ actionId: 'action-1'
60
+ }
61
+ ]);
62
+ });
63
+ });
64
+ });
@@ -39,6 +39,12 @@ export function iamSwaggerConfig(enableCompanyFeature = false, permissionMode =
39
39
  'branchId'
40
40
  ]
41
41
  },
42
+ {
43
+ schemaName: 'ActionTreeForPermissionDto',
44
+ properties: [
45
+ 'companyId'
46
+ ]
47
+ },
42
48
  // Response DTOs with branchId
43
49
  {
44
50
  schemaName: 'UserActionResponseDto',
@@ -0,0 +1,95 @@
1
+ import { iamSwaggerConfig } from './iam-swagger.config';
2
+ import { IAMPermissionMode } from '../enums/permission-type.enum';
3
+ describe('iamSwaggerConfig', ()=>{
4
+ it('should default to company disabled, FULL mode, single database', ()=>{
5
+ const config = iamSwaggerConfig();
6
+ expect(config.title).toBe('IAM API');
7
+ expect(config.path).toBe('api/docs/iam');
8
+ expect(config.bearerAuth).toBe(true);
9
+ // enableCompanyFeature defaults to false, so company-scoped exclusions apply
10
+ expect(config.excludeSchemaProperties?.length).toBeGreaterThan(0);
11
+ expect(config.excludeQueryParameters?.length).toBeGreaterThan(0);
12
+ expect(config.description).not.toContain('Multi-Tenant Mode');
13
+ });
14
+ it('should always exclude auth-related tags regardless of configuration', ()=>{
15
+ const config = iamSwaggerConfig(true, IAMPermissionMode.FULL);
16
+ expect(config.excludeTags).toEqual(expect.arrayContaining([
17
+ 'Authentication',
18
+ 'Users',
19
+ 'Companies',
20
+ 'Branches'
21
+ ]));
22
+ });
23
+ it('should exclude company-scoped schema properties and query params when company feature is off', ()=>{
24
+ const config = iamSwaggerConfig(false);
25
+ expect(config.excludeSchemaProperties).toEqual(expect.arrayContaining([
26
+ expect.objectContaining({
27
+ schemaName: 'AssignUserActionsDto',
28
+ properties: [
29
+ 'companyId',
30
+ 'branchId'
31
+ ]
32
+ })
33
+ ]));
34
+ expect(config.excludeQueryParameters).toEqual(expect.arrayContaining([
35
+ expect.objectContaining({
36
+ pathPattern: '/iam/permissions/user-actions/*',
37
+ method: 'get'
38
+ })
39
+ ]));
40
+ expect(config.excludeTags).toContain('IAM - Company Action Permissions');
41
+ });
42
+ it('should not exclude company-scoped schema properties, query params, or tags when company feature is on', ()=>{
43
+ const config = iamSwaggerConfig(true);
44
+ expect(config.excludeSchemaProperties).toEqual([]);
45
+ expect(config.excludeQueryParameters).toEqual([]);
46
+ expect(config.excludeTags).not.toContain('IAM - Company Action Permissions');
47
+ });
48
+ it('should hide DIRECT-mode tags in RBAC mode', ()=>{
49
+ const config = iamSwaggerConfig(false, IAMPermissionMode.RBAC);
50
+ expect(config.excludeTags).toContain('IAM - Permissions (Direct)');
51
+ expect(config.excludeTags).not.toContain('IAM - Permissions (RBAC)');
52
+ expect(config.excludeTags).not.toContain('IAM - Roles');
53
+ });
54
+ it('should hide RBAC-mode tags (including Roles) in DIRECT mode', ()=>{
55
+ const config = iamSwaggerConfig(false, IAMPermissionMode.DIRECT);
56
+ expect(config.excludeTags).toContain('IAM - Permissions (RBAC)');
57
+ expect(config.excludeTags).toContain('IAM - Roles');
58
+ expect(config.excludeTags).not.toContain('IAM - Permissions (Direct)');
59
+ });
60
+ it('should hide neither RBAC nor DIRECT tags in FULL mode', ()=>{
61
+ const config = iamSwaggerConfig(false, IAMPermissionMode.FULL);
62
+ expect(config.excludeTags).not.toContain('IAM - Permissions (RBAC)');
63
+ expect(config.excludeTags).not.toContain('IAM - Permissions (Direct)');
64
+ expect(config.excludeTags).not.toContain('IAM - Roles');
65
+ });
66
+ it('should mention multi-tenant mode in the description only for multi-tenant database mode', ()=>{
67
+ const multiTenant = iamSwaggerConfig(false, IAMPermissionMode.FULL, 'multi-tenant');
68
+ const single = iamSwaggerConfig(false, IAMPermissionMode.FULL, 'single');
69
+ expect(multiTenant.description).toContain('Multi-Tenant Mode');
70
+ expect(single.description).not.toContain('Multi-Tenant Mode');
71
+ });
72
+ it('should describe RBAC as active and DIRECT as disabled in the endpoints summary for RBAC mode', ()=>{
73
+ const config = iamSwaggerConfig(false, IAMPermissionMode.RBAC);
74
+ expect(config.description).toContain('**Roles**: CRUD operations');
75
+ expect(config.description).toContain('❌ **User-Actions**: Disabled (DIRECT mode not active)');
76
+ });
77
+ it('should describe DIRECT as active and Roles as disabled in the endpoints summary for DIRECT mode', ()=>{
78
+ const config = iamSwaggerConfig(false, IAMPermissionMode.DIRECT);
79
+ expect(config.description).toContain('✅ **User-Actions**: Direct action assignment to users');
80
+ expect(config.description).toContain('❌ **Roles**: Disabled (RBAC mode not active)');
81
+ });
82
+ it('should describe company features as active only when company feature is enabled', ()=>{
83
+ const withCompany = iamSwaggerConfig(true, IAMPermissionMode.FULL);
84
+ const withoutCompany = iamSwaggerConfig(false, IAMPermissionMode.FULL);
85
+ expect(withCompany.description).toContain('Company Features (Active)');
86
+ expect(withCompany.description).toContain('✅ **Company-Actions**: Whitelist actions for companies');
87
+ expect(withoutCompany.description).not.toContain('Company Features (Active)');
88
+ expect(withoutCompany.description).toContain('❌ **Company-Actions**: Disabled (company feature not enabled)');
89
+ });
90
+ it('should describe the current permission mode label correctly for each mode', ()=>{
91
+ expect(iamSwaggerConfig(false, IAMPermissionMode.RBAC).description).toContain('**RBAC** (Role-Based Access Control)');
92
+ expect(iamSwaggerConfig(false, IAMPermissionMode.DIRECT).description).toContain('**DIRECT** (Direct User Permissions)');
93
+ expect(iamSwaggerConfig(false, IAMPermissionMode.FULL).description).toContain('**FULL** (RBAC + Direct)');
94
+ });
95
+ });
@@ -199,6 +199,20 @@ _ts_decorate([
199
199
  }),
200
200
  _ts_metadata("design:type", Array)
201
201
  ], ActionTreeDto.prototype, "children", void 0);
202
+ export class ActionTreeForPermissionDto {
203
+ constructor(){
204
+ _define_property(this, "companyId", void 0);
205
+ }
206
+ }
207
+ _ts_decorate([
208
+ ApiProperty({
209
+ description: 'Company to scope the action whitelist to. Defaults to the current user session company when omitted.',
210
+ required: false
211
+ }),
212
+ IsUUID(),
213
+ IsOptional(),
214
+ _ts_metadata("design:type", String)
215
+ ], ActionTreeForPermissionDto.prototype, "companyId", void 0);
202
216
  export class ActionTreeQueryDto {
203
217
  constructor(){
204
218
  _define_property(this, "search", void 0);
@@ -0,0 +1,52 @@
1
+ import { getIAMEntitiesByConfig } from './index';
2
+ import { Action } from './action.entity';
3
+ import { Role } from './role.entity';
4
+ import { RoleWithCompany } from './role-with-company.entity';
5
+ import { UserIamPermission } from './user-iam-permission.entity';
6
+ import { UserIamPermissionWithCompany } from './permission-with-company.entity';
7
+ describe('getIAMEntitiesByConfig', ()=>{
8
+ it('should always include Action', ()=>{
9
+ expect(getIAMEntitiesByConfig(false)).toContain(Action);
10
+ expect(getIAMEntitiesByConfig(true)).toContain(Action);
11
+ });
12
+ it('should default permissionMode to FULL and include Role + UserIamPermission when company feature is off', ()=>{
13
+ const entities = getIAMEntitiesByConfig(false);
14
+ expect(entities).toEqual(expect.arrayContaining([
15
+ Action,
16
+ UserIamPermission,
17
+ Role
18
+ ]));
19
+ expect(entities).not.toContain(RoleWithCompany);
20
+ expect(entities).not.toContain(UserIamPermissionWithCompany);
21
+ });
22
+ it('should use the company-scoped permission entity when enableCompanyFeature is true', ()=>{
23
+ const entities = getIAMEntitiesByConfig(true, 'FULL');
24
+ expect(entities).toContain(UserIamPermissionWithCompany);
25
+ expect(entities).not.toContain(UserIamPermission);
26
+ });
27
+ it('should use the company-scoped role entity when enableCompanyFeature is true', ()=>{
28
+ const entities = getIAMEntitiesByConfig(true, 'RBAC');
29
+ expect(entities).toContain(RoleWithCompany);
30
+ expect(entities).not.toContain(Role);
31
+ });
32
+ it('should include Role for RBAC mode', ()=>{
33
+ expect(getIAMEntitiesByConfig(false, 'RBAC')).toContain(Role);
34
+ });
35
+ it('should include Role for FULL mode', ()=>{
36
+ expect(getIAMEntitiesByConfig(false, 'FULL')).toContain(Role);
37
+ });
38
+ it('should exclude Role entirely for DIRECT mode', ()=>{
39
+ const entities = getIAMEntitiesByConfig(false, 'DIRECT');
40
+ expect(entities).not.toContain(Role);
41
+ expect(entities).not.toContain(RoleWithCompany);
42
+ expect(entities).toEqual(expect.arrayContaining([
43
+ Action,
44
+ UserIamPermission
45
+ ]));
46
+ });
47
+ it('should exclude the company-scoped Role for DIRECT mode even with company feature enabled', ()=>{
48
+ const entities = getIAMEntitiesByConfig(true, 'DIRECT');
49
+ expect(entities).not.toContain(RoleWithCompany);
50
+ expect(entities).toContain(UserIamPermissionWithCompany);
51
+ });
52
+ });
@@ -0,0 +1,64 @@
1
+ import { ForbiddenException } from '@nestjs/common';
2
+ import { AUTH_MESSAGES } from '@flusys/nestjs-shared/constants';
3
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
4
+ import { validateCompanyAccess } from './company-access.helper';
5
+ describe('validateCompanyAccess', ()=>{
6
+ function buildConfig(isCompanyFeatureEnabled) {
7
+ return {
8
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(isCompanyFeatureEnabled)
9
+ };
10
+ }
11
+ it('does not throw when company feature is disabled', ()=>{
12
+ const config = buildConfig(false);
13
+ const user = buildMockUser({
14
+ companyId: 'company-uuid-1'
15
+ });
16
+ expect(()=>validateCompanyAccess(config, 'company-uuid-2', user)).not.toThrow();
17
+ });
18
+ it('does not throw when companyId is undefined', ()=>{
19
+ const config = buildConfig(true);
20
+ const user = buildMockUser({
21
+ companyId: 'company-uuid-1'
22
+ });
23
+ expect(()=>validateCompanyAccess(config, undefined, user)).not.toThrow();
24
+ });
25
+ it('does not throw when the companyId matches the user company', ()=>{
26
+ const config = buildConfig(true);
27
+ const user = buildMockUser({
28
+ companyId: 'company-uuid-1'
29
+ });
30
+ expect(()=>validateCompanyAccess(config, 'company-uuid-1', user)).not.toThrow();
31
+ });
32
+ it('throws ForbiddenException with default message/key when the company does not match', ()=>{
33
+ const config = buildConfig(true);
34
+ const user = buildMockUser({
35
+ companyId: 'company-uuid-1'
36
+ });
37
+ try {
38
+ validateCompanyAccess(config, 'company-uuid-2', user);
39
+ fail('expected validateCompanyAccess to throw');
40
+ } catch (error) {
41
+ expect(error).toBeInstanceOf(ForbiddenException);
42
+ expect(error.getResponse()).toEqual(expect.objectContaining({
43
+ message: 'You do not have access to this company',
44
+ messageKey: AUTH_MESSAGES.COMPANY_NO_ACCESS
45
+ }));
46
+ }
47
+ });
48
+ it('throws with a custom message and messageKey when provided', ()=>{
49
+ const config = buildConfig(true);
50
+ const user = buildMockUser({
51
+ companyId: 'company-uuid-1'
52
+ });
53
+ try {
54
+ validateCompanyAccess(config, 'company-uuid-2', user, 'Custom message', 'custom.message.key');
55
+ fail('expected validateCompanyAccess to throw');
56
+ } catch (error) {
57
+ expect(error).toBeInstanceOf(ForbiddenException);
58
+ expect(error.getResponse()).toEqual(expect.objectContaining({
59
+ message: 'Custom message',
60
+ messageKey: 'custom.message.key'
61
+ }));
62
+ }
63
+ });
64
+ });
@@ -0,0 +1,50 @@
1
+ import { IAMPermissionMode } from '../enums/permission-type.enum';
2
+ import { PermissionModeHelper } from './permission-mode.helper';
3
+ describe('PermissionModeHelper', ()=>{
4
+ describe('fromString', ()=>{
5
+ it('converts "RBAC" to IAMPermissionMode.RBAC', ()=>{
6
+ expect(PermissionModeHelper.fromString('RBAC')).toBe(IAMPermissionMode.RBAC);
7
+ });
8
+ it('converts "DIRECT" to IAMPermissionMode.DIRECT', ()=>{
9
+ expect(PermissionModeHelper.fromString('DIRECT')).toBe(IAMPermissionMode.DIRECT);
10
+ });
11
+ it('converts "FULL" to IAMPermissionMode.FULL', ()=>{
12
+ expect(PermissionModeHelper.fromString('FULL')).toBe(IAMPermissionMode.FULL);
13
+ });
14
+ it('defaults to FULL when the string is undefined', ()=>{
15
+ expect(PermissionModeHelper.fromString(undefined)).toBe(IAMPermissionMode.FULL);
16
+ });
17
+ it('defaults to FULL when the string is empty', ()=>{
18
+ expect(PermissionModeHelper.fromString('')).toBe(IAMPermissionMode.FULL);
19
+ });
20
+ it('defaults to FULL when the string is not a known mode', ()=>{
21
+ expect(PermissionModeHelper.fromString('INVALID_MODE')).toBe(IAMPermissionMode.FULL);
22
+ });
23
+ it('is case-sensitive and falls back to FULL for lowercase input', ()=>{
24
+ expect(PermissionModeHelper.fromString('rbac')).toBe(IAMPermissionMode.FULL);
25
+ });
26
+ });
27
+ describe('toString', ()=>{
28
+ it('converts IAMPermissionMode.RBAC to "RBAC"', ()=>{
29
+ expect(PermissionModeHelper.toString(IAMPermissionMode.RBAC)).toBe('RBAC');
30
+ });
31
+ it('converts IAMPermissionMode.DIRECT to "DIRECT"', ()=>{
32
+ expect(PermissionModeHelper.toString(IAMPermissionMode.DIRECT)).toBe('DIRECT');
33
+ });
34
+ it('converts IAMPermissionMode.FULL to "FULL"', ()=>{
35
+ expect(PermissionModeHelper.toString(IAMPermissionMode.FULL)).toBe('FULL');
36
+ });
37
+ });
38
+ describe('round trip', ()=>{
39
+ it('fromString(toString(mode)) returns the original mode for every enum value', ()=>{
40
+ const modes = [
41
+ IAMPermissionMode.RBAC,
42
+ IAMPermissionMode.DIRECT,
43
+ IAMPermissionMode.FULL
44
+ ];
45
+ for (const mode of modes){
46
+ expect(PermissionModeHelper.fromString(PermissionModeHelper.toString(mode))).toBe(mode);
47
+ }
48
+ });
49
+ });
50
+ });