Skip to content

Commit bffa019

Browse files
test: harden integration suite and error assertions
1 parent c71e51c commit bffa019

File tree

9 files changed

+924
-60
lines changed

9 files changed

+924
-60
lines changed

jest.config.js

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,45 @@ export default {
2121
// collectCoverage: true,
2222

2323
// An array of glob patterns indicating a set of files for which coverage information should be collected
24-
// collectCoverageFrom: [
25-
// '<rootDir>/server.js',
26-
// '<rootDir>/config/**/*.js',
27-
// '<rootDir>/modules/*/**/*.js',
28-
// ],
24+
collectCoverageFrom: [
25+
'<rootDir>/lib/**/*.js',
26+
'<rootDir>/modules/**/*.js',
27+
// Exclude schema/model definitions — no business logic, just DB structure
28+
'!<rootDir>/modules/**/*.model.mongoose.js',
29+
'!<rootDir>/modules/**/*.model.sequelize.js',
30+
// Exclude static config files — just object exports, nothing to test
31+
'!<rootDir>/config/defaults/**/*.js',
32+
// Exclude entry point
33+
'!<rootDir>/server.js',
34+
// Exclude Sequelize service — MySQL support is disabled/commented out in app.js
35+
'!<rootDir>/lib/services/sequelize.js',
36+
// Exclude OAuth strategy configs — no business logic, just passport.use() calls;
37+
// real OAuth credentials required to test, which are not available in CI
38+
'!<rootDir>/modules/auth/config/strategies/apple.js',
39+
'!<rootDir>/modules/auth/config/strategies/google.js',
40+
// Exclude dead code — never imported anywhere in the codebase
41+
'!<rootDir>/modules/users/services/users.data.service.js',
42+
],
2943
// The directory where Jest should output its coverage files
30-
// coverageDirectory: 'coverage',
44+
coverageDirectory: 'coverage',
3145

3246
// An array of regexp pattern strings used to skip coverage collection
3347
// coveragePathIgnorePatterns: [
3448
// '/node_modules/',
3549
// ],
3650

3751
// A list of reporter names that Jest uses when writing coverage reports
38-
// coverageReporters: [
39-
// 'json',
40-
// // "text",
41-
// 'lcov',
42-
// // "clover"
43-
// ],
52+
coverageReporters: ['json', 'lcov', 'clover', 'text'],
4453

4554
// An object that configures minimum threshold enforcement for coverage results
46-
// coverageThreshold: null,
55+
coverageThreshold: {
56+
global: {
57+
statements: 85,
58+
branches: 75,
59+
functions: 85,
60+
lines: 85,
61+
},
62+
},
4763

4864
// Make calling deprecated APIs throw helpful error messages
4965
// errorOnDeprecated: false,

modules/auth/tests/auth.integration.tests.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import request from 'supertest';
55
import path from 'path';
66
import _ from 'lodash';
7+
import { jest } from '@jest/globals';
78

89
import { bootstrap } from '../../../lib/app.js';
910
import mongooseService from '../../../lib/services/mongoose.js';
@@ -29,6 +30,7 @@ describe('Auth integration tests:', () => {
2930
agent = request.agent(init.app);
3031
} catch (err) {
3132
console.log(err);
33+
expect(err).toBeFalsy();
3234
}
3335
});
3436

@@ -235,6 +237,22 @@ describe('Auth integration tests:', () => {
235237
}
236238
});
237239

