diff --git a/README.md b/README.md index fb44850..b2addbc 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ The **customer-service-app** is an application that allows users logs complaints - Key Application features -1. Support Request +1. Functional requirements - Creation of Support requests - Fetching support requests - Updating support requests - Closing support requests logged + - Download CSV file of closed support requests for the last month 2. Comment - Users can comment on a support request @@ -36,22 +37,25 @@ The **customer-service-app** is an application that allows users logs complaints - Clone the repo and cd into it: - ``` + ``` bash + #!/bin/bash git clone https://github.com/dbytecoderc/test-app.git ``` - Install dependencies using the command bellow: - ``` + ``` bash + #!/bin/bash yarn install ``` - Make a copy of the .env.sample file in the app folder and rename it to .env and update the variables accordingly, **it important that you copy the email and password details in that file just the way it is, you would need it to test admin functionalities and make sure the db urls are set to make sure the tests run**: - ``` + ``` bash + #!/bin/bash PORT=3000 - MONGODB_URI="mongourl" - MONGO_URI_TEST="test mongourl" + MONGODB_URI="mongodb://127.0.0.1/fliqpay" + MONGO_URI_TEST="mongodb://127.0.0.1/fliqpay-test" SECRET_KEY="secretKey" ADMIN_PASSWORD="Admin0007" NON_ADMIN_PASSWORD="User0007" @@ -59,32 +63,37 @@ The **customer-service-app** is an application that allows users logs complaints HASHED_NON_ADMIN_PASSWORD='$2a$10$WSwcXM1dIaygWLaSQMxAD.cNBDZmykPNJOWOkjwpiFiPr8CrT68ha' ``` -* Run the application with the command +NOTE: It is important that the `MONGO_URI_TEST` is set because the integration tests depend on it to run -``` +- Run the application with the command -yarn dev +``` bash + #!/bin/bash +yarn start:dev ``` - Data is seeded into the application as soon as you fire up the server, without needing to create a user you can login and create a json web token which is to be attached to the header in this format -``` +``` bash + #!/bin/bash Bearer 'sample token' ``` - Use these details to login an admin user -``` +``` bash + #!/bin/bash { "email": "admin@admin.com", "password": "Admin0007" } ``` -- Use these details to login an non-admin user +- Use these details to login a non-admin user -``` +``` bash + #!/bin/bash { "email": "nonadmin@nonadmin.com", "password": "User0007" @@ -93,22 +102,20 @@ Bearer 'sample token' ## Running tests -Make sure the test database is set for this to work - -``` +Make sure the environment variable for the test database `MONGO_URI_TEST` is set for this to work +``` bash + #!/bin/bash yarn test ``` +NOTE: All the seed data are baked into the testing process, you won't need to run any script to seed data for tests, and as soon as the application relevant data is seeded into the application to ease manual testing + ## API Endpoints - Use the link below in the thumbnail to download a postman collection for the endpoints - [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/9452a28c0f505b49eea3) - -- Alternatively you can use this [link](https://documenter.getpostman.com/view/6057580/T1DjkziE?version=latest#a2542775-3976-45ca-a981-4453e29e2a6e) to view the api documentation in your browser. - -## Notes + [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/880c258c0500487ff4e6?action=collection%2Fimport#?env%5BFLIQPAY%5D=W3sia2V5IjoiYmFzZS11cmwiLCJ2YWx1ZSI6IlxuaHR0cDovL2xvY2FsaG9zdDozMDAwL2FwaS92MSIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoidXNlci10b2tlbiIsInZhbHVlIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lKcWIyaHVaRzlsUUdkdFlXbHNMbU52YlNJc0ltRmtiV2x1SWpwbVlXeHpaU3dpYVdGMElqb3hOakU0TlRFek56azVMQ0psZUhBaU9qRTJNVGt4TVRnMU9UbDkuOWl2RFhMd05CZHhuTm5EeDhmWEI3TG56a1I0TEpOSE9IQmk3ZG5PZURsdyIsImVuYWJsZWQiOnRydWV9LHsia2V5Ijoic2Vjb25kLXVzZXItdG9rZW4iLCJ2YWx1ZSI6ImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUp6ZFdJaU9pSnFiMmh1Wkc5bE1rQm5iV0ZwYkM1amIyMGlMQ0poWkcxcGJpSTZabUZzYzJVc0ltbGhkQ0k2TVRZeE9EVXhPRGMxTkN3aVpYaHdJam94TmpFNU1USXpOVFUwZlEuaTRKd1J5R0NXUVp0OW1idTlfclBqdnVEekdEMDdKOUhPWHBlc2g5Tko1ZyIsImVuYWJsZWQiOnRydWV9LHsia2V5IjoiYWRtaW4tdXNlci10b2tlbiIsInZhbHVlIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnpkV0lpT2lKaFpHMXBia0JuYldGcGJDNWpiMjBpTENKaFpHMXBiaUk2ZEhKMVpTd2lhV0YwSWpveE5qRTROVEUzT1RnMExDSmxlSEFpT2pFMk1Ua3hNakkzT0RSOS5yYnA3dDlVVE1HMnAxNTA5QXpnNGVialBXandfNDZJaVU3M1pKZl81d3NBIiwiZW5hYmxlZCI6dHJ1ZX1d) -- For feedback I thought the assessment specs could be better in terms of clarifying some of the instructions for ease of understanding. -- Due to time constraints I couldn't increase the test coverage, although I covered all the essential parts of the application. +- Alternatively you can use this [link](https://documenter.getpostman.com/view/6057580/TzJrBeaA) to view the api documentation in your browser. +- After visiting the link you can click on the dropdowns of each request to view preset examples of request edge-cases covered. diff --git a/src/@types/express/index.d.ts b/src/@types/express/index.d.ts index 4a1ebbb..5cec43b 100644 --- a/src/@types/express/index.d.ts +++ b/src/@types/express/index.d.ts @@ -8,6 +8,18 @@ interface CreateUserInput { password: string; } + +interface SupportRequest extends Document { + description: string; + owner?: User; + comments?: Comment[] +} + +interface Comment extends Document { + description: string; + owner?: User | string +} + interface User extends Document { name: string; email: string; diff --git a/src/app.ts b/src/app.ts index eb00dde..be6b4e0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,21 +1,20 @@ -import express, { Application } from 'express'; +import express, { Application, Request, Response } from 'express'; import dotenv from 'dotenv'; dotenv.config(); import cors from 'cors'; -import * as bodyparser from 'body-parser'; import dbconnect from './config/connection.db'; import morgan from 'morgan'; import modules from './modules'; -// import seedData from './database/seeders/seeder'; +import seedData from './database/seeders/seeder'; const app: Application = express(); const { PORT } = process.env; app.use(cors()); -app.use(bodyparser.json()); - +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); if (process.env.NODE_ENV === 'development') { app.use(morgan('dev')); } @@ -24,16 +23,16 @@ if (process.env.NODE_ENV === 'development') { modules(app); // catch all routers -app.use('*', (req, res) => { - res.status(404).json({ +app.use('*', (request: Request, response: Response) => { + return response.status(404).json({ message: 'Not Found. Use /api/{app version} to access the Api', }); }); dbconnect().then(async () => { - // if (process.env.NODE_ENV !== 'test') { - // await seedData(); - // } + if (process.env.NODE_ENV !== 'test') { + await seedData(); + } if (!module.parent) { app.listen(PORT, () => { console.log( diff --git a/src/database/seeders/seeder.ts b/src/database/seeders/seeder.ts index 15b3b3c..fc15843 100644 --- a/src/database/seeders/seeder.ts +++ b/src/database/seeders/seeder.ts @@ -2,6 +2,7 @@ import dotenv from 'dotenv'; import User from '../models/User'; import SupportRequest from '../models/SupportRequest'; +import Comment from '../models/Comment' dotenv.config(); @@ -10,6 +11,7 @@ const { HASHED_ADMIN_PASSWORD, HASHED_NON_ADMIN_PASSWORD } = process.env; const seedData = async () => { await User.deleteMany({}); await SupportRequest.deleteMany({}); + await Comment.deleteMany({}); const user1 = new User({ _id: '5e1863eeb0eb0406250967ba', name: 'Admin user', @@ -24,18 +26,16 @@ const seedData = async () => { password: HASHED_NON_ADMIN_PASSWORD, }); - // t - await user1.save(); await user2.save(); await User.findOneAndUpdate({ email: 'admin@admin.com' }, { admin: true }); const user = await User.findOne({ email: 'nonadmin@nonadmin.com' }); - const createdAt1 = new Date(2020, 4, 30); - const endedAt1 = new Date(2020, 5, 27); - const endedAt2 = new Date(2020, 5, 25); - const endedAt3 = new Date(2020, 5, 22); - const endedAt4 = new Date(2020, 5, 10); + const createdAt1 = new Date(2021, 4, 16); + const endedAt1 = new Date(2021, 4, 17); + const endedAt2 = new Date(2021, 5, 25); + const endedAt3 = new Date(2021, 5, 22); + const endedAt4 = new Date(2021, 5, 10); const supportRequest1 = new SupportRequest({ _id: '5f14396f8cd92082e4bcb2f8', diff --git a/src/index.ts b/src/index.ts index 736e9c1..7109579 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,6 @@ import chalk from "chalk"; import { env } from "./config"; import app from "./app"; import logger from "./config/logger"; -// import Utils from "./util/Utils"; -// import Error from "./util/Error"; export const server: http.Server = http.createServer(app); diff --git a/src/modules/comment/__test__/comment.spec.ts b/src/modules/comment/__test__/comment.spec.ts index e94bafd..60270d5 100644 --- a/src/modules/comment/__test__/comment.spec.ts +++ b/src/modules/comment/__test__/comment.spec.ts @@ -61,6 +61,7 @@ describe('TEST SUITE FOR COMMENTS', () => { afterAll(async (done) => { await User.deleteMany({}); await SupportRequest.deleteMany({}); + await Comment.deleteMany({}); await mongoose.connection.close(); server.close(); done(); diff --git a/src/modules/comment/comment.controller.ts b/src/modules/comment/comment.controller.ts index c45cf79..80ac32e 100644 --- a/src/modules/comment/comment.controller.ts +++ b/src/modules/comment/comment.controller.ts @@ -2,7 +2,6 @@ import { Request, Response } from 'express'; import CommentRepository from './comment.repository'; import SupportRequestRepository from '../support-request/support-request.repository'; -// import Utils from '../../utils/utils'; import Error from '../../utils/Error'; export default class SupportRequestController { @@ -33,11 +32,11 @@ export default class SupportRequestController { return Error.handleError('Support request not found', 400, response); } - if (!admin && supportRequest.owner._id.toString() !== _id.toString()) { + if (!admin && supportRequest.owner!._id.toString() !== _id.toString()) { return Error.handleError('Not allowed', 400, response); } - if (supportRequest.comments.length === 0 && !request.user.admin) { + if (supportRequest.comments!.length === 0 && !request.user.admin) { return Error.handleError( 'No support agent has responded to this request', 400, @@ -45,7 +44,7 @@ export default class SupportRequestController { ); } - if (supportRequest.comments.length === 0 && request.user.admin) { + if (supportRequest.comments!.length === 0 && request.user.admin) { await SupportRequestRepository.updateSupportRequest( supportRequest._id, { status: 'INPROGRESS' }, diff --git a/src/modules/comment/comment.repository.ts b/src/modules/comment/comment.repository.ts index 1be0d6c..c05673a 100644 --- a/src/modules/comment/comment.repository.ts +++ b/src/modules/comment/comment.repository.ts @@ -1,10 +1,19 @@ import CommentRequest from '../../database/models/Comment'; +import { SupportRequest, Comment } from '../../@types/express'; export default class CommentRepository { - static async createComment(commentDetails: any, supportRequestDetails: any) { + /** + * @param {Comment} id - ID of data to be fetched + * @param {SupportRequest} support request detsils + * @returns {Promise} returned datase object + */ + static async createComment( + commentDetails: Comment, + supportRequestDetails: SupportRequest, + ): Promise { const comment = new CommentRequest(commentDetails); await comment.save(); - supportRequestDetails.comments.push(comment); + supportRequestDetails.comments!.push(comment); await supportRequestDetails.save(); return comment; } diff --git a/src/modules/support-request/__test__/support-request.spec.ts b/src/modules/support-request/__test__/support-request.spec.ts index 4ca4fb9..264e8bd 100644 --- a/src/modules/support-request/__test__/support-request.spec.ts +++ b/src/modules/support-request/__test__/support-request.spec.ts @@ -18,6 +18,7 @@ import { import User from '../../../database/models/User'; import SupportRequest from '../../../database/models/SupportRequest'; import logger from '../../../config/logger'; +// import SupportRequestRepository from '../support-request.repository'; const request = supertest(server); @@ -154,6 +155,22 @@ describe('TEST SUITE FOR SUPPORT REQUEST', () => { done(); }); + it('A user not should be able to fetch a support request they did not create', async (done) => { + const supportRequest = await SupportRequest.findOne({ + description: 'Test description', + }); + + const response = await request + .get(`${baseUrl}/support_request/${supportRequest._id}`) + .set('Content-Type', 'application/json') + .set('authorization', `Bearer ${secondToken}`); + + expect(response.status).toEqual(400); + expect(response.body.success).toEqual(false); + expect(response.body.error).toEqual('Not allowed'); + done(); + }); + it('A user not should be able to fetch a single support request with an invalid id', async (done) => { const response = await request .get(`${baseUrl}/support_request/invalidId`) @@ -392,17 +409,13 @@ describe('TEST SUITE FOR SUPPORT REQUEST', () => { const response = await request .patch(`${baseUrl}/support_request/close_request/${supportRequest._id}`) .set('Content-Type', 'application/json') - .set('authorization', `Bearer ${adminToken}`) - .send({ - status: 'CLOSED', - }); + .set('authorization', `Bearer ${adminToken}`); expect(response.status).toEqual(200); expect(response.body.success).toEqual(true); expect(response.body.message).toEqual( 'You have successfully closed this support request', ); - expect(response.body.data.status).toEqual('CLOSED'); done(); }); @@ -412,10 +425,7 @@ describe('TEST SUITE FOR SUPPORT REQUEST', () => { `${baseUrl}/support_request/close_request/607767fe1341087782da1be1`, ) .set('Content-Type', 'application/json') - .set('authorization', `Bearer ${adminToken}`) - .send({ - status: 'CLOSED', - }); + .set('authorization', `Bearer ${adminToken}`); expect(response.status).toEqual(400); expect(response.body.success).toEqual(false); @@ -431,16 +441,24 @@ describe('TEST SUITE FOR SUPPORT REQUEST', () => { const response = await request .patch(`${baseUrl}/support_request/close_request/${supportRequest._id}`) .set('Content-Type', 'application/json') - .set('authorization', `Bearer ${token}`) - .send({ - status: 'CLOSED', - }); + .set('authorization', `Bearer ${token}`); expect(response.status).toEqual(400); expect(response.body.success).toEqual(false); expect(response.body.error).toEqual('Unathorized access'); done(); }); + + it('An admin user should be able to update a support request status', async (done) => { + const response = await request + .get(`${baseUrl}/download_report`) + .set('Content-Type', 'application/json') + .set('authorization', `Bearer ${adminToken}`); + + expect(response.status).toEqual(200); + expect(response).toHaveProperty('text'); + done(); + }); }); describe('AUTHENTICATION CONTROLLER UNIT TESTS', () => { @@ -530,4 +548,15 @@ describe('AUTHENTICATION CONTROLLER UNIT TESTS', () => { expect(json).toHaveBeenCalledTimes(1); done(); }); + + it('calls status and json methods to generate response when fetching csv file', async (done) => { + jest.spyOn(global, 'Date').mockImplementation(() => { + throw new Error('Failed to fetch file'); + }); + + await UnmockedSupportRequestController.downloadReport(req, res); + expect(status).toHaveBeenCalledTimes(1); + expect(json).toHaveBeenCalledTimes(1); + done(); + }); }); diff --git a/src/modules/support-request/index.ts b/src/modules/support-request/index.ts index 452bc85..6d15110 100644 --- a/src/modules/support-request/index.ts +++ b/src/modules/support-request/index.ts @@ -12,7 +12,7 @@ const { getSupportRequests, updateSupportRequest, closeRequest, - // downloadReport, + downloadReport, } = SupportRequestController; const { @@ -27,7 +27,6 @@ const { SupportRequestSchema: { createSupportRequestSchema, singleSupportRequestSchema, - supportRequestStatusSchema, }, } = Schemas; @@ -58,17 +57,17 @@ supportRequestRouter.patch( supportRequestRouter.patch( '/support_request/close_request/:id', validateRequest(singleSupportRequestSchema(), 'params'), - validateRequest(supportRequestStatusSchema(), 'body'), validateToken, adminAuth, closeRequest, ); -// supportRequestRouter.get( -// '/download_report', -// validateToken, -// adminAuth, -// downloadReport, -// ); + +supportRequestRouter.get( + '/download_report', + validateToken, + adminAuth, + downloadReport, +); export default supportRequestRouter; diff --git a/src/modules/support-request/support-request.controller.ts b/src/modules/support-request/support-request.controller.ts index c28937c..ae3c92f 100644 --- a/src/modules/support-request/support-request.controller.ts +++ b/src/modules/support-request/support-request.controller.ts @@ -1,8 +1,9 @@ import { Request, Response } from 'express'; import SupportRequestRepository from './support-request.repository'; -// import Utils from '../../utils/utils'; import Error from '../../utils/Error'; +import Utils from '../../utils/utils'; +// import { User } from '../../@types/express'; export default class SupportRequestController { /** @@ -14,8 +15,7 @@ export default class SupportRequestController { * @param req.body {Object} The JSON payload. * * @function - * @returns {Boolean} success - * @returns {string} message + * @returns {Promise>>} message */ static async createSupportRequest( request: Request, @@ -49,8 +49,7 @@ export default class SupportRequestController { * @param req.user {Object} The JSON payload containing user details * * @function - * @returns {Boolean} success - * @returns {string} message + * @returns {Promise>>} message */ static async updateSupportRequest( request: Request, @@ -67,7 +66,7 @@ export default class SupportRequestController { return Error.handleError('Support request not found', 400, response); } - if (supportRequest.owner._id.toString() !== _id.toString()) { + if (supportRequest.owner!._id.toString() !== _id.toString()) { return Error.handleError('Not allowed', 400, response); } @@ -97,8 +96,7 @@ export default class SupportRequestController { * Only an admin has access to this route * * @function - * @returns {Boolean} success - * @returns {string} message + * @returns {Promise>>} message */ static async closeRequest( request: Request, @@ -113,15 +111,14 @@ export default class SupportRequestController { return Error.handleError('Support request not found', 400, response); } - const updatedRequest = await SupportRequestRepository.updateSupportRequest( - request.params.id, - { ...request.body, completedAt: new Date() }, - ); + await SupportRequestRepository.updateSupportRequest(request.params.id, { + status: 'CLOSED', + completedAt: new Date(), + }); return response.status(200).json({ success: true, message: 'You have successfully closed this support request', - data: updatedRequest, }); } catch (error) { return Error.handleError('Server error', 500, response, error); @@ -143,6 +140,7 @@ export default class SupportRequestController { response: Response, ): Promise>> { try { + const { _id } = request.user; const supportRequest = await SupportRequestRepository.getSupportRequest( request.params.id, ); @@ -151,6 +149,10 @@ export default class SupportRequestController { return Error.handleError('Support request not found', 400, response); } + if (supportRequest.owner!._id.toString() !== _id.toString()) { + return Error.handleError('Not allowed', 400, response); + } + response.status(200).json({ success: true, message: 'You have successfully retrieved this support request', @@ -195,47 +197,48 @@ export default class SupportRequestController { } } - // /** - // * Returns success if registration was successful and error if not - // * @name /download_report GET - // * - // * @param request {Object} The request. - // * @param response {Object} The response. - // * - // * @function - // * @returns {Boolean} success - // * @returns {string} message - // */ - // static async downloadReport(request: Request, response: Response) { - // try { - // const supportRequest = await SupportRequestRepository.getClosedSupportRequests(); - - // const currentDatetime = new Date(); - // const lastMonth = currentDatetime.setMonth( - // currentDatetime.getMonth() - 1, - // ); - - // const lastMonthData = supportRequest.filter( - // (data) => data && lastMonth < data.completedAt, - // ); - - // const csvFields = [ - // '_id', - // 'comments', - // 'status', - // 'description', - // 'owner', - // 'createdAt', - // ]; - // return downloadResource( - // response, - // 'supportReport.csv', - // csvFields, - // lastMonthData, - // ); - // } catch (error) { - // console.log(error); - // return errorHandler(error, 500, response); - // } - // } + /** + * Returns success if registration was successful and error if not + * @name /download_report GET + * + * @param request {Object} The request. + * @param response {Object} The response. + * + * @function + * @returns {Boolean} success + * @returns {Promise>>} message + */ + static async downloadReport( + request: Request, + response: Response, + ): Promise>> { + try { + const currentDatetime = new Date(); + const lastMonth = currentDatetime.setMonth( + currentDatetime.getMonth() - 1, + ); + + const supportRequestData = await SupportRequestRepository.getClosedSupportRequests( + lastMonth, + ); + + const csvFields = [ + '_id', + 'comments', + 'status', + 'description', + 'owner', + 'createdAt', + ]; + + return Utils.downloadResource( + response, + 'supportReport.csv', + csvFields, + supportRequestData, + ); + } catch (error) { + return Error.handleError('Server error', 500, response, error); + } + } } diff --git a/src/modules/support-request/support-request.repository.ts b/src/modules/support-request/support-request.repository.ts index 29b8ad3..18d94b1 100644 --- a/src/modules/support-request/support-request.repository.ts +++ b/src/modules/support-request/support-request.repository.ts @@ -1,57 +1,70 @@ -import SupportRequest from '../../database/models/SupportRequest'; +import SupportRequestModel from '../../database/models/SupportRequest'; +import { SupportRequest } from '../../@types/express'; export default class SupportRequestRepository { /** - * @param {Object} requestDetails - Support request details to be saved - * @returns {Object} saved datase object + * @param {SupportRequest} requestDetails - Support request details to be saved + * @returns {Promise} saved datase object */ - static async createSupportRequest(requestDetails: any) { - const supportRequest = new SupportRequest(requestDetails); + static async createSupportRequest( + requestDetails: SupportRequest, + ): Promise { + const supportRequest = new SupportRequestModel(requestDetails); return await supportRequest.save(); } /** - * @param {Object} id - ID of data to be fetched - * @returns {Object} returned datase object + * @param {string} id - ID of data to be fetched + * @returns {Promise} returned datase object */ - static async getSupportRequest(id: string) { - // return await SupportRequest.findOne({ _id: id }).populate('owner comments'); - return await SupportRequest.findOne({ _id: id }).populate('owner'); + static async getSupportRequest(id: string): Promise { + return await SupportRequestModel.findOne({ + _id: id, + status: { $ne: 'CLOSED' }, + }).populate('owner comments', '-password'); } /** - * @returns {Object} fetched resource + * @returns {Promise} fetched resource */ - static async getSupportRequests() { - // return await SupportRequest.find().populate('owner comments'); - return await SupportRequest.find().populate('owner'); + static async getSupportRequests(): Promise { + return await SupportRequestModel.find().populate( + 'owner comments', + '-password', + ); } - // /** - // * @returns {Object} fetched resource - // */ - // static async getClosedSupportRequests() { - // return await SupportRequest.find({ status: 'CLOSED' }).populate( - // 'owner comments', - // ); - // } + /** + * @param {number} time from last month + * @returns {Promise} fetched resource + */ + static async getClosedSupportRequests( + lastMonth: number, + ): Promise { + return await SupportRequestModel.find({ + status: 'CLOSED', + completedAt: { $gt: lastMonth }, + }).populate('owner comments', '-password'); + } /** - * @param {Object} id - ID of data to be fetched - * @returns {Object} fetched resource + * @param {string} id - ID of data to be fetched + * @returns {Promise} fetched resource */ - static async getUserSupportRequests(id: string) { - // return await SupportRequest.find({ owner: id }).populate('owner comments'); - return await SupportRequest.find({ owner: id }).populate('owner'); + static async getUserSupportRequests(id: string): Promise { + return await SupportRequestModel.find({ + owner: id, + status: { $ne: 'CLOSED' }, + }).populate('owner comments', '-password'); } /** - * @param {Object} id - ID of data to be fetched + * @param {string} id - ID of data to be fetched * @param {Object} data data to facilitate update - * @returns {Object} Updated data + * @returns {Promise} Updated data */ - static async updateSupportRequest(id: string, data: any) { - return await SupportRequest.findByIdAndUpdate( + static async updateSupportRequest(id: string, data: any): Promise { + return await SupportRequestModel.findByIdAndUpdate( { _id: id }, { ...data }, { new: true }, diff --git a/src/utils/__test__/utils.spec.ts b/src/utils/__test__/utils.spec.ts index 404829f..b36521e 100644 --- a/src/utils/__test__/utils.spec.ts +++ b/src/utils/__test__/utils.spec.ts @@ -1,90 +1,34 @@ -// import jsonwebtoken from 'jsonwebtoken'; -// import bcrypt from 'bcrypt'; - -// import Utils from '../utils'; - -// describe('UNIT TEST FOR UTILITY FUNCTION', () => { -// let json: any, status: any, statusCode: any, response: any, error: any; // eslint-disable-line - -// beforeEach(() => { -// json = jest.fn(); -// status = jest.fn(() => ({ json })); -// response = { status }; -// statusCode = 404; -// error = 'Error'; -// }); - - // it('Returns correct error message', () => { - // expect.assertions(2); - // Utils.errorHandler(error, statusCode, response); - // expect(json).toHaveBeenCalledTimes(1); - // expect(json).toHaveBeenCalledWith({ success: false, error }); - // }); - -// // it('Returns the correct status code', () => { -// // expect.assertions(2); -// // Utils.errorHandler(error, statusCode, response); -// // expect(status).toHaveBeenCalledTimes(1); -// // expect(status).toHaveBeenCalledWith(404); -// // }); - -// it('Generates auth a token', async () => { -// const data = 'mockreturntokendata'; -// const token = { sub: 'test', admin: true }; - -// jest -// .spyOn(jsonwebtoken, 'sign') -// .mockImplementation(() => Promise.resolve(data) as any); - -// const mocks: string = await Utils.generateToken(token); - -// expect(mocks).toEqual(data); -// }); - -// it('Decodes a token', async () => { -// const data = 'mockreturntokendata'; - -// jest -// .spyOn(jsonwebtoken, 'verify') -// .mockImplementation(() => Promise.resolve(data) as any); - -// const mocks: any = await Utils.decodeToken('token'); - -// expect(mocks).toEqual(data); -// }); - -// it('Encrypts a string', async () => { -// const data = 'mockreturntokendata'; - -// jest -// .spyOn(bcrypt, 'hash') -// .mockImplementation(() => Promise.resolve(data) as any); - -// const mocks: any = await Utils.hashPassword('password'); - -// expect(mocks).toEqual(data); -// }); - -// it('Compares two strings', async () => { -// jest -// .spyOn(bcrypt, 'compareSync') -// .mockImplementation(() => Promise.resolve(true) as any); - -// const mocks: any = await Utils.comparePassword( -// 'encryptedPassword', -// 'normalPasswordString', -// ); - -// expect(mocks).toEqual(true); -// }); -// }); import jsonwebtoken from "jsonwebtoken"; import bcrypt from "bcryptjs"; +import { Parser } from 'json2csv'; import Utils from "../utils"; import { validationErrorDetails } from "./__mocks__/utils.mocks"; +jest.mock("json2csv"); + describe("AUTH UTILS TEST SUITE", () => { + let send: any, res: any, header: any, attachment: any; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + send = jest.fn(); + header = jest.fn(); + attachment = jest.fn(); + res = { send, header, attachment }; + }); + + afterEach(async () => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + afterAll(async () => { + jest.clearAllMocks(); + jest.resetModules(); + }); + it("Should generate an auth token", (done) => { expect(true).toBe(true); const data = "mockreturntokendata"; @@ -126,4 +70,20 @@ describe("AUTH UTILS TEST SUITE", () => { expect(parseValidation).toHaveProperty('email'); done(); }); + + it("Should parse and send file", (done) => { + const data = 'data' + jest.spyOn(new Parser, "parse").mockImplementation(() => data as any); + const csvFields = [ + '_id', + 'comments', + 'status', + 'description', + 'owner', + 'createdAt', + ]; + Utils.downloadResource(res, 'supportReport.csv', csvFields, 'data'); + expect(send).toHaveBeenCalledTimes(1); + done(); + }); }); diff --git a/src/utils/schema/support-request.schema.ts b/src/utils/schema/support-request.schema.ts index 0e8b3fd..195ad24 100644 --- a/src/utils/schema/support-request.schema.ts +++ b/src/utils/schema/support-request.schema.ts @@ -1,6 +1,6 @@ import Joi from '@hapi/joi'; -import BaseSchema from './base.schema'; +// import BaseSchema from './base.schema'; export default class SupportRequestSchema { static createSupportRequestSchema() { @@ -9,11 +9,11 @@ export default class SupportRequestSchema { }); } - static supportRequestStatusSchema() { - return Joi.object({ - status: BaseSchema.stringSchema().uppercase().valid('CLOSED').required(), - }); - } + // static supportRequestStatusSchema() { + // return Joi.object({ + // status: BaseSchema.stringSchema().uppercase().valid('CLOSED').required(), + // }); + // } static singleSupportRequestSchema() { return Joi.object({ diff --git a/src/utils/schema/user.schema.ts b/src/utils/schema/user.schema.ts index 1d7002a..88e5ba2 100644 --- a/src/utils/schema/user.schema.ts +++ b/src/utils/schema/user.schema.ts @@ -7,7 +7,7 @@ export default class UserSchema { return Joi.object({ name: Joi.string().required(), email: BaseSchema.email().required(), - password: Joi.string().min(5).regex(/\d/).required(), + password: Joi.string().min(5).required(), }); } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index bad1ddf..bb48e47 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,7 +1,7 @@ -// import { Response } from 'express'; +import { Response } from 'express'; import bcrypt from 'bcryptjs'; import jsonwebtoken from 'jsonwebtoken'; -// import { Parser } from 'json2csv'; +import { Parser } from 'json2csv'; // import dotenv from 'dotenv'; // import { Token } from './interfaces/utils'; @@ -61,7 +61,6 @@ export default class Utils { public static parseValidationErrors(errorDetails: any) { const validationErrors: any = {}; - // console.log(errorDetails) errorDetails.forEach((errorItem: any) => { const index = errorItem.message.indexOf(' '); @@ -83,26 +82,27 @@ export default class Utils { return validationErrors; } - // /** - // * - // * - // * @export - // * @param {Response} response - // * @param {string} fileName - // * @param {string[]} fields - // * @param {any} data - // * @returns {Promise>> } - // */ - // static downloadResource( - // response: Response, - // fileName: string, - // fields: string[], - // data: any, - // ): Response> { - // const json2csv = new Parser({ fields }); - // const csv = json2csv.parse(data); - // response.header('Content-Type', 'text/csv'); - // response.attachment(fileName); - // return response.send(csv); - // } + /** + * + * + * @export + * @param {Response} response + * @param {string} fileName + * @param {string[]} fields + * @param {any} data + * @returns {Response>} + */ + public static downloadResource( + response: Response, + fileName: string, + fields: string[], + data: any, + ): Response> { + const json2csv = new Parser({ fields }); + const csv = json2csv.parse(data); + response.header('Content-Type', 'text/csv'); + response.header("Content-Disposition", `attachment;filename=${fileName}.csv`); + response.attachment(fileName); + return response.send(csv); + } }