Skip to content

Commit

Permalink
Feat(Approval or Rejection):changes request status
Browse files Browse the repository at this point in the history
- Add route for approving or rejecting a request
- Add authorization middleware
- Add approving or rejecting request validations
- Add approving rejecting request controller
- Add documantation for this route
- Add tests for this route

[starts #169817553]
  • Loading branch information
gadishimwe committed Jan 10, 2020
1 parent 9351436 commit 315b90c
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 3 deletions.
25 changes: 25 additions & 0 deletions src/controllers/request.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import RequestService from '../services/request.service';
import ResponseService from '../services/response.service';

/**
*
*
* @class RequestController
*/
class RequestController {
/**
*
*
* @static
* @param {req} req
* @param {res} res
* @returns {response} @memberof RequestController
*/
static async updateRequestStatus(req, res) {
const [, [{ dataValues }]] = await RequestService.updateRequest({ id: req.params.requestId }, { status: `${req.body.status}` });
ResponseService.setSuccess(200, `Request has successfully ${dataValues.status}`, dataValues);
return ResponseService.send(res);
}
}

export default RequestController;
8 changes: 8 additions & 0 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ const authMiddleware = {
ResponseService.setError(401, 'No Token supplied');
return ResponseService.send(res);
}
},
checkIfUserIsManager: async (req, res, next) => {
const { role } = await UserService.findUserByProperty({ id: req.userData.id });
if (role !== 'manager') {
ResponseService.setError(403, 'Forbidden. Only Managers can perform this action');
return ResponseService.send(res);
}
next();
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import authRoute from './auth.route';
import tripRoute from './trip.route';
import profileSettingsRoute from './profile-settings.route';
import accommodationRoute from './accommodation.route';
import requestRoute from './request.route';

const app = express();

app.use('/api/auth', authRoute);
app.use('/api/', tripRoute);
app.use('/api/accommodations', accommodationRoute);
app.use('/api/users', profileSettingsRoute);
app.use('/api/requests', requestRoute);

export default app;
10 changes: 10 additions & 0 deletions src/routes/request.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import express from 'express';
import RequestController from '../controllers/request.controller';
import authMiddleware from '../middlewares/auth.middleware';
import { validateChangingRequestStatus } from '../validations/request.validation';

const router = express.Router();

router.patch('/:requestId', authMiddleware.checkUserLoggedIn, authMiddleware.checkIfUserIsManager, validateChangingRequestStatus, RequestController.updateRequestStatus);

export default router;
2 changes: 1 addition & 1 deletion src/routes/trip.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CommentController from '../controllers/comment.controller';
import oneWayTripValidation from '../validations/one-way-trip.validation';
import authMiddleware from '../middlewares/auth.middleware';
import TripValidation from '../validations/trip.validation';
import requestValidation from '../validations/request.validation';
import { requestValidation } from '../validations/request.validation';
import multiCityTripValidation from '../validations/multi-city-trip.validation';
import UserValidation from '../validations/user.validation';

Expand Down
28 changes: 28 additions & 0 deletions src/services/request.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ class RequestService {
static createRequest(newRequest) {
return Request.create(newRequest);
}

/**
* find Request
* @static
* @param {object} property
* @memberof AccommodationService
* @returns {object} data
*/
static findTypeByProperty(property) {
return Request.findOne({
where: { ...property }
});
}

/**
*
*
* @static
* @param {item} requestId request column to be updated
* @param {value} requestInfo to be updated
* @returns {updated} @memberof RequestService
*/
static updateRequest(requestId, requestInfo) {
return Request.update(requestInfo, {
where: requestId,
returning: true
});
}
}


Expand Down
48 changes: 48 additions & 0 deletions src/swagger/request.swagger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @swagger
* definitions:
* Request:
* type: object
* properties:
* status:
* type: string
* required:
* - status
*/

/**
* @swagger
* /api/requests/{requestId}:
* patch:
* tags:
* - Request
* name: Request
* summary: A manager should be able to approve or reject requests of his direct report
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - in: header
* name: Authorization
* required: true
* type: string
* - name: requestId
* in: path
* - name: status
* in: body
* schema:
* $ref: '#/definitions/Request'
* type: object
* responses:
* '200':
* description: Request approved or rejected successfully
* '400':
* description: invalid parameters.
* '401':
* description: No valid token supplied
* '403':
* description: Unauthorized
* '422':
* description: Rejecting rejected or approved request
*/
18 changes: 18 additions & 0 deletions src/tests/fixtures/request.fixture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import faker from 'faker';
import RequestService from '../../services/request.service';
import { createManagers } from './users.fixture';

const createRequest = async () => {
const { manager1 } = await createManagers();
const request = {
id: faker.random.number(),
requesterId: faker.random.number(),
tripId: faker.random.number(),
status: faker.random.word(),
lineManagerId: manager1.id
};
const { dataValues } = await RequestService.createRequest(request);
return dataValues;
};

export default createRequest;
42 changes: 42 additions & 0 deletions src/tests/fixtures/users.fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,46 @@ export const loggedInToken = JwtService.generateToken({
email: loggedInUser.email,
});

// create managers for management actions
export const loggedInManager1 = {
id: faker.random.number(),
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
password: BcryptService.hashPassword(userPassword),
isVerified: true,
role: 'manager',
createdAt: new Date(),
updatedAt: new Date(),
};
export const managerToken1 = JwtService.generateToken({
id: loggedInManager1.id
});
export const loggedInManager2 = {
id: faker.random.number(),
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
password: BcryptService.hashPassword(userPassword),
isVerified: true,
role: 'manager',
createdAt: new Date(),
updatedAt: new Date(),
};
export const managerToken2 = JwtService.generateToken({
id: loggedInManager2.id
});

export const createManagers = async () => {
await Users.destroy({ where: {} });
const manager1 = await Users.create({ ...loggedInManager1, token: managerToken1 });
const manager2 = await Users.create({ ...loggedInManager2, token: managerToken2 });
return {
manager1: manager1.dataValues,
manager2: manager2.dataValues
};
};

// crete real user to that help receive email
const realUser = 'icyiiddy@gmail.com';

Expand Down Expand Up @@ -162,6 +202,8 @@ export const createUsers = async () => {
await Users.create(lineManager);
await Users.create({ ...notAllowedManager, token: tokenOfNotAllowedManager });
await Users.create({ ...userWithNoTrip, token: userWithNoTripToken });
await Users.create({ ...loggedInManager1, token: managerToken1 });
await Users.create({ ...loggedInManager2, token: managerToken2 });
};
export const cleanDb = async () => {
await Users.destroy({ where: {} });
Expand Down
89 changes: 89 additions & 0 deletions src/tests/request/request.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import app from '../../app';
import createRequest from '../fixtures/request.fixture';
import { managerToken1, loggedInToken, managerToken2, createUsers } from '../fixtures/users.fixture';

chai.should();
chai.use(chaiHttp);

describe('Test rejecting a request:', () => {
let request;
before(async () => {
request = await createRequest();
await createUsers();
});
it('Should return status code of 200 on successful request rejection', (done) => {
chai.request(app)
.patch(`/api/requests/${request.id}`)
.set('Authorization', managerToken1)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.have.property('data');
expect(res.body.data).to.have.property('status');
expect(res.body.data.status).eqls('rejected');
done();
});
});
it('Should return status code of 403 for user who is not a manager', (done) => {
chai.request(app)
.patch(`/api/requests/${request.id}`)
.set('Authorization', loggedInToken)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(403);
expect(res.body).to.have.property('message');
expect(res.body.message).eqls('Forbidden. Only Managers can perform this action');
done();
});
});
it('Should return status code of 400 for invalid requestId parameter', (done) => {
chai.request(app)
.patch('/api/requests/gkkk')
.set('Authorization', managerToken1)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(400);
expect(res.body).to.have.property('message');
expect(res.body.message[0]).eqls('requestId must be a number');
done();
});
});
it('Should return status code of 404 for requestId which does not exist', (done) => {
chai.request(app)
.patch('/api/requests/44')
.set('Authorization', managerToken1)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(404);
expect(res.body).to.have.property('message');
expect(res.body.message).eqls('This request does not exist');
done();
});
});
it('Should return status code of 403 for different request line manager', (done) => {
chai.request(app)
.patch(`/api/requests/${request.id}`)
.set('Authorization', managerToken2)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(403);
expect(res.body).to.have.property('message');
expect(res.body.message).eqls('Forbidden. you are not line manager of this request');
done();
});
});
it('Should return status code of 422 for request which is already rejected', (done) => {
chai.request(app)
.patch(`/api/requests/${request.id}`)
.set('Authorization', managerToken1)
.send({ status: 'rejected' })
.end((err, res) => {
expect(res).to.have.status(422);
expect(res.body).to.have.property('message');
expect(res.body.message).eqls('This request is already rejected');
done();
});
});
});
38 changes: 36 additions & 2 deletions src/validations/request.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import ResponseService from '../services/response.service';
import JwtService from '../services/jwt.service';
import UserService from '../services/user.service';
import TripService from '../services/trip.service';
import RequestService from '../services/request.service';