240+
test('should reject login with correct email but wrong password', async () => {
241+
try {
242+
const result = await agent
243+
.post('/api/auth/signin')
244+
.send({
245+
email: credentials[0].email,
246+
password: 'WrongPassword!123',
247+
})
248+
.expect(401);
249+
expect(result.error.text).toBe('Unauthorized');
250+
} catch (err) {
251+
console.log(err);
252+
expect(err).toBeFalsy();
253+
}
254+
});
255+
238256
test('forgot password request for non-existent email should return 400', async () => {
239257
try {
240258
const result = await agent
@@ -431,12 +449,189 @@ describe('Auth integration tests:', () => {
431449
});
432450
});
433451

452+
describe('OAuth profile and service branches', () => {
453+
let AuthController;
454+
const oauthUsers = [];
455+
456+
beforeAll(async () => {
457+
AuthController = (await import(path.resolve('./modules/auth/controllers/auth.controller.js'))).default;
458+
// clean up any leftover users from a previously failed run
459+
for (const email of ['noprovider@auth-test.com', 'oauthprofile@test.com', 'oauthfind@test.com']) {
460+
try {
461+
const existing = await UserService.getBrut({ email });
462+
if (existing) await UserService.remove(existing);
463+
} catch (_) { /* cleanup – ignore errors */ }
464+
}
465+
});
466+
467+
test('should create user with default provider when none is specified', async () => {
468+
const result = await UserService.create({
469+
firstName: 'No',
470+
lastName: 'Provider',
471+
email: 'noprovider@auth-test.com',
472+
password: 'P@ss!W0rd123',
473+
roles: ['user'],
474+
// provider intentionally omitted to trigger the default branch
475+
});
476+
expect(result.provider).toBe('local');
477+
oauthUsers.push(result);
478+
});
479+
480+
test('should create an OAuth user without password via checkOAuthUserProfile', async () => {
481+
const profil = {
482+
firstName: 'OAuth',
483+
lastName: 'Test',
484+
email: 'oauthprofile@test.com',
485+
avatar: '',
486+
providerData: { id: 'google-fake-id-999' },
487+
};
488+
const mockRes = { status() { return this; }, json() {}, cookie() { return this; } };
489+
const result = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes);
490+
expect(result).toBeDefined();
491+
expect(result.id).toBeDefined();
492+
expect(result.email).toBe(profil.email);
493+
oauthUsers.push(result);
494+
});
495+
496+
test('should find an existing OAuth user via checkOAuthUserProfile', async () => {
497+
// Create an OAuth user directly first
498+
const createdUser = await UserService.create({
499+
firstName: 'OAuth',
500+
lastName: 'Find',
501+
email: 'oauthfind@test.com',
502+
provider: 'google',
503+
providerData: { id: 'google-find-id-777' },
504+
roles: ['user'],
505+
});
506+
507+
const profil = {
508+
firstName: 'OAuth',
509+
lastName: 'Find',
510+
email: 'oauthfind@test.com',
511+
avatar: '',
512+
providerData: { id: 'google-find-id-777' },
513+
};
514+
const mockRes = { status() { return this; }, json() {}, cookie() { return this; } };
515+
// Second call — should find the existing user (search.length === 1 branch)
516+
const found = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes);
517+
expect(found).toBeDefined();
518+
519+
// cleanup
520+
try {
521+
await UserService.remove(createdUser);
522+
} catch (_) { /* cleanup – ignore errors */ }
523+
});
524+
525+
afterAll(async () => {
526+
for (const u of oauthUsers) {
527+
try {
528+
await UserService.remove(u);
529+
} catch (_) { /* cleanup – ignore errors */ }
530+
}
531+
});
532+
});
533+
534+
describe('Password reset endpoint', () => {
535+
beforeEach(async () => {
536+
credentials = [
537+
{
538+
email: 'resetpwd@test.com',
539+
password: 'W@os.jsI$Aw3$0m3',
540+
},
541+
];
542+
_user = {
543+
firstName: 'Reset',
544+
lastName: 'User',
545+
email: credentials[0].email,
546+
password: credentials[0].password,
547+
provider: 'local',
548+
};
549+
try {
550+
const result = await agent.post('/api/auth/signup').send(_user).expect(200);
551+
user = result.body.user;
552+
} catch (err) {
553+
console.log(err);
554+
expect(err).toBeFalsy();
555+
}
556+
});
557+
558+
test('should return 400 when token or password fields are missing', async () => {
559+
try {
560+
const result = await agent.post('/api/auth/reset').send({ newPassword: 'NewP@ss123' }).expect(400);
561+
expect(result.body.message).toBe('Bad Request');
562+
expect(result.body.description).toBe('Password or Token fields must not be blank');
563+
} catch (err) {
564+
console.log(err);
565+
expect(err).toBeFalsy();
566+
}
567+
});
568+
569+
test('should return 400 when reset token is invalid or not found', async () => {
570+
try {
571+
const result = await agent.post('/api/auth/reset').send({ token: 'invalid-token-xyz', newPassword: 'NewP@ss!Word123' }).expect(400);
572+
expect(result.body.message).toBe('Bad Request');
573+
expect(result.body.description).toBe('Password reset token is invalid or has expired.');
574+
} catch (err) {
575+
console.log(err);
576+
expect(err).toBeFalsy();
577+
}
578+
});
579+
580+
test('should successfully reset password with a valid token', async () => {
581+
// Trigger forgot to generate a reset token (email send fails in test env, which is expected)
582+
try {
583+
await agent.post('/api/auth/forgot').send({ email: credentials[0].email }).expect(400);
584+
} catch (err) {
585+
console.log(err);
586+
expect(err).toBeFalsy();
587+
}
588+
589+
// Fetch the token directly via UserService
590+
let resetToken;
591+
try {
592+
const userWithToken = await UserService.getBrut({ email: credentials[0].email });
593+
resetToken = userWithToken.resetPasswordToken;
594+
expect(resetToken).toBeDefined();
595+
} catch (err) {
596+
console.log(err);
597+
expect(err).toBeFalsy();
598+
}
599+
600+
// Reset password with the valid token
601+
try {
602+
const result = await agent.post('/api/auth/reset').send({ token: resetToken, newPassword: 'NewP@ss!Word123' }).expect(200);
603+
expect(result.body.message).toBe('Password changed successfully');
604+
expect(result.body.user).toBeDefined();
605+
} catch (err) {
606+
console.log(err);
607+
expect(err).toBeFalsy();
608+
}
609+
});
610+
611+
afterEach(async () => {
612+
try {
613+
await UserService.remove(user);
614+
} catch (err) {
615+
console.log(err);
616+
}
617+
});
618+
});
619+
620+
describe('Error paths', () => {
621+
test('should redirect to invalid when validateResetToken getBrut throws', async () => {
622+
jest.spyOn(UserService, 'getBrut').mockRejectedValueOnce(new Error('DB error'));
623+
const result = await agent.get('/api/auth/reset/sometoken').expect(302);
624+
expect(result.headers.location).toBe('/api/password/reset/invalid');
625+
});
626+
});
627+
434628
// Mongoose disconnect
435629
afterAll(async () => {
436630
try {
437631
await mongooseService.disconnect();
438632
} catch (err) {
439633
console.log(err);
634+
expect(err).toBeFalsy();
440635
}
441636
});
442637
});

