Skip to content

Commit

Permalink
ft(search functionality): User can search trip requests by preference
Browse files Browse the repository at this point in the history
- Add tests
- Add verify token validation
- Add user is verified validation
- Add validate query parameters validation middleware
- Add search query parameter query function
- Add search controller
- Add route API documentation

[Finishes #169258648]
  • Loading branch information
nziokaivy committed Dec 12, 2019
1 parent 9f2cb44 commit 2a7355a
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 13 deletions.
28 changes: 28 additions & 0 deletions src/controllers/SearchController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Response from '../helpers/Response';
import SearchService from '../services/SearchService';


const { searchByPreference } = SearchService;
/**
* @exports
* @class SearchController
*/
class SearchController {
/**
* User can search trips by preference
* @static
* @param {object} req request object
* @param {object} res response object
* @memberof SearchController
* @returns {object} data
*/
static async searchedByPreference(req, res) {
const searchedTrips = await searchByPreference(req);
const searchedTripsMap = searchedTrips.filter(request => request.tripRequest !== null);
if (searchedTripsMap.length === 0) {
return Response.errorMessage(req, res, 'Oooops! No trips matching this search query parameter were found!', 404);
}
return Response.successMessage(req, res, 'Successfully retrieved trip requests by that search query parameter', searchedTripsMap, 200);
}
}
export default SearchController;
29 changes: 29 additions & 0 deletions src/middlewares/SearchMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Response from '../helpers/Response';
import UserService from '../services/UserService';

const { getAUser } = UserService;

/**
* @export
* @class SearchMiddleware
*/
class SearchMiddleware {
/**
* check if user exists
* @static
* @param {Object} req request object
* @param {Object} res response object
* @param {Function} next next function
* @returns {Object} if an error exists, returns a bad request error response
*/
static async checkIfUserExists(req, res, next) {
const { userName } = req.params;
const user = await getAUser(userName);
if (!user.length) {
return Response.errorMessage(req, res, 'Oops! user doesn\'t exist', 404);
}
return next();
}
}

export default SearchMiddleware;
28 changes: 28 additions & 0 deletions src/middlewares/validateSearchQueries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Response from '../helpers/Response';

const validateSearchQueries = (req, res, next) => {
const { query } = req;
const allKeys = Object.keys(query);
if (allKeys.length === 0) {
return Response.errorMessage(req, res, 'Please provide a search query key parameter', 400);
}

const searchKeys = allKeys.map(key => {
if (key === 'originId' || key === 'destinationId' || key === 'startDate' || key === 'returnDate' || key === 'firstName' || key === 'status' || key === 'tripType') {
return true;
}
return false;
});

const invalidKeys = searchKeys.filter(key => key === false);
if (invalidKeys.length > 0) {
return Response.errorMessage(req, res, 'You provided an invalid search query key(s) parameter.Your search key should be either originId, destinationId, startDate, returnDate, firstName, status, or tripType', 400);
}

if (query.originId === '' || query.destinationId === '' || query.startDate === '' || query.returnDate === '' || query.firstName === '' || query.tripType === '') {
return Response.errorMessage(req, res, 'Please provide a search value parameter', 400);
}
return next();
};

export default validateSearchQueries;
3 changes: 2 additions & 1 deletion src/routes/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import locationRoute from './locationRoute';
import commentRoute from './commentRoute';
import accommodationRoute from './accommodationRoute';
import bookingRouter from './bookingRoute';
import searchRouter from './searchRoute';

const router = Router();
router.use('/auth', authRoute);
Expand All @@ -19,6 +20,6 @@ router.use('/location', locationRoute);
router.use('/users', userRoute);
router.use('/accommodations', accommodationRoute);
router.use('/trips', bookingRouter);

router.use('/search', searchRouter);

export default router;
62 changes: 62 additions & 0 deletions src/routes/api/searchRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import express from 'express';
import verifyToken from '../../middlewares/verifyToken';
import isUserVerified from '../../middlewares/isUserVerified';
import SearchController from '../../controllers/SearchController';
import validateSearchQueries from '../../middlewares/validateSearchQueries';

const searchRoute = express.Router();
const { searchedByPreference } = SearchController;

/**
* @swagger
*
* /search:
* get:
* summary: User can get trips searched by the preference.
* description: user can search trips by originId, destinationId,
* status, trip type,trip owner's first name
* tags:
* - Search
* parameters:
* - name: token
* in: header
* required: true
* description: user token
* schema:
* $ref: '#/components/schemas/Search'
* - name: searchPreference
* in: path
* required: true
* description: search trips by originId, destinationId,
* status, trip type,trip owner's first name
* schema:
* $ref: '#/components/schemas/Search'
* - name: search query key
* in: query
* required: true
* description: search trips by originId, destinationId,
* status, trip type,trip owner's first name
* schema:
* type: string
* responses:
* 200:
* description: Successfully retrieved trip requests by that search query parameter
* 401:
* description: Unauthorized
* 400:
* description: Bad Request
* 403:
* description: Forbidden
* 500:
* description: Internal server error
*/

searchRoute
.get(
'/',
verifyToken,
isUserVerified,
validateSearchQueries,
searchedByPreference,
);
export default searchRoute;
24 changes: 24 additions & 0 deletions src/services/SearchService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { trips } from '../database/models';
import CommonQueries from './CommonQueries';
import commonSearchQueries from './commonSearchQueries';
/**
* @exports
* @class searchService
*/
class SearchService {
/**
* users can search for a trip by preference
* @static
* @description GET /api/search
* @param {object} req request object
* @memberof SearchService
* @returns {object} data
*/
static async searchByPreference(req) {
const { query } = req;
const searchingTrips = await CommonQueries.findAll(trips, commonSearchQueries(query));
return searchingTrips;
}
}

export default SearchService;
76 changes: 76 additions & 0 deletions src/services/commonSearchQueries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Op } from 'sequelize';
import {
tripRequests, status, tripTypes, users
} from '../database/models';


