@friggframework/core 2.0.0-next.43 → 2.0.0-next.45
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/database/config.js +29 -1
- package/database/use-cases/test-encryption-use-case.js +6 -5
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
- package/handlers/WEBHOOKS.md +653 -0
- package/handlers/backend-utils.js +118 -3
- package/handlers/integration-event-dispatcher.test.js +68 -0
- package/handlers/routers/integration-webhook-routers.js +67 -0
- package/handlers/routers/integration-webhook-routers.test.js +126 -0
- package/handlers/webhook-flow.integration.test.js +356 -0
- package/handlers/workers/integration-defined-workers.test.js +184 -0
- package/index.js +16 -0
- package/integrations/WEBHOOK-QUICKSTART.md +151 -0
- package/integrations/integration-base.js +74 -3
- package/integrations/repositories/process-repository-factory.js +46 -0
- package/integrations/repositories/process-repository-interface.js +90 -0
- package/integrations/repositories/process-repository-mongo.js +190 -0
- package/integrations/repositories/process-repository-postgres.js +217 -0
- package/integrations/tests/doubles/dummy-integration-class.js +1 -8
- package/integrations/use-cases/create-process.js +128 -0
- package/integrations/use-cases/create-process.test.js +178 -0
- package/integrations/use-cases/get-process.js +87 -0
- package/integrations/use-cases/get-process.test.js +190 -0
- package/integrations/use-cases/index.js +8 -0
- package/integrations/use-cases/update-process-metrics.js +201 -0
- package/integrations/use-cases/update-process-metrics.test.js +308 -0
- package/integrations/use-cases/update-process-state.js +119 -0
- package/integrations/use-cases/update-process-state.test.js +256 -0
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +44 -0
- package/prisma-postgresql/schema.prisma +45 -0
- package/queues/queuer-util.js +10 -0
- package/user/repositories/user-repository-mongo.js +53 -12
- package/user/repositories/user-repository-postgres.js +53 -14
- package/user/tests/use-cases/login-user.test.js +85 -5
- package/user/tests/user-password-encryption-isolation.test.js +237 -0
- package/user/tests/user-password-hashing.test.js +235 -0
- package/user/use-cases/login-user.js +1 -1
- package/user/user.js +2 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.45",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/boom": "^10.0.1",
|
|
7
7
|
"@prisma/client": "^6.16.3",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"uuid": "^9.0.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
27
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
28
|
-
"@friggframework/test": "2.0.0-next.
|
|
26
|
+
"@friggframework/eslint-config": "2.0.0-next.45",
|
|
27
|
+
"@friggframework/prettier-config": "2.0.0-next.45",
|
|
28
|
+
"@friggframework/test": "2.0.0-next.45",
|
|
29
29
|
"@types/lodash": "4.17.15",
|
|
30
30
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
31
31
|
"chai": "^4.3.6",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "996a15bcdfaa4252b9891a33f1b1c84548d66bbc"
|
|
68
68
|
}
|
|
@@ -46,6 +46,7 @@ model User {
|
|
|
46
46
|
credentials Credential[]
|
|
47
47
|
entities Entity[]
|
|
48
48
|
integrations Integration[]
|
|
49
|
+
processes Process[]
|
|
49
50
|
|
|
50
51
|
@@unique([email])
|
|
51
52
|
@@unique([username])
|
|
@@ -172,6 +173,7 @@ model Integration {
|
|
|
172
173
|
associations Association[]
|
|
173
174
|
syncs Sync[]
|
|
174
175
|
mappings IntegrationMapping[]
|
|
176
|
+
processes Process[]
|
|
175
177
|
|
|
176
178
|
@@index([userId])
|
|
177
179
|
@@index([status])
|
|
@@ -206,6 +208,48 @@ model IntegrationMapping {
|
|
|
206
208
|
@@map("IntegrationMapping")
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// PROCESS MODELS
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
/// Generic Process Model - tracks any long-running operation
|
|
216
|
+
/// Used for: CRM syncs, data migrations, bulk operations, etc.
|
|
217
|
+
model Process {
|
|
218
|
+
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
219
|
+
|
|
220
|
+
// Core references
|
|
221
|
+
userId String @db.ObjectId
|
|
222
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
223
|
+
integrationId String @db.ObjectId
|
|
224
|
+
integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)
|
|
225
|
+
|
|
226
|
+
// Process identification
|
|
227
|
+
name String // e.g., "zoho-crm-contact-sync", "pipedrive-lead-sync"
|
|
228
|
+
type String // e.g., "CRM_SYNC", "DATA_MIGRATION", "BULK_OPERATION"
|
|
229
|
+
|
|
230
|
+
// State machine
|
|
231
|
+
state String // Current state (integration-defined states)
|
|
232
|
+
|
|
233
|
+
// Flexible storage
|
|
234
|
+
context Json @default("{}") // Process-specific data (pagination, metadata, etc.)
|
|
235
|
+
results Json @default("{}") // Process results and metrics
|
|
236
|
+
|
|
237
|
+
// Hierarchy support
|
|
238
|
+
childProcesses String[] @db.ObjectId
|
|
239
|
+
parentProcessId String? @db.ObjectId
|
|
240
|
+
|
|
241
|
+
// Timestamps
|
|
242
|
+
createdAt DateTime @default(now())
|
|
243
|
+
updatedAt DateTime @updatedAt
|
|
244
|
+
|
|
245
|
+
@@index([userId])
|
|
246
|
+
@@index([integrationId])
|
|
247
|
+
@@index([type])
|
|
248
|
+
@@index([state])
|
|
249
|
+
@@index([name])
|
|
250
|
+
@@map("Process")
|
|
251
|
+
}
|
|
252
|
+
|
|
209
253
|
// ============================================================================
|
|
210
254
|
// SYNC MODELS
|
|
211
255
|
// ============================================================================
|
|
@@ -46,6 +46,7 @@ model User {
|
|
|
46
46
|
credentials Credential[]
|
|
47
47
|
entities Entity[]
|
|
48
48
|
integrations Integration[]
|
|
49
|
+
processes Process[]
|
|
49
50
|
|
|
50
51
|
@@unique([email])
|
|
51
52
|
@@unique([username])
|
|
@@ -164,6 +165,7 @@ model Integration {
|
|
|
164
165
|
associations Association[]
|
|
165
166
|
syncs Sync[]
|
|
166
167
|
mappings IntegrationMapping[]
|
|
168
|
+
processes Process[]
|
|
167
169
|
|
|
168
170
|
@@index([userId])
|
|
169
171
|
@@index([status])
|
|
@@ -281,6 +283,49 @@ enum AssociationType {
|
|
|
281
283
|
MANY_TO_ONE
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// PROCESS MODELS
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/// Generic Process Model - tracks any long-running operation
|
|
291
|
+
/// Used for: CRM syncs, data migrations, bulk operations, etc.
|
|
292
|
+
model Process {
|
|
293
|
+
id Int @id @default(autoincrement())
|
|
294
|
+
|
|
295
|
+
// Core references
|
|
296
|
+
userId Int
|
|
297
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
298
|
+
integrationId Int
|
|
299
|
+
integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)
|
|
300
|
+
|
|
301
|
+
// Process identification
|
|
302
|
+
name String // e.g., "zoho-crm-contact-sync", "pipedrive-lead-sync"
|
|
303
|
+
type String // e.g., "CRM_SYNC", "DATA_MIGRATION", "BULK_OPERATION"
|
|
304
|
+
|
|
305
|
+
// State machine
|
|
306
|
+
state String // Current state (integration-defined states)
|
|
307
|
+
|
|
308
|
+
// Flexible storage
|
|
309
|
+
context Json @default("{}") // Process-specific data (pagination, metadata, etc.)
|
|
310
|
+
results Json @default("{}") // Process results and metrics
|
|
311
|
+
|
|
312
|
+
// Hierarchy support - self-referential relation
|
|
313
|
+
parentProcessId Int?
|
|
314
|
+
parentProcess Process? @relation("ProcessHierarchy", fields: [parentProcessId], references: [id], onDelete: SetNull)
|
|
315
|
+
childProcesses Process[] @relation("ProcessHierarchy")
|
|
316
|
+
|
|
317
|
+
// Timestamps
|
|
318
|
+
createdAt DateTime @default(now())
|
|
319
|
+
updatedAt DateTime @updatedAt
|
|
320
|
+
|
|
321
|
+
@@index([userId])
|
|
322
|
+
@@index([integrationId])
|
|
323
|
+
@@index([type])
|
|
324
|
+
@@index([state])
|
|
325
|
+
@@index([name])
|
|
326
|
+
@@index([parentProcessId])
|
|
327
|
+
}
|
|
328
|
+
|
|
284
329
|
// ============================================================================
|
|
285
330
|
// UTILITY MODELS
|
|
286
331
|
// ============================================================================
|
package/queues/queuer-util.js
CHANGED
|
@@ -14,6 +14,16 @@ AWS.config.update(awsConfigOptions());
|
|
|
14
14
|
const sqs = new AWS.SQS();
|
|
15
15
|
|
|
16
16
|
const QueuerUtil = {
|
|
17
|
+
send: async (message, queueUrl) => {
|
|
18
|
+
console.log(`Enqueuing message to SQS queue ${queueUrl}`);
|
|
19
|
+
return sqs
|
|
20
|
+
.sendMessage({
|
|
21
|
+
MessageBody: JSON.stringify(message),
|
|
22
|
+
QueueUrl: queueUrl,
|
|
23
|
+
})
|
|
24
|
+
.promise();
|
|
25
|
+
},
|
|
26
|
+
|
|
17
27
|
batchSend: async (entries = [], queueUrl) => {
|
|
18
28
|
console.log(
|
|
19
29
|
`Enqueuing ${entries.length} entries on SQS to queue ${queueUrl}`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
2
|
const { prisma } = require('../../database/prisma');
|
|
3
3
|
const {
|
|
4
4
|
createTokenRepository,
|
|
@@ -95,19 +95,38 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
95
95
|
* Replaces: IndividualUser.create(params)
|
|
96
96
|
*
|
|
97
97
|
* @param {Object} params - User creation parameters
|
|
98
|
+
* @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
98
99
|
* @returns {Promise<Object>} Created user object with string IDs
|
|
99
100
|
*/
|
|
100
101
|
async createIndividualUser(params) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const data = {
|
|
103
|
+
type: 'INDIVIDUAL',
|
|
104
|
+
email: params.email,
|
|
105
|
+
username: params.username,
|
|
106
|
+
appUserId: params.appUserId,
|
|
107
|
+
organizationId: params.organization || params.organizationId,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
params.hashword !== undefined &&
|
|
112
|
+
params.hashword !== null &&
|
|
113
|
+
params.hashword !== ''
|
|
114
|
+
) {
|
|
115
|
+
if (typeof params.hashword !== 'string') {
|
|
116
|
+
throw new Error('Password must be a string');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
120
|
+
if (params.hashword.startsWith('$2')) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
data.hashword = await bcrypt.hash(params.hashword, 10);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return await this.prisma.user.create({ data });
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
/**
|
|
@@ -204,12 +223,34 @@ class UserRepositoryMongo extends UserRepositoryInterface {
|
|
|
204
223
|
* Update individual user
|
|
205
224
|
* @param {string} userId - User ID
|
|
206
225
|
* @param {Object} updates - Fields to update
|
|
226
|
+
* @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
207
227
|
* @returns {Promise<Object>} Updated user object with string IDs
|
|
208
228
|
*/
|
|
209
229
|
async updateIndividualUser(userId, updates) {
|
|
230
|
+
const data = { ...updates };
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
data.hashword !== undefined &&
|
|
234
|
+
data.hashword !== null &&
|
|
235
|
+
data.hashword !== ''
|
|
236
|
+
) {
|
|
237
|
+
if (typeof data.hashword !== 'string') {
|
|
238
|
+
throw new Error('Password must be a string');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
242
|
+
if (data.hashword.startsWith('$2')) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
data.hashword = await bcrypt.hash(data.hashword, 10);
|
|
249
|
+
}
|
|
250
|
+
|
|
210
251
|
return await this.prisma.user.update({
|
|
211
252
|
where: { id: userId },
|
|
212
|
-
data
|
|
253
|
+
data,
|
|
213
254
|
});
|
|
214
255
|
}
|
|
215
256
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
2
|
const { prisma } = require('../../database/prisma');
|
|
3
3
|
const {
|
|
4
4
|
createTokenRepository,
|
|
@@ -130,21 +130,40 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
130
130
|
* Replaces: IndividualUser.create(params)
|
|
131
131
|
*
|
|
132
132
|
* @param {Object} params - User creation parameters (with string IDs from application layer)
|
|
133
|
+
* @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
133
134
|
* @returns {Promise<Object>} Created user object with string IDs
|
|
134
135
|
*/
|
|
135
136
|
async createIndividualUser(params) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
const data = {
|
|
138
|
+
type: 'INDIVIDUAL',
|
|
139
|
+
email: params.email,
|
|
140
|
+
username: params.username,
|
|
141
|
+
appUserId: params.appUserId,
|
|
142
|
+
organizationId: this._convertId(
|
|
143
|
+
params.organization || params.organizationId
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
params.hashword !== undefined &&
|
|
149
|
+
params.hashword !== null &&
|
|
150
|
+
params.hashword !== ''
|
|
151
|
+
) {
|
|
152
|
+
if (typeof params.hashword !== 'string') {
|
|
153
|
+
throw new Error('Password must be a string');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
157
|
+
if (params.hashword.startsWith('$2')) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
data.hashword = await bcrypt.hash(params.hashword, 10);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const user = await this.prisma.user.create({ data });
|
|
148
167
|
return this._convertUserIds(user);
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -249,13 +268,14 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
249
268
|
* Update individual user
|
|
250
269
|
* @param {string} userId - User ID (string from application layer)
|
|
251
270
|
* @param {Object} updates - Fields to update (with string IDs from application layer)
|
|
271
|
+
* @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
|
|
252
272
|
* @returns {Promise<Object>} Updated user object with string IDs
|
|
253
273
|
*/
|
|
254
274
|
async updateIndividualUser(userId, updates) {
|
|
255
275
|
const intId = this._convertId(userId);
|
|
256
276
|
|
|
257
|
-
// Convert organizationId if present in updates
|
|
258
277
|
const data = { ...updates };
|
|
278
|
+
|
|
259
279
|
if (data.organizationId !== undefined) {
|
|
260
280
|
data.organizationId = this._convertId(data.organizationId);
|
|
261
281
|
}
|
|
@@ -264,6 +284,25 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
|
|
|
264
284
|
delete data.organization;
|
|
265
285
|
}
|
|
266
286
|
|
|
287
|
+
if (
|
|
288
|
+
data.hashword !== undefined &&
|
|
289
|
+
data.hashword !== null &&
|
|
290
|
+
data.hashword !== ''
|
|
291
|
+
) {
|
|
292
|
+
if (typeof data.hashword !== 'string') {
|
|
293
|
+
throw new Error('Password must be a string');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
|
|
297
|
+
if (data.hashword.startsWith('$2')) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
'Password appears to be already hashed. Pass plain text password only.'
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
data.hashword = await bcrypt.hash(data.hashword, 10);
|
|
304
|
+
}
|
|
305
|
+
|
|
267
306
|
const user = await this.prisma.user.update({
|
|
268
307
|
where: { id: intId },
|
|
269
308
|
data,
|
|
@@ -3,7 +3,7 @@ const { LoginUser } = require('../../use-cases/login-user');
|
|
|
3
3
|
const { TestUserRepository } = require('../doubles/test-user-repository');
|
|
4
4
|
|
|
5
5
|
jest.mock('bcryptjs', () => ({
|
|
6
|
-
|
|
6
|
+
compare: jest.fn(),
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
9
|
describe('LoginUser Use Case', () => {
|
|
@@ -16,7 +16,7 @@ describe('LoginUser Use Case', () => {
|
|
|
16
16
|
userRepository = new TestUserRepository({ userConfig });
|
|
17
17
|
loginUser = new LoginUser({ userRepository, userConfig });
|
|
18
18
|
|
|
19
|
-
bcrypt.
|
|
19
|
+
bcrypt.compare.mockClear();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
describe('With Password Authentication', () => {
|
|
@@ -28,11 +28,11 @@ describe('LoginUser Use Case', () => {
|
|
|
28
28
|
hashword: 'hashed-password',
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
bcrypt.
|
|
31
|
+
bcrypt.compare.mockResolvedValue(true);
|
|
32
32
|
|
|
33
33
|
const user = await loginUser.execute({ username, password });
|
|
34
34
|
|
|
35
|
-
expect(bcrypt.
|
|
35
|
+
expect(bcrypt.compare).toHaveBeenCalledWith(
|
|
36
36
|
password,
|
|
37
37
|
'hashed-password'
|
|
38
38
|
);
|
|
@@ -48,7 +48,7 @@ describe('LoginUser Use Case', () => {
|
|
|
48
48
|
hashword: 'hashed-password',
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
bcrypt.
|
|
51
|
+
bcrypt.compare.mockResolvedValue(false);
|
|
52
52
|
|
|
53
53
|
await expect(
|
|
54
54
|
loginUser.execute({ username, password })
|
|
@@ -137,4 +137,84 @@ describe('LoginUser Use Case', () => {
|
|
|
137
137
|
).rejects.toThrow('user not found');
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
describe('Bcrypt Hash Verification', () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
userConfig = { usePassword: true, individualUserRequired: true, organizationUserRequired: false };
|
|
144
|
+
userRepository = new TestUserRepository({ userConfig });
|
|
145
|
+
loginUser = new LoginUser({ userRepository, userConfig });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should verify bcrypt.compare is called with plain password and hash', async () => {
|
|
149
|
+
const username = 'bcrypt-test-user';
|
|
150
|
+
const plainPassword = 'MyPlainPassword123';
|
|
151
|
+
const bcryptHash = '$2b$10$abcdefghijklmnopqrstuv';
|
|
152
|
+
|
|
153
|
+
await userRepository.createIndividualUser({
|
|
154
|
+
username,
|
|
155
|
+
hashword: bcryptHash,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
bcrypt.compare.mockResolvedValue(true);
|
|
159
|
+
|
|
160
|
+
await loginUser.execute({ username, password: plainPassword });
|
|
161
|
+
|
|
162
|
+
expect(bcrypt.compare).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(bcrypt.compare).toHaveBeenCalledWith(plainPassword, bcryptHash);
|
|
164
|
+
|
|
165
|
+
const [firstArg, secondArg] = bcrypt.compare.mock.calls[0];
|
|
166
|
+
expect(firstArg).toBe(plainPassword);
|
|
167
|
+
expect(secondArg).toBe(bcryptHash);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should verify stored password has bcrypt hash format', async () => {
|
|
171
|
+
const username = 'format-test-user';
|
|
172
|
+
const bcryptHash = '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
|
|
173
|
+
|
|
174
|
+
await userRepository.createIndividualUser({
|
|
175
|
+
username,
|
|
176
|
+
hashword: bcryptHash,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const user = await userRepository.findIndividualUserByUsername(username);
|
|
180
|
+
|
|
181
|
+
expect(user.hashword).toMatch(/^\$2[ab]\$/);
|
|
182
|
+
expect(user.hashword.length).toBeGreaterThan(50);
|
|
183
|
+
expect(user.hashword).not.toContain(':');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should reject passwords that look encrypted (have colon separators)', async () => {
|
|
187
|
+
const username = 'encrypted-format-user';
|
|
188
|
+
const encryptedLookingValue = 'kms:us-east-1:key:ciphertext';
|
|
189
|
+
|
|
190
|
+
await userRepository.createIndividualUser({
|
|
191
|
+
username,
|
|
192
|
+
hashword: encryptedLookingValue,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
bcrypt.compare.mockResolvedValue(false);
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
loginUser.execute({ username, password: 'any-password' })
|
|
199
|
+
).rejects.toThrow('Incorrect username or password');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should verify bcrypt.compare returns false for mismatched passwords', async () => {
|
|
203
|
+
const username = 'mismatch-test-user';
|
|
204
|
+
const correctHash = '$2b$10$correcthash';
|
|
205
|
+
|
|
206
|
+
await userRepository.createIndividualUser({
|
|
207
|
+
username,
|
|
208
|
+
hashword: correctHash,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
bcrypt.compare.mockResolvedValue(false);
|
|
212
|
+
|
|
213
|
+
await expect(
|
|
214
|
+
loginUser.execute({ username, password: 'wrong-password' })
|
|
215
|
+
).rejects.toThrow('Incorrect username or password');
|
|
216
|
+
|
|
217
|
+
expect(bcrypt.compare).toHaveBeenCalledWith('wrong-password', correctHash);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
140
220
|
});
|