@tomei/sso 0.65.1-test.1 → 0.65.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.
- package/.gitlab-ci.yml +33 -214
- package/__tests__/unit/components/group/group.spec.ts +0 -6
- package/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.ts +1 -9
- package/__tests__/unit/components/login-user/login.spec.ts +5 -1199
- package/__tests__/unit/components/login-user/password-expiry.spec.ts +87 -0
- package/__tests__/unit/components/system/system.spec.ts +0 -1
- package/dist/__tests__/unit/components/group/group.spec.js +0 -4
- package/dist/__tests__/unit/components/group/group.spec.js.map +1 -1
- package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js +1 -9
- package/dist/__tests__/unit/components/group-reporting-user/group-reporting-user.spec.js.map +1 -1
- package/dist/__tests__/unit/components/login-user/login.spec.js +0 -703
- package/dist/__tests__/unit/components/login-user/login.spec.js.map +1 -1
- package/dist/__tests__/unit/components/login-user/password-expiry.spec.js +86 -0
- package/dist/__tests__/unit/components/login-user/password-expiry.spec.js.map +1 -0
- package/dist/__tests__/unit/components/system/system.spec.js +0 -1
- package/dist/__tests__/unit/components/system/system.spec.js.map +1 -1
- package/dist/src/components/login-user/user.d.ts +5 -0
- package/dist/src/components/login-user/user.js +44 -0
- package/dist/src/components/login-user/user.js.map +1 -1
- package/dist/src/components/user-password-history/user-password-history.d.ts +1 -0
- package/dist/src/components/user-password-history/user-password-history.js +41 -2
- package/dist/src/components/user-password-history/user-password-history.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jest.config.js +0 -2
- package/package.json +2 -2
- package/src/components/login-user/user.ts +78 -0
- package/src/components/user-password-history/user-password-history.ts +57 -6
- package/__tests__/unit/components/api-key/api-key.spec.ts +0 -201
- package/__tests__/unit/components/user-password-history/user-password-history.spec.ts +0 -165
- package/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.ts +0 -233
- package/coverage/cobertura-coverage.xml +0 -6837
- package/coverage/test-report.xml +0 -161
- package/dist/__tests__/unit/components/api-key/api-key.spec.js +0 -158
- package/dist/__tests__/unit/components/api-key/api-key.spec.js.map +0 -1
- package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.d.ts +0 -1
- package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.js +0 -138
- package/dist/__tests__/unit/components/user-password-history/user-password-history.spec.js.map +0 -1
- package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.d.ts +0 -1
- package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.js +0 -182
- package/dist/__tests__/unit/components/user-reporting-hierarchy/user-reporting-hierarchy.spec.js.map +0 -1
- package/migrations/20250805085707-add-bulk-approval-code-to-sso-user.js +0 -29
- /package/dist/__tests__/unit/components/{api-key/api-key.spec.d.ts → login-user/password-expiry.spec.d.ts} +0 -0
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomei/sso",
|
|
3
|
-
"version": "0.65.1
|
|
3
|
+
"version": "0.65.1",
|
|
4
4
|
"description": "Tomei SSO Package",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start:dev": "tsc -w",
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"prepare": "husky
|
|
9
|
+
"prepare": "husky",
|
|
10
10
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
11
11
|
"lint": "npx eslint . --fix",
|
|
12
12
|
"test": "jest --forceExit --detectOpenHandles --coverage"
|
|
@@ -2988,6 +2988,7 @@ export class User extends UserBase {
|
|
|
2988
2988
|
|
|
2989
2989
|
this.Password = passwordHash;
|
|
2990
2990
|
this.LastPasswordChangedAt = new Date();
|
|
2991
|
+
this.NeedToChangePasswordYN = YN.No;
|
|
2991
2992
|
|
|
2992
2993
|
const entityValueAfter: IUserAttr = {
|
|
2993
2994
|
UserId: this.UserId,
|
|
@@ -3022,6 +3023,7 @@ export class User extends UserBase {
|
|
|
3022
3023
|
{
|
|
3023
3024
|
Password: this.Password,
|
|
3024
3025
|
LastPasswordChangedAt: this.LastPasswordChangedAt,
|
|
3026
|
+
NeedToChangePasswordYN: this.NeedToChangePasswordYN,
|
|
3025
3027
|
},
|
|
3026
3028
|
{
|
|
3027
3029
|
where: {
|
|
@@ -3053,6 +3055,82 @@ export class User extends UserBase {
|
|
|
3053
3055
|
}
|
|
3054
3056
|
}
|
|
3055
3057
|
|
|
3058
|
+
// PASSWORD_EXPIRY_MONTHS=0 disables the policy entirely (AC#1) - checked before NeedToChangePasswordYN/history.
|
|
3059
|
+
async getPasswordExpiryStatus(dbTransaction?: any): Promise<{
|
|
3060
|
+
expired: boolean;
|
|
3061
|
+
expiringSoon: boolean;
|
|
3062
|
+
expiresInDays: number | null;
|
|
3063
|
+
}> {
|
|
3064
|
+
try {
|
|
3065
|
+
const expiryMonthsRaw = Number(process.env.PASSWORD_EXPIRY_MONTHS);
|
|
3066
|
+
const expiryMonths = Number.isFinite(expiryMonthsRaw)
|
|
3067
|
+
? Math.max(0, Math.floor(expiryMonthsRaw))
|
|
3068
|
+
: 0;
|
|
3069
|
+
|
|
3070
|
+
if (expiryMonths === 0) {
|
|
3071
|
+
return { expired: false, expiringSoon: false, expiresInDays: null };
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// Accounts authenticated without a local password cannot rotate one here.
|
|
3075
|
+
if (!this.Password) {
|
|
3076
|
+
return { expired: false, expiringSoon: false, expiresInDays: null };
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
// Forces first-login change for temp-password/admin-created accounts (AC#4), regardless of password age.
|
|
3080
|
+
if (this.NeedToChangePasswordYN === YN.Yes) {
|
|
3081
|
+
return { expired: true, expiringSoon: false, expiresInDays: null };
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const warningDaysRaw = Number(process.env.PASSWORD_EXPIRY_WARNING_DAYS);
|
|
3085
|
+
const warningDays = Number.isFinite(warningDaysRaw)
|
|
3086
|
+
? Math.max(0, Math.floor(warningDaysRaw))
|
|
3087
|
+
: 14;
|
|
3088
|
+
|
|
3089
|
+
const latestHistory = await UserPasswordHistory.getLatestByUserId(
|
|
3090
|
+
dbTransaction,
|
|
3091
|
+
this.UserId,
|
|
3092
|
+
);
|
|
3093
|
+
|
|
3094
|
+
if (!latestHistory) {
|
|
3095
|
+
// Legacy/pre-existing accounts without a history row.
|
|
3096
|
+
return { expired: true, expiringSoon: false, expiresInDays: null };
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
const createdAt = latestHistory.CreatedAt;
|
|
3100
|
+
const expiryMonth = createdAt.getUTCMonth() + expiryMonths;
|
|
3101
|
+
const lastDayOfExpiryMonth = new Date(
|
|
3102
|
+
Date.UTC(createdAt.getUTCFullYear(), expiryMonth + 1, 0),
|
|
3103
|
+
).getUTCDate();
|
|
3104
|
+
const expiryDate = new Date(
|
|
3105
|
+
Date.UTC(
|
|
3106
|
+
createdAt.getUTCFullYear(),
|
|
3107
|
+
expiryMonth,
|
|
3108
|
+
Math.min(createdAt.getUTCDate(), lastDayOfExpiryMonth),
|
|
3109
|
+
createdAt.getUTCHours(),
|
|
3110
|
+
createdAt.getUTCMinutes(),
|
|
3111
|
+
createdAt.getUTCSeconds(),
|
|
3112
|
+
createdAt.getUTCMilliseconds(),
|
|
3113
|
+
),
|
|
3114
|
+
);
|
|
3115
|
+
|
|
3116
|
+
const now = new Date();
|
|
3117
|
+
if (now.getTime() >= expiryDate.getTime()) {
|
|
3118
|
+
return { expired: true, expiringSoon: false, expiresInDays: null };
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
3122
|
+
const expiringSoon =
|
|
3123
|
+
now.getTime() >= expiryDate.getTime() - warningDays * msPerDay;
|
|
3124
|
+
const expiresInDays = Math.ceil(
|
|
3125
|
+
(expiryDate.getTime() - now.getTime()) / msPerDay,
|
|
3126
|
+
);
|
|
3127
|
+
|
|
3128
|
+
return { expired: false, expiringSoon, expiresInDays };
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
throw error;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3056
3134
|
async verifyPasscode(passcode: string) {
|
|
3057
3135
|
try {
|
|
3058
3136
|
// Return error if this._Userid is empty "User is not created yet."
|
|
@@ -76,16 +76,30 @@ export class UserPasswordHistory
|
|
|
76
76
|
): Promise<void> {
|
|
77
77
|
// This method used to check if password entered is valid by checking previous password history
|
|
78
78
|
try {
|
|
79
|
-
// Part 1-2: Retrieve password history policy
|
|
79
|
+
// Part 1-2: Retrieve password history policy: env var takes precedence over component config, call ComponentConfig. by passing:
|
|
80
80
|
// - ComponentName: "@tomei/sso"
|
|
81
81
|
// - ConfigKey: "passwordHistory"
|
|
82
82
|
// If no password history set, use default value 3
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const envPasswordHistoryPolicy = Number(
|
|
85
|
+
process.env.PASSWORD_HISTORY_LIMIT,
|
|
86
|
+
);
|
|
87
|
+
const componentPasswordHistoryPolicy = Number(
|
|
85
88
|
ComponentConfig.getComponentConfigValue(
|
|
86
89
|
'@tomei/sso',
|
|
87
90
|
'passwordHistory',
|
|
88
|
-
)
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
const passwordHistoryPolicy =
|
|
94
|
+
(Number.isFinite(envPasswordHistoryPolicy) &&
|
|
95
|
+
envPasswordHistoryPolicy > 0
|
|
96
|
+
? Math.floor(envPasswordHistoryPolicy)
|
|
97
|
+
: 0) ||
|
|
98
|
+
(Number.isFinite(componentPasswordHistoryPolicy) &&
|
|
99
|
+
componentPasswordHistoryPolicy > 0
|
|
100
|
+
? Math.floor(componentPasswordHistoryPolicy)
|
|
101
|
+
: 0) ||
|
|
102
|
+
3;
|
|
89
103
|
|
|
90
104
|
// Part 3-4: Retrieve records from the table by using class._repo findAll() by passing:
|
|
91
105
|
// where: { UserId: params.UserId }
|
|
@@ -137,16 +151,30 @@ export class UserPasswordHistory
|
|
|
137
151
|
): Promise<void> {
|
|
138
152
|
// This method used to check if password entered is valid by checking previous password history
|
|
139
153
|
try {
|
|
140
|
-
// Part 1-2: Retrieve password history policy
|
|
154
|
+
// Part 1-2: Retrieve password history policy: env var takes precedence over component config, call ComponentConfig. by passing:
|
|
141
155
|
// - ComponentName: "@tomei/sso"
|
|
142
156
|
// - ConfigKey: "passwordHistory"
|
|
143
157
|
// If no password history set, use default value 3
|
|
144
158
|
|
|
145
|
-
const
|
|
159
|
+
const envPasswordHistoryPolicy = Number(
|
|
160
|
+
process.env.PASSWORD_HISTORY_LIMIT,
|
|
161
|
+
);
|
|
162
|
+
const componentPasswordHistoryPolicy = Number(
|
|
146
163
|
ComponentConfig.getComponentConfigValue(
|
|
147
164
|
'@tomei/sso',
|
|
148
165
|
'passwordHistory',
|
|
149
|
-
)
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
const passwordHistoryPolicy =
|
|
169
|
+
(Number.isFinite(envPasswordHistoryPolicy) &&
|
|
170
|
+
envPasswordHistoryPolicy > 0
|
|
171
|
+
? Math.floor(envPasswordHistoryPolicy)
|
|
172
|
+
: 0) ||
|
|
173
|
+
(Number.isFinite(componentPasswordHistoryPolicy) &&
|
|
174
|
+
componentPasswordHistoryPolicy > 0
|
|
175
|
+
? Math.floor(componentPasswordHistoryPolicy)
|
|
176
|
+
: 0) ||
|
|
177
|
+
3;
|
|
150
178
|
|
|
151
179
|
// Part 3: Insert new password history by calling class _repo create() method.
|
|
152
180
|
const userPasswordHistory = new UserPasswordHistory();
|
|
@@ -184,4 +212,27 @@ export class UserPasswordHistory
|
|
|
184
212
|
throw error;
|
|
185
213
|
}
|
|
186
214
|
}
|
|
215
|
+
|
|
216
|
+
// Latest history row's CreatedAt is the source of truth for password age (used by User.getPasswordExpiryStatus()).
|
|
217
|
+
public static async getLatestByUserId(
|
|
218
|
+
dbTransaction: any,
|
|
219
|
+
UserId: number,
|
|
220
|
+
): Promise<UserPasswordHistory | null> {
|
|
221
|
+
try {
|
|
222
|
+
const passwordHistory = await UserPasswordHistory._Repo.findAll({
|
|
223
|
+
where: { UserId: UserId },
|
|
224
|
+
order: [['CreatedAt', 'DESC']],
|
|
225
|
+
limit: 1,
|
|
226
|
+
transaction: dbTransaction,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!passwordHistory || passwordHistory.length < 1) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new UserPasswordHistory(passwordHistory[0].get({ plain: true }));
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
187
238
|
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { APIKey } from '../../../../src/components/api-key/api-key';
|
|
2
|
-
import { APIKeyRepository } from '../../../../src/components/api-key/api-key.repository';
|
|
3
|
-
import { LoginUser } from '../../../../src/components/login-user/login-user';
|
|
4
|
-
import { ApplicationConfig } from '@tomei/config';
|
|
5
|
-
import { Activity } from '@tomei/activity-history';
|
|
6
|
-
import { ClassError } from '@tomei/general';
|
|
7
|
-
import { System } from '../../../../src/components/system/system';
|
|
8
|
-
import { APIKeyStatusEnum } from '../../../../src/enum/api-key.enum';
|
|
9
|
-
|
|
10
|
-
describe('APIKey', () => {
|
|
11
|
-
const loginUser = new (LoginUser.prototype as any).constructor();
|
|
12
|
-
loginUser.ObjectId = '1';
|
|
13
|
-
loginUser.UserId = 1;
|
|
14
|
-
|
|
15
|
-
const apiKeyData = {
|
|
16
|
-
APIKeyId: 1,
|
|
17
|
-
ApiKey: 'abc123def456',
|
|
18
|
-
Name: 'Test Key',
|
|
19
|
-
SystemCode: 'TST',
|
|
20
|
-
Description: 'Test description',
|
|
21
|
-
Status: APIKeyStatusEnum.ACTIVE,
|
|
22
|
-
ExpirationDate: new Date('2027-01-01'),
|
|
23
|
-
RevokedById: null,
|
|
24
|
-
RevokedAt: null,
|
|
25
|
-
CreatedAt: new Date('2024-01-01'),
|
|
26
|
-
CreatedById: 1,
|
|
27
|
-
get: function (opts?: any) {
|
|
28
|
-
if (opts?.plain) return this;
|
|
29
|
-
return this;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
afterEach(() => {
|
|
34
|
-
jest.clearAllMocks();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
beforeEach(() => {
|
|
38
|
-
jest
|
|
39
|
-
.spyOn(ApplicationConfig, 'getComponentConfigValue')
|
|
40
|
-
.mockReturnValue('TST' as any);
|
|
41
|
-
jest
|
|
42
|
-
.spyOn(Activity.prototype, 'create')
|
|
43
|
-
.mockResolvedValue(undefined as any);
|
|
44
|
-
jest
|
|
45
|
-
.spyOn(LoginUser.prototype, 'checkPrivileges')
|
|
46
|
-
.mockResolvedValue(true as any);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe('init', () => {
|
|
50
|
-
it('should return a new APIKey instance when no ApiKeyId is provided', async () => {
|
|
51
|
-
const result = await APIKey.init();
|
|
52
|
-
expect(result).toBeInstanceOf(APIKey);
|
|
53
|
-
expect(result.APIKeyId).toBeNaN();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should initialize APIKey with valid ApiKeyId', async () => {
|
|
57
|
-
jest
|
|
58
|
-
.spyOn(APIKeyRepository.prototype, 'findByPk')
|
|
59
|
-
.mockResolvedValueOnce(apiKeyData as any);
|
|
60
|
-
|
|
61
|
-
const result = await APIKey.init(1);
|
|
62
|
-
|
|
63
|
-
expect(result).toBeInstanceOf(APIKey);
|
|
64
|
-
expect(result.Name).toBe('Test Key');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should throw ClassError when ApiKeyId is not found', async () => {
|
|
68
|
-
jest
|
|
69
|
-
.spyOn(APIKeyRepository.prototype, 'findByPk')
|
|
70
|
-
.mockResolvedValueOnce(null);
|
|
71
|
-
|
|
72
|
-
await expect(APIKey.init(999)).rejects.toThrow('APIKey not found');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('generate', () => {
|
|
77
|
-
it('should throw when user lacks API_KEY_CREATE privilege', async () => {
|
|
78
|
-
jest
|
|
79
|
-
.spyOn(LoginUser.prototype, 'checkPrivileges')
|
|
80
|
-
.mockResolvedValueOnce(false as any);
|
|
81
|
-
|
|
82
|
-
const apiKey = await APIKey.init();
|
|
83
|
-
apiKey.SystemCode = 'TST';
|
|
84
|
-
|
|
85
|
-
await expect(apiKey.generate(loginUser)).rejects.toThrow(
|
|
86
|
-
'User does not have privilege to generate API key.',
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should generate an API key successfully', async () => {
|
|
91
|
-
jest
|
|
92
|
-
.spyOn(System, 'init')
|
|
93
|
-
.mockResolvedValueOnce(new (System.prototype as any).constructor() as any);
|
|
94
|
-
jest
|
|
95
|
-
.spyOn(APIKeyRepository.prototype, 'create')
|
|
96
|
-
.mockResolvedValueOnce({ ...apiKeyData, APIKeyId: 1 } as any);
|
|
97
|
-
|
|
98
|
-
const apiKey = await APIKey.init();
|
|
99
|
-
apiKey.SystemCode = 'TST';
|
|
100
|
-
apiKey.Name = 'Test Key';
|
|
101
|
-
apiKey.Status = APIKeyStatusEnum.ACTIVE;
|
|
102
|
-
|
|
103
|
-
const result = await apiKey.generate(loginUser);
|
|
104
|
-
|
|
105
|
-
expect(result).toHaveProperty('ApiKey');
|
|
106
|
-
expect(result.Name).toBe('Test Key');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('findAll', () => {
|
|
111
|
-
it('should throw when user lacks API_KEY_VIEW privilege', async () => {
|
|
112
|
-
jest
|
|
113
|
-
.spyOn(LoginUser.prototype, 'checkPrivileges')
|
|
114
|
-
.mockResolvedValueOnce(false as any);
|
|
115
|
-
|
|
116
|
-
await expect(
|
|
117
|
-
APIKey.findAll({ page: 1, limit: 10 }, loginUser, null, {}),
|
|
118
|
-
).rejects.toThrow('User does not have privilege to generate API key.');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should return paginated API keys', async () => {
|
|
122
|
-
jest
|
|
123
|
-
.spyOn(APIKeyRepository.prototype, 'findAllWithPagination')
|
|
124
|
-
.mockResolvedValueOnce({
|
|
125
|
-
count: 1,
|
|
126
|
-
rows: [new (APIKey.prototype as any).constructor(apiKeyData)],
|
|
127
|
-
} as any);
|
|
128
|
-
|
|
129
|
-
const result = await APIKey.findAll(
|
|
130
|
-
{ page: 1, limit: 10 },
|
|
131
|
-
loginUser,
|
|
132
|
-
null,
|
|
133
|
-
{ SystemCode: 'TST' },
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
expect(result.total).toBe(1);
|
|
137
|
-
expect(result.ApiKeys).toHaveLength(1);
|
|
138
|
-
expect(result.page).toBe(1);
|
|
139
|
-
expect(result.limit).toBe(10);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should apply filters correctly', async () => {
|
|
143
|
-
const spy = jest
|
|
144
|
-
.spyOn(APIKeyRepository.prototype, 'findAllWithPagination')
|
|
145
|
-
.mockResolvedValueOnce({ count: 0, rows: [] } as any);
|
|
146
|
-
|
|
147
|
-
const fromDate = new Date('2024-01-01');
|
|
148
|
-
const toDate = new Date('2024-12-31');
|
|
149
|
-
|
|
150
|
-
await APIKey.findAll(
|
|
151
|
-
{ page: 1, limit: 10 },
|
|
152
|
-
loginUser,
|
|
153
|
-
null,
|
|
154
|
-
{
|
|
155
|
-
Status: APIKeyStatusEnum.ACTIVE,
|
|
156
|
-
ExpirationDate: { FromDate: fromDate, ToDate: toDate },
|
|
157
|
-
CreatedById: 1,
|
|
158
|
-
},
|
|
159
|
-
{ SortBy: 'CreatedAt', SortOrder: 'ASC' },
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('revoke', () => {
|
|
167
|
-
it('should throw when API key is not found', async () => {
|
|
168
|
-
jest
|
|
169
|
-
.spyOn(APIKeyRepository.prototype, 'findOne')
|
|
170
|
-
.mockResolvedValueOnce(null);
|
|
171
|
-
|
|
172
|
-
const apiKey = await APIKey.init();
|
|
173
|
-
await expect(
|
|
174
|
-
apiKey.revoke('nonexistent-key', loginUser, null),
|
|
175
|
-
).rejects.toThrow('API key not found.');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should revoke an API key successfully', async () => {
|
|
179
|
-
const apiKeyRecord = {
|
|
180
|
-
...apiKeyData,
|
|
181
|
-
Status: APIKeyStatusEnum.ACTIVE,
|
|
182
|
-
RevokedAt: null,
|
|
183
|
-
RevokedById: null,
|
|
184
|
-
RevokedReason: null,
|
|
185
|
-
get: (opts?: any) => ({ ...apiKeyData }),
|
|
186
|
-
};
|
|
187
|
-
jest
|
|
188
|
-
.spyOn(APIKeyRepository.prototype, 'findOne')
|
|
189
|
-
.mockResolvedValueOnce(apiKeyRecord as any);
|
|
190
|
-
jest
|
|
191
|
-
.spyOn(APIKeyRepository.prototype, 'update')
|
|
192
|
-
.mockResolvedValueOnce(undefined as any);
|
|
193
|
-
|
|
194
|
-
const apiKey = await APIKey.init();
|
|
195
|
-
const result = await apiKey.revoke('abc123def456', loginUser, null, 'No longer needed');
|
|
196
|
-
|
|
197
|
-
expect(result.Status).toBe(APIKeyStatusEnum.REVOKED);
|
|
198
|
-
expect(result.RevokedReason).toBe('No longer needed');
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
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
|
-
});
|