Skip to content
This repository has been archived by the owner on Jul 20, 2020. It is now read-only.

Commit

Permalink
feature(search): user search for a request
Browse files Browse the repository at this point in the history
- add the search controller
- add test for the search functionality
- add the validation rule middleware
- document the search feature

[Maintains #170947564]
  • Loading branch information
izzett222 committed Mar 3, 2020
1 parent eeae8f5 commit fec2625
Show file tree
Hide file tree
Showing 21 changed files with 338 additions and 31 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dev": "nodemon --exec babel-node src/index.js",
"build": "rm -rf ./dist && babel -d ./dist ./src",
"test": "npm run db:reset --env test && npx sequelize db:seed:undo:all --env test && npm run seed --env test && npm run test:unit",
"test:unit": "NODE_ENV=test nyc --require @babel/register mocha ./src/tests/*.test.js --timeout 20000 --exit",
"test:unit": "NODE_ENV=test nyc --require @babel/register mocha --recursive './src/**/*.test.js' --timeout 20000 --exit",
"db:migrate": "npx sequelize db:migrate --env test",
"db:reset": "npx sequelize db:migrate:undo:all --env test && npm run db:migrate --env test",
"create": "npx sequelize db:migrate",
Expand Down Expand Up @@ -44,10 +44,10 @@
"jsonwebtoken": "^8.5.1",
"localStorage": "^1.0.4",
"mailgen": "^2.0.10",
"multer": "^1.4.2",
"passport": "^0.4.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth20": "^2.0.0",
"multer": "^1.4.2",
"path": "^0.12.7",
"pg": "^7.18.1",
"pg-hstore": "^2.3.3",
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default class AuthController {
const token = provideToken(user.id, user.isVerified, email, user.role);
const link = `http://${process.env.BASE_URL}/api/v1/auth/verification?token=${token}&email=${email}`;
localStorage.setItem('token', token);
sendMsg(email, firstName, content, link);
await sendMsg(email, firstName, content, link);
return Response.signupResponse(res, 201, res.__('User is successfully registered'), token);
} catch (error) {
return Response.errorResponse(res, 500, res.__(`${error.message}`));
Expand Down Expand Up @@ -182,7 +182,7 @@ export default class AuthController {
text: req.__('button text'),
signature: req.__('email signature')
};
AuthService.forgotPassword(user, content);
await AuthService.forgotPassword(user, content);
return Response.success(res, 200, res.__('check your email to reset your password'));
} catch (err) {
return Response.errorResponse(res, 500, res.__('server error'));
Expand Down
35 changes: 35 additions & 0 deletions src/controllers/tripsController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import uuid from 'uuid/v4';
import db from '../models';
import Response from '../utils/ResponseHandler';
import TripsService from '../services/tripServices';
import stringHelper from '../utils/stringHelper';

/**
* @description RequestController Controller
Expand Down Expand Up @@ -294,4 +296,37 @@ export default class requestController {
return Response.errorResponse(res, 500, err.message);
}
}

/**
* @description request feature
* @static
* @param {Object} req
* @param {Object} res
* @returns {Object} array of request
* @memberof requestController
*/
static async requestSearch(req, res) {
try {
const { user } = req;
const {
id, location, destination, status, reason, departureDate, returnDate
} = req.query;
const field = {
id,
location,
destination,
status,
reason,
departureDate,
returnDate
};
const requests = await TripsService.searchRequest(field, user);
if (requests === stringHelper.requestNotFound) {
return Response.errorResponse(res, 404, res.__(requests));
}
return Response.success(res, 200, res.__('requests found'), requests);
} catch (err) {
return Response.errorResponse(res, 500, res.__('server error'));
}
}
}
1 change: 0 additions & 1 deletion src/middlewares/protectRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export default class protectRoutes {
if (user.role !== 'manager') {
return Response.errorResponse(res, 401, res.__('you are not authorised for this operation'));
}

next();
}

Expand Down
9 changes: 8 additions & 1 deletion src/routes/tripsRoutes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@

import express from 'express';
import tripsController from '../controllers/tripsController';
import { requestRules, returnTripRules, multiCityTripRules } from '../validation/validationRules';
import {
requestRules,
returnTripRules,
multiCityTripRules,
searchQueryRules
} from '../validation/validationRules';
import validationResult from '../validation/validationResult';
import protectRoute from '../middlewares/protectRoute';
import rememberProfile from '../utils/rememberProfile';
Expand All @@ -15,4 +20,6 @@ router.patch('/edit', protectRoute.verifyUser, protectRoute.verifyRequester, rem
router.get('/view', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.availTripRequests);
router.put('/:requestId/confirm', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.confirmRequest);
router.patch('/:requestId/reject', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.rejectRequest);
router.get('/search', protectRoute.verifyUser, searchQueryRules, validationResult, tripsController.requestSearch);

export default router;
23 changes: 11 additions & 12 deletions src/seeders/20200222234112-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module.exports = {
'Requests', [
{
id: '51e74db7-5510-4f50-9f15-e23710331ld5',
location: 'Nairobi',
destination: 'Nairobi',
location: 'nairobi',
destination: 'nairobi',
reason: 'meeting with partners',
departureDate: '2020-01-01',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
Expand All @@ -18,8 +18,8 @@ module.exports = {
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld5',
location: 'Boston',
destination: 'Kigali',
location: 'boston',
destination: 'kigali',
reason: 'meeting with engineers',
departureDate: '2020-12-01',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
Expand All @@ -33,8 +33,8 @@ module.exports = {
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld4',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'gisenyi',
reason: 'meeting with engineers write',
departureDate: '2020-12-01',
email: 'jdev@andela.com',
Expand All @@ -48,8 +48,8 @@ module.exports = {
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld9',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'gisenyi',
reason: 'meeting with engineers write',
departureDate: '2020-12-01',
email: 'jdev@andela.com',
Expand All @@ -62,8 +62,8 @@ module.exports = {
},
{
id: '0ad0ef9d-a926-4c5c-86e6-4d1e22e9ab88',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'bisenyi',
reason: 'meeting with engineers write',
departureDate: '2020-12-01',
email: 'rejectuser@andela.com',
Expand All @@ -73,8 +73,7 @@ module.exports = {
managerId: '79660e6f-4b7d-4d21-81ad-74f64e9e1c8a',
createdAt: new Date(),
updatedAt: new Date(),
}
],
}],
{},
),

Expand Down
12 changes: 9 additions & 3 deletions src/services/localesServices/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"User is successfully logged in": "User is successfully logged in",
"Incorrect email or password": "Incorrect email or password",
"User is successfully logged out": "User is successfully logged out",
"No trip requests available": "No trip requests available",
"you are not authorised for this operation": "you are not authorised for this operation",
"Request updated successfully": "Request updated successfully",
"The request does not exist or it's either been approved or rejected": "The request does not exist or it's either been approved or rejected",
Expand Down Expand Up @@ -92,12 +91,19 @@
"Passport name should be only characters": "Passport name should be only characters",
"Passport name must be atleast 4 characters": "Passport name must be atleast 4 characters",
"The allowable roles are manager, travel team member, requester, travel administrator, super administrator": "The allowable roles are manager, travel team member, requester, travel administrator, super administrator",
"Gender must either be Male or Female": "Gender must either be Male or Female",
"The passport name is required": "The passport name is required",
"The role is required": "The role is required",
"Gender is required": "Gender is required",
"destination should only be letter": "destination should only be letter",
"Route %s Not found.": "Route /api/v1/trips/on Not found.",
"Route /api Not found.": "Route /api Not found.",
"Route %s not found": "Route %s not found"
"Route %s not found": "Route %s not found",
"status can only be open, rejected or approved": "status can only be open, rejected or approved",
"request id must be a valid uuid": "request id must be a valid uuid",
"requests found": "requests found",
"No trip requests available": "No trip requests available",
"reason must be atleast one character": "reason must be atleast one character",
"request ID can't be empty, please check your input and enter a valid ID": "request ID can't be empty, please check your input and enter a valid ID",
"destination should only contain letter": "destination should only contain letter",
"Gender must either be Male or Female": "Gender must either be Male or Female"
}
7 changes: 6 additions & 1 deletion src/services/localesServices/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,10 @@
"this room is booked or it does not exist": "cette chambre est réservée ou elle n'existe pas",
"Booking created successfully": "Réservation créée avec succès",
"the checkout date must be greater than checkin date": "la date de départ doit être supérieure à la date d'arrivée",
"Route %s not found": "itinéraire %s introuvable"
"Route %s not found": "itinéraire %s introuvable",
"requests found": "demandes trouvées",
"request ID can't be empty, please check your input and enter a valid ID": "l'ID de la demande ne peut pas être vide, veuillez vérifier votre saisie et saisir un ID valide",
"status can only be open, rejected or approved": "le statut ne peut être ouvert, rejeté ou approuvé",
"reason must be atleast one character": "la raison doit être au moins un caractère",
"destination should only contain letter": "la destination ne doit contenir qu'une lettre"
}
41 changes: 41 additions & 0 deletions src/services/tripServices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Sequelize } from 'sequelize';
import db from '../models';
import stringHelper from '../utils/stringHelper';

const { Op } = Sequelize;
/**
* @class TripsService
* @description this class handles trips services
*/
export default class TripsService {
/**
* set the user token and send the email(servicee)
* @param {object} obj
* @param {object} user
* @returns {object} request array
*/
static async searchRequest(obj, user) {
const searchObj = {};
searchObj.where = {};
let field = { ...obj };
field = JSON.parse(JSON.stringify(field));
const fieldArr = Object.keys(field);
if (user.role === 'requester') {
searchObj.where.email = user.email;
}
if (user.role === 'manager') {
searchObj.where.managerId = user.id;
}
fieldArr.forEach((el) => {
searchObj.where[el] = obj[el];
if (el === 'location' || el === 'departure' || el === 'status' || el === 'accommodation' || el === 'reason' || el === 'destination') {
searchObj.where[el] = { [Op.iLike]: `%${field[el].trim()}%` };
}
});
const requests = await db.Request.findAll(searchObj);
if (requests.length < 1) {
return stringHelper.requestNotFound;
}
return requests;
}
}
36 changes: 36 additions & 0 deletions src/swagger/trips.swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,39 @@
* '401':
* description: This request is not yours it is for another manager
* */
/**
* @swagger
* /api/v1/trips/search:
* get:
* tags:
* - Trips
* name: search a trip request
* summary: user can search for a trip request using different request attribute
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: token
* in: header
* description: jwt token of the user
* - name: id
* in: query
* - name: location
* in: query
* - name: destination
* in: query
* - name: departureDate
* in: query
* - name: returnDate
* in: query
* - name: reason
* in: query
* - name: status
* in: query
* responses:
* '200':
* description: request found.
* '404':
* description: request not found.
* */
7 changes: 5 additions & 2 deletions src/tests/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ const {
} = chai;
chai.use(chaiHttp);
describe('Signup Tests', () => {
before(() => {
sinon.stub(sgMail, 'send').returns({
beforeEach(() => {
sinon.stub(sgMail, 'send').resolves({
to: 'aime@amgil.com',
from: 'devrepublic.team@gmail.com',
subject: 'barefoot nomad',
html: 'this is stubbing message'
});
});
afterEach(() => {
sinon.restore();
});
it('should return account created sucessfully.', (done) => {
chai.request(index)
.post('/api/v1/auth/register')
Expand Down
13 changes: 13 additions & 0 deletions src/tests/resetPassword.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import chai from 'chai';
import sgMail from '@sendgrid/mail';
import sinon from 'sinon';
import chaiHttp from 'chai-http';
import index from '../index';
import { provideToken } from '../utils/tokenHandler';
Expand All @@ -10,6 +12,17 @@ chai.use(chaiHttp);
let token;
const notSignupToken = provideToken('wrongtototo', false, 'ade@gmail.com');
describe('Forgot password feature', () => {
beforeEach(() => {
sinon.stub(sgMail, 'send').resolves({
to: 'aime@amgil.com',
from: 'devrepublic.team@gmail.com',
subject: 'barefoot nomad',
html: 'this is stubbing message'
});
});
afterEach(() => {
sinon.restore();
});
it('should not send an email to the user who didn\'t provide valid email', (done) => {
chai
.request(index)
Expand Down
Loading

0 comments on commit fec2625

Please sign in to comment.