From 538f1fbc0a6ccf83765fb74ea50d6cb976a7a395 Mon Sep 17 00:00:00 2001 From: Anjula Shanaka Date: Mon, 11 Sep 2023 07:11:47 +0530 Subject: [PATCH] Add the approve mentor and create category endpoints (#63) --- .github/workflows/sonarQube.yml | 19 ----- mocks.ts | 17 ++++ src/controllers/admin/category.controller.ts | 27 +++++++ src/controllers/admin/mentor.controller.ts | 36 +++++++++ src/controllers/admin/user.controller.ts | 3 +- src/controllers/mentor.controller.ts | 4 +- src/enums/index.ts | 2 +- src/routes/admin/admin.route.ts | 11 ++- .../admin/category/category.route.test.ts | 60 ++++++++++++++ src/routes/admin/category/category.route.ts | 9 +++ src/routes/admin/mentor/mentor.route.test.ts | 78 +++++++++++++++++++ src/routes/admin/mentor/mentor.route.ts | 9 +++ .../user.route.test.ts} | 34 +++----- src/routes/admin/user/user.route.ts | 9 +++ src/routes/auth/auth.route.test.ts | 15 ++-- src/routes/mentor/mentor.route.test.ts | 16 ++-- src/routes/profile/profile.route.test.ts | 11 +-- src/services/admin/category.service.ts | 27 +++++++ src/services/admin/mentor.service.ts | 38 +++++++++ .../user.service.ts} | 4 +- src/services/mentor.service.ts | 4 +- 21 files changed, 349 insertions(+), 84 deletions(-) delete mode 100644 .github/workflows/sonarQube.yml create mode 100644 src/controllers/admin/category.controller.ts create mode 100644 src/controllers/admin/mentor.controller.ts create mode 100644 src/routes/admin/category/category.route.test.ts create mode 100644 src/routes/admin/category/category.route.ts create mode 100644 src/routes/admin/mentor/mentor.route.test.ts create mode 100644 src/routes/admin/mentor/mentor.route.ts rename src/routes/admin/{admin.route.test.ts => user/user.route.test.ts} (71%) create mode 100644 src/routes/admin/user/user.route.ts create mode 100644 src/services/admin/category.service.ts create mode 100644 src/services/admin/mentor.service.ts rename src/services/{admin.service.ts => admin/user.service.ts} (66%) diff --git a/.github/workflows/sonarQube.yml b/.github/workflows/sonarQube.yml deleted file mode 100644 index 3280df19..00000000 --- a/.github/workflows/sonarQube.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Code scan - -on: - push: - branches: - - main - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/mocks.ts b/mocks.ts index dbd25c50..14c0b33a 100644 --- a/mocks.ts +++ b/mocks.ts @@ -1,3 +1,20 @@ +const randomString = Math.random().toString(36) + +export const mockMentor = { + email: `mentor${randomString}@gmail.com`, + password: '123' +} + +export const mockAdmin = { + email: `admin${randomString}@gmail.com`, + password: 'admin123' +} + +export const mockUser = { + email: `user${randomString}@gmail.com`, + password: '123' +} + export const mentorApplicationInfo = { application: [ { diff --git a/src/controllers/admin/category.controller.ts b/src/controllers/admin/category.controller.ts new file mode 100644 index 00000000..e6ded81b --- /dev/null +++ b/src/controllers/admin/category.controller.ts @@ -0,0 +1,27 @@ +import type { Request, Response } from 'express' +import type Profile from '../../entities/profile.entity' +import { ProfileTypes } from '../../enums' +import { createCategory } from '../../services/admin/category.service' + +export const addCategory = async ( + req: Request, + res: Response +): Promise => { + try { + const user = req.user as Profile + const { categoryName } = req.body + + if (user.type !== ProfileTypes.ADMIN) { + res.status(403).json({ message: 'Only Admins are allowed' }) + } else { + const { category, statusCode, message } = await createCategory( + categoryName + ) + + res.status(statusCode).json({ category, message }) + } + } catch (err) { + console.error('Error executing query', err) + res.status(500).json({ error: err }) + } +} diff --git a/src/controllers/admin/mentor.controller.ts b/src/controllers/admin/mentor.controller.ts new file mode 100644 index 00000000..4d0ee0d9 --- /dev/null +++ b/src/controllers/admin/mentor.controller.ts @@ -0,0 +1,36 @@ +import type { Request, Response } from 'express' +import { updateMentorStatus } from '../../services/admin/mentor.service' +import { ApplicationStatus, ProfileTypes } from '../../enums' +import type Profile from '../../entities/profile.entity' + +export const mentorStatusHandler = async ( + req: Request, + res: Response +): Promise => { + try { + const user = req.user as Profile + const { status } = req.body + const { mentorId } = req.params + + if (user.type !== ProfileTypes.ADMIN) { + res.status(403).json({ message: 'Only Admins are allowed' }) + } else { + if (!(status.toUpperCase() in ApplicationStatus)) { + res.status(400).json({ message: 'Please provide a valid status' }) + return + } + const { mentor, statusCode, message } = await updateMentorStatus( + mentorId, + status + ) + res.status(statusCode).json({ mentor, message }) + } + } catch (err) { + if (err instanceof Error) { + console.error('Error executing query', err) + res + .status(500) + .json({ error: 'Internal server error', message: err.message }) + } + } +} diff --git a/src/controllers/admin/user.controller.ts b/src/controllers/admin/user.controller.ts index aeb23c57..2d6e6052 100644 --- a/src/controllers/admin/user.controller.ts +++ b/src/controllers/admin/user.controller.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express' -import { getAllUsers } from '../../services/admin.service' +import { getAllUsers } from '../../services/admin/user.service' import type Profile from '../../entities/profile.entity' import { ProfileTypes } from '../../enums' @@ -9,6 +9,7 @@ export const getAllUsersHandler = async ( ): Promise => { try { const user = req.user as Profile + if (user.type !== ProfileTypes.ADMIN) { res.status(403).json({ message: 'Only Admins are allowed' }) } else { diff --git a/src/controllers/mentor.controller.ts b/src/controllers/mentor.controller.ts index e69cb0f5..d823938d 100644 --- a/src/controllers/mentor.controller.ts +++ b/src/controllers/mentor.controller.ts @@ -9,14 +9,12 @@ export const mentorApplicationHandler = async ( try { const user = req.user as Profile const { application, categoryId } = req.body + const { mentor, statusCode, message } = await createMentor( user, application, categoryId ) - if (!mentor) { - res.status(404).json({ message: 'Mentor not created' }) - } res.status(statusCode).json({ mentor, message }) } catch (err) { diff --git a/src/enums/index.ts b/src/enums/index.ts index 5695d5db..2557093f 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -12,5 +12,5 @@ export enum EmailStatusTypes { export enum ApplicationStatus { PENDING = 'pending', REJECTED = 'rejected', - ACCEPTED = 'accepted' + APPROVED = 'approved' } diff --git a/src/routes/admin/admin.route.ts b/src/routes/admin/admin.route.ts index 4bf7c01a..17e5d20b 100644 --- a/src/routes/admin/admin.route.ts +++ b/src/routes/admin/admin.route.ts @@ -1,9 +1,12 @@ import express from 'express' -import { getAllUsersHandler } from '../../controllers/admin/user.controller' -import { requireAuth } from '../../controllers/auth.controller' +import userRouter from './user/user.route' +import mentorRouter from './mentor/mentor.route' +import categoryRouter from './category/category.route' -const adminRouter = express.Router() +const adminRouter = express() -adminRouter.get('/users', requireAuth, getAllUsersHandler) +adminRouter.use('/users', userRouter) +adminRouter.use('/mentors', mentorRouter) +adminRouter.use('/categories', categoryRouter) export default adminRouter diff --git a/src/routes/admin/category/category.route.test.ts b/src/routes/admin/category/category.route.test.ts new file mode 100644 index 00000000..affa4848 --- /dev/null +++ b/src/routes/admin/category/category.route.test.ts @@ -0,0 +1,60 @@ +import { startServer } from '../../../app' +import type { Express } from 'express' +import supertest from 'supertest' +import Profile from '../../../entities/profile.entity' +import { ProfileTypes } from '../../../enums' +import { dataSource } from '../../../configs/dbConfig' +import bcrypt from 'bcrypt' +import { mockUser, mockAdmin } from '../../../../mocks' + +const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 + +let server: Express +let agent: supertest.SuperAgentTest +let adminAgent: supertest.SuperAgentTest + +describe('Admin category routes', () => { + beforeAll(async () => { + server = await startServer(port) + agent = supertest.agent(server) + adminAgent = supertest.agent(server) + + await supertest(server) + .post('/api/auth/register') + .send(mockUser) + .expect(201) + await agent.post('/api/auth/login').send(mockUser).expect(200) + + const profileRepository = dataSource.getRepository(Profile) + + const hashedPassword = await bcrypt.hash(mockAdmin.password, 10) + const newProfile = profileRepository.create({ + primary_email: mockAdmin.email, + password: hashedPassword, + contact_email: '', + first_name: '', + last_name: '', + image_url: '', + linkedin_url: '', + type: ProfileTypes.ADMIN + }) + + await profileRepository.save(newProfile) + + await adminAgent.post('/api/auth/login').send(mockAdmin).expect(200) + }, 5000) + + it('should add a category', async () => { + await adminAgent + .post('/api/admin/categories') + .send({ categoryName: 'Computer Science' }) + .expect(201) + }) + + it('should only allow admins to add a category', async () => { + await agent + .post('/api/admin/categories') + .send({ categoryName: 'Computer Science' }) + .expect(403) + }) +}) diff --git a/src/routes/admin/category/category.route.ts b/src/routes/admin/category/category.route.ts new file mode 100644 index 00000000..7f21fc0c --- /dev/null +++ b/src/routes/admin/category/category.route.ts @@ -0,0 +1,9 @@ +import express from 'express' +import { requireAuth } from '../../../controllers/auth.controller' +import { addCategory } from '../../../controllers/admin/category.controller' + +const categoryRouter = express.Router() + +categoryRouter.post('/', requireAuth, addCategory) + +export default categoryRouter diff --git a/src/routes/admin/mentor/mentor.route.test.ts b/src/routes/admin/mentor/mentor.route.test.ts new file mode 100644 index 00000000..4b78e3a6 --- /dev/null +++ b/src/routes/admin/mentor/mentor.route.test.ts @@ -0,0 +1,78 @@ +import { startServer } from '../../../app' +import type { Express } from 'express' +import supertest from 'supertest' +import Profile from '../../../entities/profile.entity' +import { ProfileTypes } from '../../../enums' +import { dataSource } from '../../../configs/dbConfig' +import bcrypt from 'bcrypt' +import { mentorApplicationInfo, mockAdmin, mockMentor } from '../../../../mocks' + +const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 + +let server: Express +let mentorAgent: supertest.SuperAgentTest +let adminAgent: supertest.SuperAgentTest +let mentorId: string + +describe('Admin mentor routes', () => { + beforeAll(async () => { + server = await startServer(port) + mentorAgent = supertest.agent(server) + adminAgent = supertest.agent(server) + + await mentorAgent.post('/api/auth/register').send(mockMentor).expect(201) + await mentorAgent.post('/api/auth/login').send(mockMentor).expect(200) + + const profileRepository = dataSource.getRepository(Profile) + const hashedPassword = await bcrypt.hash(mockAdmin.password, 10) + const newProfile = profileRepository.create({ + primary_email: mockAdmin.email, + password: hashedPassword, + contact_email: '', + first_name: '', + last_name: '', + image_url: '', + linkedin_url: '', + type: ProfileTypes.ADMIN + }) + + await profileRepository.save(newProfile) + + await adminAgent.post('/api/auth/login').send(mockAdmin).expect(200) + const categoryResponse = await adminAgent + .post('/api/admin/categories') + .send({ categoryName: 'Computer Science' }) + .expect(201) + + const response = await mentorAgent + .post('/api/mentors') + .send({ + ...mentorApplicationInfo, + categoryId: categoryResponse.body.category.uuid + }) + .expect(201) + + mentorId = response.body.mentor.uuid + }, 5000) + + it('should update the mentor application state', async () => { + await adminAgent + .put(`/api/admin/mentors/${mentorId}/status`) + .send({ status: 'approved' }) + .expect(200) + }) + + it('should return 400 when an invalid status was provided', async () => { + await adminAgent + .put(`/api/admin/mentors/${mentorId}/status`) + .send({ status: 'invalid status' }) + .expect(400) + }) + + it('should only allow admins to update the status', async () => { + await mentorAgent + .put(`/api/admin/mentors/${mentorId}/status`) + .send({ status: 'approved' }) + .expect(403) + }) +}) diff --git a/src/routes/admin/mentor/mentor.route.ts b/src/routes/admin/mentor/mentor.route.ts new file mode 100644 index 00000000..c9775994 --- /dev/null +++ b/src/routes/admin/mentor/mentor.route.ts @@ -0,0 +1,9 @@ +import express from 'express' +import { requireAuth } from '../../../controllers/auth.controller' +import { mentorStatusHandler } from '../../../controllers/admin/mentor.controller' + +const mentorRouter = express.Router() + +mentorRouter.put('/:mentorId/status', requireAuth, mentorStatusHandler) + +export default mentorRouter diff --git a/src/routes/admin/admin.route.test.ts b/src/routes/admin/user/user.route.test.ts similarity index 71% rename from src/routes/admin/admin.route.test.ts rename to src/routes/admin/user/user.route.test.ts index d88ef533..60f153aa 100644 --- a/src/routes/admin/admin.route.test.ts +++ b/src/routes/admin/user/user.route.test.ts @@ -1,47 +1,35 @@ -import { startServer } from '../../app' +import { startServer } from '../../../app' import type { Express } from 'express' import supertest from 'supertest' -import Profile from '../../entities/profile.entity' -import { ProfileTypes } from '../../enums' -import { dataSource } from '../../configs/dbConfig' +import Profile from '../../../entities/profile.entity' +import { ProfileTypes } from '../../../enums' +import { dataSource } from '../../../configs/dbConfig' import bcrypt from 'bcrypt' +import { mockAdmin, mockUser } from '../../../../mocks' const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 -const randomString = Math.random().toString(36) -const randomStringAdmin = Math.random().toString(36) let server: Express let agent: supertest.SuperAgentTest let adminAgent: supertest.SuperAgentTest -describe('Get all users route', () => { +describe('Admin user routes', () => { beforeAll(async () => { server = await startServer(port) agent = supertest.agent(server) adminAgent = supertest.agent(server) - const defaultUser = { - email: `test${randomString}@gmail.com`, - password: '123' - } - await supertest(server) .post('/api/auth/register') - .send(defaultUser) + .send(mockUser) .expect(201) - - await agent.post('/api/auth/login').send(defaultUser).expect(200) - - const adminUser = { - email: `test${randomStringAdmin}@gmail.com`, - password: 'admin123' - } + await agent.post('/api/auth/login').send(mockUser).expect(200) const profileRepository = dataSource.getRepository(Profile) - const hashedPassword = await bcrypt.hash(adminUser.password, 10) + const hashedPassword = await bcrypt.hash(mockAdmin.password, 10) const newProfile = profileRepository.create({ - primary_email: adminUser.email, + primary_email: mockAdmin.email, password: hashedPassword, contact_email: '', first_name: '', @@ -53,7 +41,7 @@ describe('Get all users route', () => { await profileRepository.save(newProfile) - await adminAgent.post('/api/auth/login').send(adminUser).expect(200) + await adminAgent.post('/api/auth/login').send(mockAdmin).expect(200) }, 5000) it('should return a 401 when a valid access token is not provided', async () => { diff --git a/src/routes/admin/user/user.route.ts b/src/routes/admin/user/user.route.ts new file mode 100644 index 00000000..2af5a34b --- /dev/null +++ b/src/routes/admin/user/user.route.ts @@ -0,0 +1,9 @@ +import express from 'express' +import { getAllUsersHandler } from '../../../controllers/admin/user.controller' +import { requireAuth } from '../../../controllers/auth.controller' + +const userRouter = express.Router() + +userRouter.get('/', requireAuth, getAllUsersHandler) + +export default userRouter diff --git a/src/routes/auth/auth.route.test.ts b/src/routes/auth/auth.route.test.ts index 7055187b..c0ee5fcb 100644 --- a/src/routes/auth/auth.route.test.ts +++ b/src/routes/auth/auth.route.test.ts @@ -2,18 +2,13 @@ import { startServer } from '../../app' import type { Express } from 'express' import supertest from 'supertest' import { dataSource } from '../../configs/dbConfig' +import { mockUser } from '../../../mocks' -const randomString = Math.random().toString(36) const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 let server: Express let agent: supertest.SuperAgentTest -const testUser = { - email: `test${randomString}@gmail.com`, - password: '123' -} - beforeAll(async () => { server = await startServer(port) agent = supertest.agent(server) @@ -28,7 +23,7 @@ describe('auth controllers', () => { it('should return a 201 with a user profile after successful registration', async () => { const response = await supertest(server) .post('/api/auth/register') - .send(testUser) + .send(mockUser) .expect(201) expect(response.body).toHaveProperty('message') @@ -48,7 +43,7 @@ describe('auth controllers', () => { it('should return a 400 when registering with a duplicate email', async () => { await supertest(server) .post('/api/auth/register') - .send(testUser) + .send(mockUser) .expect(409) }) }) @@ -61,7 +56,7 @@ describe('auth controllers', () => { it('should return a 200 after successful login', async () => { const response = await supertest(server) .post('/api/auth/login') - .send(testUser) + .send(mockUser) .expect(200) expect(response.body).toHaveProperty('message') @@ -69,7 +64,7 @@ describe('auth controllers', () => { it('should return a 401 when logging in with incorrect credentials', async () => { const incorrectUser = { - email: `test${randomString}@gmail.com`, + email: mockUser.email, password: 'incorrect_password' } diff --git a/src/routes/mentor/mentor.route.test.ts b/src/routes/mentor/mentor.route.test.ts index f14d63a7..2f7f2714 100644 --- a/src/routes/mentor/mentor.route.test.ts +++ b/src/routes/mentor/mentor.route.test.ts @@ -2,11 +2,10 @@ import { startServer } from '../../app' import type { Express } from 'express' import supertest from 'supertest' import { dataSource } from '../../configs/dbConfig' -import { mentorApplicationInfo } from '../../../mocks' +import { mentorApplicationInfo, mockUser } from '../../../mocks' import { v4 as uuidv4 } from 'uuid' import Category from '../../entities/category.entity' -const randomString = Math.random().toString(36) const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 let server: Express @@ -18,17 +17,12 @@ describe('Mentor application', () => { server = await startServer(port) agent = supertest.agent(server) - const testUser = { - email: `test${randomString}@gmail.com`, - password: '123' - } - await supertest(server) .post('/api/auth/register') - .send(testUser) + .send(mockUser) .expect(201) - await agent.post('/api/auth/login').send(testUser).expect(200) + await agent.post('/api/auth/login').send(mockUser).expect(200) const categoryRepository = dataSource.getRepository(Category) const newCategory = new Category('Random Category', []) @@ -36,11 +30,11 @@ describe('Mentor application', () => { }, 5000) describe('Apply as a mentor route', () => { - it('should return a 200 with a mentor object and the message', async () => { + it('should return a 201 with a mentor and the message', async () => { const response = await agent .post('/api/mentors') .send({ ...mentorApplicationInfo, categoryId: savedCategory.uuid }) - .expect(200) + .expect(201) expect(response.body).toHaveProperty('mentor') expect(response.body).toHaveProperty('message') diff --git a/src/routes/profile/profile.route.test.ts b/src/routes/profile/profile.route.test.ts index cd6ec293..cceedacd 100644 --- a/src/routes/profile/profile.route.test.ts +++ b/src/routes/profile/profile.route.test.ts @@ -2,8 +2,8 @@ import { startServer } from '../../app' import type { Express } from 'express' import supertest from 'supertest' import { dataSource } from '../../configs/dbConfig' +import { mockUser } from '../../../mocks' -const randomString = Math.random().toString(36) const port = Math.floor(Math.random() * (9999 - 3000 + 1)) + 3000 let server: Express @@ -14,17 +14,12 @@ describe('profile', () => { server = await startServer(port) agent = supertest.agent(server) - const testUser = { - email: `test${randomString}@gmail.com`, - password: '123' - } - await supertest(server) .post('/api/auth/register') - .send(testUser) + .send(mockUser) .expect(201) - await agent.post('/api/auth/login').send(testUser).expect(200) + await agent.post('/api/auth/login').send(mockUser).expect(200) }, 5000) describe('Get profile route', () => { diff --git a/src/services/admin/category.service.ts b/src/services/admin/category.service.ts new file mode 100644 index 00000000..0b1cc3f7 --- /dev/null +++ b/src/services/admin/category.service.ts @@ -0,0 +1,27 @@ +import { dataSource } from '../../configs/dbConfig' +import Category from '../../entities/category.entity' + +export const createCategory = async ( + categoryName: string +): Promise<{ + statusCode: number + category?: Category | null + message: string +}> => { + try { + const categoryRepository = dataSource.getRepository(Category) + + const newCategory = new Category(categoryName, []) + + const saveCategory = await categoryRepository.save(newCategory) + + return { + statusCode: 201, + category: saveCategory, + message: 'Category created successfully' + } + } catch (err) { + console.error('Error creating category', err) + throw new Error('Error creating category') + } +} diff --git a/src/services/admin/mentor.service.ts b/src/services/admin/mentor.service.ts new file mode 100644 index 00000000..99a41bc2 --- /dev/null +++ b/src/services/admin/mentor.service.ts @@ -0,0 +1,38 @@ +import { dataSource } from '../../configs/dbConfig' +import Mentor from '../../entities/mentor.entity' +import type { ApplicationStatus } from '../../enums' + +export const updateMentorStatus = async ( + mentorId: string, + status: ApplicationStatus +): Promise<{ + statusCode: number + mentor?: Mentor | null + message: string +}> => { + try { + const mentorRepository = dataSource.getRepository(Mentor) + + const mentor = await mentorRepository.findOne({ + where: { uuid: mentorId } + }) + + if (!mentor) { + return { + statusCode: 404, + message: 'Mentor not found' + } + } + + await mentorRepository.update({ uuid: mentorId }, { state: status }) + + return { + statusCode: 200, + mentor, + message: 'Updated Mentor application status successfully' + } + } catch (err) { + console.error('Error updating the mentor status', err) + throw new Error('Error updating the mentor status') + } +} diff --git a/src/services/admin.service.ts b/src/services/admin/user.service.ts similarity index 66% rename from src/services/admin.service.ts rename to src/services/admin/user.service.ts index e4a9fa3d..1ccaa270 100644 --- a/src/services/admin.service.ts +++ b/src/services/admin/user.service.ts @@ -1,5 +1,5 @@ -import { dataSource } from '../configs/dbConfig' -import Profile from '../entities/profile.entity' +import { dataSource } from '../../configs/dbConfig' +import Profile from '../../entities/profile.entity' export const getAllUsers = async (): Promise => { const profileRepository = dataSource.getRepository(Profile) diff --git a/src/services/mentor.service.ts b/src/services/mentor.service.ts index da1177a4..e8193e6a 100644 --- a/src/services/mentor.service.ts +++ b/src/services/mentor.service.ts @@ -40,7 +40,7 @@ export const createMentor = async ( statusCode: 409, message: 'The mentor application is pending' } - case ApplicationStatus.ACCEPTED: + case ApplicationStatus.APPROVED: return { mentor, statusCode: 409, @@ -63,7 +63,7 @@ export const createMentor = async ( await mentorRepository.save(newMentor) return { - statusCode: 200, + statusCode: 201, mentor: newMentor, message: 'Mentor application is successful' }