@flusys/nestjs-auth 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 (83) hide show
  1. package/cjs/controllers/authentication.controller.spec.js +219 -0
  2. package/cjs/controllers/branch.controller.spec.js +134 -0
  3. package/cjs/controllers/company-selection.controller.spec.js +84 -0
  4. package/cjs/controllers/company.controller.spec.js +131 -0
  5. package/cjs/controllers/email-verification.controller.spec.js +142 -0
  6. package/cjs/controllers/user-permission.controller.spec.js +138 -0
  7. package/cjs/controllers/user.controller.spec.js +204 -0
  8. package/cjs/docs/auth-swagger.config.spec.js +130 -0
  9. package/cjs/entities/company-branch.entity.js +2 -3
  10. package/cjs/entities/index.spec.js +73 -0
  11. package/cjs/helpers/auth-response.mapper.js +46 -0
  12. package/cjs/helpers/auth-response.mapper.spec.js +139 -0
  13. package/cjs/helpers/index.js +19 -0
  14. package/cjs/helpers/token.helper.js +58 -0
  15. package/cjs/helpers/token.helper.spec.js +86 -0
  16. package/cjs/index.js +1 -0
  17. package/cjs/interceptors/clear-token.interceptor.spec.js +107 -0
  18. package/cjs/interceptors/set-token.interceptor.spec.js +165 -0
  19. package/cjs/modules/auth.module.spec.js +243 -0
  20. package/cjs/services/auth-config.service.spec.js +97 -0
  21. package/cjs/services/auth-datasource.provider.spec.js +161 -0
  22. package/cjs/services/auth-email.service.spec.js +404 -0
  23. package/cjs/services/authentication.service.js +37 -109
  24. package/cjs/services/authentication.service.spec.js +904 -0
  25. package/cjs/services/branch.service.js +30 -3
  26. package/cjs/services/branch.service.spec.js +154 -0
  27. package/cjs/services/company-selection-session.service.spec.js +90 -0
  28. package/cjs/services/company.service.js +34 -3
  29. package/cjs/services/company.service.spec.js +113 -0
  30. package/cjs/services/user-permission.service.js +35 -4
  31. package/cjs/services/user-permission.service.spec.js +202 -0
  32. package/cjs/services/user.service.spec.js +704 -0
  33. package/cjs/strategies/jwt.strategy.js +1 -1
  34. package/cjs/strategies/jwt.strategy.spec.js +166 -0
  35. package/cjs/validators/password.validator.spec.js +161 -0
  36. package/entities/company-branch.entity.d.ts +1 -1
  37. package/entities/pending-registration.entity.d.ts +2 -7
  38. package/fesm/controllers/authentication.controller.spec.js +215 -0
  39. package/fesm/controllers/branch.controller.spec.js +130 -0
  40. package/fesm/controllers/company-selection.controller.spec.js +80 -0
  41. package/fesm/controllers/company.controller.spec.js +127 -0
  42. package/fesm/controllers/email-verification.controller.spec.js +138 -0
  43. package/fesm/controllers/user-permission.controller.spec.js +134 -0
  44. package/fesm/controllers/user.controller.spec.js +200 -0
  45. package/fesm/docs/auth-swagger.config.spec.js +126 -0
  46. package/fesm/entities/company-branch.entity.js +2 -3
  47. package/fesm/entities/index.spec.js +69 -0
  48. package/fesm/helpers/auth-response.mapper.js +25 -0
  49. package/fesm/helpers/auth-response.mapper.spec.js +135 -0
  50. package/fesm/helpers/index.js +2 -0
  51. package/fesm/helpers/token.helper.js +40 -0
  52. package/fesm/helpers/token.helper.spec.js +82 -0
  53. package/fesm/index.js +1 -0
  54. package/fesm/interceptors/clear-token.interceptor.spec.js +103 -0
  55. package/fesm/interceptors/set-token.interceptor.spec.js +161 -0
  56. package/fesm/interfaces/authentication.interface.js +1 -1
  57. package/fesm/modules/auth.module.spec.js +239 -0
  58. package/fesm/services/auth-config.service.spec.js +93 -0
  59. package/fesm/services/auth-datasource.provider.spec.js +157 -0
  60. package/fesm/services/auth-email.service.spec.js +359 -0
  61. package/fesm/services/authentication.service.js +37 -109
  62. package/fesm/services/authentication.service.spec.js +859 -0
  63. package/fesm/services/branch.service.js +32 -5
  64. package/fesm/services/branch.service.spec.js +150 -0
  65. package/fesm/services/company-selection-session.service.spec.js +86 -0
  66. package/fesm/services/company.service.js +36 -5
  67. package/fesm/services/company.service.spec.js +109 -0
  68. package/fesm/services/user-permission.service.js +36 -5
  69. package/fesm/services/user-permission.service.spec.js +198 -0
  70. package/fesm/services/user.service.spec.js +659 -0
  71. package/fesm/strategies/jwt.strategy.js +1 -1
  72. package/fesm/strategies/jwt.strategy.spec.js +162 -0
  73. package/fesm/validators/password.validator.spec.js +157 -0
  74. package/helpers/auth-response.mapper.d.ts +22 -0
  75. package/helpers/index.d.ts +2 -0
  76. package/helpers/token.helper.d.ts +8 -0
  77. package/index.d.ts +1 -0
  78. package/interfaces/authentication.interface.d.ts +7 -0
  79. package/package.json +8 -3
  80. package/services/authentication.service.d.ts +1 -8
  81. package/services/branch.service.d.ts +7 -5
  82. package/services/company.service.d.ts +7 -4
  83. package/services/user-permission.service.d.ts +4 -1