const query = (searchQueryParams) => ({
where: {
...searchQueryParams.startDate && {
startDate: {
[Op.iLike]: `%${searchQueryParams.startDate.trim()}%`
}
},
...searchQueryParams.returnDate && {
returnDate: {
[Op.iLike]: `%${searchQueryParams.returnDate.trim()}%`
}
},
...searchQueryParams.originId && {
originId: {
[Op.eq]: parseInt((searchQueryParams.originId), 10)
}
},
...searchQueryParams.destinationId && {
destinationId: {
[Op.eq]: parseInt((searchQueryParams.destinationId), 10)
}
},
},
attributes: ['originId', 'destinationId', 'reason', 'startDate', 'returnDate'],
include: [
{
model: tripRequests,
attributes: ['id'],
include: [
{
model: status,
where: {
...searchQueryParams.status && {
status: {
[Op.iLike]: `%${searchQueryParams.status.trim()}%`
}
}
},
attributes: ['status'],
},
{
model: tripTypes,
where: {
...searchQueryParams.tripType && {
tripType: {
[Op.iLike]: `%${searchQueryParams.tripType.trim()}%`
}
}
},
attributes: ['tripType'],
},
{
model: users,
where: {
...searchQueryParams.firstName && {
firstName: {
[Op.iLike]: `%${searchQueryParams.firstName.trim()}%`
}
}
},
attributes: ['firstName'],
}
]
}
],
}
);


export default query;
4 changes: 2 additions & 2 deletions src/tests/100-bookingTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ describe('Book an accomadition facility', () => {
.set('token', userToken1)
.send(bookMockData.bookedAccommodation)
.end((err, res) => {
expect(res.body.message).eql('Room booked by other client');
res.should.have.status(403);
expect(res.body.message).eql('The accommodation has already booked');
res.should.have.status(409);
res.body.should.be.an('object');
done(err);
});
Expand Down
94 changes: 94 additions & 0 deletions src/tests/110-searchTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import dotenv from 'dotenv';
import app from '../index';
import mockData from './mock/mockData';

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

dotenv.config();
let userToken;

describe('Search functionality tests', () => {
before((done) => {
chai.request(app)
.post('/api/v1/auth/signin')
.send(mockData.testUser)
.end((err, res) => {
userToken = res.body.data;

done(err);
});
});

before((done) => {
chai.request(app)
.get('/api/v1/search')
.set('token', userToken)
.send(mockData.usersSignin)
.end(() => {
done();
});
});

it('should not search anything if no search query key parameter has been provided', (done) => {
chai.request(app)
.get('/api/v1/search')
.set('token', userToken)
.end((err, res) => {
expect(res.body.message).eql('Please provide a search query key parameter');
res.should.have.status(400);
res.body.should.be.an('object');
done(err);
});
});

it('should not search anything if the search query key parameter provided is invalid', (done) => {
chai.request(app)
.get('/api/v1/search?monkey')
.set('token', userToken)
.end((err, res) => {
expect(res.body.message).eql('You provided an invalid search query key(s) parameter.Your search key should be either originId, destinationId, startDate, returnDate, firstName, status, or tripType');
res.should.have.status(400);
res.body.should.be.an('object');
done(err);
});
});

it('should not search anything if no search query value parameter has not been provided', (done) => {
chai.request(app)
.get('/api/v1/search?startDate=')
.set('token', userToken)
.end((err, res) => {
expect(res.body.message).eql('Please provide a search value parameter');
res.should.have.status(400);
res.body.should.be.an('object');
done(err);
});
});

it('should return an error message if search for trips if the search query cannot be found has been provided', (done) => {
chai.request(app)
.get('/api/v1/search?firstName=Ivy')
.set('token', userToken)
.end((err, res) => {
expect(res.body.message).eql('Oooops! No trips matching this search query parameter were found!');
res.should.have.status(404);
res.body.should.be.an('object');
done(err);
});
});

it('should return searched trips if search query information is valid', (done) => {
chai.request(app)
.get('/api/v1/search?status=rejected')
.set('token', userToken)
.end((err, res) => {
expect(res.body.message).eql('Successfully retrieved trip requests by that search query parameter');
res.should.have.status(200);
res.body.should.be.an('object');
done(err);
});
});
});
Loading

0 comments on commit 2a7355a

Please sign in to comment.