@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
@@ -14,8 +14,13 @@ const _common = require("@nestjs/common");
14
14
  const _typeorm = require("typeorm");
15
15
  const _config = require("../config");
16
16
  const _actionentity = require("../entities/action.entity");
17
+ const _permissionwithcompanyentity = require("../entities/permission-with-company.entity");
18
+ const _useriampermissionentity = require("../entities/user-iam-permission.entity");
19
+ const _iamentitytypeenum = require("../enums/iam-entity-type.enum");
20
+ const _iampermissiontypeenum = require("../enums/iam-permission-type.enum");
17
21
  const _iamconfigservice = require("./iam-config.service");
18
22
  const _iamdatasourceservice = require("./iam-datasource.service");
23
+ const _permissioncacheservice = require("./permission-cache.service");
19
24
  const _permissionservice = require("./permission.service");
20
25
  function _define_property(obj, key, value) {
21
26
  if (key in obj) {
@@ -45,6 +50,81 @@ function _ts_param(paramIndex, decorator) {
45
50
  };
46
51
  }
47
52
  let ActionService = class ActionService extends _classes.ApiService {
53
+ async beforeDeleteOperation(dto, user, queryRunner) {
54
+ const actionIds = Array.isArray(dto.id) ? dto.id : [
55
+ dto.id
56
+ ];
57
+ if (actionIds.length === 0) {
58
+ return;
59
+ }
60
+ const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
61
+ const permissionEntity = enableCompanyFeature ? _permissionwithcompanyentity.UserIamPermissionWithCompany : _useriampermissionentity.UserIamPermission;
62
+ const permissionRepo = queryRunner.manager.getRepository(permissionEntity);
63
+ const [roleActionRows, userActionRows, companyActionRows] = await Promise.all([
64
+ permissionRepo.find({
65
+ where: {
66
+ permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
67
+ sourceType: _iamentitytypeenum.IamEntityType.ROLE,
68
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
69
+ targetId: (0, _typeorm.In)(actionIds)
70
+ }
71
+ }),
72
+ permissionRepo.find({
73
+ where: {
74
+ permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
75
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
76
+ targetId: (0, _typeorm.In)(actionIds)
77
+ }
78
+ }),
79
+ enableCompanyFeature ? permissionRepo.find({
80
+ where: {
81
+ permissionType: _iampermissiontypeenum.IamPermissionType.COMPANY_ACTION,
82
+ sourceType: _iamentitytypeenum.IamEntityType.COMPANY,
83
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
84
+ targetId: (0, _typeorm.In)(actionIds)
85
+ }
86
+ }) : Promise.resolve([])
87
+ ]);
88
+ const affectedUserIds = new Set(userActionRows.map((row)=>row.userId).filter((id)=>!!id));
89
+ const affectedCompanyIds = [
90
+ ...new Set(companyActionRows.map((row)=>row.sourceId).filter((id)=>!!id))
91
+ ];
92
+ const affectedRoleIds = [
93
+ ...new Set(roleActionRows.map((row)=>row.sourceId))
94
+ ];
95
+ if (affectedRoleIds.length > 0) {
96
+ const userRoleRows = await permissionRepo.find({
97
+ where: {
98
+ permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
99
+ targetType: _iamentitytypeenum.IamEntityType.ROLE,
100
+ targetId: (0, _typeorm.In)(affectedRoleIds)
101
+ }
102
+ });
103
+ userRoleRows.forEach((row)=>row.userId && affectedUserIds.add(row.userId));
104
+ }
105
+ await permissionRepo.delete({
106
+ permissionType: _iampermissiontypeenum.IamPermissionType.ROLE_ACTION,
107
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
108
+ targetId: (0, _typeorm.In)(actionIds)
109
+ });
110
+ await permissionRepo.delete({
111
+ permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
112
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
113
+ targetId: (0, _typeorm.In)(actionIds)
114
+ });
115
+ await permissionRepo.delete({
116
+ permissionType: _iampermissiontypeenum.IamPermissionType.COMPANY_ACTION,
117
+ sourceType: _iamentitytypeenum.IamEntityType.COMPANY,
118
+ targetType: _iamentitytypeenum.IamEntityType.ACTION,
119
+ targetId: (0, _typeorm.In)(actionIds)
120
+ });
121
+ await Promise.all([
122
+ affectedUserIds.size > 0 ? this.permissionCacheService.invalidateUsers([
123
+ ...affectedUserIds
124
+ ]) : Promise.resolve(),
125
+ ...affectedCompanyIds.map((companyId)=>this.permissionService.invalidateCompanyMembersCache(companyId))
126
+ ]);
127
+ }
48
128
  // Query Customization
49
129
  async getSelectQuery(query, _user, select) {
50
130
  if (!select?.length) {
@@ -108,12 +188,13 @@ let ActionService = class ActionService extends _classes.ApiService {
108
188
  });
109
189
  }
110
190
  }
111
- /** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
191
+ /** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user, companyId) {
112
192
  await this.ensureDataSourceRepository();
113
193
  this.requireUser(user, 'getActionsForPermission');
114
194
  let whereClause = {};
115
- if (this.iamConfigService.isCompanyFeatureEnabled() && user.companyId) {
116
- const companyActionIds = await this.permissionService.getCompanyActionIds(user.companyId);
195
+ const targetCompanyId = companyId || user.companyId;
196
+ if (this.iamConfigService.isCompanyFeatureEnabled() && targetCompanyId) {
197
+ const companyActionIds = await this.permissionService.getCompanyActionIds(targetCompanyId);
117
198
  if (companyActionIds.length === 0) {
118
199
  return [];
119
200
  }
@@ -176,9 +257,9 @@ let ActionService = class ActionService extends _classes.ApiService {
176
257
  }
177
258
  return rootNodes;
178
259
  }
179
- constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService){
180
- super('action', cacheManager, utilsService, ActionService.name, true, 'iam', _actionentity.Action, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionService", void 0), // Custom Methods
181
- _define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionService = permissionService, this.actionSelectFields = [
260
+ constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService, permissionCacheService){
261
+ super('action', cacheManager, utilsService, ActionService.name, true, 'iam', _actionentity.Action, dataSourceProvider), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "permissionService", void 0), _define_property(this, "permissionCacheService", void 0), // Custom Methods
262
+ _define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.permissionService = permissionService, this.permissionCacheService = permissionCacheService, this.actionSelectFields = [
182
263
  'id',
183
264
  'code',
184
265
  'name',
@@ -200,12 +281,14 @@ ActionService = _ts_decorate([
200
281
  _ts_param(2, (0, _common.Inject)(_iamconfigservice.IAMConfigService)),
201
282
  _ts_param(3, (0, _common.Inject)(_iamdatasourceservice.IAMDataSourceService)),
202
283
  _ts_param(4, (0, _common.Inject)(_permissionservice.PermissionService)),
284
+ _ts_param(5, (0, _common.Inject)(_permissioncacheservice.PermissionCacheService)),
203
285
  _ts_metadata("design:type", Function),
204
286
  _ts_metadata("design:paramtypes", [
205
287
  typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
206
288
  typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
207
289
  typeof _iamconfigservice.IAMConfigService === "undefined" ? Object : _iamconfigservice.IAMConfigService,
208
290
  typeof _iamdatasourceservice.IAMDataSourceService === "undefined" ? Object : _iamdatasourceservice.IAMDataSourceService,
209
- typeof _permissionservice.PermissionService === "undefined" ? Object : _permissionservice.PermissionService
291
+ typeof _permissionservice.PermissionService === "undefined" ? Object : _permissionservice.PermissionService,
292
+ typeof _permissioncacheservice.PermissionCacheService === "undefined" ? Object : _permissioncacheservice.PermissionCacheService
210
293
  ])
211
294
  ], ActionService);
@@ -0,0 +1,314 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _testing = require("@nestjs/testing");
6
+ const _modules = require("@flusys/nestjs-shared/modules");
7
+ const _repositorymock = require("@test-utils/mocks/repository.mock");
8
+ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
9
+ const _enums = require("../enums");
10
+ const _actionservice = require("./action.service");
11
+ const _iamconfigservice = require("./iam-config.service");
12
+ const _iamdatasourceservice = require("./iam-datasource.service");
13
+ const _permissioncacheservice = require("./permission-cache.service");
14
+ const _permissionservice = require("./permission.service");
15
+ function buildAction(overrides = {}) {
16
+ return {
17
+ id: 'action-uuid-1',
18
+ readOnly: false,
19
+ name: 'View Products',
20
+ description: null,
21
+ code: 'products.read',
22
+ actionType: _enums.ActionType.BACKEND,
23
+ permissionLogic: null,
24
+ serial: 1,
25
+ isActive: true,
26
+ parentId: null,
27
+ createdAt: new Date(),
28
+ updatedAt: new Date(),
29
+ deletedAt: null,
30
+ createdById: null,
31
+ updatedById: null,
32
+ deletedById: null,
33
+ ...overrides
34
+ };
35
+ }
36
+ describe('ActionService', ()=>{
37
+ let service;
38
+ let mockRepo;
39
+ let mockIamConfigService;
40
+ let mockPermissionService;
41
+ let mockPermissionCacheService;
42
+ beforeEach(async ()=>{
43
+ mockRepo = (0, _repositorymock.createMockRepository)();
44
+ const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
45
+ mockIamConfigService = {
46
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
47
+ };
48
+ mockPermissionService = {
49
+ getCompanyActionIds: jest.fn(),
50
+ invalidateCompanyMembersCache: jest.fn()
51
+ };
52
+ mockPermissionCacheService = {
53
+ invalidateUsers: jest.fn()
54
+ };
55
+ const module = await _testing.Test.createTestingModule({
56
+ providers: [
57
+ _actionservice.ActionService,
58
+ {
59
+ provide: 'CACHE_INSTANCE',
60
+ useValue: {}
61
+ },
62
+ {
63
+ provide: _modules.UtilsService,
64
+ useValue: {}
65
+ },
66
+ {
67
+ provide: _iamconfigservice.IAMConfigService,
68
+ useValue: mockIamConfigService
69
+ },
70
+ {
71
+ provide: _iamdatasourceservice.IAMDataSourceService,
72
+ useValue: mockDataSourceProvider
73
+ },
74
+ {
75
+ provide: _permissionservice.PermissionService,
76
+ useValue: mockPermissionService
77
+ },
78
+ {
79
+ provide: _permissioncacheservice.PermissionCacheService,
80
+ useValue: mockPermissionCacheService
81
+ }
82
+ ]
83
+ }).compile();
84
+ service = await module.resolve(_actionservice.ActionService);
85
+ });
86
+ describe('getActionsForPermission', ()=>{
87
+ it('returns all actions when the company feature is disabled', async ()=>{
88
+ mockRepo.find.mockResolvedValue([
89
+ buildAction()
90
+ ]);
91
+ const result = await service.getActionsForPermission((0, _loggedusermock.buildMockUser)());
92
+ expect(mockPermissionService.getCompanyActionIds).not.toHaveBeenCalled();
93
+ expect(result).toHaveLength(1);
94
+ expect(result[0].code).toBe('products.read');
95
+ });
96
+ it('scopes actions to the company whitelist when the company feature is enabled', async ()=>{
97
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
98
+ mockPermissionService.getCompanyActionIds.mockResolvedValue([
99
+ 'action-uuid-1'
100
+ ]);
101
+ mockRepo.find.mockResolvedValue([
102
+ buildAction()
103
+ ]);
104
+ await service.getActionsForPermission((0, _loggedusermock.buildMockUser)(), 'company-uuid-1');
105
+ expect(mockPermissionService.getCompanyActionIds).toHaveBeenCalledWith('company-uuid-1');
106
+ expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
107
+ where: {
108
+ id: expect.anything()
109
+ }
110
+ }));
111
+ });
112
+ it('short-circuits without querying when the company has no whitelisted actions', async ()=>{
113
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
114
+ mockPermissionService.getCompanyActionIds.mockResolvedValue([]);
115
+ const result = await service.getActionsForPermission((0, _loggedusermock.buildMockUser)(), 'company-uuid-1');
116
+ expect(result).toEqual([]);
117
+ expect(mockRepo.find).not.toHaveBeenCalled();
118
+ });
119
+ it('throws when no user is provided', async ()=>{
120
+ await expect(service.getActionsForPermission(null)).rejects.toThrow();
121
+ });
122
+ });
123
+ describe('getActionTree', ()=>{
124
+ it('nests child actions under their parent', async ()=>{
125
+ const parent = buildAction({
126
+ id: 'parent-1',
127
+ parentId: null,
128
+ serial: 1
129
+ });
130
+ const child = buildAction({
131
+ id: 'child-1',
132
+ parentId: 'parent-1',
133
+ serial: 2,
134
+ code: 'products.create'
135
+ });
136
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([
137
+ parent,
138
+ child
139
+ ]);
140
+ const tree = await service.getActionTree((0, _loggedusermock.buildMockUser)());
141
+ expect(tree).toHaveLength(1);
142
+ expect(tree[0].id).toBe('parent-1');
143
+ expect(tree[0].children).toHaveLength(1);
144
+ expect(tree[0].children?.[0].id).toBe('child-1');
145
+ });
146
+ it('throws when no user is provided', async ()=>{
147
+ await expect(service.getActionTree(null)).rejects.toThrow();
148
+ });
149
+ it('treats an action with a missing/unknown parentId as a root node', async ()=>{
150
+ const orphan = buildAction({
151
+ id: 'orphan-1',
152
+ parentId: 'does-not-exist'
153
+ });
154
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([
155
+ orphan
156
+ ]);
157
+ const tree = await service.getActionTree((0, _loggedusermock.buildMockUser)());
158
+ expect(tree).toHaveLength(1);
159
+ expect(tree[0].id).toBe('orphan-1');
160
+ });
161
+ it('excludes soft-deleted actions by default and applies search/isActive filters', async ()=>{
162
+ const query = mockRepo.createQueryBuilder();
163
+ query.getMany.mockResolvedValue([]);
164
+ await service.getActionTree((0, _loggedusermock.buildMockUser)(), 'view', true, false);
165
+ expect(query.andWhere).toHaveBeenCalledWith('action.deletedAt IS NULL');
166
+ expect(query.andWhere).toHaveBeenCalledWith('action.isActive = :isActive', {
167
+ isActive: true
168
+ });
169
+ expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search)', {
170
+ search: '%view%'
171
+ });
172
+ });
173
+ it('includes soft-deleted actions when withDeleted is true', async ()=>{
174
+ const query = mockRepo.createQueryBuilder();
175
+ query.getMany.mockResolvedValue([]);
176
+ await service.getActionTree((0, _loggedusermock.buildMockUser)(), undefined, undefined, true);
177
+ expect(query.andWhere).not.toHaveBeenCalledWith('action.deletedAt IS NULL');
178
+ });
179
+ });
180
+ describe('getSelectQuery', ()=>{
181
+ it('selects the default field set when none is provided', async ()=>{
182
+ const query = mockRepo.createQueryBuilder();
183
+ await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)());
184
+ expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
185
+ 'action.code',
186
+ 'action.actionType',
187
+ 'action.permissionLogic'
188
+ ]));
189
+ });
190
+ it('uses the explicitly provided select list as-is', async ()=>{
191
+ const query = mockRepo.createQueryBuilder();
192
+ await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
193
+ 'id',
194
+ 'code'
195
+ ]);
196
+ expect(query.select).toHaveBeenCalledWith([
197
+ 'action.id',
198
+ 'action.code'
199
+ ]);
200
+ });
201
+ });
202
+ describe('getGlobalSearchQuery', ()=>{
203
+ it('filters by name, code, or description', async ()=>{
204
+ const query = mockRepo.createQueryBuilder();
205
+ await service.getGlobalSearchQuery(query, 'view');
206
+ expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search OR action.description LIKE :search)', {
207
+ search: '%view%'
208
+ });
209
+ });
210
+ });
211
+ describe('convertEntityToResponseDto', ()=>{
212
+ it('maps every entity field onto the response shape', ()=>{
213
+ const entity = buildAction({
214
+ code: 'user.view'
215
+ });
216
+ const dto = service.convertEntityToResponseDto(entity, false);
217
+ expect(dto).toEqual(expect.objectContaining({
218
+ id: entity.id,
219
+ code: 'user.view',
220
+ actionType: _enums.ActionType.BACKEND
221
+ }));
222
+ });
223
+ });
224
+ describe('beforeDeleteOperation', ()=>{
225
+ function buildQueryRunner(permissionRepo) {
226
+ return {
227
+ manager: {
228
+ getRepository: jest.fn().mockReturnValue(permissionRepo)
229
+ }
230
+ };
231
+ }
232
+ it('does nothing when the delete DTO has no ids', async ()=>{
233
+ const permissionRepo = (0, _repositorymock.createMockRepository)();
234
+ const queryRunner = buildQueryRunner(permissionRepo);
235
+ await service.beforeDeleteOperation({
236
+ id: [],
237
+ type: 'delete'
238
+ }, (0, _loggedusermock.buildMockUser)(), queryRunner);
239
+ expect(permissionRepo.find).not.toHaveBeenCalled();
240
+ });
241
+ it('cascades role/user/company action-permission cleanup and invalidates affected caches', async ()=>{
242
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
243
+ const permissionRepo = (0, _repositorymock.createMockRepository)();
244
+ // Company feature is disabled, so companyActionRows resolves via Promise.resolve([]) and
245
+ // never calls permissionRepo.find() — only 3 real find() calls happen: role-action,
246
+ // user-action, then the role -> user cascade lookup.
247
+ permissionRepo.find.mockResolvedValueOnce([
248
+ {
249
+ sourceId: 'role-1'
250
+ }
251
+ ]) // role-action rows referencing the deleted action
252
+ .mockResolvedValueOnce([
253
+ {
254
+ userId: 'user-1'
255
+ }
256
+ ]) // direct user-action rows
257
+ .mockResolvedValueOnce([
258
+ {
259
+ userId: 'user-2'
260
+ }
261
+ ]); // users holding role-1 (cascaded lookup)
262
+ const queryRunner = buildQueryRunner(permissionRepo);
263
+ await service.beforeDeleteOperation({
264
+ id: 'action-uuid-1',
265
+ type: 'delete'
266
+ }, (0, _loggedusermock.buildMockUser)(), queryRunner);
267
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
268
+ permissionType: 'role_action'
269
+ }));
270
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
271
+ permissionType: 'user_action'
272
+ }));
273
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
274
+ permissionType: 'company_action'
275
+ }));
276
+ expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith(expect.arrayContaining([
277
+ 'user-1',
278
+ 'user-2'
279
+ ]));
280
+ expect(mockPermissionService.invalidateCompanyMembersCache).not.toHaveBeenCalled();
281
+ });
282
+ it('also queries and invalidates company whitelist entries when the company feature is enabled', async ()=>{
283
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
284
+ const permissionRepo = (0, _repositorymock.createMockRepository)();
285
+ permissionRepo.find.mockResolvedValueOnce([]) // role-action rows
286
+ .mockResolvedValueOnce([]) // direct user-action rows
287
+ .mockResolvedValueOnce([
288
+ {
289
+ sourceId: 'company-1'
290
+ }
291
+ ]); // company-action rows
292
+ const queryRunner = buildQueryRunner(permissionRepo);
293
+ await service.beforeDeleteOperation({
294
+ id: [
295
+ 'action-uuid-1'
296
+ ],
297
+ type: 'delete'
298
+ }, (0, _loggedusermock.buildMockUser)(), queryRunner);
299
+ expect(mockPermissionService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
300
+ });
301
+ it('does not invalidate any cache when nothing is affected', async ()=>{
302
+ const permissionRepo = (0, _repositorymock.createMockRepository)();
303
+ permissionRepo.find.mockResolvedValue([]);
304
+ const queryRunner = buildQueryRunner(permissionRepo);
305
+ await service.beforeDeleteOperation({
306
+ id: [
307
+ 'action-uuid-1'
308
+ ],
309
+ type: 'delete'
310
+ }, (0, _loggedusermock.buildMockUser)(), queryRunner);
311
+ expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
312
+ });
313
+ });
314
+ });
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _permissiontypeenum = require("../enums/permission-type.enum");
6
+ const _iamconfigservice = require("./iam-config.service");
7
+ describe('IAMConfigService', ()=>{
8
+ describe('with no injected options (defaults)', ()=>{
9
+ let service;
10
+ beforeEach(()=>{
11
+ service = new _iamconfigservice.IAMConfigService();
12
+ });
13
+ it('defaults to single database mode', ()=>{
14
+ expect(service.getDatabaseMode()).toBe('single');
15
+ expect(service.isMultiTenant()).toBe(false);
16
+ });
17
+ it('defaults company feature to disabled', ()=>{
18
+ expect(service.isCompanyFeatureEnabled()).toBe(false);
19
+ });
20
+ it('defaults permission mode to FULL', ()=>{
21
+ expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
22
+ expect(service.isRbacEnabled()).toBe(true);
23
+ expect(service.isDirectPermissionEnabled()).toBe(true);
24
+ });
25
+ it('returns the fallback options object from getOptions', ()=>{
26
+ expect(service.getOptions()).toEqual({
27
+ global: false,
28
+ includeController: false
29
+ });
30
+ });
31
+ });
32
+ describe('with injected options', ()=>{
33
+ function buildService(overrides) {
34
+ const options = {
35
+ global: false,
36
+ includeController: false,
37
+ bootstrapAppConfig: {
38
+ databaseMode: 'single',
39
+ enableCompanyFeature: false,
40
+ ...overrides
41
+ }
42
+ };
43
+ return new _iamconfigservice.IAMConfigService(options);
44
+ }
45
+ it('reports multi-tenant mode when configured', ()=>{
46
+ const service = buildService({
47
+ databaseMode: 'multi-tenant'
48
+ });
49
+ expect(service.getDatabaseMode()).toBe('multi-tenant');
50
+ expect(service.isMultiTenant()).toBe(true);
51
+ });
52
+ it('reports company feature enabled when configured', ()=>{
53
+ const service = buildService({
54
+ enableCompanyFeature: true
55
+ });
56
+ expect(service.isCompanyFeatureEnabled()).toBe(true);
57
+ });
58
+ describe('permission mode: RBAC', ()=>{
59
+ let service;
60
+ beforeEach(()=>{
61
+ service = buildService({
62
+ permissionMode: 'RBAC'
63
+ });
64
+ });
65
+ it('enables RBAC but not direct permissions', ()=>{
66
+ expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.RBAC);
67
+ expect(service.isRbacEnabled()).toBe(true);
68
+ expect(service.isDirectPermissionEnabled()).toBe(false);
69
+ });
70
+ });
71
+ describe('permission mode: DIRECT', ()=>{
72
+ let service;
73
+ beforeEach(()=>{
74
+ service = buildService({
75
+ permissionMode: 'DIRECT'
76
+ });
77
+ });
78
+ it('enables direct permissions but not RBAC', ()=>{
79
+ expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.DIRECT);
80
+ expect(service.isRbacEnabled()).toBe(false);
81
+ expect(service.isDirectPermissionEnabled()).toBe(true);
82
+ });
83
+ });
84
+ describe('permission mode: FULL', ()=>{
85
+ let service;
86
+ beforeEach(()=>{
87
+ service = buildService({
88
+ permissionMode: 'FULL'
89
+ });
90
+ });
91
+ it('enables both RBAC and direct permissions', ()=>{
92
+ expect(service.getPermissionMode()).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
93
+ expect(service.isRbacEnabled()).toBe(true);
94
+ expect(service.isDirectPermissionEnabled()).toBe(true);
95
+ });
96
+ });
97
+ it('returns the exact injected options object from getOptions', ()=>{
98
+ const options = {
99
+ global: true,
100
+ includeController: true,
101
+ bootstrapAppConfig: {
102
+ databaseMode: 'single',
103
+ enableCompanyFeature: true
104
+ }
105
+ };
106
+ const service = new _iamconfigservice.IAMConfigService(options);
107
+ expect(service.getOptions()).toBe(options);
108
+ });
109
+ });
110
+ });