/**
* @param {req} req
* @param {req} res
* @param {next} next
* @returns {validation} this function validate request route
*/
async function requestValidation(req, res, next) {
export async function requestValidation(req, res, next) {
const signInUser = JwtService.verifyToken(req.headers.authorization);

This comment has been minimized.

Copy link
@higustave-ops

higustave-ops Jan 10, 2020

Contributor

@gadishimwe You have used authMiddleware.checkUserLoggedIn no need to verify token here as that middleware has all checks for token validation

const { userId } = req.params;

Expand Down Expand Up @@ -50,4 +51,37 @@ async function requestValidation(req, res, next) {
next();
}

export default requestValidation;
export const validateChangingRequestStatus = async (req, res, next) => {
const schema = Joi.object({
requestId: Joi.number().required(),
status: Joi.string().valid('approved', 'rejected').trim()
.messages({
'any.only': 'status" must be one of approved or rejected'
})
.required()
}).options({ abortEarly: false });

const results = schema.validate({ ...req.params, ...req.body });
if (results.error) {
const errorMessages = [];
results.error.details.forEach((error) => {

This comment has been minimized.

Copy link
@higustave-ops

higustave-ops Jan 10, 2020

Contributor

@gadishimwe .map has given me feedback to not use foreach. Better you use map too so that he do not give repetitive feedback.

errorMessages.push(error.message.replace(/[^a-zA-Z0-9 .-]/g, ''));
});
ResponseService.setError(400, errorMessages);
return ResponseService.send(res);
}
const isRequestExist = await RequestService.findTypeByProperty({ id: req.params.requestId });
if (!isRequestExist) {
ResponseService.setError(404, 'This request does not exist');
return ResponseService.send(res);
}
if (isRequestExist.dataValues.lineManagerId !== req.userData.id) {
ResponseService.setError(403, 'Forbidden. you are not line manager of this request');
return ResponseService.send(res);
}
if (req.body.status === isRequestExist.dataValues.status) {
ResponseService.setError(422, `This request is already ${req.body.status}`);
return ResponseService.send(res);
}
next();
};

0 comments on commit 315b90c

Please sign in to comment.