@tomei/sso 0.65.0 → 0.65.1-test.1

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 (33) hide show
  1. package/.gitlab-ci.yml +234 -13
  2. package/.husky/commit-msg +0 -0
  3. package/.husky/pre-commit +0 -0
  4. package/__tests__/unit/components/api-key/api-key.spec.ts +201 -0
  5. package/__tests__/unit/components/group/group.spec.ts +6 -0
  6. package/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.ts +9 -1
  7. package/__tests__/unit/components/login-user/login.spec.ts +1199 -5
  8. package/__tests__/unit/components/system/system.spec.ts +1 -0
  9. package/__tests__/unit/components/user-password-history/user-password-history.spec.ts +165 -0
  10. package/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.ts +233 -0
  11. package/coverage/cobertura-coverage.xml +6837 -0
  12. package/coverage/test-report.xml +161 -0
  13. package/dist/__tests__/unit/components/api-key/api-key.spec.d.ts +1 -0
  14. package/dist/__tests__/unit/components/api-key/api-key.spec.js +158 -0
  15. package/dist/__tests__/unit/components/api-key/api-key.spec.js.map +1 -0
  16. package/dist/__tests__/unit/components/group/group.spec.js +4 -0
  17. package/dist/__tests__/unit/components/group/group.spec.js.map +1 -1
  18. package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js +9 -1
  19. package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js.map +1 -1
  20. package/dist/__tests__/unit/components/login-user/login.spec.js +703 -0
  21. package/dist/__tests__/unit/components/login-user/login.spec.js.map +1 -1
  22. package/dist/__tests__/unit/components/system/system.spec.js +1 -0
  23. package/dist/__tests__/unit/components/system/system.spec.js.map +1 -1
  24. package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.d.ts +1 -0
  25. package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.js +138 -0
  26. package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.js.map +1 -0
  27. package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.d.ts +1 -0
  28. package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.js +182 -0
  29. package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.js.map +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/jest.config.js +2 -0
  32. package/migrations/20250805085707-add-bulk-approval-code-to-sso-user.js +29 -0
  33. package/package.json +2 -2
