Skip to content

Commit

Permalink
add new tests for buyers login
Browse files Browse the repository at this point in the history
  • Loading branch information
EddyShimwa committed May 14, 2024
1 parent 618f648 commit 6635ff7
Show file tree
Hide file tree
Showing 8 changed files with 707 additions and 125 deletions.
588 changes: 578 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"jest": "^29.7.0",
"joi": "^17.13.1",
"jsonwebtoken": "^9.0.2",
"mailgun-js": "^0.22.0",
"morgan": "^1.10.0",
"nodemailer": "^6.9.13",
"nodemon": "^3.1.0",
Expand Down Expand Up @@ -72,6 +73,7 @@
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/mailgun-js": "^0.22.18",
"@types/morgan": "^1.9.9",
"@types/nodemailer": "^6.4.15",
"@types/passport": "^1.0.16",
Expand Down
10 changes: 7 additions & 3 deletions src/__test__/testSetup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { DbConnection } from '../database/index';
import UserModel from '../database/models/userModel';

import { Role } from '../database/models';

export async function beforeAllHook() {
await DbConnection.instance.initializeDb();
// removing all data from role table

// Get repositories
const userRepository = await DbConnection.connection.getRepository(UserModel);
const roleRepository = await DbConnection.connection.getRepository(Role);

// Delete all users and roles
await userRepository.createQueryBuilder().delete().execute();
await roleRepository.createQueryBuilder().delete().execute();
}

Expand All @@ -17,4 +21,4 @@ export async function afterAllHook() {
console.log(repository);

await DbConnection.instance.disconnectDb();
}
}
58 changes: 42 additions & 16 deletions src/__test__/userController.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import request from 'supertest';
import app from '../app';
// import { Role } from '../database/models';
import { afterAllHook, beforeAllHook } from './testSetup';
import jwt from 'jsonwebtoken';
import dbConnection from '../database';
// import bcrpt from 'bcrypt';
import UserModel from '../database/models/userModel';
// import { use } from 'passport';
const userRepository = dbConnection.getRepository(UserModel);

