Skip to content

Commit

Permalink
feat(email-verification): implement email integraton
Browse files Browse the repository at this point in the history
-implement email verification
-implement 2fa authentication

[Delivers #4]
  • Loading branch information
jkarenzi committed Jun 13, 2024
1 parent c1d809f commit 51c09e7
Show file tree
Hide file tree
Showing 16 changed files with 496 additions and 87 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/dockerci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI for taskMaster Project

on:
push:
branches: ['develop']
pull_request:
branches: ['develop']

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build

- name: Set up Docker Compose
run: sudo apt-get install docker-compose

- name: Build and run tests
run: docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test

- run: npm run lint
- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: jkarenzi/task-master-be
directory: coverage/

cleanup:
runs-on: ubuntu-latest
if: always()
steps:
- name: Shutdown and remove services
run: docker-compose -f docker-compose.test.yml down
10 changes: 10 additions & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM node:latest

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npm", "run", "test"]
196 changes: 115 additions & 81 deletions __tests__/authController.test.ts
Original file line number Diff line number Diff line change
@@ -1,134 +1,168 @@
export {};
const request = require('supertest');
const app = require('../src/app');
const bcrypt = require('bcrypt');
const {
signUpSchema,
loginSchema,
} = require('../src/middleware/validators/authSchema');
const jwt = require('jsonwebtoken')
require('dotenv').config()
const User = require('../src/models/User');
const sendEmail = require('../src/utils/sendEmail')
const jwt = require('jsonwebtoken')
import {Document} from 'mongoose'
const {connectDB, disconnectDB, getToken} = require('./testSetup')

jest.mock('../src/models/User');
jest.mock('../src/middleware/validators/authSchema');
jest.mock('bcrypt');
jest.mock('jsonwebtoken')

describe('Auth Controller Tests', () => {
const signUpFormData = {
fullName: 'test tester',
email: '[email protected]',
password: 'test123456',
};

const loginFormData = {
email: '[email protected]',
password: 'test123456',
};

const returnedUser = {
_id:'some id',
fullName:'mock user',
email:'[email protected]',
password:'password1234',
createdAt: 'some date',
updatedAt: 'some date'
}
beforeAll(connectDB)
afterAll(disconnectDB)

it('should return a 201 if signup is successful', async () => {
signUpSchema.validate.mockReturnValueOnce({ error: null });
let token:string;

beforeAll(async() => {
token = await getToken()
})

User.findOne.mockImplementationOnce(() => Promise.resolve(null));
jest.mock('../src/utils/sendEmail')

bcrypt.hash.mockResolvedValueOnce('hashed password');
interface IUser extends Document {
_id:any,
fullName:string,
email:string,
password:string,
imageUrl:{
public_id:string,
url?:string
},
isVerified:boolean,
twoFactorAuth:{
isEnabled:true,
code: number
},
role:string
}

User.prototype.save.mockResolvedValueOnce(returnedUser);

describe('Auth Controller Tests', () => {
let user:IUser;
const signUpFormData = {
fullName: 'test tester',
email: '[email protected]',
password: 'test123456',
};

const loginFormData = {
email: '[email protected]',
password: 'test123456',
};

sendEmail.mockImplementationOnce(() => Promise.resolve({response:"ok"}))


it('should return a 201 if signup is successful', async () => {
const response = await request(app).post('/api/auth/signup').send(signUpFormData);
expect(response.status).toBe(201);
expect(User.prototype.save).toHaveBeenCalled();
});

it('should return a 400 if validation fails on signup', async () => {
signUpSchema.validate.mockReturnValueOnce({ error: { details: [{ message: 'Validation failed' }] } });
const formData = {
fullName: 'test tester',
email: 'testgmail.com',
password: 'test123456',
}

const response = await request(app).post('/api/auth/signup').send(signUpFormData);
const response = await request(app).post('/api/auth/signup').send(formData);
expect(response.status).toBe(400);
expect(response.body.message).toBeDefined();
})

it('should return a 409 if email already exists', async () => {
signUpSchema.validate.mockReturnValueOnce({ error: null });

User.findOne.mockImplementationOnce(() => Promise.resolve({
_id:'some id',
fullName:'mock user',
email:'[email protected]',
password:'password1234',
createdAt: 'some date',
updatedAt: 'some date'
}));

const response = await request(app).post('/api/auth/signup').send(signUpFormData);
expect(response.status).toBe(409);
expect(response.body.message).toBe('Email already in use');
})

it('should return a 500 if an error occurs during signup', async () => {
signUpSchema.validate.mockReturnValueOnce({ error: null });
User.findOne.mockImplementationOnce(() => {throw new Error('DB error')})

const response = await request(app).post('/api/auth/signup').send(signUpFormData);
expect(response.status).toBe(500);
expect(response.body.message).toBe('Internal Server Error');
it('should return a 200 if login is successful with 2fa disabled', async () => {
const response = await request(app).post('/api/auth/login').send(loginFormData);
expect(response.status).toBe(200);
expect(response.body.message).toBe('Login successful');
})

it('should return a 200 if login is successful', async () => {
loginSchema.validate.mockReturnValueOnce({ error: null });
User.findOne.mockImplementationOnce(() => Promise.resolve(returnedUser))

bcrypt.compare.mockResolvedValueOnce(true)

jwt.sign.mockResolvedValueOnce('fake token')

it('should return a 200 if upon login with 2fa enabled', async () => {
user = await User.findOne({email:loginFormData.email})
await User.findByIdAndUpdate(user._id,{"twoFactorAuth.isEnabled":true})
const response = await request(app).post('/api/auth/login').send(loginFormData);
expect(response.status).toBe(200);
expect(response.body.message).toBe('Login successful');
expect(response.body.message).toBe('A Two Factor Auth Code has been sent to your email');
})

it('should return a 400 if validation fails on login', async () => {
loginSchema.validate.mockReturnValueOnce({ error: { details: [{ message: 'Validation failed' }] } });
const formData = {
email:"testgmail.com",
password:"karenzi123456"
}

const response = await request(app).post('/api/auth/login').send(loginFormData);
const response = await request(app).post('/api/auth/login').send(formData);
expect(response.status).toBe(400);
expect(response.body.message).toBeDefined()
})

it('should return a 404 if account if not found during login', async () => {
loginSchema.validate.mockReturnValueOnce({ error: null });
User.findOne.mockImplementationOnce(() => Promise.resolve(null))
const response = await request(app).post('/api/auth/login').send(loginFormData);
const formData = {
email:"[email protected]",
password:"user123456"
}

const response = await request(app).post('/api/auth/login').send(formData);
expect(response.status).toBe(404);
expect(response.body.message).toBe('Account not found');
})

it('should return a 401 if password provided is incorrect, in login', async () => {
loginSchema.validate.mockReturnValueOnce({ error: null });
User.findOne.mockImplementationOnce(() => Promise.resolve(returnedUser))

bcrypt.compare.mockResolvedValueOnce(false)
const formData = {
email:"[email protected]",
password:"user123456"
}

const response = await request(app).post('/api/auth/login').send(loginFormData);
const response = await request(app).post('/api/auth/login').send(formData);
expect(response.status).toBe(401);
expect(response.body.message).toBe('Incorrect password');
})

it('should return a 500 if an error occurs during login', async () => {
loginSchema.validate.mockReturnValueOnce({ error: null });
User.findOne.mockImplementationOnce(() => {throw new Error('DB error')})
// email verification tests
it('should verify email successfully', async () => {
const token = await jwt.sign({_id:user._id}, process.env.JWT_SECRET, {expiresIn:'1h'})
const response = await request(app).get(`/api/auth/verify_email/${token}`)
expect(response.status).toBe(200);
expect(response.body.message).toBe('Email verification successful');
})

const response = await request(app).post('/api/auth/login').send(loginFormData);
expect(response.status).toBe(500);
expect(response.body.message).toBe('Internal Server Error');
it('should request new email link successfully', async () => {
const response = await request(app).post('/api/auth/request_new_link').set('Authorization', `Bearer ${token}`)
expect(response.status).toBe(200);
expect(response.body.message).toBe('Email sent successfully');

})

// 2fa tests
it('should verify 2fa code successfully', async () => {
await request(app).post('/api/auth/login').send(loginFormData);
const {twoFactorAuth} = await User.findById(user._id)

const response = await request(app).get(`/api/auth/verify_code/${user._id.toHexString()}`).send({
twoFactorCode:twoFactorAuth.code
})

expect(response.status).toBe(200);
expect(response.body.message).toBe('Login successful');

})

it('should request new 2fa code successfully', async () => {
const response = await request(app).get(`/api/auth/request_new_code/${user._id.toHexString()}`)
expect(response.status).toBe(200);
expect(response.body.message).toBe('A Two Factor Auth Code has been sent to your email');
})

it('should update 2fa enabled status', async () => {
console.log(token)
const response = await request(app).patch('/api/auth/toggle_2fa').send({status:true}).set('Authorization', `Bearer ${token}`)
expect(response.status).toBe(200);
expect(response.body.message).toBe('Update successful');
})
});
38 changes: 38 additions & 0 deletions __tests__/testSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export {}
const request = require('supertest')
const app = require('../src/app');
const mongoose = require('mongoose')
require('dotenv').config()

const url = process.env.MONGO_URL
const dbName = process.env.DB_NAME


const connectDB = async () => {
await mongoose.connect(url, {dbName})
console.log('Mongo db connected')
}

const disconnectDB = async () => {
await mongoose.connection.close()
}

const getToken = async() => {
const signupFormData = {
fullName: "Testing Tester",
email:"[email protected]",
password:"test123456"
}

const loginFormData = {
fullName: "Testing Tester",
email:"[email protected]",
password:"test123456"
}

await request(app).post('/api/auth/signup').send(signupFormData)
const loginResponse = await request(app).post('/api/auth/login').send(loginFormData)
return loginResponse.body.token
}

module.exports = {getToken, connectDB, disconnectDB}
21 changes: 21 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: '3.8'
services:
test:
build:
context: .
dockerfile: Dockerfile.test
env_file:
- .env
environment:
- NODE_ENV=test
- MONGO_URL=mongodb://admin:taskmaster@mongo:27017
- DB_NAME=test
depends_on:
- mongo
mongo:
image: mongo:latest
ports:
- "32768:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=taskmaster
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
version: '3'
services:
app:
build: .
build:
context: .
dockerfile: Dockerfile
ports:
- '8000:3000'
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
env_file:
- .env
- .env
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
"testTimeout": 50000,
testPathIgnorePatterns: [
"/__tests__/testSetup.ts"
]
};
Loading

0 comments on commit 51c09e7

Please sign in to comment.