Skip to content

Commit

Permalink
feat(tests): Add test cases for forget password flow and password res…
Browse files Browse the repository at this point in the history
…et functionality
  • Loading branch information
sanjaysah101 committed Sep 15, 2024
1 parent 7dd105b commit b70dfea
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 20 deletions.
83 changes: 83 additions & 0 deletions src/api/v1/auth/__test__/reset-password.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
expectBadRequestResponseForValidationError,
expectFindUserByUsernameSuccess,
expectForgetPasswordSuccess,
expectLoginFailed,
expectLoginSuccess,
expectOTPVerificationSuccess,
expectResetPasswordSuccess,
expectSignUpSuccess,
findUserByUsername,
forgetPassword,
login,
resetPassword,
retrieveOTP,
signUp,
verifyAccount,
verifyOTP,
} from '@/utils/test';
import { Response } from 'supertest';
import { GetUser } from '../../user/user.validation';

const user = {
username: 'username',
email: 'validemail@example.com',
password: 'ValidPassword123!',
confirmPassword: 'ValidPassword123!',
};

describe('Reset Password', () => {
let verifiedOTPResponse: Response;

beforeAll(async () => {
const response = await signUp(user);
expectSignUpSuccess(response);

verifyAccount(user);
});

it('should throw error if token, password, confirmPassword is not provided', async () => {
const res = await resetPassword({ token: '', password: '', confirmPassword: '' });
expectBadRequestResponseForValidationError(res);
});

it('should throw error if token is invalid', async () => {
const res = await resetPassword({ token: 'a', password: 'a', confirmPassword: 'a' });
expectBadRequestResponseForValidationError(res);
});

it('should throw error if password and confirmPassword do not match', async () => {
const res = await resetPassword({ token: 'a', password: 'a', confirmPassword: 'b' });
expectBadRequestResponseForValidationError(res);
});

it('should verify OTP successfully', async () => {
const res = await forgetPassword(user.email);
expectForgetPasswordSuccess(res);

const userResponse = await findUserByUsername(user.username);
expectFindUserByUsernameSuccess(userResponse, user);
const userDetails: GetUser = userResponse.body.user;

const otpData = await retrieveOTP(userDetails.id, 'sendForgetPasswordOTP');
verifiedOTPResponse = await verifyOTP(otpData, user.email);
expectOTPVerificationSuccess(verifiedOTPResponse);
});

it('should reset password successfully', async () => {
const { token } = verifiedOTPResponse.body;

const res = await resetPassword({ token, password: 'ValidPassword123@', confirmPassword: 'ValidPassword123@' });
expectResetPasswordSuccess(res);
});

it('should not login with old password', async () => {
const res = await login(user);
expectLoginFailed(res);
});

it('should login with new password', async () => {
const res = await login({ ...user, password: 'ValidPassword123@' });
expectLoginSuccess(res);
});
});
2 changes: 1 addition & 1 deletion src/api/v1/auth/auth.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ router.post('/auth/logout', validateAccessToken, AuthController.logout);
router.post('/auth/renew-token', validateRefreshToken, AuthController.renewToken);
router.post('/auth/verify-token', validateAccessToken, AuthController.verifyToken);
router.post('/auth/forget-password', AuthController.forgetPassword);
router.post('/auth/reset-password', AuthController.resetPassword);

router.get('/auth/google', signInWithGoogle);
router.get('/auth/google/callback', signInWithGoogleCallback, AuthController.signInWithGoogleCallback);
router.post('/auth/reset-password', AuthController.resetPassword);

export { router as authRoutes };
4 changes: 2 additions & 2 deletions src/api/v1/otp/otp.dal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export class OtpDAL {
return await newOtp.save();
}