modules/core/tests/core.integration.tests.js

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import path from 'path';
55

6+
import { jest } from '@jest/globals';
67
import config from '../../../config/index.js';
78
import mongooseService from '../../../lib/services/mongoose.js';
89
import seed from '../../../lib/services/seed.js';
@@ -25,6 +26,7 @@ describe('Core integration tests:', () => {
2526
TaskService = (await import(path.resolve('./modules/tasks/services/tasks.service.js'))).default;
2627
} catch (err) {
2728
console.log(err);
29+
expect(err).toBeFalsy();
2830
}
2931
});
3032

@@ -256,10 +258,44 @@ describe('Core integration tests:', () => {
256258
});
257259
});
258260

261+
describe('Seed service', () => {
262+
it('should log results when logResults option is enabled', async () => {
263+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
264+
265+
const result = await seed.start({ logResults: true }, UserService, AuthService, TaskService);
266+
267+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Database Seeding'));
268+
expect(result).toBeInstanceOf(Array);
269+
consoleSpy.mockRestore();
270+
});
271+
272+
it('should seed a single user via seed.user()', async () => {
273+
const seedUser = {
274+
firstName: 'Seed',
275+
lastName: 'Test',
276+
email: 'seedtest@unit.com',
277+
provider: 'local',
278+
roles: ['user'],
279+
};
280+
281+
const result = await seed.user(seedUser, UserService, AuthService);
282+
expect(result).toBeInstanceOf(Array);
283+
expect(result).toHaveLength(1);
284+
285+
// cleanup
286+
try {
287+
await UserService.remove(result[0]);
288+
} catch (_) { /* cleanup – ignore errors */ }
289+
});
290+
});
291+
259292
// Mongoose disconnect
260-
afterAll(() =>
261-
mongooseService.disconnect().catch((e) => {
293+
afterAll(async () => {
294+
try {
295+
await mongooseService.disconnect();
296+
} catch (e) {
262297
console.log(e);
263-
}),
264-
);
298+
expect(e).toBeFalsy();
299+
}
300+
});
265301
});

0 commit comments

Comments
 (0)