@@ -0,0 +1,130 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { BranchController } from './branch.controller';
3
+ function buildBranch(overrides = {}) {
4
+ return {
5
+ id: 'branch-1',
6
+ name: 'Main Branch',
7
+ slug: 'main-branch',
8
+ companyId: 'company-1',
9
+ parentId: null,
10
+ logoId: null,
11
+ address: null,
12
+ phone: null,
13
+ email: null,
14
+ serial: null,
15
+ isActive: true,
16
+ readOnly: false,
17
+ additionalFields: null,
18
+ createdAt: new Date(),
19
+ updatedAt: new Date(),
20
+ ...overrides
21
+ };
22
+ }
23
+ describe('BranchController', ()=>{
24
+ let controller;
25
+ let mockBranchService;
26
+ beforeEach(()=>{
27
+ mockBranchService = {
28
+ insert: jest.fn(),
29
+ insertMany: jest.fn(),
30
+ findById: jest.fn(),
31
+ findByIds: jest.fn(),
32
+ update: jest.fn(),
33
+ updateMany: jest.fn(),
34
+ bulkUpsert: jest.fn(),
35
+ getAll: jest.fn(),
36
+ delete: jest.fn()
37
+ };
38
+ controller = new BranchController(mockBranchService);
39
+ });
40
+ it('wires the injected service onto the generated controller base', ()=>{
41
+ expect(controller.branchService).toBe(mockBranchService);
42
+ expect(controller.service).toBe(mockBranchService);
43
+ });
44
+ describe('insert', ()=>{
45
+ it('creates a branch and returns the mapped response', async ()=>{
46
+ const user = buildMockUser();
47
+ const branch = buildBranch();
48
+ mockBranchService.insert.mockResolvedValue(branch);
49
+ const result = await controller.insert({
50
+ name: 'Main Branch',
51
+ slug: 'main-branch',
52
+ companyId: 'company-1'
53
+ }, user);
54
+ expect(mockBranchService.insert).toHaveBeenCalledWith({
55
+ name: 'Main Branch',
56
+ slug: 'main-branch',
57
+ companyId: 'company-1'
58
+ }, user);
59
+ expect(result.success).toBe(true);
60
+ expect(result.data).toEqual(expect.objectContaining({
61
+ id: 'branch-1',
62
+ slug: 'main-branch'
63
+ }));
64
+ });
65
+ });
66
+ describe('getById', ()=>{
67
+ it('retrieves a branch by id', async ()=>{
68
+ const branch = buildBranch();
69
+ mockBranchService.findById.mockResolvedValue(branch);
70
+ const result = await controller.getById('branch-1', {}, null);
71
+ expect(mockBranchService.findById).toHaveBeenCalledWith('branch-1', null, undefined);
72
+ expect(result.data).toEqual(expect.objectContaining({
73
+ id: 'branch-1'
74
+ }));
75
+ });
76
+ });
77
+ describe('getAll', ()=>{
78
+ it('paginates branches and maps the response list', async ()=>{
79
+ mockBranchService.getAll.mockResolvedValue({
80
+ data: [
81
+ buildBranch()
82
+ ],
83
+ total: 1
84
+ });
85
+ const result = await controller.getAll({}, buildMockUser(), 'main');
86
+ expect(mockBranchService.getAll).toHaveBeenCalledWith('main', {}, buildMockUser());
87
+ expect(result.data).toHaveLength(1);
88
+ expect(result.meta.total).toBe(1);
89
+ });
90
+ });
91
+ describe('update', ()=>{
92
+ it('updates a branch and returns the mapped response', async ()=>{
93
+ const branch = buildBranch({
94
+ name: 'Updated Branch'
95
+ });
96
+ mockBranchService.update.mockResolvedValue(branch);
97
+ const result = await controller.update({
98
+ id: 'branch-1',
99
+ name: 'Updated Branch'
100
+ }, buildMockUser());
101
+ expect(mockBranchService.update).toHaveBeenCalledWith({
102
+ id: 'branch-1',
103
+ name: 'Updated Branch'
104
+ }, buildMockUser());
105
+ expect(result.data).toEqual(expect.objectContaining({
106
+ name: 'Updated Branch'
107
+ }));
108
+ });
109
+ });
110
+ describe('delete', ()=>{
111
+ it('delegates soft-delete to the service', async ()=>{
112
+ mockBranchService.delete.mockResolvedValue({
113
+ count: 1
114
+ });
115
+ const result = await controller.delete({
116
+ id: [
117
+ 'branch-1'
118
+ ],
119
+ type: 'delete'
120
+ }, buildMockUser());
121
+ expect(mockBranchService.delete).toHaveBeenCalledWith({
122
+ id: [
123
+ 'branch-1'
124
+ ],
125
+ type: 'delete'
126
+ }, buildMockUser());
127
+ expect(result.success).toBe(true);
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,80 @@
1
+ import { CompanySelectionController } from './company-selection.controller';
2
+ describe('CompanySelectionController', ()=>{
3
+ let controller;
4
+ let mockAuthService;
5
+ beforeEach(()=>{
6
+ mockAuthService = {
7
+ select: jest.fn(),
8
+ switchCompany: jest.fn(),
9
+ getUserCompanies: jest.fn(),
10
+ getUserBranches: jest.fn()
11
+ };
12
+ controller = new CompanySelectionController(mockAuthService);
13
+ });
14
+ describe('select', ()=>{
15
+ it('completes login with the selected company/branch', async ()=>{
16
+ mockAuthService.select.mockResolvedValue({
17
+ accessToken: 'a'
18
+ });
19
+ const result = await controller.select({
20
+ sessionId: 'sess-1',
21
+ companyId: 'company-1',
22
+ branchId: 'branch-1'
23
+ });
24
+ expect(mockAuthService.select).toHaveBeenCalledWith('sess-1', 'company-1', 'branch-1');
25
+ expect(result).toEqual(expect.objectContaining({
26
+ success: true,
27
+ data: {
28
+ accessToken: 'a'
29
+ }
30
+ }));
31
+ });
32
+ });
33
+ describe('switchCompany', ()=>{
34
+ it('switches company for the authenticated user', async ()=>{
35
+ mockAuthService.switchCompany.mockResolvedValue({
36
+ accessToken: 'b'
37
+ });
38
+ const result = await controller.switchCompany('user-1', {
39
+ companyId: 'company-2',
40
+ branchId: 'branch-2'
41
+ });
42
+ expect(mockAuthService.switchCompany).toHaveBeenCalledWith('user-1', 'company-2', 'branch-2');
43
+ expect(result.data).toEqual({
44
+ accessToken: 'b'
45
+ });
46
+ });
47
+ });
48
+ describe('getUserCompanies', ()=>{
49
+ it('returns the companies the user has access to', async ()=>{
50
+ mockAuthService.getUserCompanies.mockResolvedValue([
51
+ {
52
+ id: 'company-1'
53
+ }
54
+ ]);
55
+ const result = await controller.getUserCompanies('user-1');
56
+ expect(mockAuthService.getUserCompanies).toHaveBeenCalledWith('user-1');
57
+ expect(result.data).toEqual([
58
+ {
59
+ id: 'company-1'
60
+ }
61
+ ]);
62
+ });
63
+ });
64
+ describe('getUserBranches', ()=>{
65
+ it('returns the branches for the given company', async ()=>{
66
+ mockAuthService.getUserBranches.mockResolvedValue([
67
+ {
68
+ id: 'branch-1'
69
+ }
70
+ ]);
71
+ const result = await controller.getUserBranches('user-1', 'company-1');
72
+ expect(mockAuthService.getUserBranches).toHaveBeenCalledWith('user-1', 'company-1');
73
+ expect(result.data).toEqual([
74
+ {
75
+ id: 'branch-1'
76
+ }
77
+ ]);
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,127 @@
1
+ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
2
+ import { CompanyController } from './company.controller';
3
+ function buildCompany(overrides = {}) {
4
+ return {
5
+ id: 'company-1',
6
+ name: 'Acme Corporation',
7
+ slug: 'acme-corp',
8
+ logoId: null,
9
+ address: null,
10
+ phone: null,
11
+ email: null,
12
+ website: null,
13
+ serial: null,
14
+ isActive: true,
15
+ readOnly: false,
16
+ settings: null,
17
+ additionalFields: null,
18
+ createdAt: new Date(),
19
+ updatedAt: new Date(),
20
+ ...overrides
21
+ };
22
+ }
23
+ describe('CompanyController', ()=>{
24
+ let controller;
25
+ let mockCompanyService;
26
+ beforeEach(()=>{
27
+ mockCompanyService = {
28
+ insert: jest.fn(),
29
+ insertMany: jest.fn(),
30
+ findById: jest.fn(),
31
+ findByIds: jest.fn(),
32
+ update: jest.fn(),
33
+ updateMany: jest.fn(),
34
+ bulkUpsert: jest.fn(),
35
+ getAll: jest.fn(),
36
+ delete: jest.fn()
37
+ };
38
+ controller = new CompanyController(mockCompanyService);
39
+ });
40
+ it('wires the injected service onto the generated controller base', ()=>{
41
+ expect(controller.companyService).toBe(mockCompanyService);
42
+ expect(controller.service).toBe(mockCompanyService);
43
+ });
44
+ describe('insert', ()=>{
45
+ it('creates a company and returns the mapped response', async ()=>{
46
+ const user = buildMockUser();
47
+ const company = buildCompany();
48
+ mockCompanyService.insert.mockResolvedValue(company);
49
+ const result = await controller.insert({
50
+ name: 'Acme Corporation',
51
+ slug: 'acme-corp'
52
+ }, user);
53
+ expect(mockCompanyService.insert).toHaveBeenCalledWith({
54
+ name: 'Acme Corporation',
55
+ slug: 'acme-corp'
56
+ }, user);
57
+ expect(result.success).toBe(true);
58
+ expect(result.data).toEqual(expect.objectContaining({
59
+ id: 'company-1',
60
+ slug: 'acme-corp'
61
+ }));
62
+ });
63
+ });
64
+ describe('getById', ()=>{
65
+ it('retrieves a company by id', async ()=>{
66
+ mockCompanyService.findById.mockResolvedValue(buildCompany());
67
+ const result = await controller.getById('company-1', {}, null);
68
+ expect(mockCompanyService.findById).toHaveBeenCalledWith('company-1', null, undefined);
69
+ expect(result.data).toEqual(expect.objectContaining({
70
+ id: 'company-1'
71
+ }));
72
+ });
73
+ });
74
+ describe('getAll', ()=>{
75
+ it('paginates companies and maps the response list', async ()=>{
76
+ mockCompanyService.getAll.mockResolvedValue({
77
+ data: [
78
+ buildCompany()
79
+ ],
80
+ total: 1
81
+ });
82
+ const result = await controller.getAll({}, buildMockUser(), 'acme');
83
+ expect(mockCompanyService.getAll).toHaveBeenCalledWith('acme', {}, buildMockUser());
84
+ expect(result.data).toHaveLength(1);
85
+ expect(result.meta.total).toBe(1);
86
+ });
87
+ });
88
+ describe('update', ()=>{
89
+ it('updates a company and returns the mapped response', async ()=>{
90
+ const company = buildCompany({
91
+ name: 'Updated Co'
92
+ });
93
+ mockCompanyService.update.mockResolvedValue(company);
94
+ const result = await controller.update({
95
+ id: 'company-1',
96
+ name: 'Updated Co'
97
+ }, buildMockUser());
98
+ expect(mockCompanyService.update).toHaveBeenCalledWith({
99
+ id: 'company-1',
100
+ name: 'Updated Co'
101
+ }, buildMockUser());
102
+ expect(result.data).toEqual(expect.objectContaining({
103
+ name: 'Updated Co'
104
+ }));
105
+ });
106
+ });
107
+ describe('delete', ()=>{
108
+ it('delegates soft-delete to the service', async ()=>{
109
+ mockCompanyService.delete.mockResolvedValue({
110
+ count: 1
111
+ });
112
+ const result = await controller.delete({
113
+ id: [
114
+ 'company-1'
115
+ ],
116
+ type: 'delete'
117
+ }, buildMockUser());
118
+ expect(mockCompanyService.delete).toHaveBeenCalledWith({
119
+ id: [
120
+ 'company-1'
121
+ ],
122
+ type: 'delete'
123
+ }, buildMockUser());
124
+ expect(result.success).toBe(true);
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,138 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { EmailVerificationController } from './email-verification.controller';
3
+ function buildRequest() {
4
+ return {
5
+ headers: {},
6
+ get: jest.fn((h)=>h === 'host' ? 'app.example.com' : undefined),
7
+ protocol: 'https'
8
+ };
9
+ }
10
+ describe('EmailVerificationController', ()=>{
11
+ let controller;
12
+ let mockAuthService;
13
+ let mockAuthEmailService;
14
+ let mockAuthConfig;
15
+ let mockDataSourceProvider;
16
+ beforeEach(()=>{
17
+ mockAuthService = {
18
+ createUserFromPendingRegistration: jest.fn()
19
+ };
20
+ mockAuthEmailService = {
21
+ forgotPassword: jest.fn(),
22
+ resetPassword: jest.fn(),
23
+ resendVerificationEmail: jest.fn(),
24
+ verifyEmail: jest.fn()
25
+ };
26
+ mockAuthConfig = {};
27
+ mockDataSourceProvider = {
28
+ isEmailVerificationEnabled: jest.fn().mockReturnValue(true)
29
+ };
30
+ controller = new EmailVerificationController(mockAuthService, mockAuthEmailService, mockAuthConfig, mockDataSourceProvider);
31
+ });
32
+ describe('when email verification is disabled for the tenant', ()=>{
33
+ beforeEach(()=>{
34
+ mockDataSourceProvider.isEmailVerificationEnabled.mockReturnValue(false);
35
+ });
36
+ it('rejects forgotPassword', async ()=>{
37
+ await expect(controller.forgotPassword(buildRequest(), {
38
+ email: 'a@example.com'
39
+ })).rejects.toThrow(BadRequestException);
40
+ });
41
+ it('rejects resetPassword', async ()=>{
42
+ await expect(controller.resetPassword({
43
+ token: 't',
44
+ newPassword: 'New1!aaaa'
45
+ })).rejects.toThrow(BadRequestException);
46
+ });
47
+ it('rejects resendVerification', async ()=>{
48
+ await expect(controller.resendVerification(buildRequest(), {
49
+ email: 'a@example.com'
50
+ })).rejects.toThrow(BadRequestException);
51
+ });
52
+ it('rejects verifyEmail', async ()=>{
53
+ await expect(controller.verifyEmail({
54
+ token: 't'
55
+ })).rejects.toThrow(BadRequestException);
56
+ });
57
+ });
58
+ describe('forgotPassword', ()=>{
59
+ it('delegates to authEmailService with a built frontend reset URL', async ()=>{
60
+ mockAuthEmailService.forgotPassword.mockResolvedValue({
61
+ success: true,
62
+ message: 'sent',
63
+ messageKey: 'auth.password.reset.email.sent'
64
+ });
65
+ const result = await controller.forgotPassword(buildRequest(), {
66
+ email: 'a@example.com'
67
+ });
68
+ expect(mockAuthEmailService.forgotPassword).toHaveBeenCalledWith('a@example.com', expect.any(String));
69
+ expect(result.success).toBe(true);
70
+ });
71
+ });
72
+ describe('resetPassword', ()=>{
73
+ it('delegates to authEmailService.resetPassword', async ()=>{
74
+ mockAuthEmailService.resetPassword.mockResolvedValue({
75
+ success: true,
76
+ message: 'done',
77
+ messageKey: 'auth.password.reset.success'
78
+ });
79
+ const result = await controller.resetPassword({
80
+ token: 'tok',
81
+ newPassword: 'New1!aaaa'
82
+ });
83
+ expect(mockAuthEmailService.resetPassword).toHaveBeenCalledWith('tok', 'New1!aaaa');
84
+ expect(result.success).toBe(true);
85
+ });
86
+ });
87
+ describe('resendVerification', ()=>{
88
+ it('delegates to authEmailService.resendVerificationEmail', async ()=>{
89
+ mockAuthEmailService.resendVerificationEmail.mockResolvedValue({
90
+ success: true,
91
+ message: 'sent',
92
+ messageKey: 'email.verification.resend.success'
93
+ });
94
+ await controller.resendVerification(buildRequest(), {
95
+ email: 'a@example.com'
96
+ });
97
+ expect(mockAuthEmailService.resendVerificationEmail).toHaveBeenCalledWith('a@example.com', expect.any(String));
98
+ });
99
+ });
100
+ describe('verifyEmail', ()=>{
101
+ it('completes registration when the token resolves a pending registration', async ()=>{
102
+ mockAuthEmailService.verifyEmail.mockResolvedValue({
103
+ type: 'pending',
104
+ pending: {
105
+ id: 'pending-1'
106
+ }
107
+ });
108
+ mockAuthService.createUserFromPendingRegistration.mockResolvedValue({
109
+ accessToken: 'a'
110
+ });
111
+ const result = await controller.verifyEmail({
112
+ token: 'tok'
113
+ });
114
+ expect(mockAuthService.createUserFromPendingRegistration).toHaveBeenCalledWith({
115
+ id: 'pending-1'
116
+ });
117
+ expect(result.data).toEqual({
118
+ accessToken: 'a'
119
+ });
120
+ });
121
+ it('returns the verification message when the token verifies an existing user token', async ()=>{
122
+ mockAuthEmailService.verifyEmail.mockResolvedValue({
123
+ type: 'verified',
124
+ message: 'Email has been verified successfully.',
125
+ messageKey: 'email.verification.success'
126
+ });
127
+ const result = await controller.verifyEmail({
128
+ token: 'tok'
129
+ });
130
+ expect(result).toEqual({
131
+ success: true,
132
+ message: 'Email has been verified successfully.',
133
+ messageKey: 'email.verification.success'
134
+ });
135
+ expect(mockAuthService.createUserFromPendingRegistration).not.toHaveBeenCalled();
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,134 @@
1
+ import { UserPermissionController } from './user-permission.controller';
2
+ import { PermissionType } from '../entities/user-company-permission.entity';
3
+ describe('UserPermissionController', ()=>{
4
+ let controller;
5
+ let mockPermissionService;
6
+ beforeEach(()=>{
7
+ mockPermissionService = {
8
+ findUserCompanies: jest.fn(),
9
+ findUserBranches: jest.fn(),
10
+ assignPermission: jest.fn(),
11
+ revokePermission: jest.fn()
12
+ };
13
+ controller = new UserPermissionController(mockPermissionService);
14
+ });
15
+ describe('assignUserCompany', ()=>{
16
+ it('assigns and revokes company permissions in a single batch', async ()=>{
17
+ const result = await controller.assignUserCompany({
18
+ userId: 'user-1',
19
+ items: [
20
+ {
21
+ targetId: 'company-1',
22
+ isAdd: true
23
+ },
24
+ {
25
+ targetId: 'company-2',
26
+ isAdd: false
27
+ }
28
+ ]
29
+ });
30
+ expect(mockPermissionService.assignPermission).toHaveBeenCalledWith('user-1', 'company-1', PermissionType.COMPANY);
31
+ expect(mockPermissionService.revokePermission).toHaveBeenCalledWith('user-1', 'company-2', PermissionType.COMPANY);
32
+ expect(result.messageVariables).toEqual({
33
+ added: 1,
34
+ removed: 1
35
+ });
36
+ });
37
+ });
38
+ describe('getUserCompanies', ()=>{
39
+ it('maps permission entities to the response DTO shape', async ()=>{
40
+ mockPermissionService.findUserCompanies.mockResolvedValue([
41
+ {
42
+ id: 'perm-1',
43
+ userId: 'user-1',
44
+ targetId: 'company-1',
45
+ company: {
46
+ id: 'company-1',
47
+ name: 'Acme',
48
+ slug: 'acme'
49
+ }
50
+ }
51
+ ]);
52
+ const result = await controller.getUserCompanies('user-1');
53
+ expect(result.data).toEqual([
54
+ {
55
+ id: 'perm-1',
56
+ userId: 'user-1',
57
+ companyId: 'company-1',
58
+ companyName: 'Acme',
59
+ companySlug: 'acme'
60
+ }
61
+ ]);
62
+ });
63
+ it('falls back to targetId and empty strings when the company relation is missing', async ()=>{
64
+ mockPermissionService.findUserCompanies.mockResolvedValue([
65
+ {
66
+ id: 'perm-1',
67
+ userId: 'user-1',
68
+ targetId: 'company-1',
69
+ company: undefined
70
+ }
71
+ ]);
72
+ const result = await controller.getUserCompanies('user-1');
73
+ expect(result.data[0]).toEqual({
74
+ id: 'perm-1',
75
+ userId: 'user-1',
76
+ companyId: 'company-1',
77
+ companyName: '',
78
+ companySlug: ''
79
+ });
80
+ });
81
+ });
82
+ describe('assignUserBranch', ()=>{
83
+ it('assigns and revokes branch permissions in a single batch', async ()=>{
84
+ const result = await controller.assignUserBranch({
85
+ userId: 'user-1',
86
+ items: [
87
+ {
88
+ targetId: 'branch-1',
89
+ isAdd: true
90
+ }
91
+ ]
92
+ });
93
+ expect(mockPermissionService.assignPermission).toHaveBeenCalledWith('user-1', 'branch-1', PermissionType.BRANCH);
94
+ expect(result.messageVariables).toEqual({
95
+ added: 1,
96
+ removed: 0
97
+ });
98
+ });
99
+ });
100
+ describe('getUserBranches', ()=>{
101
+ it('maps permission entities to the response DTO shape', async ()=>{
102
+ mockPermissionService.findUserBranches.mockResolvedValue([
103
+ {
104
+ id: 'perm-1',
105
+ userId: 'user-1',
106
+ targetId: 'branch-1',
107
+ branch: {
108
+ id: 'branch-1',
109
+ name: 'Main',
110
+ slug: 'main',
111
+ companyId: 'company-1'
112
+ }
113
+ }
114
+ ]);
115
+ const result = await controller.getUserBranches('user-1', 'company-1');
116
+ expect(mockPermissionService.findUserBranches).toHaveBeenCalledWith('user-1', 'company-1');
117
+ expect(result.data).toEqual([
118
+ {
119
+ id: 'perm-1',
120
+ userId: 'user-1',
121
+ branchId: 'branch-1',
122
+ branchName: 'Main',
123
+ branchSlug: 'main',
124
+ companyId: 'company-1'
125
+ }
126
+ ]);
127
+ });
128
+ it('supports an optional companyId filter', async ()=>{
129
+ mockPermissionService.findUserBranches.mockResolvedValue([]);
130
+ await controller.getUserBranches('user-1');
131
+ expect(mockPermissionService.findUserBranches).toHaveBeenCalledWith('user-1', undefined);
132
+ });
133
+ });
134
+ });