diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 699171cf..de7772bf 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -31,7 +31,7 @@ export const authenticateViaGoogle = (req: Request, res: Response, next: NextFun })(req, res, next); }; // calculate password expiration -const calculatePasswordExpirationDate = (user: UserAttributes): Date | null => { +export const calculatePasswordExpirationDate = (user: UserAttributes): Date | null => { const expirationMinutes = parseInt(process.env.PASSWORD_EXPIRATION_MINUTES as string); let expirationDate: Date | null = null; @@ -47,7 +47,7 @@ const calculatePasswordExpirationDate = (user: UserAttributes): Date | null => { return expirationDate; }; -const redirectToPasswordUpdate = (res: Response): void => { +export const redirectToPasswordUpdate = (res: Response): void => { res.redirect('/api/user/passwordUpdate'); }; // login function @@ -55,15 +55,11 @@ export const login = async (req: Request, res: Response): Promise => { try { const { email, password } = req.body; - const requiredFields = ['email', 'password']; - const missingFields = validateFields(req, requiredFields); - // Field validation - if (missingFields.length > 0) { - logger.error(`Adding User:Required fields are missing:${missingFields.join(', ')}`); + if (!email || !password) { res.status(400).json({ ok: false, - message: `Required fields are missing: ${missingFields.join(', ')}`, + message: 'Required fields are missing', }); return; } @@ -124,24 +120,27 @@ export const verifyOTP = async (req: Request, res: Response) => { }; // Function to create OTP Token, Save it Postgres, export const sendOTP = async (req: Request, res: Response, email: string) => { - const userInfo = await User.findOne({ where: { email } }); - if (userInfo) { - const { id, email, firstName } = userInfo.dataValues; - - const token = await createOTPToken(id, email, firstName); - - const otpSaved = await saveOTPDB(id, token); - - if (otpSaved) { - /** - * The token used for comparing the received OTP via email with the - * generated token, which contains the user's ID. - */ - const accessToken = jwt.sign({ id, FAEnabled: true }, process.env.SECRET_KEY as string, { - expiresIn: process.env.JWT_EXPIRATION as string, - }); - res.status(200).json({ ok: true, token: accessToken }); + try { + const userInfo = await User.findOne({ where: { email } }); + + if (userInfo) { + const { id, email, firstName } = userInfo.dataValues; + const token = await createOTPToken(id, email, firstName); + const otpSaved = await saveOTPDB(id, token); + + if (otpSaved) { + const accessToken = jwt.sign({ id, FAEnabled: true }, process.env.SECRET_KEY as string, { + expiresIn: process.env.JWT_EXPIRATION as string, + }); + return res.status(200).json({ ok: true, token: accessToken }); + } else { + return res.status(500).json({ ok: false, message: 'Failed to save OTP' }); + } + } else { + return res.status(404).json({ ok: false, message: 'User not found' }); } + } catch (error) { + return res.status(500).json({ ok: false, message: 'Internal Server Error' }); } }; diff --git a/src/controllers/testController.ts b/src/controllers/testController.ts deleted file mode 100644 index e7d7e4a5..00000000 --- a/src/controllers/testController.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function addition(a: number, b: number) { - return a + b; -} diff --git a/src/test/auth/auth.test.ts b/src/test/auth/auth.test.ts new file mode 100644 index 00000000..93e218b2 --- /dev/null +++ b/src/test/auth/auth.test.ts @@ -0,0 +1,263 @@ +import { Request } from 'express'; +import bcrypt from 'bcrypt'; +import dotenv from 'dotenv'; +import User from '../../database/models/user'; +import Role from '../../database/models/role'; +import * as authController from '../../controllers/authController'; +import { sendErrorResponse } from '../../helpers/helper'; +import { sendInternalErrorResponse } from '../../validations'; + +dotenv.config(); + +const mockResponse = () => { + const res = {} as any; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + res.send = jest.fn().mockReturnValue(res); + return res; +}; + +jest.mock('../../database/models/user', () => ({ + __esModule: true, + default: { + findOne: jest.fn(), + findByPk: jest.fn(), + hasMany: jest.fn(), + }, +})); + +jest.mock('../../database/models/role', () => ({ + __esModule: true, + default: { + findOne: jest.fn(), + }, +})); + +jest.mock('../../database/models/otp', () => ({ + __esModule: true, + default: { + belongsTo: jest.fn(), + }, +})); + +jest.mock('bcrypt', () => ({ + compare: jest.fn(() => Promise.resolve(true)), +})); + +jest.mock('jsonwebtoken', () => ({ + sign: jest.fn(() => 'dgshdgshdgshgdhs-hghgashagsh-jhj'), +})); + +jest.mock('../../helpers/helper', () => ({ + sendErrorResponse: jest.fn(), +})); + +jest.mock('../../validations', () => ({ + sendInternalErrorResponse: jest.fn(), +})); + +jest.mock('../../controllers/authController', () => ({ + ...jest.requireActual('../../controllers/authController'), + calculatePasswordExpirationDate: jest.fn(), + redirectToPasswordUpdate: jest.fn(), +})); + +describe('AuthController', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('POST/ User login route', () => { + it('should return 400 if required fields are missing', async () => { + const req = { + body: { + email: 'mypass@gmail.com', + }, + } as Request; + const res = mockResponse(); + + await authController.login(req, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(res.json).toHaveBeenCalledWith({ + ok: false, + message: 'Required fields are missing', + }); + + expect(User.findOne).not.toHaveBeenCalled(); + expect(User.findByPk).not.toHaveBeenCalled(); + expect(Role.findOne).not.toHaveBeenCalled(); + expect(bcrypt.compare).not.toHaveBeenCalled(); + }); + + it('should return 401 if user status is inactive', async () => { + const req = { + body: { + email: 'inactiveuser@example.com', + password: 'correctPassword', + }, + } as Request; + const res = mockResponse(); + + const mockUser = { + id: 1, + email: 'inactiveuser@example.com', + password: 'hashedPassword', + status: 'inactive', + verified: true, + dataValues: { + RoleId: 1, + enable2FA: false, + email: 'inactiveuser@example.com', + }, + }; + + (User.findOne as jest.Mock).mockResolvedValueOnce(mockUser); + (bcrypt.compare as jest.Mock).mockResolvedValueOnce(true); + + await authController.login(req, res); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'inactiveuser@example.com' } }); + expect(sendErrorResponse).toHaveBeenCalledWith(res, 'inactiveUser'); + }); + + it('should return 401 if user is not verified', async () => { + const req = { + body: { + email: 'unverifieduser@example.com', + password: 'correctPassword', + }, + } as Request; + const res = mockResponse(); + + const mockUser = { + id: 1, + email: 'unverifieduser@example.com', + password: 'hashedPassword', + status: 'active', + verified: false, + dataValues: { + RoleId: 1, + enable2FA: false, + email: 'unverifieduser@example.com', + }, + }; + + (User.findOne as jest.Mock).mockResolvedValueOnce(mockUser); + (bcrypt.compare as jest.Mock).mockResolvedValueOnce(true); + + await authController.login(req, res); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'unverifieduser@example.com' } }); + expect(sendErrorResponse).toHaveBeenCalledWith(res, 'unverifiedUser'); + }); + + it('should handle internal server error', async () => { + const req = { + body: { + email: 'unverifieduser@example.com', + password: 'correctPassword', + }, + } as Request; + const res = mockResponse(); + + const mockError = new Error('Database connection failed'); + (User.findOne as jest.Mock).mockRejectedValueOnce(mockError); + + await authController.login(req, res); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'unverifieduser@example.com' } }); + expect(sendInternalErrorResponse).toHaveBeenCalledWith(res, mockError); + }); + + it('should return 401 if user is not found', async () => { + const req = { + body: { + email: 'noneuser@example.com', + password: 'myPassword', + }, + } as Request; + const res = mockResponse(); + + (User.findOne as jest.Mock).mockResolvedValueOnce(null); + + await authController.login(req, res); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'noneuser@example.com' } }); + expect(sendErrorResponse).toHaveBeenCalledWith(res, 'invalidCredentials'); + }); + + it('should return 401 if password is invalid', async () => { + const req = { + body: { + email: 'user@example.com', + password: 'wrongPassword', + }, + } as Request; + const res = mockResponse(); + + const mockUser = { + id: 1, + email: 'user@example.com', + password: 'hashedPassword', + status: 'active', + verified: true, + dataValues: { + RoleId: 1, + enable2FA: false, + email: 'user@example.com', + }, + }; + + (User.findOne as jest.Mock).mockResolvedValueOnce(mockUser); + (bcrypt.compare as jest.Mock).mockResolvedValueOnce(false); + + await authController.login(req, res); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'user@example.com' } }); + expect(bcrypt.compare).toHaveBeenCalledWith('wrongPassword', 'hashedPassword'); + }); + + it('should return 200 if login is successful', async () => { + const req = { + body: { + email: 'mypass@gmail.com', + password: 'correctPassword', + }, + } as Request; + const res = mockResponse(); + + const mockUser = { + id: 1, + email: 'mypass@gmail.com', + password: 'hashedPassword', + status: 'active', + verified: true, + dataValues: { + RoleId: 1, + enable2FA: false, + email: 'mypass@gmail.com', + }, + }; + + const mockRole = { + dataValues: { + name: 'buyer', + }, + }; + + (User.findOne as jest.Mock).mockResolvedValueOnce(mockUser); + (User.findByPk as jest.Mock).mockResolvedValueOnce(mockUser); + (Role.findOne as jest.Mock).mockResolvedValueOnce(mockRole); + (bcrypt.compare as jest.Mock).mockResolvedValueOnce(true); + + await authController.login(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + ok: true, + token: 'dgshdgshdgshgdhs-hghgashagsh-jhj', + }); + }); + }); +}); diff --git a/src/test/auth/authenticateViaGoogle.test.ts b/src/test/auth/authenticateViaGoogle.test.ts new file mode 100644 index 00000000..ea9f2f1f --- /dev/null +++ b/src/test/auth/authenticateViaGoogle.test.ts @@ -0,0 +1,71 @@ +import { authenticateViaGoogle } from '../../controllers/authController'; +import { Request, Response, NextFunction } from 'express'; +import passport from 'passport'; +import jwt from 'jsonwebtoken'; + +jest.mock('passport'); +jest.mock('jsonwebtoken'); +jest.mock('../../helpers/helper', () => ({ + sendInternalErrorResponse: jest.fn(), +})); + +describe('authenticateViaGoogle', () => { + let reqMock: Partial; + let resMock: Partial; + let nextMock: NextFunction; + + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + redirect: jest.fn(), + }; + nextMock = jest.fn(); + jest.clearAllMocks(); + }); + + it('should handle an error from passport.authenticate', () => { + const mockError = new Error('Authentication error'); + (passport.authenticate as jest.Mock).mockImplementation((strategy, callback) => () => { + callback(mockError, null); + }); + + authenticateViaGoogle(reqMock as Request, resMock as Response, nextMock); + + expect(passport.authenticate).toHaveBeenCalledWith('google', expect.any(Function)); + }); + + it('should handle a situation where no user is returned', () => { + (passport.authenticate as jest.Mock).mockImplementation((strategy, callback) => () => { + callback(null, null); + }); + + authenticateViaGoogle(reqMock as Request, resMock as Response, nextMock); + + expect(passport.authenticate).toHaveBeenCalledWith('google', expect.any(Function)); + expect(resMock.status).toHaveBeenCalledWith(401); + expect(resMock.json).toHaveBeenCalledWith({ error: 'Authentication failed' }); + }); + + it('should authenticate user and redirect with JWT', () => { + const mockUser = { id: 'mockUserId' }; + const mockToken = 'mockToken'; + (passport.authenticate as jest.Mock).mockImplementation((strategy, callback) => () => { + callback(null, mockUser); + }); + (jwt.sign as jest.Mock).mockReturnValue(mockToken); + + process.env.SECRET_KEY = 'test_secret'; + process.env.JWT_EXPIRATION = '1h'; + process.env.FRONT_END_BASEURL = 'http://localhost:3000'; + + authenticateViaGoogle(reqMock as Request, resMock as Response, nextMock); + + expect(passport.authenticate).toHaveBeenCalledWith('google', expect.any(Function)); + expect(jwt.sign).toHaveBeenCalledWith({ id: mockUser.id }, process.env.SECRET_KEY, { + expiresIn: process.env.JWT_EXPIRATION, + }); + expect(resMock.redirect).toHaveBeenCalledWith(`${process.env.FRONT_END_BASEURL}/auth/success/${mockToken}`); + }); +}); diff --git a/src/test/auth/calculatePasswordExpirationDate.test.ts b/src/test/auth/calculatePasswordExpirationDate.test.ts new file mode 100644 index 00000000..46202e9a --- /dev/null +++ b/src/test/auth/calculatePasswordExpirationDate.test.ts @@ -0,0 +1,32 @@ +import { calculatePasswordExpirationDate } from '../../controllers/authController'; +import { mockUser } from './mocks/user'; + +describe('calculatePasswordExpirationDate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should calculate expiration date based on lastPasswordUpdated', () => { + const expirationDate = calculatePasswordExpirationDate(mockUser); + const expectedExpirationDate = new Date('2023-06-08T10:40:00.000Z'); + + expect(expirationDate).toEqual(expectedExpirationDate); + }); + + it('should calculate expiration date based on createdAt if lastPasswordUpdated is not present', () => { + const userWithoutLastPasswordUpdated = { ...mockUser, lastPasswordUpdated: undefined }; + + const expirationDate = calculatePasswordExpirationDate(userWithoutLastPasswordUpdated); + const expectedExpirationDate = new Date('2023-01-08T10:40:00.000Z'); + + expect(expirationDate).toEqual(expectedExpirationDate); + }); + + it('should return null if neither lastPasswordUpdated nor createdAt are present', () => { + const userWithoutDates = { ...mockUser, lastPasswordUpdated: undefined, createdAt: undefined }; + + const expirationDate = calculatePasswordExpirationDate(userWithoutDates); + + expect(expirationDate).toBeNull(); + }); +}); diff --git a/src/test/auth/forgotPassword.test.ts b/src/test/auth/forgotPassword.test.ts new file mode 100644 index 00000000..ed20356f --- /dev/null +++ b/src/test/auth/forgotPassword.test.ts @@ -0,0 +1,86 @@ +import { forgotPassword } from '../../controllers/authController'; +import { Request, Response } from 'express'; +import User from '../../database/models/user'; +import { userToken } from '../../helpers/token.generator'; +import { sendEmail } from '../../helpers/send-email'; +import logger from '../../logs/config'; +import { sendInternalErrorResponse } from '../../validations'; + +jest.mock('../../database/models/user'); +jest.mock('../../helpers/token.generator'); +jest.mock('../../helpers/send-email'); +jest.mock('../../logs/config'); +jest.mock('../../validations'); + +jest.mock('../../database/models/otp', () => ({ + __esModule: true, + default: { + belongsTo: jest.fn(), + }, +})); + +describe('POST/ forgot Password', () => { + let reqMock: Partial; + let resMock: Partial; + + beforeEach(() => { + reqMock = { + body: { email: 'npemailmock@example.com' }, + }; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + + it('should respond with an error if the user does not exist', async () => { + (User.findOne as jest.Mock).mockResolvedValue(null); + + await forgotPassword(reqMock as Request, resMock as Response); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'npemailmock@example.com' } }); + expect(resMock.status).toHaveBeenCalledWith(404); + expect(resMock.json).toHaveBeenCalledWith({ ok: false, error: 'User with this email does not exist' }); + }); + + it('should send a reset email if the user exists', async () => { + const mockUser = { + id: '1', + email: 'npemailmock@example.com', + firstName: 'NP', + lastName: 'Leon', + }; + const mockToken = 'mockToken'; + const mockLink = `${process.env.URL_HOST}/api/auth/reset-password/${mockToken}`; + + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + (userToken as jest.Mock).mockResolvedValue(mockToken); + (sendEmail as jest.Mock).mockResolvedValue(true); + + await forgotPassword(reqMock as Request, resMock as Response); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'npemailmock@example.com' } }); + expect(userToken).toHaveBeenCalledWith('1', 'npemailmock@example.com'); + expect(sendEmail).toHaveBeenCalledWith('reset_password', { + name: 'NP Leon', + email: 'npemailmock@example.com', + link: mockLink, + }); + expect(resMock.status).toHaveBeenCalledWith(200); + expect(resMock.json).toHaveBeenCalledWith({ + ok: true, + message: 'A password reset link has been sent to your email.', + }); + }); + + it('should handle errors and call sendInternalErrorResponse', async () => { + const mockError = new Error('Internal error'); + (User.findOne as jest.Mock).mockRejectedValue(mockError) + + await forgotPassword(reqMock as Request, resMock as Response) + + expect(logger.error).toHaveBeenCalledWith('Error requesting password reset: ', mockError) + expect(sendInternalErrorResponse).toHaveBeenCalledWith(resMock, mockError) + }); +}); diff --git a/src/test/auth/mocks/user.ts b/src/test/auth/mocks/user.ts new file mode 100644 index 00000000..8fcc535b --- /dev/null +++ b/src/test/auth/mocks/user.ts @@ -0,0 +1,23 @@ +import { UserAttributes } from '../../../database/models/user'; + +enum UserStatus { + ACTIVE = 'active', + INACTIVE = 'inactive', +} + +export const mockUser: UserAttributes = { + id: '1', + firstName: 'Np', + lastName: 'Leon', + email: 'nlp@gmail.com', + password: 'mockPassword', + gender: 'male', + phoneNumber: '1234567890', + verified: true, + status: UserStatus.ACTIVE, + createdAt: new Date('2023-01-01T12:00:00Z'), + updatedAt: new Date('2023-01-01T12:00:00Z'), + RoleId: '1', + enable2FA: false, + lastPasswordUpdated: new Date('2023-06-01T12:00:00Z'), +}; diff --git a/src/test/auth/redirectToPasswordUpdate.test.ts b/src/test/auth/redirectToPasswordUpdate.test.ts new file mode 100644 index 00000000..9a4d69d7 --- /dev/null +++ b/src/test/auth/redirectToPasswordUpdate.test.ts @@ -0,0 +1,18 @@ +import { redirectToPasswordUpdate } from '../../controllers/authController'; +import { Response } from 'express'; + +describe('redirectToPasswordUpdate', () => { + let resMock: Partial; + + beforeEach(() => { + resMock = { + redirect: jest.fn(), + }; + }); + + it('should call res.redirect with the correct URL', () => { + redirectToPasswordUpdate(resMock as Response); + + expect(resMock.redirect).toHaveBeenCalledWith('/api/user/passwordUpdate'); + }); +}); diff --git a/src/test/auth/resetPassword.test.ts b/src/test/auth/resetPassword.test.ts new file mode 100644 index 00000000..b4095184 --- /dev/null +++ b/src/test/auth/resetPassword.test.ts @@ -0,0 +1,93 @@ +import { resetPassword } from '../../controllers/authController'; +import { Request, Response } from 'express'; +import User from '../../database/models/user'; +import { validatePassword } from '../../validations'; +import { passwordEncrypt } from '../../helpers/encrypt'; +import logger from '../../logs/config'; +import { sendInternalErrorResponse } from '../../validations'; + +jest.mock('../../database/models/user'); +jest.mock('../../validations'); +jest.mock('../../helpers/encrypt'); +jest.mock('../../logs/config'); +jest.mock('../../validations'); + +jest.mock('../../database/models/otp', () => ({ + __esModule: true, + default: { + belongsTo: jest.fn(), + }, +})); + +describe('POST/ reset Password Route', () => { + let reqMock: Partial; + let resMock: Partial; + + beforeEach(() => { + reqMock = { + body: { newPassword: 'NewP@ssw0rd' }, + user: { id: '1' } as User, + }; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + + it('should respond with an error if the user does not exist', async () => { + (User.findOne as jest.Mock).mockResolvedValue(null); + + await resetPassword(reqMock as Request, resMock as Response); + + expect(User.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + expect(resMock.status).toHaveBeenCalledWith(404); + expect(resMock.json).toHaveBeenCalledWith({ ok: false, error: 'User does not exist' }); + }); + + it('should respond with an error if the password is invalid', async () => { + const mockUser = { id: '1' }; + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + (validatePassword as jest.Mock).mockReturnValue(false); + + await resetPassword(reqMock as Request, resMock as Response); + + expect(User.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + expect(validatePassword).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(resMock.status).toHaveBeenCalledWith(400); + expect(resMock.json).toHaveBeenCalledWith({ + ok: false, + error: 'Password must contain at least 1 letter, 1 number, and 1 special character, minumun 8 characters', + }); + }); + + it('should update the user password and respond with success message', async () => { + const mockUser = { id: '1', update: jest.fn() }; + const mockHashedPassword = 'hashedPassword'; + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + (validatePassword as jest.Mock).mockReturnValue(true); + (passwordEncrypt as jest.Mock).mockResolvedValue(mockHashedPassword); + + await resetPassword(reqMock as Request, resMock as Response); + + expect(User.findOne).toHaveBeenCalledWith({ where: { id: '1' } }); + expect(validatePassword).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(passwordEncrypt).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(mockUser.update).toHaveBeenCalledWith({ password: mockHashedPassword }); + expect(resMock.status).toHaveBeenCalledWith(200); + expect(resMock.json).toHaveBeenCalledWith({ + ok: true, + message: 'Password reset successfully', + }); + }); + + it('should handle errors and call sendInternalErrorResponse', async () => { + const mockError = new Error('Internal error'); + (User.findOne as jest.Mock).mockRejectedValue(mockError); + + await resetPassword(reqMock as Request, resMock as Response); + + expect(logger.error).toHaveBeenCalledWith('Error resetting password: ', mockError); + expect(sendInternalErrorResponse).toHaveBeenCalledWith(resMock, mockError); + }); +}); diff --git a/src/test/auth/sendOTP.test.ts b/src/test/auth/sendOTP.test.ts new file mode 100644 index 00000000..f3a53225 --- /dev/null +++ b/src/test/auth/sendOTP.test.ts @@ -0,0 +1,101 @@ +import { sendOTP } from '../../controllers/authController'; +import { Request, Response } from 'express'; +import User from '../../database/models/user'; +import { createOTPToken, saveOTPDB } from '../../middlewares/otpAuthMiddleware'; +import jwt from 'jsonwebtoken'; + +jest.mock('../../database/models/user'); +jest.mock('../../middlewares/otpAuthMiddleware'); +jest.mock('jsonwebtoken'); + +jest.mock('../../database/models/otp', () => ({ + __esModule: true, + default: { + belongsTo: jest.fn(), + }, +})); + +describe('POST/ sendOTP route', () => { + let reqMock: Partial; + let resMock: Partial; + + beforeEach(() => { + reqMock = {}; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + + it('should respond with a token when OTP is successfully created and saved', async () => { + const mockUser = { + dataValues: { + id: '1', + email: 'email@gmail.com', + firstName: 'NP', + }, + }; + const mockOTPToken = 'mockOTPToken'; + const mockAccessToken = 'mockAccessToken'; + + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + (createOTPToken as jest.Mock).mockResolvedValue(mockOTPToken); + (saveOTPDB as jest.Mock).mockResolvedValue(true); + (jwt.sign as jest.Mock).mockReturnValue(mockAccessToken); + + await sendOTP(reqMock as Request, resMock as Response, 'email@gmail.com'); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'email@gmail.com' } }); + expect(createOTPToken).toHaveBeenCalledWith('1', 'email@gmail.com', 'NP'); + expect(saveOTPDB).toHaveBeenCalledWith('1', mockOTPToken); + expect(jwt.sign).toHaveBeenCalledWith({ id: '1', FAEnabled: true }, process.env.SECRET_KEY, { + expiresIn: process.env.JWT_EXPIRATION, + }); + expect(resMock.status).toHaveBeenCalledWith(200); + expect(resMock.json).toHaveBeenCalledWith({ ok: true, token: mockAccessToken }); + }); + + it('should respond with 404 if user is not found', async () => { + (User.findOne as jest.Mock).mockResolvedValue(null); + + await sendOTP(reqMock as Request, resMock as Response, 'email@gmail.com'); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'email@gmail.com' } }); + expect(resMock.status).toHaveBeenCalledWith(404); + expect(resMock.json).toHaveBeenCalledWith({ ok: false, message: 'User not found' }); + }); + + it('should handle errors and respond with 500', async () => { + const mockError = new Error('Internal error'); + (User.findOne as jest.Mock).mockRejectedValue(mockError); + + await sendOTP(reqMock as Request, resMock as Response, 'email@gmail.com'); + + expect(resMock.status).toHaveBeenCalledWith(500); + expect(resMock.json).toHaveBeenCalledWith({ ok: false, message: 'Internal Server Error' }); + }); + + it('should respond with 500 if saving OTP fails', async () => { + const mockUser = { + dataValues: { + id: '1', + email: 'email@gmail.com', + firstName: 'NP', + }, + }; + const mockOTPToken = 'mockOTPToken'; + + (User.findOne as jest.Mock).mockResolvedValue(mockUser); + (createOTPToken as jest.Mock).mockResolvedValue(mockOTPToken); + (saveOTPDB as jest.Mock).mockResolvedValue(false); + + await sendOTP(reqMock as Request, resMock as Response, 'email@gmail.com'); + + expect(User.findOne).toHaveBeenCalledWith({ where: { email: 'email@gmail.com' } }); + expect(createOTPToken).toHaveBeenCalledWith('1', 'email@gmail.com', 'NP'); + expect(saveOTPDB).toHaveBeenCalledWith('1', mockOTPToken); + expect(resMock.status).toHaveBeenCalledWith(500); + expect(resMock.json).toHaveBeenCalledWith({ ok: false, message: 'Failed to save OTP' }); + }); +}); diff --git a/src/test/auth/updatePassword.test.ts b/src/test/auth/updatePassword.test.ts new file mode 100644 index 00000000..7a6a1e92 --- /dev/null +++ b/src/test/auth/updatePassword.test.ts @@ -0,0 +1,90 @@ +import { updatePassword } from '../../controllers/authController'; +import { Request, Response } from 'express'; +import User from '../../database/models/user'; +import { validatePassword } from '../../validations'; +import { passwordEncrypt, passwordCompare } from '../../helpers/encrypt'; +import logger from '../../logs/config'; +import { sendInternalErrorResponse } from '../../validations'; +import { sendErrorResponse } from '../../helpers/helper'; + +jest.mock('../../database/models/user'); +jest.mock('../../validations'); +jest.mock('../../helpers/encrypt'); +jest.mock('../../logs/config'); +jest.mock('../../helpers/helper'); + +jest.mock('../../database/models/otp', () => ({ + __esModule: true, + default: { + belongsTo: jest.fn(), + }, +})); + +describe('PUT/ update Password route', () => { + let reqMock: Partial; + let resMock: Partial; + + beforeEach(() => { + reqMock = { + body: { oldPassword: 'OldP@ssw0rd', newPassword: 'NewP@ssw0rd' }, + user: { id: '1', password: 'hashedOldPassword' }, + }; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + + it('should respond with an error if the old password is incorrect', async () => { + (passwordCompare as jest.Mock).mockResolvedValue(false); + + await updatePassword(reqMock as Request, resMock as Response); + + expect(passwordCompare).toHaveBeenCalledWith('OldP@ssw0rd', 'hashedOldPassword'); + expect(sendErrorResponse).toHaveBeenCalledWith(resMock, 'The old password is incorrect!'); + }); + + it('should respond with an error if the new password is invalid', async () => { + (passwordCompare as jest.Mock).mockResolvedValue(true); + (validatePassword as jest.Mock).mockReturnValue(false); + + await updatePassword(reqMock as Request, resMock as Response); + + expect(passwordCompare).toHaveBeenCalledWith('OldP@ssw0rd', 'hashedOldPassword'); + expect(validatePassword).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(sendErrorResponse).toHaveBeenCalledWith( + resMock, + 'Ensuring it contains at least 1 letter, 1 number, and 1 special character, minumun 8 characters' + ); + }); + + it('should update the user password and respond with success message', async () => { + const mockHashedNewPassword = 'hashedNewPassword'; + (passwordCompare as jest.Mock).mockResolvedValue(true); + (validatePassword as jest.Mock).mockReturnValue(true); + (passwordEncrypt as jest.Mock).mockResolvedValue(mockHashedNewPassword); + + await updatePassword(reqMock as Request, resMock as Response); + + expect(passwordCompare).toHaveBeenCalledWith('OldP@ssw0rd', 'hashedOldPassword'); + expect(validatePassword).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(passwordEncrypt).toHaveBeenCalledWith('NewP@ssw0rd'); + expect(User.update).toHaveBeenCalledWith({ password: mockHashedNewPassword }, { where: { id: '1' } }); + expect(resMock.status).toHaveBeenCalledWith(200); + expect(resMock.json).toHaveBeenCalledWith({ + ok: true, + message: 'Successfully updated user password!', + }); + }); + + it('should handle errors and call sendInternalErrorResponse', async () => { + const mockError = new Error('Internal error'); + (passwordCompare as jest.Mock).mockRejectedValue(mockError); + + await updatePassword(reqMock as Request, resMock as Response); + + expect(logger.error).toHaveBeenCalledWith('Error updating user:', mockError); + expect(sendInternalErrorResponse).toHaveBeenCalledWith(resMock, mockError); + }); +}); diff --git a/src/test/auth/verifyOTP.test.ts b/src/test/auth/verifyOTP.test.ts new file mode 100644 index 00000000..5a803557 --- /dev/null +++ b/src/test/auth/verifyOTP.test.ts @@ -0,0 +1,45 @@ +import { verifyOTP } from '../../controllers/authController'; +import { Request, Response } from 'express'; +import { userToken } from '../../helpers/token.generator'; +import { sendInternalErrorResponse } from '../../validations'; +import logger from '../../logs/config'; + +jest.mock('../../helpers/token.generator'); +jest.mock('../../validations'); +jest.mock('../../logs/config'); + +describe('verifyOTP', () => { + let reqMock: Partial; + let resMock: Partial; + + beforeEach(() => { + reqMock = { + user: { id: 'mockUserId' }, + }; + resMock = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + }; + jest.clearAllMocks(); + }); + + it('should respond with a token when userToken resolves successfully', async () => { + const mockToken = 'mockToken'; + (userToken as jest.Mock).mockResolvedValue(mockToken); + + await verifyOTP(reqMock as Request, resMock as Response); + + expect(resMock.status).toHaveBeenCalledWith(200); + expect(resMock.json).toHaveBeenCalledWith({ ok: true, token: mockToken }); + }); + + it('should handle errors and call sendInternalErrorResponse', async () => { + const mockError = new Error('Internal error'); + (userToken as jest.Mock).mockRejectedValue(mockError); + + await verifyOTP(reqMock as Request, resMock as Response); + + expect(logger.error).toHaveBeenCalledWith('VerifyOTP Internal Server Error', mockError); + expect(sendInternalErrorResponse).toHaveBeenCalledWith(resMock, mockError); + }); +}); diff --git a/src/test/index.test.ts b/src/test/index.test.ts deleted file mode 100644 index 1950c3aa..00000000 --- a/src/test/index.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { addition } from '../controllers/testController'; - -test('Adding 2 and 1 equals 3', () => { - expect(2 + 1).toBe(3); -}); -test('Addition function adds two numbers correctly', () => { - // Test case 1: Testing addition of positive numbers - expect(addition(2, 3)).toBe(5); // Expected result: 2 + 3 = 5 - - // Test case 2: Testing addition of negative numbers - expect(addition(-2, -3)).toBe(-5); // Expected result: -2 + (-3) = -5 - - // Test case 3: Testing addition of a positive and a negative number - expect(addition(5, -3)).toBe(2); // Expected result: 5 + (-3) = 2 - - // Test case 4: Testing addition of zero and a number - expect(addition(0, 7)).toBe(7); // Expected result: 0 + 7 = 7 - - // Test case 5: Testing addition of a number and zero - expect(addition(4, 0)).toBe(4); // Expected result: 4 + 0 = 4 -});