@flusys/nestjs-iam 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/cjs/controllers/action.controller.js +17 -9
  2. package/cjs/controllers/action.controller.spec.js +79 -0
  3. package/cjs/controllers/company-action-permission.controller.spec.js +65 -0
  4. package/cjs/controllers/my-permission.controller.spec.js +56 -0
  5. package/cjs/controllers/role-permission.controller.spec.js +140 -0
  6. package/cjs/controllers/role.controller.spec.js +86 -0
  7. package/cjs/controllers/user-action-permission.controller.js +0 -3
  8. package/cjs/controllers/user-action-permission.controller.spec.js +68 -0
  9. package/cjs/docs/iam-swagger.config.js +6 -0
  10. package/cjs/docs/iam-swagger.config.spec.js +99 -0
  11. package/cjs/dtos/action.dto.js +17 -0
  12. package/cjs/entities/index.spec.js +56 -0
  13. package/cjs/helpers/company-access.helper.spec.js +68 -0
  14. package/cjs/helpers/permission-mode.helper.spec.js +54 -0
  15. package/cjs/modules/iam.module.js +3 -1
  16. package/cjs/modules/iam.module.spec.js +293 -0
  17. package/cjs/services/action.service.js +90 -7
  18. package/cjs/services/action.service.spec.js +314 -0
  19. package/cjs/services/iam-config.service.spec.js +110 -0
  20. package/cjs/services/iam-datasource.service.spec.js +117 -0
  21. package/cjs/services/permission-cache.service.js +113 -110
  22. package/cjs/services/permission-cache.service.spec.js +252 -0
  23. package/cjs/services/permission.service.js +213 -92
  24. package/cjs/services/permission.service.spec.js +967 -0
  25. package/cjs/services/role.service.js +47 -3
  26. package/cjs/services/role.service.spec.js +312 -0
  27. package/controllers/action.controller.d.ts +5 -3
  28. package/dtos/action.dto.d.ts +3 -0
  29. package/entities/index.d.ts +2 -2
  30. package/fesm/controllers/action.controller.js +18 -10
  31. package/fesm/controllers/action.controller.spec.js +75 -0
  32. package/fesm/controllers/company-action-permission.controller.spec.js +61 -0
  33. package/fesm/controllers/my-permission.controller.spec.js +52 -0
  34. package/fesm/controllers/role-permission.controller.spec.js +136 -0
  35. package/fesm/controllers/role.controller.spec.js +82 -0
  36. package/fesm/controllers/user-action-permission.controller.js +0 -3
  37. package/fesm/controllers/user-action-permission.controller.spec.js +64 -0
  38. package/fesm/docs/iam-swagger.config.js +6 -0
  39. package/fesm/docs/iam-swagger.config.spec.js +95 -0
  40. package/fesm/dtos/action.dto.js +14 -0
  41. package/fesm/entities/index.spec.js +52 -0
  42. package/fesm/helpers/company-access.helper.spec.js +64 -0
  43. package/fesm/helpers/permission-mode.helper.spec.js +50 -0
  44. package/fesm/modules/iam.module.js +4 -2
  45. package/fesm/modules/iam.module.spec.js +289 -0
  46. package/fesm/services/action.service.js +90 -7
  47. package/fesm/services/action.service.spec.js +310 -0
  48. package/fesm/services/iam-config.service.spec.js +106 -0
  49. package/fesm/services/iam-datasource.service.spec.js +113 -0
  50. package/fesm/services/permission-cache.service.js +101 -108
  51. package/fesm/services/permission-cache.service.spec.js +248 -0
  52. package/fesm/services/permission.service.js +213 -92
  53. package/fesm/services/permission.service.spec.js +963 -0
  54. package/fesm/services/role.service.js +48 -4
  55. package/fesm/services/role.service.spec.js +308 -0
  56. package/package.json +3 -3
  57. package/services/action.service.d.ts +7 -3
  58. package/services/permission-cache.service.d.ts +13 -19
  59. package/services/permission.service.d.ts +6 -3
  60. package/services/role.service.d.ts +7 -3
