@flusys/nestjs-iam 7.1.0 → 8.0.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 (175) hide show
  1. package/config/index.d.ts +3 -3
  2. package/config/message-keys.d.ts +2 -0
  3. package/controllers/action.controller.d.ts +4 -4
  4. package/controllers/company-action-permission.controller.d.ts +2 -2
  5. package/controllers/index.d.ts +6 -6
  6. package/controllers/my-permission.controller.d.ts +3 -3
  7. package/controllers/role-permission.controller.d.ts +5 -4
  8. package/controllers/role.controller.d.ts +2 -2
  9. package/controllers/user-action-permission.controller.d.ts +5 -4
  10. package/docs/iam-swagger.config.d.ts +2 -2
  11. package/docs/index.d.ts +1 -1
  12. package/dtos/action.dto.d.ts +2 -2
  13. package/dtos/index.d.ts +3 -3
  14. package/dtos/permission.dto.d.ts +16 -0
  15. package/entities/action.entity.d.ts +2 -2
  16. package/entities/index.d.ts +11 -11
  17. package/entities/permission-with-company.entity.d.ts +1 -1
  18. package/entities/role-with-company.entity.d.ts +1 -1
  19. package/entities/role.entity.d.ts +1 -1
  20. package/entities/user-iam-permission.entity.d.ts +1 -1
  21. package/enums/index.d.ts +4 -4
  22. package/fesm/284.js +1316 -0
  23. package/fesm/867.js +3801 -0
  24. package/fesm/chunks/364.js +86 -0
  25. package/fesm/config/index.js +135 -3
  26. package/fesm/controllers/index.js +18 -7
  27. package/fesm/docs/index.js +240 -1
  28. package/fesm/dtos/index.js +1168 -3
  29. package/fesm/entities/index.js +670 -31
  30. package/fesm/enums/index.js +97 -4
  31. package/fesm/helpers/index.js +125 -2
  32. package/fesm/index.js +818 -11
  33. package/fesm/interfaces/index.js +17 -3
  34. package/fesm/modules/index.js +259 -1
  35. package/fesm/runtime.js +134 -0
  36. package/fesm/services/index.js +43 -6
  37. package/fesm/types/index.js +15 -1
  38. package/helpers/company-access.helper.d.ts +2 -2
  39. package/helpers/index.d.ts +2 -2
  40. package/helpers/permission-mode.helper.d.ts +1 -1
  41. package/index.d.ts +11 -11
  42. package/interfaces/action.interface.d.ts +2 -2
  43. package/interfaces/iam-module-options.interface.d.ts +3 -3
  44. package/interfaces/index.d.ts +3 -3
  45. package/modules/iam.module.d.ts +2 -2
  46. package/modules/index.d.ts +1 -1
  47. package/package.json +30 -37
  48. package/services/action.service.d.ts +9 -9
  49. package/services/iam-config.service.d.ts +4 -4
  50. package/services/iam-datasource.service.d.ts +3 -3
  51. package/services/index.d.ts +6 -6
  52. package/services/permission-cache.service.d.ts +5 -5
  53. package/services/permission.service.d.ts +6 -4
  54. package/services/role.service.d.ts +8 -8
  55. package/types/index.d.ts +1 -1
  56. package/cjs/config/event-actions.js +0 -36
  57. package/cjs/config/iam.constants.js +0 -11
  58. package/cjs/config/index.js +0 -20
  59. package/cjs/config/message-keys.js +0 -71
  60. package/cjs/controllers/action.controller.js +0 -174
  61. package/cjs/controllers/action.controller.spec.js +0 -79
  62. package/cjs/controllers/company-action-permission.controller.js +0 -119
  63. package/cjs/controllers/company-action-permission.controller.spec.js +0 -65
  64. package/cjs/controllers/index.js +0 -23
  65. package/cjs/controllers/my-permission.controller.js +0 -147
  66. package/cjs/controllers/my-permission.controller.spec.js +0 -128
  67. package/cjs/controllers/role-permission.controller.js +0 -191
  68. package/cjs/controllers/role-permission.controller.spec.js +0 -140
  69. package/cjs/controllers/role.controller.js +0 -102
  70. package/cjs/controllers/role.controller.spec.js +0 -86
  71. package/cjs/controllers/user-action-permission.controller.js +0 -129
  72. package/cjs/controllers/user-action-permission.controller.spec.js +0 -68
  73. package/cjs/docs/iam-swagger.config.js +0 -209
  74. package/cjs/docs/iam-swagger.config.spec.js +0 -99
  75. package/cjs/docs/index.js +0 -18
  76. package/cjs/dtos/action.dto.js +0 -282
  77. package/cjs/dtos/index.js +0 -21
  78. package/cjs/dtos/permission.dto.js +0 -637
  79. package/cjs/dtos/role.dto.js +0 -155
  80. package/cjs/entities/action.entity.js +0 -138
  81. package/cjs/entities/index.js +0 -80
  82. package/cjs/entities/index.spec.js +0 -56
  83. package/cjs/entities/permission-base.entity.js +0 -107
  84. package/cjs/entities/permission-with-company.entity.js +0 -100
  85. package/cjs/entities/role-base.entity.js +0 -80
  86. package/cjs/entities/role-with-company.entity.js +0 -55
  87. package/cjs/entities/role.entity.js +0 -25
  88. package/cjs/entities/user-iam-permission.entity.js +0 -43
  89. package/cjs/enums/action-type.enum.js +0 -22
  90. package/cjs/enums/iam-entity-type.enum.js +0 -17
  91. package/cjs/enums/iam-permission-type.enum.js +0 -17
  92. package/cjs/enums/index.js +0 -21
  93. package/cjs/enums/permission-type.enum.js +0 -16
  94. package/cjs/helpers/company-access.helper.js +0 -23
  95. package/cjs/helpers/company-access.helper.spec.js +0 -68
  96. package/cjs/helpers/index.js +0 -19
  97. package/cjs/helpers/permission-mode.helper.js +0 -49
  98. package/cjs/helpers/permission-mode.helper.spec.js +0 -54
  99. package/cjs/index.js +0 -28
  100. package/cjs/interfaces/action.interface.js +0 -4
  101. package/cjs/interfaces/iam-module-options.interface.js +0 -4
  102. package/cjs/interfaces/index.js +0 -20
  103. package/cjs/interfaces/role.interface.js +0 -7
  104. package/cjs/modules/iam.module.js +0 -186
  105. package/cjs/modules/iam.module.spec.js +0 -282
  106. package/cjs/modules/index.js +0 -18
  107. package/cjs/services/action.service.js +0 -337
  108. package/cjs/services/action.service.spec.js +0 -351
  109. package/cjs/services/iam-config.service.js +0 -87
  110. package/cjs/services/iam-config.service.spec.js +0 -110
  111. package/cjs/services/iam-datasource.service.js +0 -129
  112. package/cjs/services/iam-datasource.service.spec.js +0 -110
  113. package/cjs/services/index.js +0 -23
  114. package/cjs/services/permission-cache.service.js +0 -544
  115. package/cjs/services/permission-cache.service.spec.js +0 -718
  116. package/cjs/services/permission.service.js +0 -914
  117. package/cjs/services/permission.service.spec.js +0 -918
  118. package/cjs/services/role.service.js +0 -224
  119. package/cjs/services/role.service.spec.js +0 -347
  120. package/cjs/types/index.js +0 -18
  121. package/cjs/types/logic-node.type.js +0 -30
  122. package/fesm/config/event-actions.js +0 -15
  123. package/fesm/config/iam.constants.js +0 -1
  124. package/fesm/config/message-keys.js +0 -35
  125. package/fesm/controllers/action.controller.js +0 -164
  126. package/fesm/controllers/action.controller.spec.js +0 -75
  127. package/fesm/controllers/company-action-permission.controller.js +0 -109
  128. package/fesm/controllers/company-action-permission.controller.spec.js +0 -61
  129. package/fesm/controllers/my-permission.controller.js +0 -137
  130. package/fesm/controllers/my-permission.controller.spec.js +0 -124
  131. package/fesm/controllers/role-permission.controller.js +0 -181
  132. package/fesm/controllers/role-permission.controller.spec.js +0 -136
  133. package/fesm/controllers/role.controller.js +0 -92
  134. package/fesm/controllers/role.controller.spec.js +0 -82
  135. package/fesm/controllers/user-action-permission.controller.js +0 -119
  136. package/fesm/controllers/user-action-permission.controller.spec.js +0 -64
  137. package/fesm/docs/iam-swagger.config.js +0 -199
  138. package/fesm/docs/iam-swagger.config.spec.js +0 -95
  139. package/fesm/dtos/action.dto.js +0 -252
  140. package/fesm/dtos/permission.dto.js +0 -571
  141. package/fesm/dtos/role.dto.js +0 -134
  142. package/fesm/entities/action.entity.js +0 -128
  143. package/fesm/entities/index.spec.js +0 -52
  144. package/fesm/entities/permission-base.entity.js +0 -100
  145. package/fesm/entities/permission-with-company.entity.js +0 -90
  146. package/fesm/entities/role-base.entity.js +0 -70
  147. package/fesm/entities/role-with-company.entity.js +0 -45
  148. package/fesm/entities/role.entity.js +0 -15
  149. package/fesm/entities/user-iam-permission.entity.js +0 -33
  150. package/fesm/enums/action-type.enum.js +0 -12
  151. package/fesm/enums/iam-entity-type.enum.js +0 -7
  152. package/fesm/enums/iam-permission-type.enum.js +0 -7
  153. package/fesm/enums/permission-type.enum.js +0 -6
  154. package/fesm/helpers/company-access.helper.js +0 -18
  155. package/fesm/helpers/company-access.helper.spec.js +0 -64
  156. package/fesm/helpers/permission-mode.helper.js +0 -49
  157. package/fesm/helpers/permission-mode.helper.spec.js +0 -50
  158. package/fesm/interfaces/action.interface.js +0 -3
  159. package/fesm/interfaces/iam-module-options.interface.js +0 -3
  160. package/fesm/interfaces/role.interface.js +0 -4
  161. package/fesm/modules/iam.module.js +0 -176
  162. package/fesm/modules/iam.module.spec.js +0 -278
  163. package/fesm/services/action.service.js +0 -327
  164. package/fesm/services/action.service.spec.js +0 -347
  165. package/fesm/services/iam-config.service.js +0 -77
  166. package/fesm/services/iam-config.service.spec.js +0 -106
  167. package/fesm/services/iam-datasource.service.js +0 -78
  168. package/fesm/services/iam-datasource.service.spec.js +0 -106
  169. package/fesm/services/permission-cache.service.js +0 -527
  170. package/fesm/services/permission-cache.service.spec.js +0 -714
  171. package/fesm/services/permission.service.js +0 -904
  172. package/fesm/services/permission.service.spec.js +0 -914
  173. package/fesm/services/role.service.js +0 -214
  174. package/fesm/services/role.service.spec.js +0 -343
  175. package/fesm/types/logic-node.type.js +0 -27
