@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,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _iamswaggerconfig = require("./iam-swagger.config");
6
+ const _permissiontypeenum = require("../enums/permission-type.enum");
7
+ describe('iamSwaggerConfig', ()=>{
8
+ it('should default to company disabled, FULL mode, single database', ()=>{
9
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)();
10
+ expect(config.title).toBe('IAM API');
11
+ expect(config.path).toBe('api/docs/iam');
12
+ expect(config.bearerAuth).toBe(true);
13
+ // enableCompanyFeature defaults to false, so company-scoped exclusions apply
14
+ expect(config.excludeSchemaProperties?.length).toBeGreaterThan(0);
15
+ expect(config.excludeQueryParameters?.length).toBeGreaterThan(0);
16
+ expect(config.description).not.toContain('Multi-Tenant Mode');
17
+ });
18
+ it('should always exclude auth-related tags regardless of configuration', ()=>{
19
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(true, _permissiontypeenum.IAMPermissionMode.FULL);
20
+ expect(config.excludeTags).toEqual(expect.arrayContaining([
21
+ 'Authentication',
22
+ 'Users',
23
+ 'Companies',
24
+ 'Branches'
25
+ ]));
26
+ });
27
+ it('should exclude company-scoped schema properties and query params when company feature is off', ()=>{
28
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false);
29
+ expect(config.excludeSchemaProperties).toEqual(expect.arrayContaining([
30
+ expect.objectContaining({
31
+ schemaName: 'AssignUserActionsDto',
32
+ properties: [
33
+ 'companyId',
34
+ 'branchId'
35
+ ]
36
+ })
37
+ ]));
38
+ expect(config.excludeQueryParameters).toEqual(expect.arrayContaining([
39
+ expect.objectContaining({
40
+ pathPattern: '/iam/permissions/user-actions/*',
41
+ method: 'get'
42
+ })
43
+ ]));
44
+ expect(config.excludeTags).toContain('IAM - Company Action Permissions');
45
+ });
46
+ it('should not exclude company-scoped schema properties, query params, or tags when company feature is on', ()=>{
47
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(true);
48
+ expect(config.excludeSchemaProperties).toEqual([]);
49
+ expect(config.excludeQueryParameters).toEqual([]);
50
+ expect(config.excludeTags).not.toContain('IAM - Company Action Permissions');
51
+ });
52
+ it('should hide DIRECT-mode tags in RBAC mode', ()=>{
53
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.RBAC);
54
+ expect(config.excludeTags).toContain('IAM - Permissions (Direct)');
55
+ expect(config.excludeTags).not.toContain('IAM - Permissions (RBAC)');
56
+ expect(config.excludeTags).not.toContain('IAM - Roles');
57
+ });
58
+ it('should hide RBAC-mode tags (including Roles) in DIRECT mode', ()=>{
59
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.DIRECT);
60
+ expect(config.excludeTags).toContain('IAM - Permissions (RBAC)');
61
+ expect(config.excludeTags).toContain('IAM - Roles');
62
+ expect(config.excludeTags).not.toContain('IAM - Permissions (Direct)');
63
+ });
64
+ it('should hide neither RBAC nor DIRECT tags in FULL mode', ()=>{
65
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.FULL);
66
+ expect(config.excludeTags).not.toContain('IAM - Permissions (RBAC)');
67
+ expect(config.excludeTags).not.toContain('IAM - Permissions (Direct)');
68
+ expect(config.excludeTags).not.toContain('IAM - Roles');
69
+ });
70
+ it('should mention multi-tenant mode in the description only for multi-tenant database mode', ()=>{
71
+ const multiTenant = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.FULL, 'multi-tenant');
72
+ const single = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.FULL, 'single');
73
+ expect(multiTenant.description).toContain('Multi-Tenant Mode');
74
+ expect(single.description).not.toContain('Multi-Tenant Mode');
75
+ });
76
+ it('should describe RBAC as active and DIRECT as disabled in the endpoints summary for RBAC mode', ()=>{
77
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.RBAC);
78
+ expect(config.description).toContain('**Roles**: CRUD operations');
79
+ expect(config.description).toContain('❌ **User-Actions**: Disabled (DIRECT mode not active)');
80
+ });
81
+ it('should describe DIRECT as active and Roles as disabled in the endpoints summary for DIRECT mode', ()=>{
82
+ const config = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.DIRECT);
83
+ expect(config.description).toContain('✅ **User-Actions**: Direct action assignment to users');
84
+ expect(config.description).toContain('❌ **Roles**: Disabled (RBAC mode not active)');
85
+ });
86
+ it('should describe company features as active only when company feature is enabled', ()=>{
87
+ const withCompany = (0, _iamswaggerconfig.iamSwaggerConfig)(true, _permissiontypeenum.IAMPermissionMode.FULL);
88
+ const withoutCompany = (0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.FULL);
89
+ expect(withCompany.description).toContain('Company Features (Active)');
90
+ expect(withCompany.description).toContain('✅ **Company-Actions**: Whitelist actions for companies');
91
+ expect(withoutCompany.description).not.toContain('Company Features (Active)');
92
+ expect(withoutCompany.description).toContain('❌ **Company-Actions**: Disabled (company feature not enabled)');
93
+ });
94
+ it('should describe the current permission mode label correctly for each mode', ()=>{
95
+ expect((0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.RBAC).description).toContain('**RBAC** (Role-Based Access Control)');
96
+ expect((0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.DIRECT).description).toContain('**DIRECT** (Direct User Permissions)');
97
+ expect((0, _iamswaggerconfig.iamSwaggerConfig)(false, _permissiontypeenum.IAMPermissionMode.FULL).description).toContain('**FULL** (RBAC + Direct)');
98
+ });
99
+ });
@@ -15,6 +15,9 @@ _export(exports, {
15
15
  get ActionTreeDto () {
16
16
  return ActionTreeDto;
17
17
  },
18
+ get ActionTreeForPermissionDto () {
19
+ return ActionTreeForPermissionDto;
20
+ },
18
21
  get ActionTreeQueryDto () {
19
22
  return ActionTreeQueryDto;
20
23
  },
@@ -226,6 +229,20 @@ _ts_decorate([
226
229
  }),
227
230
  _ts_metadata("design:type", Array)
228
231
  ], ActionTreeDto.prototype, "children", void 0);
