@flusys/nestjs-iam 7.1.0 → 8.0.0-beta.1

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