@@ -1,914 +0,0 @@
1
- import { Test } from '@nestjs/testing';
2
- import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
3
- import { createMockRepository } from '@test-utils/mocks/repository.mock';
4
- import { IAM_MODE_MESSAGES, PERMISSION_OPERATION_MESSAGES } from '../config';
5
- import { PermissionAction } from '../dtos/permission.dto';
6
- import { Action } from '../entities/action.entity';
7
- import { Role } from '../entities/role.entity';
8
- import { RoleWithCompany } from '../entities/role-with-company.entity';
9
- import { ActionType } from '../enums/action-type.enum';
10
- import { IamEntityType } from '../enums/iam-entity-type.enum';
11
- import { IamPermissionType } from '../enums/iam-permission-type.enum';
12
- import { IAMPermissionMode } from '../enums/permission-type.enum';
13
- import { IAMConfigService } from './iam-config.service';
14
- import { IAMDataSourceService } from './iam-datasource.service';
15
- import { PermissionCacheService } from './permission-cache.service';
16
- import { PermissionService } from './permission.service';
17
- function buildPermissionRow(overrides = {}) {
18
- const row = {
19
- id: 'perm-uuid-1',
20
- permissionType: IamPermissionType.USER_ACTION,
21
- sourceType: IamEntityType.USER,
22
- sourceId: 'user-1',
23
- targetType: IamEntityType.ACTION,
24
- targetId: 'action-1',
25
- companyId: null,
26
- branchId: null,
27
- validFrom: null,
28
- validUntil: null,
29
- reason: null,
30
- createdAt: new Date(),
31
- ...overrides
32
- };
33
- row.isValid = (now = new Date())=>{
34
- if (row.validFrom && now < row.validFrom) return false;
35
- if (row.validUntil && now > row.validUntil) return false;
36
- return true;
37
- };
38
- return row;
39
- }
40
- function buildDeleteResult(affected) {
41
- return {
42
- affected,
43
- raw: []
44
- };
45
- }
46
- function mockRawMany(repo, rows) {
47
- const builder = {
48
- select: jest.fn().mockReturnThis(),
49
- addSelect: jest.fn().mockReturnThis(),
50
- where: jest.fn().mockReturnThis(),
51
- andWhere: jest.fn().mockReturnThis(),
52
- getRawMany: jest.fn().mockResolvedValue(rows)
53
- };
54
- repo.createQueryBuilder.mockReturnValue(builder);
55
- return builder;
56
- }
57
- function buildAction(overrides = {}) {
58
- return {
59
- id: 'action-1',
60
- name: 'View Users',
61
- code: 'user.view',
62
- description: null,
63
- actionType: ActionType.BACKEND,
64
- parentId: null,
65
- isActive: true,
66
- ...overrides
67
- };
68
- }
69
- describe('PermissionService', ()=>{
70
- let service;
71
- let mockPermissionRepo;
72
- let mockActionRepo;
73
- let mockRoleRepo;
74
- let mockIamConfigService;
75
- let mockPermissionCacheService;
76
- let mockDataSourceProvider;
77
- beforeEach(async ()=>{
78
- mockPermissionRepo = createMockRepository();
79
- mockActionRepo = createMockRepository();
80
- mockRoleRepo = createMockRepository();
81
- mockDataSourceProvider = {
82
- getRepository: jest.fn((entity)=>{
83
- if (entity === Action) return Promise.resolve(mockActionRepo);
84
- if (entity === Role || entity === RoleWithCompany) return Promise.resolve(mockRoleRepo);
85
- return Promise.resolve(mockPermissionRepo);
86
- }),
87
- getDataSource: jest.fn()
88
- };
89
- mockIamConfigService = {
90
- isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
91
- isDirectPermissionEnabled: jest.fn().mockReturnValue(true),
92
- isRbacEnabled: jest.fn().mockReturnValue(true),
93
- getPermissionMode: jest.fn().mockReturnValue(IAMPermissionMode.FULL)
94
- };
95
- mockPermissionCacheService = {
96
- invalidateUser: jest.fn(),
97
- invalidateUsers: jest.fn(),
98
- invalidateRoleMembersCache: jest.fn(),
99
- invalidateCompanyMembersCache: jest.fn(),
100
- getMyPermissionsResponse: jest.fn()
101
- };
102
- const module = await Test.createTestingModule({
103
- providers: [
104
- PermissionService,
105
- {
106
- provide: PermissionCacheService,
107
- useValue: mockPermissionCacheService
108
- },
109
- {
110
- provide: IAMConfigService,
111
- useValue: mockIamConfigService
112
- },
113
- {
114
- provide: IAMDataSourceService,
115
- useValue: mockDataSourceProvider
116
- }
117
- ]
118
- }).compile();
119
- service = await module.resolve(PermissionService);
120
- });
121
- // ==================== User-Action Permissions ====================
122
- describe('assignUserActions', ()=>{
123
- const dto = {
124
- userId: 'user-1',
125
- items: [
126
- {
127
- id: 'action-1',
128
- action: PermissionAction.ADD
129
- },
130
- {
131
- id: 'action-2',
132
- action: PermissionAction.ADD
133
- },
134
- {
135
- id: 'action-3',
136
- action: PermissionAction.REMOVE
137
- }
138
- ]
139
- };
140
- it('throws BadRequestException when direct permissions are disabled (RBAC-only mode)', async ()=>{
141
- mockIamConfigService.isDirectPermissionEnabled.mockReturnValue(false);
142
- await expect(service.assignUserActions(dto)).rejects.toMatchObject({
143
- response: expect.objectContaining({
144
- messageKey: IAM_MODE_MESSAGES.DIRECT_MODE_UNAVAILABLE
145
- })
146
- });
147
- expect(mockPermissionRepo.find).not.toHaveBeenCalled();
148
- });
149
- it('adds only actions not already assigned, removes requested actions, and invalidates the user cache', async ()=>{
150
- mockPermissionRepo.find.mockResolvedValue([
151
- {
152
- targetId: 'action-1'
153
- }
154
- ]);
155
- mockPermissionRepo.save.mockResolvedValue([
156
- {}
157
- ]);
158
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
159
- const result = await service.assignUserActions(dto);
160
- expect(mockPermissionRepo.save).toHaveBeenCalledWith([
161
- expect.objectContaining({
162
- targetId: 'action-2',
163
- companyId: null,
164
- branchId: null
165
- })
166
- ]);
167
- expect(result).toEqual({
168
- added: 1,
169
- removed: 1,
170
- total: 3
171
- });
172
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, []);
173
- });
174
- it('scopes new permissions and cache invalidation to the company/branch when the company feature is enabled', async ()=>{
175
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
176
- mockPermissionRepo.find.mockResolvedValue([]);
177
- mockPermissionRepo.save.mockResolvedValue([
178
- {},
179
- {}
180
- ]);
181
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
182
- await service.assignUserActions({
183
- userId: 'user-1',
184
- companyId: 'company-1',
185
- branchId: 'branch-1',
186
- items: dto.items
187
- });
188
- expect(mockPermissionRepo.find).toHaveBeenCalledWith(expect.objectContaining({
189
- where: expect.objectContaining({
190
- companyId: 'company-1',
191
- branchId: 'branch-1'
192
- })
193
- }));
194
- expect(mockPermissionRepo.save).toHaveBeenCalledWith([
195
- expect.objectContaining({
196
- companyId: 'company-1',
197
- branchId: 'branch-1'
198
- }),
199
- expect.objectContaining({
200
- companyId: 'company-1',
201
- branchId: 'branch-1'
202
- })
203
- ]);
204
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', 'company-1', [
205
- 'branch-1'
206
- ]);
207
- });
208
- it('translates a duplicate-entry DB error into ConflictException', async ()=>{
209
- mockPermissionRepo.find.mockResolvedValue([]);
210
- mockPermissionRepo.save.mockRejectedValue({
211
- code: 'ER_DUP_ENTRY'
212
- });
213
- await expect(service.assignUserActions(dto)).rejects.toMatchObject({
214
- response: expect.objectContaining({
215
- messageKey: PERMISSION_OPERATION_MESSAGES.ALREADY_EXISTS
216
- })
217
- });
218
- });
219
- it('rethrows unknown errors from save as-is', async ()=>{
220
- mockPermissionRepo.find.mockResolvedValue([]);
221
- const unexpected = new Error('connection lost');
222
- mockPermissionRepo.save.mockRejectedValue(unexpected);
223
- await expect(service.assignUserActions(dto)).rejects.toBe(unexpected);
224
- });
225
- it('does nothing but still invalidates the cache when items is empty', async ()=>{
226
- const result = await service.assignUserActions({
227
- userId: 'user-1',
228
- items: []
229
- });
230
- expect(result).toEqual({
231
- added: 0,
232
- removed: 0,
233
- total: 0
234
- });
235
- expect(mockPermissionRepo.save).not.toHaveBeenCalled();
236
- expect(mockPermissionRepo.delete).not.toHaveBeenCalled();
237
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalled();
238
- });
239
- });
240
- describe('getUserActions', ()=>{
241
- it('returns an empty array without querying actions when the user has no permissions', async ()=>{
242
- mockPermissionRepo.find.mockResolvedValue([]);
243
- const result = await service.getUserActions('user-1', undefined, undefined);
244
- expect(result).toEqual([]);
245
- expect(mockActionRepo.find).not.toHaveBeenCalled();
246
- });
247
- it('joins permissions with their actions and filters out permissions for deleted actions', async ()=>{
248
- mockPermissionRepo.find.mockResolvedValue([
249
- buildPermissionRow({
250
- targetId: 'action-1'
251
- }),
252
- buildPermissionRow({
253
- id: 'perm-2',
254
- targetId: 'action-missing'
255
- })
256
- ]);
257
- mockActionRepo.find.mockResolvedValue([
258
- buildAction({
259
- id: 'action-1'
260
- })
261
- ]);
262
- const result = await service.getUserActions('user-1', undefined, undefined);
263
- expect(result).toHaveLength(1);
264
- expect(result[0]).toEqual(expect.objectContaining({
265
- actionId: 'action-1',
266
- actionCode: 'user.view'
267
- }));
268
- });
269
- it('filters by companyId/branchId when the company feature is enabled', async ()=>{
270
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
271
- mockPermissionRepo.find.mockResolvedValue([]);
272
- await service.getUserActions('user-1', 'company-1', 'branch-1');
273
- expect(mockPermissionRepo.find).toHaveBeenCalledWith({
274
- where: expect.objectContaining({
275
- companyId: 'company-1',
276
- branchId: 'branch-1'
277
- })
278
- });
279
- });
280
- });
281
- // ==================== Role-Action Permissions ====================
282
- describe('assignRoleActions', ()=>{
283
- const dto = {
284
- roleId: 'role-1',
285
- items: [
286
- {
287
- id: 'action-1',
288
- action: PermissionAction.ADD
289
- },
290
- {
291
- id: 'action-2',
292
- action: PermissionAction.REMOVE
293
- }
294
- ]
295
- };
296
- it('throws BadRequestException when RBAC is disabled (DIRECT-only mode)', async ()=>{
297
- mockIamConfigService.isRbacEnabled.mockReturnValue(false);
298
- await expect(service.assignRoleActions(dto)).rejects.toMatchObject({
299
- response: expect.objectContaining({
300
- messageKey: IAM_MODE_MESSAGES.RBAC_MODE_UNAVAILABLE
301
- })
302
- });
303
- });
304
- it('resolves the role company scope from the role repository when the company feature is enabled', async ()=>{
305
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
306
- mockRoleRepo.findOne.mockResolvedValue({
307
- id: 'role-1',
308
- companyId: 'company-9'
309
- });
310
- mockPermissionRepo.find.mockResolvedValue([]);
311
- mockPermissionRepo.save.mockResolvedValue([
312
- {}
313
- ]);
314
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
315
- await service.assignRoleActions(dto);
316
- expect(mockPermissionRepo.save).toHaveBeenCalledWith([
317
- expect.objectContaining({
318
- companyId: 'company-9'
319
- })
320
- ]);
321
- });
322
- it('translates duplicate role-action entries into ConflictException', async ()=>{
323
- mockPermissionRepo.find.mockResolvedValue([]);
324
- mockPermissionRepo.save.mockRejectedValue({
325
- message: 'Duplicate entry for key'
326
- });
327
- await expect(service.assignRoleActions(dto)).rejects.toMatchObject({
328
- response: expect.objectContaining({
329
- messageKey: PERMISSION_OPERATION_MESSAGES.ALREADY_EXISTS
330
- })
331
- });
332
- });
333
- it('removes role actions and reports counts and invalidates role member caches', async ()=>{
334
- mockPermissionRepo.find.mockResolvedValue([]); // existing check for add
335
- mockPermissionRepo.save.mockResolvedValue([
336
- {}
337
- ]);
338
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
339
- const result = await service.assignRoleActions(dto);
340
- expect(result).toEqual({
341
- added: 1,
342
- removed: 1,
343
- total: 2
344
- });
345
- expect(mockPermissionCacheService.invalidateRoleMembersCache).toHaveBeenCalledWith('role-1');
346
- });
347
- });
348
- describe('getRoleActions', ()=>{
349
- it('returns an empty array when the role has no actions assigned', async ()=>{
350
- mockPermissionRepo.find.mockResolvedValue([]);
351
- const result = await service.getRoleActions('role-1');
352
- expect(result).toEqual([]);
353
- });
354
- it('maps role-action permissions to their action details', async ()=>{
355
- mockPermissionRepo.find.mockResolvedValue([
356
- buildPermissionRow({
357
- sourceId: 'role-1',
358
- targetId: 'action-1'
359
- })
360
- ]);
361
- mockActionRepo.find.mockResolvedValue([
362
- buildAction({
363
- id: 'action-1'
364
- })
365
- ]);
366
- const result = await service.getRoleActions('role-1');
367
- expect(result).toEqual([
368
- expect.objectContaining({
369
- roleId: 'role-1',
370
- actionCode: 'user.view'
371
- })
372
- ]);
373
- });
374
- });
375
- // ==================== Company-Action Permissions ====================
376
- describe('assignCompanyActions', ()=>{
377
- function mockTransaction() {
378
- const fakeManager = {
379
- getRepository: jest.fn((entity)=>{
380
- if (entity === Role || entity === RoleWithCompany) return mockRoleRepo;
381
- return mockPermissionRepo;
382
- })
383
- };
384
- mockDataSourceProvider.getDataSource.mockResolvedValue({
385
- transaction: jest.fn().mockImplementation((cb)=>cb(fakeManager))
386
- });
387
- return fakeManager;
388
- }
389
- it('adds new company-whitelisted actions inside a transaction, skipping duplicates', async ()=>{
390
- mockTransaction();
391
- mockPermissionRepo.find.mockResolvedValue([
392
- {
393
- targetId: 'action-1'
394
- }
395
- ]);
396
- mockPermissionRepo.create.mockImplementation((data)=>data);
397
- mockPermissionRepo.save.mockResolvedValue([
398
- {}
399
- ]);
400
- const result = await service.assignCompanyActions({
401
- companyId: 'company-1',
402
- items: [
403
- {
404
- id: 'action-1',
405
- action: PermissionAction.ADD
406
- },
407
- {
408
- id: 'action-2',
409
- action: PermissionAction.ADD
410
- }
411
- ]
412
- });
413
- expect(mockPermissionRepo.save).toHaveBeenCalledWith([
414
- expect.objectContaining({
415
- targetId: 'action-2',
416
- sourceId: 'company-1'
417
- })
418
- ]);
419
- expect(result.added).toBe(1);
420
- expect(mockPermissionCacheService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
421
- });
422
- it('cascades removal to role-action and user-action permissions for the company', async ()=>{
423
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
424
- mockTransaction();
425
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(2));
426
- mockRoleRepo.find.mockResolvedValue([
427
- {
428
- id: 'role-1'
429
- }
430
- ]);
431
- mockRawMany(mockPermissionRepo, []);
432
- const result = await service.assignCompanyActions({
433
- companyId: 'company-1',
434
- items: [
435
- {
436
- id: 'action-1',
437
- action: PermissionAction.REMOVE
438
- }
439
- ]
440
- });
441
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
442
- permissionType: IamPermissionType.COMPANY_ACTION
443
- }));
444
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
445
- permissionType: IamPermissionType.ROLE_ACTION
446
- }));
447
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
448
- permissionType: IamPermissionType.USER_ACTION
449
- }));
450
- expect(result.removed).toBe(2);
451
- });
452
- });
453
- describe('getCompanyActions / getCompanyActionIds', ()=>{
454
- it('getCompanyActions returns an empty array when the company has no whitelist entries', async ()=>{
455
- mockPermissionRepo.find.mockResolvedValue([]);
456
- expect(await service.getCompanyActions('company-1')).toEqual([]);
457
- });
458
- it('getCompanyActions maps whitelist entries to action details', async ()=>{
459
- mockPermissionRepo.find.mockResolvedValue([
460
- buildPermissionRow({
461
- sourceType: IamEntityType.COMPANY,
462
- sourceId: 'company-1',
463
- targetId: 'action-1'
464
- })
465
- ]);
466
- mockActionRepo.find.mockResolvedValue([
467
- buildAction({
468
- id: 'action-1'
469
- })
470
- ]);
471
- const result = await service.getCompanyActions('company-1');
472
- expect(result).toEqual([
473
- expect.objectContaining({
474
- companyId: 'company-1',
475
- actionCode: 'user.view'
476
- })
477
- ]);
478
- });
479
- it('getCompanyActionIds returns just the whitelisted action ids', async ()=>{
480
- mockPermissionRepo.find.mockResolvedValue([
481
- {
482
- targetId: 'action-1'
483
- },
484
- {
485
- targetId: 'action-2'
486
- }
487
- ]);
488
- const result = await service.getCompanyActions('company-1', true);
489
- expect(result).toEqual([
490
- 'action-1',
491
- 'action-2'
492
- ]);
493
- });
494
- });
495
- // ==================== User-Role Permissions ====================
496
- describe('assignUserRoles', ()=>{
497
- const dto = {
498
- userId: 'user-1',
499
- items: [
500
- {
501
- id: 'role-1',
502
- action: PermissionAction.ADD
503
- }
504
- ]
505
- };
506
- it('throws BadRequestException when RBAC is disabled (DIRECT-only mode)', async ()=>{
507
- mockIamConfigService.isRbacEnabled.mockReturnValue(false);
508
- await expect(service.assignUserRoles(dto)).rejects.toMatchObject({
509
- response: expect.objectContaining({
510
- messageKey: IAM_MODE_MESSAGES.ROLE_ASSIGNMENT_UNAVAILABLE
511
- })
512
- });
513
- });
514
- it('adds new role assignments and invalidates the user cache', async ()=>{
515
- mockPermissionRepo.find.mockResolvedValue([]);
516
- mockPermissionRepo.save.mockResolvedValue([
517
- {}
518
- ]);
519
- const result = await service.assignUserRoles(dto);
520
- expect(result).toEqual({
521
- added: 1,
522
- removed: 0,
523
- total: 1
524
- });
525
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', null, [
526
- null
527
- ]);
528
- });
529
- it('translates duplicate user-role entries into ConflictException', async ()=>{
530
- mockPermissionRepo.find.mockResolvedValue([]);
531
- mockPermissionRepo.save.mockRejectedValue({
532
- code: 'ER_DUP_ENTRY'
533
- });
534
- await expect(service.assignUserRoles(dto)).rejects.toMatchObject({
535
- response: expect.objectContaining({
536
- messageKey: PERMISSION_OPERATION_MESSAGES.ALREADY_EXISTS
537
- })
538
- });
539
- });
540
- });
541
- describe('getUserRoles', ()=>{
542
- it('returns an empty array when the user has no role assignments', async ()=>{
543
- mockPermissionRepo.find.mockResolvedValue([]);
544
- expect(await service.getUserRoles('user-1')).toEqual([]);
545
- expect(mockRoleRepo.find).not.toHaveBeenCalled();
546
- });
547
- it('joins role permissions with role details and filters out permissions for deleted roles', async ()=>{
548
- mockPermissionRepo.find.mockResolvedValue([
549
- buildPermissionRow({
550
- targetId: 'role-1',
551
- targetType: IamEntityType.ROLE
552
- }),
553
- buildPermissionRow({
554
- id: 'perm-2',
555
- targetId: 'role-missing',
556
- targetType: IamEntityType.ROLE
557
- })
558
- ]);
559
- mockRoleRepo.find.mockResolvedValue([
560
- {
561
- id: 'role-1',
562
- name: 'Manager'
563
- }
564
- ]);
565
- const result = await service.getUserRoles('user-1');
566
- expect(result).toHaveLength(1);
567
- expect(result[0]).toEqual(expect.objectContaining({
568
- roleId: 'role-1',
569
- roleName: 'Manager'
570
- }));
571
- });
572
- });
573
- // ==================== Effective (Merged) Permissions ====================
574
- describe('getUserEffectiveRoles', ()=>{
575
- it('returns an empty array when the user has no role assignments', async ()=>{
576
- mockPermissionRepo.find.mockResolvedValue([]);
577
- expect(await service.getUserEffectiveRoles('user-1')).toEqual([]);
578
- expect(mockRoleRepo.find).not.toHaveBeenCalled();
579
- });
580
- it('queries only by userId when the company feature is disabled, ignoring companyId/branchId', async ()=>{
581
- mockPermissionRepo.find.mockResolvedValue([]);
582
- await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
583
- expect(mockPermissionRepo.find).toHaveBeenCalledWith({
584
- where: {
585
- permissionType: IamPermissionType.USER_ROLE,
586
- sourceType: IamEntityType.USER,
587
- sourceId: 'user-1'
588
- }
589
- });
590
- });
591
- it('merges company-wide (branchId null) and branch-specific role assignments via an OR query', async ()=>{
592
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
593
- mockPermissionRepo.find.mockResolvedValue([
594
- buildPermissionRow({
595
- id: 'perm-1',
596
- permissionType: IamPermissionType.USER_ROLE,
597
- targetType: IamEntityType.ROLE,
598
- targetId: 'role-1',
599
- companyId: 'company-1',
600
- branchId: null
601
- }),
602
- buildPermissionRow({
603
- id: 'perm-2',
604
- permissionType: IamPermissionType.USER_ROLE,
605
- targetType: IamEntityType.ROLE,
606
- targetId: 'role-2',
607
- companyId: 'company-1',
608
- branchId: 'branch-1'
609
- })
610
- ]);
611
- mockRoleRepo.find.mockResolvedValue([
612
- {
613
- id: 'role-1',
614
- name: 'Company Admin'
615
- },
616
- {
617
- id: 'role-2',
618
- name: 'Branch Manager'
619
- }
620
- ]);
621
- const result = await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
622
- expect(mockPermissionRepo.find).toHaveBeenCalledWith({
623
- where: [
624
- expect.objectContaining({
625
- sourceId: 'user-1',
626
- companyId: 'company-1',
627
- branchId: expect.anything()
628
- }),
629
- expect.objectContaining({
630
- sourceId: 'user-1',
631
- companyId: 'company-1',
632
- branchId: 'branch-1'
633
- })
634
- ]
635
- });
636
- expect(result.map((r)=>r.roleId).sort()).toEqual([
637
- 'role-1',
638
- 'role-2'
639
- ]);
640
- });
641
- it('excludes expired/not-yet-valid permission rows', async ()=>{
642
- mockPermissionRepo.find.mockResolvedValue([
643
- buildPermissionRow({
644
- permissionType: IamPermissionType.USER_ROLE,
645
- targetType: IamEntityType.ROLE,
646
- targetId: 'role-1',
647
- validUntil: new Date('2000-01-01')
648
- })
649
- ]);
650
- const result = await service.getUserEffectiveRoles('user-1');
651
- expect(result).toEqual([]);
652
- expect(mockRoleRepo.find).not.toHaveBeenCalled();
653
- });
654
- });
655
- describe('getUserEffectiveActions', ()=>{
656
- it('returns an empty array when the user has no action assignments', async ()=>{
657
- mockPermissionRepo.find.mockResolvedValue([]);
658
- expect(await service.getUserEffectiveActions('user-1')).toEqual([]);
659
- expect(mockActionRepo.find).not.toHaveBeenCalled();
660
- });
661
- it('merges company-wide (branchId null) and branch-specific action assignments via an OR query', async ()=>{
662
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
663
- mockPermissionRepo.find.mockResolvedValue([
664
- buildPermissionRow({
665
- id: 'perm-1',
666
- targetId: 'action-1',
667
- companyId: 'company-1',
668
- branchId: null
669
- }),
670
- buildPermissionRow({
671
- id: 'perm-2',
672
- targetId: 'action-2',
673
- companyId: 'company-1',
674
- branchId: 'branch-1'
675
- })
676
- ]);
677
- mockActionRepo.find.mockResolvedValue([
678
- buildAction({
679
- id: 'action-1',
680
- code: 'user.view'
681
- }),
682
- buildAction({
683
- id: 'action-2',
684
- code: 'user.create'
685
- })
686
- ]);
687
- const result = await service.getUserEffectiveActions('user-1', 'company-1', 'branch-1');
688
- expect(mockPermissionRepo.find).toHaveBeenCalledWith({
689
- where: [
690
- expect.objectContaining({
691
- sourceId: 'user-1',
692
- companyId: 'company-1',
693
- branchId: expect.anything()
694
- }),
695
- expect.objectContaining({
696
- sourceId: 'user-1',
697
- companyId: 'company-1',
698
- branchId: 'branch-1'
699
- })
700
- ]
701
- });
702
- expect(result.map((a)=>a.actionId).sort()).toEqual([
703
- 'action-1',
704
- 'action-2'
705
- ]);
706
- });
707
- it('excludes expired/not-yet-valid permission rows', async ()=>{
708
- mockPermissionRepo.find.mockResolvedValue([
709
- buildPermissionRow({
710
- targetId: 'action-1',
711
- validUntil: new Date('2000-01-01')
712
- })
713
- ]);
714
- const result = await service.getUserEffectiveActions('user-1');
715
- expect(result).toEqual([]);
716
- expect(mockActionRepo.find).not.toHaveBeenCalled();
717
- });
718
- });
719
- // ==================== My Permissions / permission-mode branching ====================
720
- describe('getMyPermissions', ()=>{
721
- it('delegates to the cache service and returns its response', async ()=>{
722
- const response = {
723
- frontendActions: [
724
- {
725
- code: 'user.view',
726
- name: 'View',
727
- description: null
728
- }
729
- ],
730
- cachedEndpoints: 1
731
- };
732
- mockPermissionCacheService.getMyPermissionsResponse.mockResolvedValue(response);
733
- const result = await service.getMyPermissions('user-1', null, 'company-1', [
734
- 'parent-code'
735
- ]);
736
- expect(mockPermissionCacheService.getMyPermissionsResponse).toHaveBeenCalledWith('user-1', null, 'company-1', [
737
- 'parent-code'
738
- ]);
739
- expect(result).toBe(response);
740
- });
741
- });
742
- describe('revokeCompanyPermissions', ()=>{
743
- it('does nothing when the company feature is disabled', async ()=>{
744
- await service.revokeCompanyPermissions('company-1');
745
- expect(mockPermissionRepo.find).not.toHaveBeenCalled();
746
- expect(mockRoleRepo.find).not.toHaveBeenCalled();
747
- });
748
- it('deletes company/role/user permissions transactionally and invalidates affected users', async ()=>{
749
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
750
- mockRoleRepo.find.mockResolvedValue([
751
- {
752
- id: 'role-1'
753
- }
754
- ]);
755
- mockPermissionRepo.find.mockResolvedValue([
756
- {
757
- sourceId: 'user-1'
758
- }
759
- ]);
760
- const fakeTxRepo = {
761
- delete: jest.fn().mockResolvedValue(buildDeleteResult(1))
762
- };
763
- const fakeTxRoleRepo = {
764
- delete: jest.fn().mockResolvedValue(buildDeleteResult(1))
765
- };
766
- mockDataSourceProvider.getDataSource.mockResolvedValue({
767
- transaction: jest.fn().mockImplementation((cb)=>cb({
768
- getRepository: jest.fn((target)=>{
769
- if (target === mockRoleRepo.target) return fakeTxRoleRepo;
770
- return fakeTxRepo;
771
- })
772
- }))
773
- });
774
- await service.revokeCompanyPermissions('company-1');
775
- expect(fakeTxRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
776
- permissionType: IamPermissionType.COMPANY_ACTION
777
- }));
778
- expect(fakeTxRoleRepo.delete).toHaveBeenCalledWith([
779
- 'role-1'
780
- ]);
781
- expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
782
- 'user-1'
783
- ], 'company-1');
784
- });
785
- });
786
- describe('revokeBranchPermissions', ()=>{
787
- it('does nothing when the company feature is disabled', async ()=>{
788
- await service.revokeBranchPermissions('branch-1', 'company-1');
789
- expect(mockPermissionRepo.delete).not.toHaveBeenCalled();
790
- });
791
- it('deletes matching branch permissions and invalidates affected users', async ()=>{
792
- mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
793
- mockPermissionRepo.find.mockResolvedValue([
794
- {
795
- sourceId: 'user-1'
796
- }
797
- ]);
798
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
799
- await service.revokeBranchPermissions('branch-1', 'company-1');
800
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
801
- branchId: 'branch-1',
802
- companyId: 'company-1'
803
- }));
804
- expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith([
805
- 'user-1'
806
- ], 'company-1', [
807
- 'branch-1'
808
- ]);
809
- });
810
- });
811
- describe('revokeUserCompanyAccess', ()=>{
812
- it('deletes user-role and user-action permissions for the company and invalidates the user cache', async ()=>{
813
- await service.revokeUserCompanyAccess('user-1', 'company-1');
814
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
815
- sourceId: 'user-1',
816
- companyId: 'company-1'
817
- }));
818
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', 'company-1');
819
- });
820
- });
821
- describe('revokeUserBranchAccess', ()=>{
822
- it('deletes branch-scoped permissions for the user and invalidates the user+branch cache', async ()=>{
823
- await service.revokeUserBranchAccess('user-1', 'branch-1', 'company-1');
824
- expect(mockPermissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
825
- sourceId: 'user-1',
826
- branchId: 'branch-1',
827
- companyId: 'company-1'
828
- }));
829
- expect(mockPermissionCacheService.invalidateUser).toHaveBeenCalledWith('user-1', 'company-1', [
830
- 'branch-1'
831
- ]);
832
- });
833
- });
834
- describe('domain events', ()=>{
835
- let publish;
836
- beforeEach(()=>{
837
- publish = jest.fn().mockResolvedValue(undefined);
838
- EventBusRegistry.reset();
839
- EventBusRegistry.setBus({
840
- publish,
841
- subscribe: jest.fn(),
842
- close: jest.fn()
843
- });
844
- EventBusRegistry.configureModule('iam', {
845
- enabled: true
846
- });
847
- mockIamConfigService.isDirectPermissionEnabled.mockReturnValue(true);
848
- mockPermissionRepo.find.mockResolvedValue([]);
849
- mockPermissionRepo.save.mockResolvedValue([
850
- {}
851
- ]);
852
- mockPermissionRepo.delete.mockResolvedValue(buildDeleteResult(1));
853
- });
854
- afterEach(()=>{
855
- EventBusRegistry.reset();
856
- });
857
- it('publishes iam.user_action.permissions-assigned with the operation counts', async ()=>{
858
- await service.assignUserActions({
859
- userId: 'user-1',
860
- items: [
861
- {
862
- id: 'action-1',
863
- action: PermissionAction.ADD
864
- }
865
- ]
866
- });
867
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
868
- name: 'iam.user_action.permissions-assigned',
869
- payload: expect.objectContaining({
870
- ids: [
871
- 'user-1'
872
- ],
873
- metadata: expect.objectContaining({
874
- total: 1
875
- })
876
- })
877
- }));
878
- });
879
- it('publishes iam.user_role.permissions-assigned when roles are assigned', async ()=>{
880
- mockRoleRepo.find.mockResolvedValue([
881
- {
882
- id: 'role-1'
883
- }
884
- ]);
885
- await service.assignUserRoles({
886
- userId: 'user-1',
887
- items: [
888
- {
889
- id: 'role-1',
890
- action: PermissionAction.ADD
891
- }
892
- ]
893
- });
894
- expect(publish).toHaveBeenCalledWith(expect.objectContaining({
895
- name: 'iam.user_role.permissions-assigned'
896
- }));
897
- });
898
- it('does not publish when the iam module has events disabled', async ()=>{
899
- EventBusRegistry.configureModule('iam', {
900
- enabled: false
901
- });
902
- await service.assignUserActions({
903
- userId: 'user-1',
904
- items: [
905
- {
906
- id: 'action-1',
907
- action: PermissionAction.ADD
908
- }
909
- ]
910
- });
911
- expect(publish).not.toHaveBeenCalled();
912
- });
913
- });
914
- });