232
+ let ActionTreeForPermissionDto = class ActionTreeForPermissionDto {
233
+ constructor(){
234
+ _define_property(this, "companyId", void 0);
235
+ }
236
+ };
237
+ _ts_decorate([
238
+ (0, _swagger.ApiProperty)({
239
+ description: 'Company to scope the action whitelist to. Defaults to the current user session company when omitted.',
240
+ required: false
241
+ }),
242
+ (0, _classvalidator.IsUUID)(),
243
+ (0, _classvalidator.IsOptional)(),
244
+ _ts_metadata("design:type", String)
245
+ ], ActionTreeForPermissionDto.prototype, "companyId", void 0);
229
246
  let ActionTreeQueryDto = class ActionTreeQueryDto {
230
247
  constructor(){
231
248
  _define_property(this, "search", void 0);
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _index = require("./index");
6
+ const _actionentity = require("./action.entity");
7
+ const _roleentity = require("./role.entity");
8
+ const _rolewithcompanyentity = require("./role-with-company.entity");
9
+ const _useriampermissionentity = require("./user-iam-permission.entity");
10
+ const _permissionwithcompanyentity = require("./permission-with-company.entity");
11
+ describe('getIAMEntitiesByConfig', ()=>{
12
+ it('should always include Action', ()=>{
13
+ expect((0, _index.getIAMEntitiesByConfig)(false)).toContain(_actionentity.Action);
14
+ expect((0, _index.getIAMEntitiesByConfig)(true)).toContain(_actionentity.Action);
15
+ });
16
+ it('should default permissionMode to FULL and include Role + UserIamPermission when company feature is off', ()=>{
17
+ const entities = (0, _index.getIAMEntitiesByConfig)(false);
18
+ expect(entities).toEqual(expect.arrayContaining([
19
+ _actionentity.Action,
20
+ _useriampermissionentity.UserIamPermission,
21
+ _roleentity.Role
22
+ ]));
23
+ expect(entities).not.toContain(_rolewithcompanyentity.RoleWithCompany);
24
+ expect(entities).not.toContain(_permissionwithcompanyentity.UserIamPermissionWithCompany);
25
+ });
26
+ it('should use the company-scoped permission entity when enableCompanyFeature is true', ()=>{
27
+ const entities = (0, _index.getIAMEntitiesByConfig)(true, 'FULL');
28
+ expect(entities).toContain(_permissionwithcompanyentity.UserIamPermissionWithCompany);
29
+ expect(entities).not.toContain(_useriampermissionentity.UserIamPermission);
30
+ });
31
+ it('should use the company-scoped role entity when enableCompanyFeature is true', ()=>{
32
+ const entities = (0, _index.getIAMEntitiesByConfig)(true, 'RBAC');
33
+ expect(entities).toContain(_rolewithcompanyentity.RoleWithCompany);
34
+ expect(entities).not.toContain(_roleentity.Role);
35
+ });
36
+ it('should include Role for RBAC mode', ()=>{
37
+ expect((0, _index.getIAMEntitiesByConfig)(false, 'RBAC')).toContain(_roleentity.Role);
38
+ });
39
+ it('should include Role for FULL mode', ()=>{
40
+ expect((0, _index.getIAMEntitiesByConfig)(false, 'FULL')).toContain(_roleentity.Role);
41
+ });
42
+ it('should exclude Role entirely for DIRECT mode', ()=>{
43
+ const entities = (0, _index.getIAMEntitiesByConfig)(false, 'DIRECT');
44
+ expect(entities).not.toContain(_roleentity.Role);
45
+ expect(entities).not.toContain(_rolewithcompanyentity.RoleWithCompany);
46
+ expect(entities).toEqual(expect.arrayContaining([
47
+ _actionentity.Action,
48
+ _useriampermissionentity.UserIamPermission
49
+ ]));
50
+ });
51
+ it('should exclude the company-scoped Role for DIRECT mode even with company feature enabled', ()=>{
52
+ const entities = (0, _index.getIAMEntitiesByConfig)(true, 'DIRECT');
53
+ expect(entities).not.toContain(_rolewithcompanyentity.RoleWithCompany);
54
+ expect(entities).toContain(_permissionwithcompanyentity.UserIamPermissionWithCompany);
55
+ });
56
+ });
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _common = require("@nestjs/common");
6
+ const _constants = require("@flusys/nestjs-shared/constants");
7
+ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
8
+ const _companyaccesshelper = require("./company-access.helper");
9
+ describe('validateCompanyAccess', ()=>{
10
+ function buildConfig(isCompanyFeatureEnabled) {
11
+ return {
12
+ isCompanyFeatureEnabled: jest.fn().mockReturnValue(isCompanyFeatureEnabled)
13
+ };
14
+ }
15
+ it('does not throw when company feature is disabled', ()=>{
16
+ const config = buildConfig(false);
17
+ const user = (0, _loggedusermock.buildMockUser)({
18
+ companyId: 'company-uuid-1'
19
+ });
20
+ expect(()=>(0, _companyaccesshelper.validateCompanyAccess)(config, 'company-uuid-2', user)).not.toThrow();
21
+ });
22
+ it('does not throw when companyId is undefined', ()=>{
23
+ const config = buildConfig(true);
24
+ const user = (0, _loggedusermock.buildMockUser)({
25
+ companyId: 'company-uuid-1'
26
+ });
27
+ expect(()=>(0, _companyaccesshelper.validateCompanyAccess)(config, undefined, user)).not.toThrow();
28
+ });
29
+ it('does not throw when the companyId matches the user company', ()=>{
30
+ const config = buildConfig(true);
31
+ const user = (0, _loggedusermock.buildMockUser)({
32
+ companyId: 'company-uuid-1'
33
+ });
34
+ expect(()=>(0, _companyaccesshelper.validateCompanyAccess)(config, 'company-uuid-1', user)).not.toThrow();
35
+ });
36
+ it('throws ForbiddenException with default message/key when the company does not match', ()=>{
37
+ const config = buildConfig(true);
38
+ const user = (0, _loggedusermock.buildMockUser)({
39
+ companyId: 'company-uuid-1'
40
+ });
41
+ try {
42
+ (0, _companyaccesshelper.validateCompanyAccess)(config, 'company-uuid-2', user);
43
+ fail('expected validateCompanyAccess to throw');
44
+ } catch (error) {
45
+ expect(error).toBeInstanceOf(_common.ForbiddenException);
46
+ expect(error.getResponse()).toEqual(expect.objectContaining({
47
+ message: 'You do not have access to this company',
48
+ messageKey: _constants.AUTH_MESSAGES.COMPANY_NO_ACCESS
49
+ }));
50
+ }
51
+ });
52
+ it('throws with a custom message and messageKey when provided', ()=>{
53
+ const config = buildConfig(true);
54
+ const user = (0, _loggedusermock.buildMockUser)({
55
+ companyId: 'company-uuid-1'
56
+ });
57
+ try {
58
+ (0, _companyaccesshelper.validateCompanyAccess)(config, 'company-uuid-2', user, 'Custom message', 'custom.message.key');
59
+ fail('expected validateCompanyAccess to throw');
60
+ } catch (error) {
61
+ expect(error).toBeInstanceOf(_common.ForbiddenException);
62
+ expect(error.getResponse()).toEqual(expect.objectContaining({
63
+ message: 'Custom message',
64
+ messageKey: 'custom.message.key'
65
+ }));
66
+ }
67
+ });
68
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _permissiontypeenum = require("../enums/permission-type.enum");
6
+ const _permissionmodehelper = require("./permission-mode.helper");
7
+ describe('PermissionModeHelper', ()=>{
8
+ describe('fromString', ()=>{
9
+ it('converts "RBAC" to IAMPermissionMode.RBAC', ()=>{
10
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('RBAC')).toBe(_permissiontypeenum.IAMPermissionMode.RBAC);
11
+ });
12
+ it('converts "DIRECT" to IAMPermissionMode.DIRECT', ()=>{
13
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('DIRECT')).toBe(_permissiontypeenum.IAMPermissionMode.DIRECT);
14
+ });
15
+ it('converts "FULL" to IAMPermissionMode.FULL', ()=>{
16
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('FULL')).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
17
+ });
18
+ it('defaults to FULL when the string is undefined', ()=>{
19
+ expect(_permissionmodehelper.PermissionModeHelper.fromString(undefined)).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
20
+ });
21
+ it('defaults to FULL when the string is empty', ()=>{
22
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('')).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
23
+ });
24
+ it('defaults to FULL when the string is not a known mode', ()=>{
25
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('INVALID_MODE')).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
26
+ });
27
+ it('is case-sensitive and falls back to FULL for lowercase input', ()=>{
28
+ expect(_permissionmodehelper.PermissionModeHelper.fromString('rbac')).toBe(_permissiontypeenum.IAMPermissionMode.FULL);
29
+ });
30
+ });
31
+ describe('toString', ()=>{
32
+ it('converts IAMPermissionMode.RBAC to "RBAC"', ()=>{
33
+ expect(_permissionmodehelper.PermissionModeHelper.toString(_permissiontypeenum.IAMPermissionMode.RBAC)).toBe('RBAC');
34
+ });
35
+ it('converts IAMPermissionMode.DIRECT to "DIRECT"', ()=>{
36
+ expect(_permissionmodehelper.PermissionModeHelper.toString(_permissiontypeenum.IAMPermissionMode.DIRECT)).toBe('DIRECT');
37
+ });
38
+ it('converts IAMPermissionMode.FULL to "FULL"', ()=>{
39
+ expect(_permissionmodehelper.PermissionModeHelper.toString(_permissiontypeenum.IAMPermissionMode.FULL)).toBe('FULL');
40
+ });
41
+ });
42
+ describe('round trip', ()=>{
43
+ it('fromString(toString(mode)) returns the original mode for every enum value', ()=>{
44
+ const modes = [
45
+ _permissiontypeenum.IAMPermissionMode.RBAC,
46
+ _permissiontypeenum.IAMPermissionMode.DIRECT,
47
+ _permissiontypeenum.IAMPermissionMode.FULL
48
+ ];
49
+ for (const mode of modes){
50
+ expect(_permissionmodehelper.PermissionModeHelper.fromString(_permissionmodehelper.PermissionModeHelper.toString(mode))).toBe(mode);
51
+ }
52
+ });
53
+ });
54
+ });
@@ -68,7 +68,9 @@ let IAMModule = class IAMModule {
68
68
  return {
69
69
  provide: _nestjsshared.PERMISSION_GUARD_CONFIG,
70
70
  useValue: {
71
- enableCompanyFeature
71
+ enableCompanyFeature,
72
+ userPermissionKeyFormat: `${_permissioncacheservice.MY_PERMISSIONS_CACHE_PREFIX}:user:{userId}`,
73
+ companyPermissionKeyFormat: `${_permissioncacheservice.MY_PERMISSIONS_CACHE_PREFIX}:company:{companyId}:branch:{branchId}:user:{userId}`
72
74
  }
73
75
  };
74
76
  }
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _nestjsshared = require("@flusys/nestjs-shared");
6
+ const _iammodule = require("./iam.module");
7
+ const _iamconstants = require("../config/iam.constants");
8
+ const _controllers = require("../controllers");
9
+ const _services = require("../services");
10
+ const _iamconfigservice = require("../services/iam-config.service");
11
+ const _iamdatasourceservice = require("../services/iam-datasource.service");
12
+ const _permissioncacheservice = require("../services/permission-cache.service");
13
+ function _define_property(obj, key, value) {
14
+ if (key in obj) {
15
+ Object.defineProperty(obj, key, {
16
+ value: value,
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true
20
+ });
21
+ } else {
22
+ obj[key] = value;
23
+ }
24
+ return obj;
25
+ }
26
+ function providerTokens(providers) {
27
+ return providers.map((p)=>typeof p === 'function' ? p : p.provide);
28
+ }
29
+ describe('IAMModule', ()=>{
30
+ describe('forRoot - controller/service registration matrix', ()=>{
31
+ it('should register no controllers by default (includeController defaults to false)', ()=>{
32
+ const dynamicModule = _iammodule.IAMModule.forRoot();
33
+ expect(dynamicModule.controllers).toEqual([]);
34
+ });
35
+ it('DIRECT mode: registers only UserActionPermissionController beyond the base set', ()=>{
36
+ const dynamicModule = _iammodule.IAMModule.forRoot({
37
+ includeController: true,
38
+ bootstrapAppConfig: {
39
+ permissionMode: 'DIRECT'
40
+ }
41
+ });
42
+ expect(dynamicModule.controllers).toEqual([
43
+ _controllers.ActionController,
44
+ _controllers.MyPermissionController,
45
+ _controllers.UserActionPermissionController
46
+ ]);
47
+ expect(providerTokens(dynamicModule.providers)).not.toContain(_services.RoleService);
48
+ });
49
+ it('RBAC mode: registers RoleController + RolePermissionController but not UserActionPermissionController', ()=>{
50
+ const dynamicModule = _iammodule.IAMModule.forRoot({
51
+ includeController: true,
52
+ bootstrapAppConfig: {
53
+ permissionMode: 'RBAC'
54
+ }
55
+ });
56
+ expect(dynamicModule.controllers).toEqual([
57
+ _controllers.ActionController,
58
+ _controllers.MyPermissionController,
59
+ _controllers.RoleController,
60
+ _controllers.RolePermissionController
61
+ ]);
62
+ expect(providerTokens(dynamicModule.providers)).toContain(_services.RoleService);
63
+ });
64
+ it('FULL mode: registers every permission controller', ()=>{
65
+ const dynamicModule = _iammodule.IAMModule.forRoot({
66
+ includeController: true,
67
+ bootstrapAppConfig: {
68
+ permissionMode: 'FULL'
69
+ }
70
+ });
71
+ expect(dynamicModule.controllers).toEqual([
72
+ _controllers.ActionController,
73
+ _controllers.MyPermissionController,
74
+ _controllers.RoleController,
75
+ _controllers.UserActionPermissionController,
76
+ _controllers.RolePermissionController
77
+ ]);
78
+ });
79
+ it('defaults to FULL mode when permissionMode is omitted', ()=>{
80
+ const dynamicModule = _iammodule.IAMModule.forRoot({
81
+ includeController: true
82
+ });
83
+ expect(dynamicModule.controllers).toEqual(expect.arrayContaining([
84
+ _controllers.RoleController,
85
+ _controllers.UserActionPermissionController,
86
+ _controllers.RolePermissionController
87
+ ]));
88
+ });
89
+ it('defaults to FULL mode when permissionMode is an unrecognized string', ()=>{
90
+ const dynamicModule = _iammodule.IAMModule.forRoot({
91
+ includeController: true,
92
+ bootstrapAppConfig: {
93
+ permissionMode: 'BOGUS'
94
+ }
95
+ });
96
+ expect(dynamicModule.controllers).toEqual(expect.arrayContaining([
97
+ _controllers.RoleController,
98
+ _controllers.UserActionPermissionController,
99
+ _controllers.RolePermissionController
100
+ ]));
101
+ });
102
+ it('adds CompanyActionPermissionController only when enableCompanyFeature is true', ()=>{
103
+ const withCompany = _iammodule.IAMModule.forRoot({
104
+ includeController: true,
105
+ bootstrapAppConfig: {
106
+ enableCompanyFeature: true
107
+ }
108
+ });
109
+ const withoutCompany = _iammodule.IAMModule.forRoot({
110
+ includeController: true,
111
+ bootstrapAppConfig: {
112
+ enableCompanyFeature: false
113
+ }
114
+ });
115
+ expect(withCompany.controllers).toContain(_controllers.CompanyActionPermissionController);
116
+ expect(withoutCompany.controllers).not.toContain(_controllers.CompanyActionPermissionController);
117
+ });
118
+ it('excludes RoleService for DIRECT mode but includes it for RBAC/FULL', ()=>{
119
+ const direct = _iammodule.IAMModule.forRoot({
120
+ bootstrapAppConfig: {
121
+ permissionMode: 'DIRECT'
122
+ }
123
+ });
124
+ const rbac = _iammodule.IAMModule.forRoot({
125
+ bootstrapAppConfig: {
126
+ permissionMode: 'RBAC'
127
+ }
128
+ });
129
+ const full = _iammodule.IAMModule.forRoot({
130
+ bootstrapAppConfig: {
131
+ permissionMode: 'FULL'
132
+ }
133
+ });
134
+ expect(providerTokens(direct.providers)).not.toContain(_services.RoleService);
135
+ expect(providerTokens(rbac.providers)).toContain(_services.RoleService);
136
+ expect(providerTokens(full.providers)).toContain(_services.RoleService);
137
+ expect(providerTokens(direct.exports)).not.toContain(_services.RoleService);
138
+ expect(providerTokens(rbac.exports)).toContain(_services.RoleService);
139
+ });
140
+ it('always registers the base providers/exports (ActionService, PermissionService, PermissionCacheService)', ()=>{
141
+ const dynamicModule = _iammodule.IAMModule.forRoot();
142
+ expect(providerTokens(dynamicModule.providers)).toEqual(expect.arrayContaining([
143
+ _iamconfigservice.IAMConfigService,
144
+ _iamdatasourceservice.IAMDataSourceService,
145
+ _services.ActionService,
146
+ _services.PermissionService,
147
+ _permissioncacheservice.PermissionCacheService
148
+ ]));
149
+ expect(dynamicModule.exports).toEqual(expect.arrayContaining([
150
+ _iamconfigservice.IAMConfigService,
151
+ _iamdatasourceservice.IAMDataSourceService,
152
+ _services.ActionService,
153
+ _services.PermissionService,
154
+ _permissioncacheservice.PermissionCacheService,
155
+ _nestjsshared.PERMISSION_GUARD_CONFIG
156
+ ]));
157
+ });
158
+ });
159
+ describe('forRoot - options/global handling', ()=>{
160
+ it('should not set `global` on the returned module when global is false/omitted', ()=>{
161
+ const dynamicModule = _iammodule.IAMModule.forRoot();
162
+ expect(dynamicModule.global).toBeUndefined();
163
+ });
164
+ it('should set global: true when requested', ()=>{
165
+ const dynamicModule = _iammodule.IAMModule.forRoot({
166
+ global: true
167
+ });
168
+ expect(dynamicModule.global).toBe(true);
169
+ });
170
+ it('should register IAM_MODULE_OPTIONS with the given options', ()=>{
171
+ const options = {
172
+ includeController: true
173
+ };
174
+ const dynamicModule = _iammodule.IAMModule.forRoot(options);
175
+ const provider = dynamicModule.providers.find((p)=>p.provide === _iamconstants.IAM_MODULE_OPTIONS);
176
+ expect(provider.useValue).toBe(options);
177
+ });
178
+ it('should build a company-aware PERMISSION_GUARD_CONFIG provider', ()=>{
179
+ const dynamicModule = _iammodule.IAMModule.forRoot({
180
+ bootstrapAppConfig: {
181
+ enableCompanyFeature: true
182
+ }
183
+ });
184
+ const provider = dynamicModule.providers.find((p)=>p.provide === _nestjsshared.PERMISSION_GUARD_CONFIG);
185
+ expect(provider.useValue.enableCompanyFeature).toBe(true);
186
+ expect(provider.useValue.userPermissionKeyFormat).toContain('{userId}');
187
+ expect(provider.useValue.companyPermissionKeyFormat).toContain('{companyId}');
188
+ });
189
+ });
190
+ describe('forRootAsync', ()=>{
191
+ it('mirrors the controller/service registration matrix of forRoot', ()=>{
192
+ const dynamicModule = _iammodule.IAMModule.forRootAsync({
193
+ includeController: true,
194
+ bootstrapAppConfig: {
195
+ permissionMode: 'DIRECT'
196
+ }
197
+ });
198
+ expect(dynamicModule.controllers).toEqual([
199
+ _controllers.ActionController,
200
+ _controllers.MyPermissionController,
201
+ _controllers.UserActionPermissionController
202
+ ]);
203
+ });
204
+ it('should forward external imports alongside CacheModule/UtilsModule', ()=>{
205
+ let FakeImportedModule = class FakeImportedModule {
206
+ };
207
+ const dynamicModule = _iammodule.IAMModule.forRootAsync({
208
+ imports: [
209
+ FakeImportedModule
210
+ ]
211
+ });
212
+ expect(dynamicModule.imports).toEqual(expect.arrayContaining([
213
+ FakeImportedModule
214
+ ]));
215
+ });
216
+ it('useFactory: registers a single options provider using the given inject tokens', async ()=>{
217
+ const useFactory = jest.fn().mockResolvedValue({
218
+ foo: 'bar'
219
+ });
220
+ const dynamicModule = _iammodule.IAMModule.forRootAsync({
221
+ useFactory,
222
+ inject: [
223
+ 'SOME_TOKEN'
224
+ ]
225
+ });
226
+ const optionsProviders = dynamicModule.providers.filter((p)=>p.provide === _iamconstants.IAM_MODULE_OPTIONS);
227
+ expect(optionsProviders).toHaveLength(1);
228
+ const provider = optionsProviders[0];
229
+ expect(provider.useFactory).toBe(useFactory);
230
+ expect(provider.inject).toEqual([
231
+ 'SOME_TOKEN'
232
+ ]);
233
+ });
234
+ it('useClass: registers both the class provider and an options provider that calls createIAMOptions()', async ()=>{
235
+ let MyOptionsFactory = class MyOptionsFactory {
236
+ constructor(){
237
+ _define_property(this, "createIAMOptions", jest.fn().mockResolvedValue({
238
+ foo: 'from-class'
239
+ }));
240
+ }
241
+ };
242
+ const dynamicModule = _iammodule.IAMModule.forRootAsync({
243
+ useClass: MyOptionsFactory
244
+ });
245
+ const classProvider = dynamicModule.providers.find((p)=>p.provide === MyOptionsFactory);
246
+ expect(classProvider.useClass).toBe(MyOptionsFactory);
247
+ const optionsProvider = dynamicModule.providers.find((p)=>p.provide === _iamconstants.IAM_MODULE_OPTIONS);
248
+ expect(optionsProvider.inject).toEqual([
249
+ MyOptionsFactory
250
+ ]);
251
+ const instance = new MyOptionsFactory();
252
+ const resolved = await optionsProvider.useFactory(instance);
253
+ expect(instance.createIAMOptions).toHaveBeenCalled();
254
+ expect(resolved).toEqual({
255
+ foo: 'from-class'
256
+ });
257
+ });
258
+ it('useExisting: registers a single options provider injecting the existing factory', async ()=>{
259
+ let MyOptionsFactory = class MyOptionsFactory {
260
+ constructor(){
261
+ _define_property(this, "createIAMOptions", jest.fn().mockResolvedValue({
262
+ foo: 'from-existing'
263
+ }));
264
+ }
265
+ };
266
+ const dynamicModule = _iammodule.IAMModule.forRootAsync({
267
+ useExisting: MyOptionsFactory
268
+ });
269
+ const optionsProviders = dynamicModule.providers.filter((p)=>p.provide === _iamconstants.IAM_MODULE_OPTIONS);
270
+ expect(optionsProviders).toHaveLength(1);
271
+ const provider = optionsProviders[0];
272
+ expect(provider.inject).toEqual([
273
+ MyOptionsFactory
274
+ ]);
275
+ const instance = new MyOptionsFactory();
276
+ const resolved = await provider.useFactory(instance);
277
+ expect(resolved).toEqual({
278
+ foo: 'from-existing'
279
+ });
280
+ });
281
+ });
282
+ describe('forFeature', ()=>{
283
+ it('should behave identically to forRoot', ()=>{
284
+ const options = {
285
+ includeController: true,
286
+ bootstrapAppConfig: {
287
+ permissionMode: 'RBAC'
288
+ }
289
+ };
290
+ expect(_iammodule.IAMModule.forFeature(options)).toEqual(_iammodule.IAMModule.forRoot(options));
291
+ });
292
+ });
293
+ });