Skip to content

Commit

Permalink
feat(profile): implement profile management functionality
Browse files Browse the repository at this point in the history
-implement endpoints for updating email, password and profile image

[Delivers #13]
  • Loading branch information
jkarenzi committed Jun 30, 2024
1 parent 744ca92 commit f1da67e
Show file tree
Hide file tree
Showing 21 changed files with 736 additions and 159 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ EMAIL =
EMAIL_PASS =
MONGO_INITDB_ROOT_USERNAME =
MONGO_INITDB_ROOT_PASSWORD =
CLOUD_NAME =
CLOUD_API_KEY =
CLOUD_API_SECRET =
MONGO_URL =
18 changes: 17 additions & 1 deletion __tests__/testSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const request = require('supertest')
const app = require('../src/app');
const mongoose = require('mongoose')
require('dotenv').config()
const User = require('../src/models/User')
const bcrypt = require('bcrypt')

const url = process.env.MONGO_URL
const dbName = process.env.DB_NAME
Expand Down Expand Up @@ -35,4 +37,18 @@ const getToken = async() => {
return loginResponse.body.token
}

module.exports = {getToken, connectDB, disconnectDB}
const getAdminToken = async() => {
const password = await bcrypt.hash('admin123456', 10);
const user = new User({
fullName: 'Test Admin',
email: '[email protected]',
password: password,
role: 'admin'
});

const newUser = await user.save();
const loginResponse = await request(app).post('/api/auth/login').send({email:newUser.email, password: 'admin123456'})
return loginResponse.body.token
}

module.exports = {getToken, getAdminToken, connectDB, disconnectDB}
219 changes: 219 additions & 0 deletions __tests__/userController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
export {};
const request = require('supertest');
const app = require('../src/app');
const mongoose = require('mongoose')
const User = require('../src/models/User')
const bcrypt = require('bcrypt')
require('dotenv').config();
const cloudinary = require('../src/middleware/cloudinary')
const { connectDB, disconnectDB, getAdminToken } = require('./testSetup');

jest.mock('../src/middleware/cloudinary')

beforeAll(connectDB);
afterAll(disconnectDB);

interface cloudinaryUploadResult {
public_id: string,
url: string,
[key: string]: unknown;
}

describe('User Controller tests', () => {
let token:string;
let userId:string;

beforeAll(async() => {
const password = await bcrypt.hash('test123456', 10);
const user = new User({
fullName: 'Test User',
email: '[email protected]',
password: password
});

const newUser = await user.save();
userId = newUser._id;

token = await getAdminToken()
})

it('should get all users', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(200);
});

it('should get a user by ID', async () => {
const res = await request(app)
.get(`/api/users/${userId}`)
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(200);
expect(res.body.data).toHaveProperty('_id', userId.toString());
});

it('should return 404 if user not found', async () => {
const nonExistentUserId = new mongoose.Types.ObjectId();
const res = await request(app)
.get(`/api/users/${nonExistentUserId}`)
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(404);
expect(res.body.message).toBe('User not found');
});

it('should change the password', async () => {
const res = await request(app)
.patch('/api/users/password')
.set('Authorization', `Bearer ${token}`)
.send({
oldPassword: 'admin123456',
newPassword: 'test123456',
})

expect(res.status).toBe(200);
expect(res.body.message).toBe('Password successfully updated');
});

it('should return 400 if password validation fails', async () => {
const res = await request(app)
.patch('/api/users/password')
.set('Authorization', `Bearer ${token}`)
.send({
oldPassword: '',
newPassword: 'short',
})

expect(res.status).toBe(400)
expect(res.body.message).toBeDefined();
});

it('should return 401 if old password is incorrect', async () => {
const res = await request(app)
.patch('/api/users/password')
.set('Authorization', `Bearer ${token}`)
.send({
oldPassword: 'wrongpassword',
newPassword: 'newpassword',
})

expect(res.status).toBe(401);
expect(res.body.message).toBe('Incorrect password');
});

it('should change the email', async () => {
const res = await request(app)
.patch('/api/users/email')
.set('Authorization', `Bearer ${token}`)
.send({
password: 'test123456',
newEmail: '[email protected]',
})

expect(res.status).toBe(200);
expect(res.body.message).toBe('Email successfully updated');
expect(res.body.data.email).toBe('[email protected]');
});

it('should return 400 if email validation fails', async () => {
const res = await request(app)
.patch('/api/users/email')
.set('Authorization', `Bearer ${token}`)
.send({
password: 'newpassword',
newEmail: 'invalid-email',
})

expect(res.status).toBe(400)
expect(res.body.message).toBeDefined();
});

it('should return 401 if password is incorrect when changing email', async () => {
const res = await request(app)
.patch('/api/users/email')
.set('Authorization', `Bearer ${token}`)
.send({
password: 'wrongpassword',
newEmail: '[email protected]',
})

expect(res.status).toBe(401);
expect(res.body.message).toBe('Incorrect password');
});

it('should return a 409 if email already exists upon changing email', async () => {
const res = await request(app)
.patch('/api/users/email')
.set('Authorization', `Bearer ${token}`)
.send({
password: 'test123456',
newEmail: '[email protected]',
})

expect(res.status).toBe(409);
expect(res.body.message).toBe('Email already exists');
});

it('should return a 204 upon successfully deleting a user', async () => {
const res = await request(app)
.delete(`/api/users/${userId}`)
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(204);
})

it('should return a 404 if user is not found upon deletion', async () => {
const nonExistentUserId = new mongoose.Types.ObjectId();
const res = await request(app)
.delete(`/api/users/${nonExistentUserId}`)
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(404);
expect(res.body.message).toBe('User not found')
})

it('should return a 409 if user tries to remove a non existent profile image', async () => {
const res = await request(app)
.delete('/api/users/profileImg')
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(409);
expect(res.body.message).toBe('No profile image exists')
})

it('should change profile image successfully', async () => {
cloudinary.uploader.upload_stream.mockImplementationOnce((callback:(error: Error|null, result: cloudinaryUploadResult) => void) => {
callback(null, {public_id:'testid', url:'https://fakeurl.com/fake.png'});
});

const res = await request(app)
.patch('/api/users/profileImg')
.set('Authorization', `Bearer ${token}`)
.set('Content-Type', 'multipart/form-data')
.attach('image', Buffer.from('mock-image-data'), 'image.jpg')

expect(res.status).toBe(200);
expect(res.body.message).toBe('Profile Image successfully updated')
})

it('should return a 400 if an error occurs while removing profile image', async () => {
cloudinary.uploader.destroy.mockImplementationOnce(() => Promise.resolve({result:'not ok'}))
const res = await request(app)
.delete('/api/users/profileImg')
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(400);
expect(res.body.message).toBe('An error occured. Try again later')
})

it('should remove profile image successfully', async () => {
cloudinary.uploader.destroy.mockImplementationOnce(() => Promise.resolve({result:'ok'}))
const res = await request(app)
.delete('/api/users/profileImg')
.set('Authorization', `Bearer ${token}`)

expect(res.status).toBe(204);
})
});
12 changes: 4 additions & 8 deletions src/controllers/boardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ const {


const createBoard = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const formData = req.body

const validationResult = createBoardSchema.validate(formData);
Expand Down Expand Up @@ -38,15 +37,13 @@ const createBoard = errorHandler(async (req:Request, res:Response) => {
})

const getBoards = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const boards = await Board.find({userId})
return res.status(200).json({status:'success', data:boards})
})