@@ -243,6 +243,7 @@ describe('System', () => {
243
243
  const result = await System.findAll(dbTransaction, loginUser);
244
244
 
245
245
  expect(findAllWithPaginationSpy).toHaveBeenCalledWith({
246
+ distinct: true,
246
247
  where: {},
247
248
  order: [['CreatedAt', 'DESC']],
248
249
  transaction: dbTransaction,
@@ -0,0 +1,165 @@
1
+ import { UserPasswordHistory } from '../../../../src/components/user-password-history/user-password-history';
2
+ import { UserPasswordHistoryRepository } from '../../../../src/components/user-password-history/user-password-history.repository';
3
+ import { PasswordHashService } from '../../../../src/components/password-hash';
4
+ import { ComponentConfig } from '@tomei/config';
5
+ import { ClassError } from '@tomei/general';
6
+
7
+ describe('UserPasswordHistory', () => {
8
+ const historyData = {
9
+ HistoryId: '1',
10
+ UserId: 1,
11
+ PasswordHash: '$argon2id$v=19$m=65536,t=3,p=4$hash1',
12
+ CreatedAt: new Date('2024-01-01'),
13
+ get: (opts?: any) => ({
14
+ HistoryId: '1',
15
+ UserId: 1,
16
+ PasswordHash: '$argon2id$v=19$m=65536,t=3,p=4$hash1',
17
+ CreatedAt: new Date('2024-01-01'),
18
+ }),
19
+ };
20
+
21
+ afterEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ describe('init', () => {
26
+ it('should return new instance when no historyId is provided', async () => {
27
+ const result = await UserPasswordHistory.init();
28
+ expect(result).toBeInstanceOf(UserPasswordHistory);
29
+ expect(result.HistoryId).toBeUndefined();
30
+ });
31
+
32
+ it('should initialize UserPasswordHistory with valid historyId', async () => {
33
+ jest
34
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findByPk')
35
+ .mockResolvedValueOnce(historyData as any);
36
+
37
+ const result = await UserPasswordHistory.init(1);
38
+
39
+ expect(result).toBeInstanceOf(UserPasswordHistory);
40
+ expect(result.UserId).toBe(1);
41
+ });
42
+
43
+ it('should throw ClassError when historyId is not found', async () => {
44
+ jest
45
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findByPk')
46
+ .mockResolvedValueOnce(null);
47
+
48
+ await expect(UserPasswordHistory.init(999)).rejects.toThrow(
49
+ 'UserPasswordHistory not found',
50
+ );
51
+ });
52
+ });
53
+
54
+ describe('validate', () => {
55
+ const passwordHashService = new PasswordHashService();
56
+
57
+ beforeEach(() => {
58
+ jest
59
+ .spyOn(ComponentConfig, 'getComponentConfigValue')
60
+ .mockReturnValue(3 as any);
61
+ });
62
+
63
+ it('should return null when no password history exists', async () => {
64
+ jest
65
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
66
+ .mockResolvedValueOnce([] as any);
67
+
68
+ const result = await UserPasswordHistory.validate(
69
+ null,
70
+ 1,
71
+ 'NewPassword1!',
72
+ passwordHashService,
73
+ );
74
+
75
+ expect(result).toBeNull();
76
+ });
77
+
78
+ it('should throw ClassError when password matches history', async () => {
79
+ jest
80
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
81
+ .mockResolvedValueOnce([historyData] as any);
82
+ jest
83
+ .spyOn(passwordHashService, 'verify')
84
+ .mockResolvedValueOnce(true as any);
85
+
86
+ await expect(
87
+ UserPasswordHistory.validate(null, 1, 'OldPassword1!', passwordHashService),
88
+ ).rejects.toThrow(ClassError);
89
+ });
90
+
91
+ it('should not throw when password does not match any history', async () => {
92
+ jest
93
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
94
+ .mockResolvedValueOnce([historyData] as any);
95
+ jest
96
+ .spyOn(passwordHashService, 'verify')
97
+ .mockResolvedValueOnce(false as any);
98
+
99
+ await expect(
100
+ UserPasswordHistory.validate(null, 1, 'NewPassword1!', passwordHashService),
101
+ ).resolves.not.toThrow();
102
+ });
103
+ });
104
+
105
+ describe('create', () => {
106
+ beforeEach(() => {
107
+ jest
108
+ .spyOn(ComponentConfig, 'getComponentConfigValue')
109
+ .mockReturnValue(3 as any);
110
+ });
111
+
112
+ it('should create a new password history record', async () => {
113
+ const createSpy = jest
114
+ .spyOn(UserPasswordHistoryRepository.prototype, 'create')
115
+ .mockResolvedValueOnce(historyData as any);
116
+ jest
117
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
118
+ .mockResolvedValueOnce([historyData] as any);
119
+
120
+ await UserPasswordHistory.create(null, 1, 'hashedPassword');
121
+
122
+ expect(createSpy).toHaveBeenCalledTimes(1);
123
+ });
124
+
125
+ it('should purge oldest records when history exceeds policy limit', async () => {
126
+ const oldRecords = [
127
+ { HistoryId: '1', CreatedAt: new Date('2024-01-01') },
128
+ { HistoryId: '2', CreatedAt: new Date('2024-02-01') },
129
+ { HistoryId: '3', CreatedAt: new Date('2024-03-01') },
130
+ { HistoryId: '4', CreatedAt: new Date('2024-04-01') },
131
+ ];
132
+
133
+ jest
134
+ .spyOn(UserPasswordHistoryRepository.prototype, 'create')
135
+ .mockResolvedValueOnce(historyData as any);
136
+ jest
137
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
138
+ .mockResolvedValueOnce(oldRecords as any);
139
+ const destroySpy = jest
140
+ .spyOn(UserPasswordHistoryRepository.prototype, 'destroyMultiple')
141
+ .mockResolvedValueOnce(undefined as any);
142
+
143
+ await UserPasswordHistory.create(null, 1, 'hashedPassword');
144
+
145
+ expect(destroySpy).toHaveBeenCalledTimes(1);
146
+ expect(destroySpy).toHaveBeenCalledWith(['4'], null);
147
+ });
148
+
149
+ it('should not purge when history is within policy limit', async () => {
150
+ jest
151
+ .spyOn(UserPasswordHistoryRepository.prototype, 'create')
152
+ .mockResolvedValueOnce(historyData as any);
153
+ jest
154
+ .spyOn(UserPasswordHistoryRepository.prototype, 'findAll')
155
+ .mockResolvedValueOnce([historyData] as any);
156
+ const destroySpy = jest
157
+ .spyOn(UserPasswordHistoryRepository.prototype, 'destroyMultiple')
158
+ .mockResolvedValueOnce(undefined as any);
159
+
160
+ await UserPasswordHistory.create(null, 1, 'hashedPassword');
161
+
162
+ expect(destroySpy).not.toHaveBeenCalled();
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,233 @@
1
+ // LoginUser must be imported first to prime the module cache and avoid circular dependency
2
+ // (user-reporting-hierarchy → user.ts → login-user.ts → user.ts circular)
3
+ import { LoginUser } from '../../../../src/components/login-user/login-user';
4
+ import { User } from '../../../../src/components/login-user/user';
5
+ import { UserReportingHierarchy } from '../../../../src/components/user-reporting-hierarchy/user-reporting-hierarchy';
6
+ import { UserReportingHierarchyRepository } from '../../../../src/components/user-reporting-hierarchy/user-reporting-hierarchy.repository';
7
+ import { ApplicationConfig } from '@tomei/config';
8
+ import { Activity } from '@tomei/activity-history';
9
+ import { ClassError } from '@tomei/general';
10
+
11
+ describe('UserReportingHierarchy', () => {
12
+ const loginUser = new (LoginUser.prototype as any).constructor();
13
+ loginUser.ObjectId = '1';
14
+ loginUser.UserId = 1;
15
+
16
+ const hierarchyData = {
17
+ UserReportingHierarchyId: 1,
18
+ ReportingUserId: 10,
19
+ UserId: 20,
20
+ Rank: 1,
21
+ Status: 'Active',
22
+ CreatedById: 1,
23
+ CreatedAt: new Date('2024-01-01'),
24
+ UpdatedById: 1,
25
+ UpdatedAt: new Date('2024-01-01'),
26
+ get: (opts?: any) => ({
27
+ UserReportingHierarchyId: 1,
28
+ ReportingUserId: 10,
29
+ UserId: 20,
30
+ Rank: 1,
31
+ Status: 'Active',
32
+ CreatedById: 1,
33
+ CreatedAt: new Date('2024-01-01'),
34
+ UpdatedById: 1,
35
+ UpdatedAt: new Date('2024-01-01'),
36
+ }),
37
+ };
38
+
39
+ afterEach(() => {
40
+ jest.clearAllMocks();
41
+ });
42
+
43
+ beforeEach(() => {
44
+ jest
45
+ .spyOn(ApplicationConfig, 'getComponentConfigValue')
46
+ .mockReturnValue('TST' as any);
47
+ jest
48
+ .spyOn(Activity.prototype, 'create')
49
+ .mockResolvedValue(undefined as any);
50
+ jest
51
+ .spyOn(LoginUser.prototype, 'checkPrivileges')
52
+ .mockResolvedValue(true as any);
53
+ });
54
+
55
+ describe('init', () => {
56
+ it('should return a new instance when no id is provided', async () => {
57
+ const result = await UserReportingHierarchy.init();
58
+ expect(result).toBeInstanceOf(UserReportingHierarchy);
59
+ expect(result.UserReportingHierarchyId).toBeNaN();
60
+ });
61
+
62
+ it('should initialize with valid id', async () => {
63
+ jest
64
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findByPk')
65
+ .mockResolvedValueOnce(hierarchyData as any);
66
+
67
+ const result = await UserReportingHierarchy.init(1);
68
+
69
+ expect(result).toBeInstanceOf(UserReportingHierarchy);
70
+ expect(result.ReportingUserId).toBe(10);
71
+ expect(result.UserId).toBe(20);
72
+ expect(result.Rank).toBe(1);
73
+ });
74
+
75
+ it('should throw ClassError when id is not found', async () => {
76
+ jest
77
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findByPk')
78
+ .mockResolvedValueOnce(null);
79
+
80
+ await expect(UserReportingHierarchy.init(999)).rejects.toThrow(
81
+ 'UserReportingHierarchy not found',
82
+ );
83
+ });
84
+ });
85
+
86
+ describe('createUserReportingHierarchy', () => {
87
+ it('should throw when user lacks USER_REPORTING_HIERARCHY_CREATE privilege', async () => {
88
+ jest
89
+ .spyOn(LoginUser.prototype, 'checkPrivileges')
90
+ .mockResolvedValueOnce(false as any);
91
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
92
+
93
+ const hierarchy = await UserReportingHierarchy.init();
94
+ await expect(
95
+ hierarchy.createUserReportingHierarchy(loginUser, null, 10, 20, 1, 'Active'),
96
+ ).rejects.toThrow('User does not have the required privileges');
97
+ });
98
+
99
+ it('should throw when relationship already exists', async () => {
100
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
101
+ jest
102
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findOne')
103
+ .mockResolvedValueOnce(hierarchyData as any);
104
+
105
+ const hierarchy = await UserReportingHierarchy.init();
106
+ await expect(
107
+ hierarchy.createUserReportingHierarchy(loginUser, null, 10, 20, 1, 'Active'),
108
+ ).rejects.toThrow('Relationship already exists');
109
+ });
110
+
111
+ it('should throw when rank already exists', async () => {
112
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
113
+ jest
114
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findOne')
115
+ .mockResolvedValueOnce(null) // no existing relationship
116
+ .mockResolvedValueOnce(hierarchyData as any); // rank exists
117
+
118
+ const hierarchy = await UserReportingHierarchy.init();
119
+ await expect(
120
+ hierarchy.createUserReportingHierarchy(loginUser, null, 10, 20, 1, 'Active'),
121
+ ).rejects.toThrow('Rank already exists');
122
+ });
123
+
124
+ it('should throw when previous rank is not yet assigned (rank > 1)', async () => {
125
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
126
+ jest
127
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findOne')
128
+ .mockResolvedValueOnce(null) // no existing relationship
129
+ .mockResolvedValueOnce(null) // rank 2 not taken
130
+ .mockResolvedValueOnce(null); // rank 1 (predecessor) not found
131
+
132
+ const hierarchy = await UserReportingHierarchy.init();
133
+ await expect(
134
+ hierarchy.createUserReportingHierarchy(loginUser, null, 10, 20, 2, 'Active'),
135
+ ).rejects.toThrow('Rank before the new rank is not yet assigned to the user');
136
+ });
137
+
138
+ it('should create hierarchy successfully', async () => {
139
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
140
+ jest
141
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findOne')
142
+ .mockResolvedValueOnce(null) // no existing relationship
143
+ .mockResolvedValueOnce(null); // rank 1 not taken
144
+ const createSpy = jest
145
+ .spyOn(UserReportingHierarchyRepository.prototype, 'create')
146
+ .mockResolvedValueOnce(hierarchyData as any);
147
+
148
+ const hierarchy = await UserReportingHierarchy.init();
149
+ const result = await hierarchy.createUserReportingHierarchy(
150
+ loginUser,
151
+ null,
152
+ 10,
153
+ 20,
154
+ 1,
155
+ 'Active',
156
+ );
157
+
158
+ expect(createSpy).toHaveBeenCalledTimes(1);
159
+ expect(result).toBeInstanceOf(UserReportingHierarchy);
160
+ });
161
+ });
162
+
163
+ describe('updateUserReportingHierarchy', () => {
164
+ it('should throw when user lacks USER_REPORTING_HIERARCHY_UPDATE privilege', async () => {
165
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
166
+ jest
167
+ .spyOn(LoginUser.prototype, 'checkPrivileges')
168
+ .mockResolvedValueOnce(false as any);
169
+
170
+ jest
171
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findByPk')
172
+ .mockResolvedValueOnce(hierarchyData as any);
173
+
174
+ const hierarchy = await UserReportingHierarchy.init(1);
175
+ await expect(
176
+ hierarchy.updateUserReportingHierarchy(loginUser, null, 10, 20, 1, 'Active'),
177
+ ).rejects.toThrow('User does not have the required privileges');
178
+ });
179
+
180
+ it('should update successfully', async () => {
181
+ jest.spyOn(User, 'init').mockResolvedValue(loginUser as any);
182
+ jest
183
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findByPk')
184
+ .mockResolvedValueOnce(hierarchyData as any);
185
+ jest
186
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findOne')
187
+ .mockResolvedValueOnce(null) // no duplicate relationship
188
+ .mockResolvedValueOnce(null); // rank 1 not taken by other
189
+ const updateSpy = jest
190
+ .spyOn(UserReportingHierarchyRepository.prototype, 'update')
191
+ .mockResolvedValueOnce(undefined as any);
192
+
193
+ const hierarchy = await UserReportingHierarchy.init(1);
194
+ const result = await hierarchy.updateUserReportingHierarchy(
195
+ loginUser,
196
+ null,
197
+ 10,
198
+ 20,
199
+ 1,
200
+ 'Active',
201
+ );
202
+
203
+ expect(updateSpy).toHaveBeenCalledTimes(1);
204
+ expect(result).toBeInstanceOf(UserReportingHierarchy);
205
+ });
206
+ });
207
+
208
+ describe('removeUserReportingHierarchy', () => {
209
+ it('should throw when user lacks USER_REPORTING_HIERARCHY_REMOVE privilege', async () => {
210
+ jest
211
+ .spyOn(LoginUser.prototype, 'checkPrivileges')
212
+ .mockResolvedValueOnce(false as any);
213
+
214
+ await expect(
215
+ UserReportingHierarchy.removeUserReportingHierarchy(loginUser, null, 1),
216
+ ).rejects.toThrow('Insufficient privileges to remove reporting hierarchy');
217
+ });
218
+
219
+ it('should remove hierarchy successfully', async () => {
220
+ jest
221
+ .spyOn(UserReportingHierarchyRepository.prototype, 'findByPk')
222
+ .mockResolvedValueOnce(hierarchyData as any);
223
+ const destroySpy = jest
224
+ .spyOn(UserReportingHierarchyRepository.prototype, 'destroy')
225
+ .mockResolvedValueOnce(undefined as any);
226
+
227
+ await UserReportingHierarchy.removeUserReportingHierarchy(loginUser, null, 1);
228
+
229
+ expect(destroySpy).toHaveBeenCalledTimes(1);
230
+ expect(destroySpy).toHaveBeenCalledWith(1, null);
231
+ });
232
+ });
233
+ });