beforeAll(beforeAllHook);
Expand Down Expand Up @@ -151,7 +148,7 @@ describe('User Registration Tests', () => {


describe('User Login Tests', () => {
it('should log in a user with valid credentials', async () => {
it('should log in a vendor with valid credentials', async () => {
const formData = {
name: 'Vendor',
permissions: ['test-permission1', 'test-permission2'],
Expand Down Expand Up @@ -180,11 +177,8 @@ describe('User Login Tests', () => {

expect(loginResponse.status).toBe(200);
expect(loginResponse.body.message).toBe('Please provide the 2FA code sent to your email.');
} else {
throw new Error('User not found');
}
});


it('should verify the 2FA code for a vendor user', async () => {
const userData = {
Expand All @@ -203,11 +197,7 @@ describe('User Login Tests', () => {
if (user) {
user.isVerified = true;
await userRepository.save(user);
} else {
throw new Error('User not found');
}


const loginResponse = await request(app).post('/api/v1/login').send({
email: userData.email,
password: userData.password,
Expand All @@ -228,12 +218,50 @@ describe('User Login Tests', () => {

expect(verifyResponse.status).toBe(200);
expect(verifyResponse.body).toHaveProperty('token');
} else {
throw new Error('User not found');
}
});


it('should log in a buyer with valid credentials', async () => {
const formData = {
name: 'Buyer',
permissions: ['test-permission1', 'test-permission2'],
};

// Create the role first
const roleResponse = await request(app).post('/api/v1/roles/create_role').send(formData);

const userData = {
firstName: 'Test',
lastName: 'User',
email: 'test2@gmail.com',
password: 'TestPassword123',
userType: roleResponse.body.id
};
await request(app).post('/api/v1/register').send(userData);

const updatedUser = await userRepository.findOne({ where: { email: userData.email } });
if (updatedUser) {
updatedUser.isVerified = true;
await userRepository.save(updatedUser);

const loginResponse = await request(app).post('/api/v1/login').send({
email: userData.email,
password: userData.password,
});

expect(loginResponse.status).toBe(200);
expect(loginResponse.body.token).toBeDefined();
expect(loginResponse.body.message).toBe('Buyer Logged in successfully');

// Decode the token and check its properties
const decodedToken = jwt.decode(loginResponse.body.token);
expect(decodedToken).toHaveProperty('userId');
expect(decodedToken).toHaveProperty('iat');
expect(decodedToken).toHaveProperty('exp');
}
});

it('should return a 401 status code if the email is not verified', async () => {
const userData = {
firstName: 'Test',
Expand All @@ -257,9 +285,7 @@ describe('User Login Tests', () => {

expect(loginResponse.status).toBe(401);
expect(loginResponse.body.message).toBe('Please verify your email. Confirmation link has been sent.'); // Corrected message
} else {
throw new Error('User not found');
}
}
});

it('should return a 401 status code if the password does not match', async () => {
Expand Down
121 changes: 50 additions & 71 deletions src/controller/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserModel from '../database/models/userModel';
import sendEmail from '../emails/index';
import { sendCode } from '../emails/mailer';
import jwt from 'jsonwebtoken';
import errorHandler from '../middlewares/errorHandler'

// Assuming dbConnection.getRepository(UserModel) returns a repository instance
const userRepository = dbConnection.getRepository(UserModel);
Expand Down Expand Up @@ -167,83 +168,61 @@ export const deleteUser = async (req: Request, res: Response) => {
}}


export const Login = async (req: Request, res: Response) => {
try {
const user = await userRepository.findOne({
where: { email: req.body['email'] },
relations: ['userType']
});
if (!user) {
return res.status(404).send({ message: 'User Not Found' });
}
const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database
if (!passwordMatch) {
return res.status(401).send({ message: 'Password does not match' });
}
if (!user.isVerified) {
// Send confirmation email if user is not verified
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET as jwt.Secret,
{ expiresIn: '1d' }
);
const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`;
await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink });
return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' });
}

// If user is a vendor, proceed with 2FA
if (user.userType.name === 'Vendor') {
// Generate a new 2FA code
const twoFactorCode = Math.floor(100000 + Math.random() * 900000);

// Store the 2FA code in the user's record in the database
await userRepository.update(user.id, { twoFactorCode });

// Send 2FA code to user's email
await sendCode(
user.email,
'Your 2FA Code',
'./templates/2fa.html',
{ name: user.firstName, twoFactorCode: twoFactorCode.toString() }
);

// Respond with a message asking for the 2FA code
res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' });
} else {
// If user is not a vendor,
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
res.status(200).json({ token });
}
} catch (error) {
res.status(500).send(error);
export const Login = errorHandler(async (req: Request, res: Response) => {
const user = await userRepository.findOne({
where: { email: req.body['email'] },
relations: ['userType']
});
if (!user) {
return res.status(404).send({ message: 'User Not Found' });
}
const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database
if (!passwordMatch) {
return res.status(401).send({ message: 'Password does not match' });
}
if (!user.isVerified) {
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET as jwt.Secret,
{ expiresIn: '1d' }
);
const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`;
await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink });
return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' });
}
}

export const verify2FA = async (req: Request, res: Response): Promise<void> => {
try {
const { code } = req.body;
const { userId } = req.params;
if (user.userType.name === 'Vendor') {
const twoFactorCode = Math.floor(100000 + Math.random() * 900000);

// Use the repository to find the user by their id
const user = await userRepository.findOne({ where: { id: Number(userId) } });
await userRepository.update(user.id, { twoFactorCode });

if (!user) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
if (code !== user.twoFactorCode) {
res.status(401).json({ error: 'Invalid code' });
return;
}
await sendCode(
user.email,
'Your 2FA Code',
'./templates/2fa.html',
{ name: user.firstName, twoFactorCode: twoFactorCode.toString() }
);

// Generate JWT
res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' });
} else {
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
res.status(200).json({ token, message: 'Buyer Logged in successfully'});
}
});

// Send JWT to the user
res.status(200).json({ token });
export const verify2FA = errorHandler(async (req: Request, res: Response) => {
const { code } = req.body;
const { userId } = req.params;

} catch (error) {
res.status(500).json({ error: (error as Error).message });
const user = await userRepository.findOne({ where: { id: Number(userId) } });

if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
if (code !== user.twoFactorCode) {
return res.status(401).json({ error: 'Invalid code' });
}
};

const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
return res.status(200).json({ token });
});
6 changes: 2 additions & 4 deletions src/database/models/userModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
ManyToOne,
} from 'typeorm';
import { Role } from './roleEntity';

Expand All @@ -25,8 +24,7 @@ export default class UserModel {
@Column({ nullable: true })
password: string;

@OneToOne(() => Role, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn()
@ManyToOne(() => Role)
userType: Role;

@Column({ nullable: true })
Expand Down
29 changes: 17 additions & 12 deletions src/emails/mailer.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import nodemailer from 'nodemailer';
import mailgun from 'mailgun-js';
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
dotenv.config()

const mg = mailgun({
apiKey: process.env.MAILGUN_TOKEN || 'default_api_key',
domain: process.env.MAILGUN_DOMAIN || 'default_domain'
});
export async function sendCode(to: string, subject: string, htmlTemplatePath: string, replacements: Record<string, string>) {
const template = await fs.promises.readFile(path.resolve(__dirname, htmlTemplatePath), 'utf8');

let html = template;
for (const placeholder in replacements) {
html = html.replace(new RegExp(`{{${placeholder}}}`, 'g'), replacements[placeholder]);
}

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});

const mailOptions = {
from: 'dynamitesecommerce@gmail.com',
to,
Expand All @@ -26,5 +23,13 @@ export async function sendCode(to: string, subject: string, htmlTemplatePath: st
};

// Send the email
return transporter.sendMail(mailOptions);
}
return new Promise((resolve, reject) => {
mg.messages().send(mailOptions, (error, body) => {
if (error) {
reject(error);
} else {
resolve(body);
}
});
});
}
18 changes: 9 additions & 9 deletions src/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import { Request, Response } from 'express';

type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => Promise<void>;
type MiddlewareFunction = (req: Request, res: Response) => Promise<Response<Record<string, unknown>> | undefined>;

function errorHandler(func: MiddlewareFunction) {
return async (req: Request, res: Response, next: NextFunction) => {
function errorHandler(func: MiddlewareFunction): MiddlewareFunction {
return async (req: Request, res: Response) => {
try {
await func(req, res, next);
} catch (error) { // Removed the type annotation from the catch clause variable because it caused liting errors
const message = error.detail || 'Internal Server Error';
res.status(500).send(message);
return await func(req, res);
} catch (error) {
const message = (error as { detail?: string }).detail || 'Internal Server Error';
return res.status(500).send(message);
}
};
}

export default errorHandler;
export default errorHandler;

0 comments on commit 6635ff7

Please sign in to comment.