const updateBoard = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const boardId = req.params.boardId
const formData = req.body

Expand Down Expand Up @@ -78,8 +75,7 @@ const updateBoard = errorHandler(async (req:Request, res:Response) => {

const deleteBoard = errorHandler(async (req:Request, res:Response) => {
const boardId = req.params.boardId
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id

const board = await Board.findOne({_id:boardId, userId})
if(!board){
Expand Down
12 changes: 4 additions & 8 deletions src/controllers/categoryController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ const {


const createCategory = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const formData = req.body

const validationResult = createCategorySchema.validate(formData);
Expand Down Expand Up @@ -38,16 +37,14 @@ const createCategory = errorHandler(async (req:Request, res:Response) => {
})

const getCategories = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const boardId = req.params.boardId
const categories = await Category.find({boardId, userId})
return res.status(200).json({status:'success', data:categories})
})

const updateCategory = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const categoryId = req.params.categoryId
const formData = req.body

Expand Down Expand Up @@ -79,8 +76,7 @@ const updateCategory = errorHandler(async (req:Request, res:Response) => {

const deleteCategory = errorHandler(async (req:Request, res:Response) => {
const categoryId = req.params.categoryId
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id

const category = await Category.findOne({_id:categoryId, userId})
if(!category){
Expand Down
12 changes: 4 additions & 8 deletions src/controllers/labelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ const {


const createLabel = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const formData = req.body

const validationResult = createLabelSchema.validate(formData);
Expand Down Expand Up @@ -38,16 +37,14 @@ const createLabel = errorHandler(async (req:Request, res:Response) => {
})

const getLabels = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const boardId = req.params.boardId
const labels = await Label.find({boardId, userId})
return res.status(200).json({status:'success', data:labels})
})

const updateLabel = errorHandler(async (req:Request, res:Response) => {
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id
const labelId = req.params.labelId
const formData = req.body

Expand Down Expand Up @@ -79,8 +76,7 @@ const updateLabel = errorHandler(async (req:Request, res:Response) => {

const deleteLabel = errorHandler(async (req:Request, res:Response) => {
const labelId = req.params.labelId
//@ts-expect-error yet to come up with the right type
const userId = req.user._id
const userId = req.user!._id

const label = await Label.findOne({_id:labelId, userId})
if(!label){
Expand Down
Loading

0 comments on commit f1da67e

Please sign in to comment.