-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(email-verification): implement email integraton
-implement email verification -implement 2fa authentication [Delivers #4]
- Loading branch information
Showing
16 changed files
with
496 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.