@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,198 @@
1
+ import { ConflictException, NotFoundException } from '@nestjs/common';
2
+ import { createMockDataSourceProvider, createMockRepository } from '@test-utils/mocks/repository.mock';
3
+ import { UserPermissionService } from './user-permission.service';
4
+ import { PermissionType } from '../entities/user-company-permission.entity';
5
+ function buildPermission(overrides = {}) {
6
+ return {
7
+ id: 'perm-uuid-1',
8
+ userId: 'user-1',
9
+ permissionType: PermissionType.COMPANY,
10
+ targetId: 'company-1',
11
+ isActive: true,
12
+ createdAt: new Date(),
13
+ updatedAt: new Date(),
14
+ ...overrides
15
+ };
16
+ }
17
+ describe('UserPermissionService', ()=>{
18
+ let service;
19
+ let mockRepo;
20
+ let mockPermissionSyncAdapter;
21
+ beforeEach(()=>{
22
+ mockRepo = createMockRepository();
23
+ // The shared query builder mock does not include join helpers used by this service.
24
+ Object.assign(mockRepo.createQueryBuilder(), {
25
+ innerJoin: jest.fn().mockReturnThis(),
26
+ leftJoinAndSelect: jest.fn().mockReturnThis()
27
+ });
28
+ const mockDataSourceProvider = createMockDataSourceProvider(mockRepo);
29
+ mockPermissionSyncAdapter = {
30
+ onCompanyDeleted: jest.fn(),
31
+ onBranchDeleted: jest.fn(),
32
+ onUserCompanyAccessRevoked: jest.fn(),
33
+ onUserBranchAccessRevoked: jest.fn()
34
+ };
35
+ service = new UserPermissionService(mockDataSourceProvider, mockPermissionSyncAdapter);
36
+ });
37
+ describe('findUserPermissions', ()=>{
38
+ it('finds company permissions with the company relation', async ()=>{
39
+ mockRepo.find.mockResolvedValue([
40
+ buildPermission()
41
+ ]);
42
+ const result = await service.findUserPermissions('user-1', PermissionType.COMPANY);
43
+ expect(result).toHaveLength(1);
44
+ expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
45
+ where: {
46
+ userId: 'user-1',
47
+ permissionType: PermissionType.COMPANY
48
+ },
49
+ relations: [
50
+ 'company'
51
+ ]
52
+ }));
53
+ });
54
+ it('finds branch permissions with the branch relation', async ()=>{
55
+ mockRepo.find.mockResolvedValue([]);
56
+ await service.findUserPermissions('user-1', PermissionType.BRANCH);
57
+ expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
58
+ relations: [
59
+ 'branch'
60
+ ]
61
+ }));
62
+ });
63
+ });
64
+ describe('assignPermission', ()=>{
65
+ it('creates and saves a new permission when none exists', async ()=>{
66
+ mockRepo.findOne.mockResolvedValue(null);
67
+ mockRepo.create.mockReturnValue(buildPermission());
68
+ mockRepo.save.mockResolvedValue(buildPermission());
69
+ await service.assignPermission('user-1', 'company-1', PermissionType.COMPANY);
70
+ expect(mockRepo.create).toHaveBeenCalledWith({
71
+ userId: 'user-1',
72
+ permissionType: PermissionType.COMPANY,
73
+ targetId: 'company-1'
74
+ });
75
+ expect(mockRepo.save).toHaveBeenCalled();
76
+ });
77
+ it('throws ConflictException when the permission already exists', async ()=>{
78
+ mockRepo.findOne.mockResolvedValue(buildPermission());
79
+ await expect(service.assignPermission('user-1', 'company-1', PermissionType.COMPANY)).rejects.toThrow(ConflictException);
80
+ expect(mockRepo.save).not.toHaveBeenCalled();
81
+ });
82
+ it('translates a duplicate-entry database error into ConflictException', async ()=>{
83
+ mockRepo.findOne.mockResolvedValue(null);
84
+ mockRepo.create.mockReturnValue(buildPermission());
85
+ mockRepo.save.mockRejectedValue({
86
+ code: 'ER_DUP_ENTRY',
87
+ message: 'Duplicate entry'
88
+ });
89
+ await expect(service.assignPermission('user-1', 'company-1', PermissionType.COMPANY)).rejects.toThrow(ConflictException);
90
+ });
91
+ it('rethrows unrelated database errors from save', async ()=>{
92
+ mockRepo.findOne.mockResolvedValue(null);
93
+ mockRepo.create.mockReturnValue(buildPermission());
94
+ const dbError = new Error('connection lost');
95
+ mockRepo.save.mockRejectedValue(dbError);
96
+ await expect(service.assignPermission('user-1', 'company-1', PermissionType.COMPANY)).rejects.toThrow('connection lost');
97
+ });
98
+ });
99
+ describe('revokePermission', ()=>{
100
+ it('throws NotFoundException when the permission does not exist', async ()=>{
101
+ mockRepo.findOne.mockResolvedValue(null);
102
+ await expect(service.revokePermission('user-1', 'company-1', PermissionType.COMPANY)).rejects.toThrow(NotFoundException);
103
+ });
104
+ it('removes a company permission and cascades revocation to its branches', async ()=>{
105
+ const companyPermission = buildPermission({
106
+ permissionType: PermissionType.COMPANY
107
+ });
108
+ mockRepo.findOne.mockResolvedValue(companyPermission);
109
+ mockRepo.remove.mockResolvedValue(companyPermission);
110
+ const branchPermission = buildPermission({
111
+ id: 'perm-branch-1',
112
+ permissionType: PermissionType.BRANCH,
113
+ targetId: 'branch-1'
114
+ });
115
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([
116
+ branchPermission
117
+ ]);
118
+ await service.revokePermission('user-1', 'company-1', PermissionType.COMPANY);
119
+ expect(mockRepo.remove).toHaveBeenCalledWith(companyPermission);
120
+ expect(mockRepo.remove).toHaveBeenCalledWith([
121
+ branchPermission
122
+ ]);
123
+ expect(mockPermissionSyncAdapter.onUserCompanyAccessRevoked).toHaveBeenCalledWith('user-1', 'company-1');
124
+ expect(mockPermissionSyncAdapter.onUserBranchAccessRevoked).toHaveBeenCalledWith('user-1', 'branch-1', 'company-1');
125
+ });
126
+ it('removes a branch permission and notifies the sync adapter with its company id', async ()=>{
127
+ const branchPermission = buildPermission({
128
+ permissionType: PermissionType.BRANCH,
129
+ targetId: 'branch-1',
130
+ branch: {
131
+ companyId: 'company-1'
132
+ }
133
+ });
134
+ mockRepo.findOne.mockResolvedValue(branchPermission);
135
+ mockRepo.remove.mockResolvedValue(branchPermission);
136
+ await service.revokePermission('user-1', 'branch-1', PermissionType.BRANCH);
137
+ expect(mockRepo.remove).toHaveBeenCalledWith(branchPermission);
138
+ expect(mockPermissionSyncAdapter.onUserBranchAccessRevoked).toHaveBeenCalledWith('user-1', 'branch-1', 'company-1');
139
+ });
140
+ it('does not notify the sync adapter for a branch permission with no resolvable company', async ()=>{
141
+ const branchPermission = buildPermission({
142
+ permissionType: PermissionType.BRANCH,
143
+ targetId: 'branch-1',
144
+ branch: undefined
145
+ });
146
+ mockRepo.findOne.mockResolvedValue(branchPermission);
147
+ mockRepo.remove.mockResolvedValue(branchPermission);
148
+ await service.revokePermission('user-1', 'branch-1', PermissionType.BRANCH);
149
+ expect(mockPermissionSyncAdapter.onUserBranchAccessRevoked).not.toHaveBeenCalled();
150
+ });
151
+ it('works without a permission sync adapter configured', async ()=>{
152
+ service = new UserPermissionService(createMockDataSourceProvider(mockRepo), undefined);
153
+ const companyPermission = buildPermission({
154
+ permissionType: PermissionType.COMPANY
155
+ });
156
+ mockRepo.findOne.mockResolvedValue(companyPermission);
157
+ mockRepo.remove.mockResolvedValue(companyPermission);
158
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([]);
159
+ await expect(service.revokePermission('user-1', 'company-1', PermissionType.COMPANY)).resolves.toBeUndefined();
160
+ });
161
+ });
162
+ describe('findUserCompanies', ()=>{
163
+ it('delegates to findUserPermissions with COMPANY type', async ()=>{
164
+ mockRepo.find.mockResolvedValue([
165
+ buildPermission()
166
+ ]);
167
+ const result = await service.findUserCompanies('user-1');
168
+ expect(result).toHaveLength(1);
169
+ expect(mockRepo.find).toHaveBeenCalledWith(expect.objectContaining({
170
+ where: {
171
+ userId: 'user-1',
172
+ permissionType: PermissionType.COMPANY
173
+ }
174
+ }));
175
+ });
176
+ });
177
+ describe('findUserBranches', ()=>{
178
+ it('queries branch permissions for a user without a company filter', async ()=>{
179
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([]);
180
+ await service.findUserBranches('user-1');
181
+ const qb = mockRepo.createQueryBuilder();
182
+ expect(qb.where).toHaveBeenCalledWith('permission.userId = :userId', {
183
+ userId: 'user-1'
184
+ });
185
+ expect(qb.andWhere).toHaveBeenCalledWith('permission.permissionType = :type', {
186
+ type: PermissionType.BRANCH
187
+ });
188
+ });
189
+ it('applies an additional company filter when companyId is provided', async ()=>{
190
+ mockRepo.createQueryBuilder().getMany.mockResolvedValue([]);
191
+ await service.findUserBranches('user-1', 'company-1');
192
+ const qb = mockRepo.createQueryBuilder();
193
+ expect(qb.andWhere).toHaveBeenCalledWith('branch.companyId = :companyId', {
194
+ companyId: 'company-1'
195
+ });
196
+ });
197
+ });
198
+ });