static async getOtp(otpSchema: GetOtp): Promise<OtpSchema | null> {
return await OtpModel.findOne(otpSchema);
static async getOtpDetailsByUserId(otpSchema: GetOtp): Promise<OtpSchema[] | null> {
return await OtpModel.find(otpSchema);
}

static async deleteOtp(otpId: string) {
Expand Down
14 changes: 12 additions & 2 deletions src/api/v1/otp/otp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { generateOTP, verifyOTP } from '@/utils';
import { success } from '@/api/v1/auth/auth.constant';
import { UserService } from '@/api/v1/user/user.service';
import { OtpDAL } from './otp.dal';
import { OtpEvent, otpEvent, OtpVerifyEvent, otpVerifyEvent } from './otp.validation';
import { GetOtp, OtpEvent, otpEvent, OtpSchema, OtpVerifyEvent, otpVerifyEvent } from './otp.validation';
import { notificationService } from '@/services/notification.services';
import { TokenService } from '../token/token.service';
import { TokenAction } from '../token/token.validation';
Expand Down Expand Up @@ -61,11 +61,13 @@ export class OtpService {

const user = await UserService.getUserByEmail(email as string);

const otpData = await OtpDAL.getOtp({
const otpDetails = await OtpService.getOtpDetailsByUserId({
userId: user.id,
otpType,
});

const otpData = otpDetails.find((data) => data.otpType === otpType);

if (!otpData) throw new Error(ErrorTypeEnum.enum.OTP_NOT_REQUESTED);

const otpToVerify = isMasterOTP && envConstants.NODE_ENV !== 'production' ? envConstants.MASTER_OTP : otpData.otp;
Expand All @@ -86,4 +88,12 @@ export class OtpService {

return { message: success.OTP_VERIFIED, token };
}

public static async getOtpDetailsByUserId(getOtpDetails: GetOtp): Promise<OtpSchema[]> {
const otpDetails = await OtpDAL.getOtpDetailsByUserId(getOtpDetails);

if (!otpDetails) throw new Error(ErrorTypeEnum.enum.OTP_NOT_REQUESTED);

return otpDetails;
}
}
30 changes: 24 additions & 6 deletions src/utils/test/auth.utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import supertest, { Response } from 'supertest';
import { app } from '@/app';
import { Login } from '@/api/v1/auth/auth.validation';
import { STATUS_CODES } from '@/constants';
import { errorMap, ErrorTypeEnum, STATUS_CODES } from '@/constants';
import { success } from '@/api/v1/auth/auth.constant';
import { expectOTPRequestSuccess, expectOTPVerificationSuccess, requestOTP, retrieveOTP, verifyOTP } from './otp.utils';
import { expectFindUserByUsernameSuccess, findUserByUsername } from './user.utils';
import { CreateUser } from '../../api/v1/user/user.validation';
import { CreateUser, ResetPassword } from '../../api/v1/user/user.validation';

export async function login(loginData: Login): Promise<Response> {
return supertest(app).post('/api/v1/auth/login').send(loginData);
Expand All @@ -29,6 +29,14 @@ export function expectLoginSuccess(response: Response): void {
});
}

export const expectLoginFailed = (response: Response) => {
const errorObject = errorMap[ErrorTypeEnum.enum.INVALID_CREDENTIALS];

expect(response.statusCode).toBe(errorObject.httpStatusCode);
expect(response.body.message).toBe(errorObject.body.message);
expect(response.body.code).toBe(errorObject.body.code);
};

export async function signUp(signUpData: {
email: string;
username: string;
Expand Down Expand Up @@ -89,11 +97,10 @@ export const verifyAccount = async (user: CreateUser) => {
expectFindUserByUsernameSuccess(userResponse, user);

// Step 2: Retrieve OTP from database
const otpData = await retrieveOTP(userResponse.body.user.id);
const otpData = await retrieveOTP(userResponse.body.user.id, 'sendEmailVerificationOTP');

// Step 3: Verify OTP
const verifyResponse = await verifyOTP(otpData?.otp, email);

const verifyResponse = await verifyOTP(otpData, email);
expectOTPVerificationSuccess(verifyResponse);
};

Expand All @@ -106,6 +113,17 @@ export function expectForgetPasswordSuccess(response: Response): void {
const { statusCode, body } = response;

expect(statusCode).toBe(STATUS_CODES.OK);

expect(body).toMatchObject({ message: success.FORGET_PASSWORD_EMAIL_SENT });
}

export async function resetPassword(resetPassword: ResetPassword): Promise<Response> {
return supertest(app).post(`/api/v1/auth/reset-password`).send(resetPassword);
}

export function expectResetPasswordSuccess(response: Response): void {
expect(response).toBeDefined();
const { statusCode, body } = response;

expect(statusCode).toBe(STATUS_CODES.OK);
expect(body).toMatchObject({ message: success.PASSWORD_RESET_SUCCESSFULLY });
}
17 changes: 8 additions & 9 deletions src/utils/test/otp.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import supertest, { Response } from 'supertest';
import { app } from '@/app';
import { STATUS_CODES } from '@/constants';
import { success } from '@/api/v1/auth/auth.constant';
import { OtpDAL } from '@/api/v1/otp/otp.dal';
import { OTP, OtpSchema } from '@/api/v1/otp/otp.validation';
import { OtpSchema, OtpType } from '@/api/v1/otp/otp.validation';
import { OtpService } from '../../api/v1/otp/otp.service';

export async function requestOTP(email: string): Promise<Response> {
return supertest(app).post('/api/v1/otp').send({
Expand All @@ -17,16 +17,15 @@ export function expectOTPRequestSuccess(response: Response): void {
expect(response.body.message).toBe(success.VERIFICATION_EMAIL_SENT);
}

export async function retrieveOTP(userId: string): Promise<OtpSchema | null> {
return OtpDAL.getOtp({
userId: userId,
otpType: 'sendEmailVerificationOTP',
});
export async function retrieveOTP(userId: string, otpType: OtpType): Promise<OtpSchema> {
const otpDetails = await OtpService.getOtpDetailsByUserId({ userId, otpType });
const otpData = otpDetails.find((data) => data.otpType === otpType);
return otpData as OtpSchema;
}

export async function verifyOTP(otp: OTP | undefined, email: string): Promise<Response> {
export async function verifyOTP({ otp, otpType }: OtpSchema, email: string): Promise<Response> {
return supertest(app).post('/api/v1/otp/verify').send({
otpType: 'sendEmailVerificationOTP',
otpType,
email,
otp,
});
Expand Down

0 comments on commit b70dfea

Please sign in to comment.