@@ -0,0 +1,310 @@
1
+ import { Test } from '@nestjs/testing';
2
+ import { UtilsService } from '@flusys/nestjs-shared/modules';
3
+ import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
4
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
5
+ import { ActionType } from '../enums';
6
+ import { ActionService } from './action.service';
7
+ import { IAMConfigService } from './iam-config.service';
8
+ import { IAMDataSourceService } from './iam-datasource.service';
9
+ import { PermissionCacheService } from './permission-cache.service';
10
+ import { PermissionService } from './permission.service';
11
+ function buildAction(overrides = {}) {
12
+ return {
13
+ id: 'action-uuid-1',
14
+ readOnly: false,
15
+ name: 'View Products',
16
+ description: null,
17
+ code: 'products.read',
18
+ actionType: ActionType.BACKEND,
19
+ permissionLogic: null,
20
+ serial: 1,
21
+ isActive: true,
22
+ parentId: null,
23
+ createdAt: new Date(),
24
+ updatedAt: new Date(),
25
+ deletedAt: null,
26
+ createdById: null,
27
+ updatedById: null,
28
+ deletedById: null,
29
+ ...overrides
30
+ };
31
+ }
32
+ describe('ActionService', ()=>{
33
+ let service;
34
+ let mockRepo;
35
+ let mockIamConfigService;
36
+ let mockPermissionService;
37
+ let mockPermissionCacheService;
38
+ beforeEach(async ()=>{
39
+ mockRepo = createMockRepository();
40
+ const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
41
+ mockIamConfigService = {
42
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false)
43
+ };
44
+ mockPermissionService = {
45
+ getCompanyActionIds: jest.fn(),
46
+ invalidateCompanyMembersCache: jest.fn()
47
+ };
48
+ mockPermissionCacheService = {
49
+ invalidateUsers: jest.fn()
50
+ };
51
+ const module = await Test.createTestingModule({
52
+ providers: [
53
+ ActionService,
54
+ {
55
+ provide: 'CACHE_INSTANCE',
56
+ useValue: {}
57
+ },
58
+ {
59
+ provide: UtilsService,
60
+ useValue: {}
61
+ },
62
+ {
63
+ provide: IAMConfigService,
64
+ useValue: mockIamConfigService
65
+ },
66
+ {
67
+ provide: IAMDataSourceService,
68
+ useValue: mockDataSourceProvider
69
+ },
70
+ {
71
+ provide: PermissionService,
72
+ useValue: mockPermissionService
73
+ },
74
+ {
75
+ provide: PermissionCacheService,
76
+ useValue: mockPermissionCacheService
77
+ }
78
+ ]
79
+ }).compile();
80
+ service = await module.resolve(ActionService);
81
+ });
82
+ describe('getActionsForPermission', ()=>{
83
+ it('returns all actions when the company feature is disabled', async ()=>{
84
+ mockRepo.find.mockResolvedValue([
85
+ buildAction()
86
+ ]);
87
+ const result = await service.getActionsForPermission(buildMockUser());
88
+ expect(mockPermissionService.getCompanyActionIds).not.toHaveBeenCalled();
89
+ expect(result).toHaveLength(1);
90
+ expect(result[0].code).toBe('products.read');
91
+ });
92
+ it('scopes actions to the company whitelist when the company feature is enabled', async ()=>{
93
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
94
+ mockPermissionService.getCompanyActionIds.mockResolvedValue([
95
+ 'action-uuid-1'
96
+ ]);
97
+ mockRepo.find.mockResolvedValue([
98
+ buildAction()
99
+ ]);
100
+ await service.getActionsForPermission(buildMockUser(), 'company-uuid-1');
101
+ expect(mockPermissionService.getCompanyActionIds).toHaveBeenCalledWith('company-uuid-1');
102
+ expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
103
+ where: {
104
+ id: expect.anything()
105
+ }
106
+ }));
107
+ });
108
+ it('short-circuits without querying when the company has no whitelisted actions', async ()=>{
109
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
110
+ mockPermissionService.getCompanyActionIds.mockResolvedValue([]);
111
+ const result = await service.getActionsForPermission(buildMockUser(), 'company-uuid-1');
112
+ expect(result).toEqual([]);
113
+ expect(mockRepo.find).not.toHaveBeenCalled();
114
+ });
115
+ it('throws when no user is provided', async ()=>{
116
+ await expect(service.getActionsForPermission(null)).rejects.toThrow();
117
+ });
118
+ });
119
+ describe('getActionTree', ()=>{
120
+ it('nests child actions under their parent', async ()=>{
121
+ const parent = buildAction({
122
+ id: 'parent-1',
123
+ parentId: null,
124
+ serial: 1
125
+ });
126
+ const child = buildAction({
127
+ id: 'child-1',
128
+ parentId: 'parent-1',
129
+ serial: 2,
130
+ code: 'products.create'
131
+ });
132
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([
133
+ parent,
134
+ child
135
+ ]);
136
+ const tree = await service.getActionTree(buildMockUser());
137
+ expect(tree).toHaveLength(1);
138
+ expect(tree[0].id).toBe('parent-1');
139
+ expect(tree[0].children).toHaveLength(1);
140
+ expect(tree[0].children?.[0].id).toBe('child-1');
141
+ });
142
+ it('throws when no user is provided', async ()=>{
143
+ await expect(service.getActionTree(null)).rejects.toThrow();
144
+ });
145
+ it('treats an action with a missing/unknown parentId as a root node', async ()=>{
146
+ const orphan = buildAction({
147
+ id: 'orphan-1',
148
+ parentId: 'does-not-exist'
149
+ });
150
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([
151
+ orphan
152
+ ]);
153
+ const tree = await service.getActionTree(buildMockUser());
154
+ expect(tree).toHaveLength(1);
155
+ expect(tree[0].id).toBe('orphan-1');
156
+ });
157
+ it('excludes soft-deleted actions by default and applies search/isActive filters', async ()=>{
158
+ const query = mockRepo.createQueryBuilder();
159
+ query.getMany.mockResolvedValue([]);
160
+ await service.getActionTree(buildMockUser(), 'view', true, false);
161
+ expect(query.andWhere).toHaveBeenCalledWith('action.deletedAt IS NULL');
162
+ expect(query.andWhere).toHaveBeenCalledWith('action.isActive = :isActive', {
163
+ isActive: true
164
+ });
165
+ expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search)', {
166
+ search: '%view%'
167
+ });
168
+ });
169
+ it('includes soft-deleted actions when withDeleted is true', async ()=>{
170
+ const query = mockRepo.createQueryBuilder();
171
+ query.getMany.mockResolvedValue([]);
172
+ await service.getActionTree(buildMockUser(), undefined, undefined, true);
173
+ expect(query.andWhere).not.toHaveBeenCalledWith('action.deletedAt IS NULL');
174
+ });
175
+ });
176
+ describe('getSelectQuery', ()=>{
177
+ it('selects the default field set when none is provided', async ()=>{
178
+ const query = mockRepo.createQueryBuilder();
179
+ await service.getSelectQuery(query, buildMockUser());
180
+ expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
181
+ 'action.code',
182
+ 'action.actionType',
183
+ 'action.permissionLogic'
184
+ ]));
185
+ });
186
+ it('uses the explicitly provided select list as-is', async ()=>{
187
+ const query = mockRepo.createQueryBuilder();
188
+ await service.getSelectQuery(query, buildMockUser(), [
189
+ 'id',
190
+ 'code'
191
+ ]);
192
+ expect(query.select).toHaveBeenCalledWith([
193
+ 'action.id',
194
+ 'action.code'
195
+ ]);
196
+ });
197
+ });
198
+ describe('getGlobalSearchQuery', ()=>{
199
+ it('filters by name, code, or description', async ()=>{
200
+ const query = mockRepo.createQueryBuilder();
201
+ await service.getGlobalSearchQuery(query, 'view');
202
+ expect(query.andWhere).toHaveBeenCalledWith('(action.name LIKE :search OR action.code LIKE :search OR action.description LIKE :search)', {
203
+ search: '%view%'
204
+ });
205
+ });
206
+ });
207
+ describe('convertEntityToResponseDto', ()=>{
208
+ it('maps every entity field onto the response shape', ()=>{
209
+ const entity = buildAction({
210
+ code: 'user.view'
211
+ });
212
+ const dto = service.convertEntityToResponseDto(entity, false);
213
+ expect(dto).toEqual(expect.objectContaining({
214
+ id: entity.id,
215
+ code: 'user.view',
216
+ actionType: ActionType.BACKEND
217
+ }));
218
+ });
219
+ });
220
+ describe('beforeDeleteOperation', ()=>{
221
+ function buildQueryRunner(permissionRepo) {
222
+ return {
223
+ manager: {
224
+ getRepository: jest.fn().mockReturnValue(permissionRepo)
225
+ }
226
+ };
227
+ }
228
+ it('does nothing when the delete DTO has no ids', async ()=>{
229
+ const permissionRepo = createMockRepository();
230
+ const queryRunner = buildQueryRunner(permissionRepo);
231
+ await service.beforeDeleteOperation({
232
+ id: [],
233
+ type: 'delete'
234
+ }, buildMockUser(), queryRunner);
235
+ expect(permissionRepo.find).not.toHaveBeenCalled();
236
+ });
237
+ it('cascades role/user/company action-permission cleanup and invalidates affected caches', async ()=>{
238
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
239
+ const permissionRepo = createMockRepository();
240
+ // Company feature is disabled, so companyActionRows resolves via Promise.resolve([]) and
241
+ // never calls permissionRepo.find() — only 3 real find() calls happen: role-action,
242
+ // user-action, then the role -> user cascade lookup.
243
+ permissionRepo.find.mockResolvedValueOnce([
244
+ {
245
+ sourceId: 'role-1'
246
+ }
247
+ ]) // role-action rows referencing the deleted action
248
+ .mockResolvedValueOnce([
249
+ {
250
+ userId: 'user-1'
251
+ }
252
+ ]) // direct user-action rows
253
+ .mockResolvedValueOnce([
254
+ {
255
+ userId: 'user-2'
256
+ }
257
+ ]); // users holding role-1 (cascaded lookup)
258
+ const queryRunner = buildQueryRunner(permissionRepo);
259
+ await service.beforeDeleteOperation({
260
+ id: 'action-uuid-1',
261
+ type: 'delete'
262
+ }, buildMockUser(), queryRunner);
263
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
264
+ permissionType: 'role_action'
265
+ }));
266
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
267
+ permissionType: 'user_action'
268
+ }));
269
+ expect(permissionRepo.delete).toHaveBeenCalledWith(expect.objectContaining({
270
+ permissionType: 'company_action'
271
+ }));
272
+ expect(mockPermissionCacheService.invalidateUsers).toHaveBeenCalledWith(expect.arrayContaining([
273
+ 'user-1',
274
+ 'user-2'
275
+ ]));
276
+ expect(mockPermissionService.invalidateCompanyMembersCache).not.toHaveBeenCalled();
277
+ });
278
+ it('also queries and invalidates company whitelist entries when the company feature is enabled', async ()=>{
279
+ mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
280
+ const permissionRepo = createMockRepository();
281
+ permissionRepo.find.mockResolvedValueOnce([]) // role-action rows
282
+ .mockResolvedValueOnce([]) // direct user-action rows
283
+ .mockResolvedValueOnce([
284
+ {
285
+ sourceId: 'company-1'
286
+ }
287
+ ]); // company-action rows
288
+ const queryRunner = buildQueryRunner(permissionRepo);
289
+ await service.beforeDeleteOperation({
290
+ id: [
291
+ 'action-uuid-1'
292
+ ],
293
+ type: 'delete'
294
+ }, buildMockUser(), queryRunner);
295
+ expect(mockPermissionService.invalidateCompanyMembersCache).toHaveBeenCalledWith('company-1');
296
+ });
297
+ it('does not invalidate any cache when nothing is affected', async ()=>{
298
+ const permissionRepo = createMockRepository();
299
+ permissionRepo.find.mockResolvedValue([]);
300
+ const queryRunner = buildQueryRunner(permissionRepo);
301
+ await service.beforeDeleteOperation({
302
+ id: [
303
+ 'action-uuid-1'
304
+ ],
305
+ type: 'delete'
306
+ }, buildMockUser(), queryRunner);
307
+ expect(mockPermissionCacheService.invalidateUsers).not.toHaveBeenCalled();
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,106 @@
1
+ import { IAMPermissionMode } from '../enums/permission-type.enum';
2
+ import { IAMConfigService } from './iam-config.service';
3
+ describe('IAMConfigService', ()=>{
4
+ describe('with no injected options (defaults)', ()=>{
5
+ let service;
6
+ beforeEach(()=>{
7
+ service = new IAMConfigService();
8
+ });
9
+ it('defaults to single database mode', ()=>{
10
+ expect(service.getDatabaseMode()).toBe('single');
11
+ expect(service.isMultiTenant()).toBe(false);
12
+ });
13
+ it('defaults company feature to disabled', ()=>{
14
+ expect(service.isCompanyFeatureEnabled()).toBe(false);
15
+ });
16
+ it('defaults permission mode to FULL', ()=>{
17
+ expect(service.getPermissionMode()).toBe(IAMPermissionMode.FULL);
18
+ expect(service.isRbacEnabled()).toBe(true);
19
+ expect(service.isDirectPermissionEnabled()).toBe(true);
20
+ });
21
+ it('returns the fallback options object from getOptions', ()=>{
22
+ expect(service.getOptions()).toEqual({
23
+ global: false,
24
+ includeController: false
25
+ });
26
+ });
27
+ });
28
+ describe('with injected options', ()=>{
29
+ function buildService(overrides) {
30
+ const options = {
31
+ global: false,
32
+ includeController: false,
33
+ bootstrapAppConfig: {
34
+ databaseMode: 'single',
35
+ enableCompanyFeature: false,
36
+ ...overrides
37
+ }
38
+ };
39
+ return new IAMConfigService(options);
40
+ }
41
+ it('reports multi-tenant mode when configured', ()=>{
42
+ const service = buildService({
43
+ databaseMode: 'multi-tenant'
44
+ });
45
+ expect(service.getDatabaseMode()).toBe('multi-tenant');
46
+ expect(service.isMultiTenant()).toBe(true);
47
+ });
48
+ it('reports company feature enabled when configured', ()=>{
49
+ const service = buildService({
50
+ enableCompanyFeature: true
51
+ });
52
+ expect(service.isCompanyFeatureEnabled()).toBe(true);
53
+ });
54
+ describe('permission mode: RBAC', ()=>{
55
+ let service;
56
+ beforeEach(()=>{
57
+ service = buildService({
58
+ permissionMode: 'RBAC'
59
+ });
60
+ });
61
+ it('enables RBAC but not direct permissions', ()=>{
62
+ expect(service.getPermissionMode()).toBe(IAMPermissionMode.RBAC);
63
+ expect(service.isRbacEnabled()).toBe(true);
64
+ expect(service.isDirectPermissionEnabled()).toBe(false);
65
+ });
66
+ });
67
+ describe('permission mode: DIRECT', ()=>{
68
+ let service;
69
+ beforeEach(()=>{
70
+ service = buildService({
71
+ permissionMode: 'DIRECT'
72
+ });
73
+ });
74
+ it('enables direct permissions but not RBAC', ()=>{
75
+ expect(service.getPermissionMode()).toBe(IAMPermissionMode.DIRECT);
76
+ expect(service.isRbacEnabled()).toBe(false);
77
+ expect(service.isDirectPermissionEnabled()).toBe(true);
78
+ });
79
+ });
80
+ describe('permission mode: FULL', ()=>{
81
+ let service;
82
+ beforeEach(()=>{
83
+ service = buildService({
84
+ permissionMode: 'FULL'
85
+ });
86
+ });
87
+ it('enables both RBAC and direct permissions', ()=>{
88
+ expect(service.getPermissionMode()).toBe(IAMPermissionMode.FULL);
89
+ expect(service.isRbacEnabled()).toBe(true);
90
+ expect(service.isDirectPermissionEnabled()).toBe(true);
91
+ });
92
+ });
93
+ it('returns the exact injected options object from getOptions', ()=>{
94
+ const options = {
95
+ global: true,
96
+ includeController: true,
97
+ bootstrapAppConfig: {
98
+ databaseMode: 'single',
99
+ enableCompanyFeature: true
100
+ }
101
+ };
102
+ const service = new IAMConfigService(options);
103
+ expect(service.getOptions()).toBe(options);
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,113 @@
1
+ import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
2
+ import { Action } from '../entities/action.entity';
3
+ import { UserIamPermission } from '../entities/user-iam-permission.entity';
4
+ import { UserIamPermissionWithCompany } from '../entities/permission-with-company.entity';
5
+ import { Role } from '../entities/role.entity';
6
+ import { RoleWithCompany } from '../entities/role-with-company.entity';
7
+ import { IAMDataSourceService } from './iam-datasource.service';
8
+ describe('IAMDataSourceService', ()=>{
9
+ let mockConfigService;
10
+ beforeEach(()=>{
11
+ mockConfigService = {
12
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(false),
13
+ getPermissionMode: jest.fn(),
14
+ getOptions: jest.fn().mockReturnValue({
15
+ global: false,
16
+ includeController: false
17
+ })
18
+ };
19
+ });
20
+ function buildService(request) {
21
+ return new IAMDataSourceService(mockConfigService, request);
22
+ }
23
+ describe('getEnableCompanyFeatureForTenant', ()=>{
24
+ it('uses the tenant override when defined (true)', ()=>{
25
+ const service = buildService();
26
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
27
+ expect(service.getEnableCompanyFeatureForTenant({
28
+ enableCompanyFeature: true
29
+ })).toBe(true);
30
+ });
31
+ it('uses the tenant override when defined (false)', ()=>{
32
+ const service = buildService();
33
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
34
+ expect(service.getEnableCompanyFeatureForTenant({
35
+ enableCompanyFeature: false
36
+ })).toBe(false);
37
+ });
38
+ it('falls back to the config service when the tenant has no override', ()=>{
39
+ const service = buildService();
40
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
41
+ expect(service.getEnableCompanyFeatureForTenant({})).toBe(true);
42
+ });
43
+ it('falls back to the config service when no tenant is provided', ()=>{
44
+ const service = buildService();
45
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
46
+ expect(service.getEnableCompanyFeatureForTenant(undefined)).toBe(true);
47
+ });
48
+ });
49
+ describe('getEnableCompanyFeatureForCurrentTenant', ()=>{
50
+ it('falls back to the config service when there is no current request/tenant', ()=>{
51
+ const service = buildService();
52
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
53
+ expect(service.getEnableCompanyFeatureForCurrentTenant()).toBe(true);
54
+ });
55
+ });
56
+ describe('getIAMEntities', ()=>{
57
+ it('returns Action + non-company permission + role entities for RBAC mode', async ()=>{
58
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
59
+ mockConfigService.getPermissionMode.mockReturnValue(1); // RBAC
60
+ const service = buildService();
61
+ const entities = await service.getIAMEntities();
62
+ expect(entities).toContain(Action);
63
+ expect(entities).toContain(UserIamPermission);
64
+ expect(entities).toContain(Role);
65
+ expect(entities).not.toContain(UserIamPermissionWithCompany);
66
+ expect(entities).not.toContain(RoleWithCompany);
67
+ });
68
+ it('returns company-scoped entities when company feature is enabled', async ()=>{
69
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
70
+ mockConfigService.getPermissionMode.mockReturnValue(3); // FULL
71
+ const service = buildService();
72
+ const entities = await service.getIAMEntities();
73
+ expect(entities).toContain(Action);
74
+ expect(entities).toContain(UserIamPermissionWithCompany);
75
+ expect(entities).toContain(RoleWithCompany);
76
+ expect(entities).not.toContain(UserIamPermission);
77
+ expect(entities).not.toContain(Role);
78
+ });
79
+ it('excludes role entities entirely in DIRECT mode', async ()=>{
80
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
81
+ mockConfigService.getPermissionMode.mockReturnValue(2); // DIRECT
82
+ const service = buildService();
83
+ const entities = await service.getIAMEntities();
84
+ expect(entities).toEqual([
85
+ Action,
86
+ UserIamPermission
87
+ ]);
88
+ });
89
+ });
90
+ describe('createDataSourceFromConfig', ()=>{
91
+ it('resolves IAM entities and delegates to the parent with them', async ()=>{
92
+ mockConfigService.isCompanyFeatureEnabled.mockReturnValue(false);
93
+ mockConfigService.getPermissionMode.mockReturnValue(1); // RBAC
94
+ const service = buildService();
95
+ const fakeDataSource = {
96
+ isInitialized: true
97
+ };
98
+ const parentSpy = jest.spyOn(MultiTenantDataSourceService.prototype, 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
99
+ const config = {
100
+ type: 'mysql',
101
+ host: 'localhost'
102
+ };
103
+ const result = await service.createDataSourceFromConfig(config);
104
+ expect(result).toBe(fakeDataSource);
105
+ expect(parentSpy).toHaveBeenCalledWith(config, [
106
+ Action,
107
+ UserIamPermission,
108
+ Role
109
+ ]);
110
+ parentSpy.mockRestore();
111
+ });
112
+ });
113
+ });