Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,45 @@ export default {
// collectCoverage: true,

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

// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// '/node_modules/',
// ],

// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// 'json',
// // "text",
// 'lcov',
// // "clover"
// ],
coverageReporters: ['json', 'lcov', 'clover', 'text'],

// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
coverageThreshold: {
global: {
statements: 85,
branches: 75,
functions: 85,
lines: 85,
},
},

// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
Expand Down
195 changes: 195 additions & 0 deletions modules/auth/tests/auth.integration.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import request from 'supertest';
import path from 'path';
import _ from 'lodash';
import { jest } from '@jest/globals';

import { bootstrap } from '../../../lib/app.js';
import mongooseService from '../../../lib/services/mongoose.js';
Expand All @@ -29,6 +30,7 @@ describe('Auth integration tests:', () => {
agent = request.agent(init.app);
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

Expand Down Expand Up @@ -235,6 +237,22 @@ describe('Auth integration tests:', () => {
}
});

test('should reject login with correct email but wrong password', async () => {
try {
const result = await agent
.post('/api/auth/signin')
.send({
email: credentials[0].email,
password: 'WrongPassword!123',
})
.expect(401);
expect(result.error.text).toBe('Unauthorized');
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

test('forgot password request for non-existent email should return 400', async () => {
try {
const result = await agent
Expand Down Expand Up @@ -431,12 +449,189 @@ describe('Auth integration tests:', () => {
});
});

describe('OAuth profile and service branches', () => {
let AuthController;
const oauthUsers = [];

beforeAll(async () => {
AuthController = (await import(path.resolve('./modules/auth/controllers/auth.controller.js'))).default;
// clean up any leftover users from a previously failed run
for (const email of ['noprovider@auth-test.com', 'oauthprofile@test.com', 'oauthfind@test.com']) {
try {
const existing = await UserService.getBrut({ email });
if (existing) await UserService.remove(existing);
} catch (_) { /* cleanup – ignore errors */ }
}
});

test('should create user with default provider when none is specified', async () => {
const result = await UserService.create({
firstName: 'No',
lastName: 'Provider',
email: 'noprovider@auth-test.com',
password: 'P@ss!W0rd123',
roles: ['user'],
// provider intentionally omitted to trigger the default branch
});
expect(result.provider).toBe('local');
oauthUsers.push(result);
});

test('should create an OAuth user without password via checkOAuthUserProfile', async () => {
const profil = {
firstName: 'OAuth',
lastName: 'Test',
email: 'oauthprofile@test.com',
avatar: '',
providerData: { id: 'google-fake-id-999' },
};
const mockRes = { status() { return this; }, json() {}, cookie() { return this; } };
const result = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes);
expect(result).toBeDefined();
expect(result.id).toBeDefined();
expect(result.email).toBe(profil.email);
oauthUsers.push(result);
});

test('should find an existing OAuth user via checkOAuthUserProfile', async () => {
// Create an OAuth user directly first
const createdUser = await UserService.create({
firstName: 'OAuth',
lastName: 'Find',
email: 'oauthfind@test.com',
provider: 'google',
providerData: { id: 'google-find-id-777' },
roles: ['user'],
});

const profil = {
firstName: 'OAuth',
lastName: 'Find',
email: 'oauthfind@test.com',
avatar: '',
providerData: { id: 'google-find-id-777' },
};
const mockRes = { status() { return this; }, json() {}, cookie() { return this; } };
// Second call — should find the existing user (search.length === 1 branch)
const found = await AuthController.checkOAuthUserProfile(profil, 'id', 'google', mockRes);
expect(found).toBeDefined();

// cleanup
try {
await UserService.remove(createdUser);
} catch (_) { /* cleanup – ignore errors */ }
});

afterAll(async () => {
for (const u of oauthUsers) {
try {
await UserService.remove(u);
} catch (_) { /* cleanup – ignore errors */ }
}
});
});

describe('Password reset endpoint', () => {
beforeEach(async () => {
credentials = [
{
email: 'resetpwd@test.com',
password: 'W@os.jsI$Aw3$0m3',
},
];
_user = {
firstName: 'Reset',
lastName: 'User',
email: credentials[0].email,
password: credentials[0].password,
provider: 'local',
};
try {
const result = await agent.post('/api/auth/signup').send(_user).expect(200);
user = result.body.user;
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

test('should return 400 when token or password fields are missing', async () => {
try {
const result = await agent.post('/api/auth/reset').send({ newPassword: 'NewP@ss123' }).expect(400);
expect(result.body.message).toBe('Bad Request');
expect(result.body.description).toBe('Password or Token fields must not be blank');
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

test('should return 400 when reset token is invalid or not found', async () => {
try {
const result = await agent.post('/api/auth/reset').send({ token: 'invalid-token-xyz', newPassword: 'NewP@ss!Word123' }).expect(400);
expect(result.body.message).toBe('Bad Request');
expect(result.body.description).toBe('Password reset token is invalid or has expired.');
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

test('should successfully reset password with a valid token', async () => {
// Trigger forgot to generate a reset token (email send fails in test env, which is expected)
try {
await agent.post('/api/auth/forgot').send({ email: credentials[0].email }).expect(400);
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}

// Fetch the token directly via UserService
let resetToken;
try {
const userWithToken = await UserService.getBrut({ email: credentials[0].email });
resetToken = userWithToken.resetPasswordToken;
expect(resetToken).toBeDefined();
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}

// Reset password with the valid token
try {
const result = await agent.post('/api/auth/reset').send({ token: resetToken, newPassword: 'NewP@ss!Word123' }).expect(200);
expect(result.body.message).toBe('Password changed successfully');
expect(result.body.user).toBeDefined();
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});

afterEach(async () => {
try {
await UserService.remove(user);
} catch (err) {
console.log(err);
}
});
});

describe('Error paths', () => {
test('should redirect to invalid when validateResetToken getBrut throws', async () => {
jest.spyOn(UserService, 'getBrut').mockRejectedValueOnce(new Error('DB error'));
const result = await agent.get('/api/auth/reset/sometoken').expect(302);
expect(result.headers.location).toBe('/api/password/reset/invalid');
});
});

// Mongoose disconnect
afterAll(async () => {
try {
await mongooseService.disconnect();
} catch (err) {
console.log(err);
expect(err).toBeFalsy();
}
});
});
44 changes: 40 additions & 4 deletions modules/core/tests/core.integration.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import path from 'path';

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

Expand Down Expand Up @@ -256,10 +258,44 @@ describe('Core integration tests:', () => {
});
});

describe('Seed service', () => {
it('should log results when logResults option is enabled', async () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});

const result = await seed.start({ logResults: true }, UserService, AuthService, TaskService);

expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Database Seeding'));
expect(result).toBeInstanceOf(Array);
consoleSpy.mockRestore();
});

it('should seed a single user via seed.user()', async () => {
const seedUser = {
firstName: 'Seed',
lastName: 'Test',
email: 'seedtest@unit.com',
provider: 'local',
roles: ['user'],
};

const result = await seed.user(seedUser, UserService, AuthService);
expect(result).toBeInstanceOf(Array);
expect(result).toHaveLength(1);

// cleanup
try {
await UserService.remove(result[0]);
} catch (_) { /* cleanup – ignore errors */ }
});
});

// Mongoose disconnect
afterAll(() =>
mongooseService.disconnect().catch((e) => {
afterAll(async () => {
try {
await mongooseService.disconnect();
} catch (e) {
console.log(e);
}),
);
expect(e).toBeFalsy();
}
});
});
Loading