diff --git a/config/paths.js b/config/paths.js index b08dd3d..44594d6 100644 --- a/config/paths.js +++ b/config/paths.js @@ -1,6 +1,9 @@ import { SCOPE_TYPE } from '@utils/constants'; import { hasScopeOverUser } from '@utils/index'; +/** + * An array of path definitions representing different endpoints in the application. + */ const paths = [ { path: '/me', @@ -30,7 +33,7 @@ const paths = [ { path: '/oauth2/scopes', scopes: [SCOPE_TYPE.SUPER_ADMIN, SCOPE_TYPE.ADMIN], - method: 'POST', + method: 'PATCH', }, { path: '/oauth2/scopes/{scopeId}', @@ -78,15 +81,34 @@ const paths = [ customValidator: async (payload) => hasScopeOverUser(payload), }, { - path: "/cabs/fetch", + path: '/cabs/fetch', + scopes: [ + SCOPE_TYPE.INTERNAL_SERVICE, + SCOPE_TYPE.SUPER_ADMIN, + SCOPE_TYPE.ADMIN, + SCOPE_TYPE.USER, + ], + method: 'POST', + }, + { + path: '/driver', scopes: [ SCOPE_TYPE.INTERNAL_SERVICE, SCOPE_TYPE.SUPER_ADMIN, SCOPE_TYPE.ADMIN, - SCOPE_TYPE.USER ], - method: "POST" - } + method: 'GET', + }, + { + path: '/driver/{driverId}', + scopes: [ + SCOPE_TYPE.INTERNAL_SERVICE, + SCOPE_TYPE.SUPER_ADMIN, + SCOPE_TYPE.ADMIN, + SCOPE_TYPE.USER, + ], + method: 'GET', + }, ]; export default paths; diff --git a/lib/models/rides.js b/lib/models/rides.js index 0409e6a..7aebd95 100644 --- a/lib/models/rides.js +++ b/lib/models/rides.js @@ -26,13 +26,13 @@ module.exports = function(sequelize, DataTypes) { }, startPoint: { field: "start_point", - type: DataTypes.GEOMETRY, + type: DataTypes.GEOMETRY('POINT'), allowNull: false }, endPoint: { field: "end_point", - type: DataTypes.GEOMETRY, - allowNull: false + type: DataTypes.GEOMETRY('POINT'), + allowNull: true }, timeStart: { field: "time_start", @@ -42,21 +42,23 @@ module.exports = function(sequelize, DataTypes) { timeEnd: { field: "time_end", type: DataTypes.DATE, - allowNull: false + allowNull: true }, fare: { type: DataTypes.DECIMAL(10,2), - allowNull: false + allowNull: true }, rideTime: { field: "ride_time", type: DataTypes.DECIMAL(10,2), - allowNull: false + allowNull: true } }, { sequelize, tableName: 'rides', timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', indexes: [ { name: "PRIMARY", diff --git a/lib/routes/cabs/tests/routes.test.js b/lib/routes/cabs/tests/routes.test.js new file mode 100644 index 0000000..c3b8095 --- /dev/null +++ b/lib/routes/cabs/tests/routes.test.js @@ -0,0 +1,63 @@ +import { resetAndMockDB } from '@utils/testUtils'; + +describe('cabs route tests', () => { + let server; + beforeEach(async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.cabs.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return null; + } + }); + }); + }); + it('should return 500 if no body is passed', async () => { + const payload = {}; + const res = await server.inject({ + method: 'POST', + url: '/cabs/fetch', + payload, + }); + // console.log(res) + expect(res.statusCode).toBe(500); + expect(res.statusMessage).toEqual('Internal Server Error'); + }); + it('should return 400 for invalid payload', async () => { + const payload = { + userid: 1, + currentLocation: { + latitude: 1, + longitude: 2, + }, + }; // payload missing the currentLocation + const resp = await server.inject({ + method: 'POST', + url: '/cabs/fetch', + payload, + }); + expect(resp.statusCode).toBe(400); + expect(resp.statusMessage).toEqual('Bad Request'); + }); + it('should return 200', async () => { + const payload = { + userId: '1', + currentLocation: { + latitude: 1, + longitude: 2, + }, + }; + const res = await server.inject({ + method: 'POST', + url: '/cabs/fetch', + payload, + }); + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.result)).toBe(true); + expect(res.result.length).toBeGreaterThan(0); + const firstCab = res.result[0]; + expect(firstCab.id).toEqual(1); + expect(firstCab.driverId).toEqual(1); + expect(firstCab.carDetailsId).toEqual(1); + expect(firstCab.status).toEqual('available'); + }); +}); diff --git a/lib/routes/driver/routes.js b/lib/routes/driver/routes.js new file mode 100644 index 0000000..e57f034 --- /dev/null +++ b/lib/routes/driver/routes.js @@ -0,0 +1,69 @@ +import { findAllDrivers, findOneDriver } from '@daos/driversDao'; +import { badImplementation, notFound } from '@utils/responseInterceptors'; +import { transformDbArrayResponseToRawResponse } from '@utils/transformerUtils'; +import Joi from 'joi'; +import { get } from 'lodash'; + +export default [ + { + method: 'GET', + path: '/{driverId}', + options: { + description: 'get one driver by id', + notes: 'GET driver info API', + tags: ['api', 'driver'], + cors: true, + validate: { + params: Joi.object({ + driverId: Joi.string().required(), + }), + }, + }, + + handler: async (request, h) => { + // circuit breaker, rate-limiter can be used here + const { driverId } = request.params; + const driverInfo = await findOneDriver(driverId); + if (driverId){ + return h.response(driverInfo).code(200); + } + return notFound(`No driver found with id: ${driverId}`); + }, + }, + { + method: 'GET', + path: '/', + handler: async (request, h) => { + const { page, limit } = request.query; + return findAllDrivers(page, limit) + .then((driversData) => { + if (get(driversData.allDrivers, 'length')) { + const { totalCount } = driversData; + const allDrivers = transformDbArrayResponseToRawResponse( + driversData.allDrivers, + ).map((driver) => driver); + + return h.response({ + results: allDrivers, + totalCount, + }); + } + return notFound('No drivers found'); + }) + .catch((error) => badImplementation(error.message)); + }, + options: { + description: 'get all drivers', + notes: 'GET drivers API', + tags: ['api', 'driver'], + plugins: { + pagination: { + enabled: true, + }, + query: { + pagination: true, + }, + }, + }, + } +]; diff --git a/lib/routes/driver/tests/routes.test.js b/lib/routes/driver/tests/routes.test.js new file mode 100644 index 0000000..fb1f047 --- /dev/null +++ b/lib/routes/driver/tests/routes.test.js @@ -0,0 +1,120 @@ +import { mockData } from '@utils/mockData'; +import { resetAndMockDB } from '@utils/testUtils'; + +const { MOCK_DRIVER: drivers } = mockData; + +describe('/driver route tests', () => { + let server; + beforeEach(async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.drivers.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return drivers; + } + }); + }); + }); + it('should return all the drivers', async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.drivers.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return drivers; + } + }); + }); + const res = await server.inject({ + method: 'GET', + url: '/driver', + }); + const expectedResult = { + results: [ + { + id: 1, + name: 'Test Driver', + license_number: 'ABC1234', + contact_number: '123456789', + ratings: 5, + status: 'available', + }, + ], + total_count: 1, + }; + const { result } = res; + expect(res.statusCode).toBe(200); // this means route exists with above parametres + expect(result.results.length).toBe(expectedResult.results.length); + const driver = result.results[0]; + const expectedDriver = expectedResult.results[0]; + expect(driver.id).toBe(expectedDriver.id); + expect(driver.name).toBe(expectedDriver.name); + expect(driver.license_number).toBe(expectedDriver.license_number); + expect(driver.contact_number).toBe(expectedDriver.contact_number); + expect(driver.ratings).toBe(expectedDriver.ratings); + expect(driver.status).toBe(expectedDriver.status); + expect(result.total_count).toBe(expectedResult.total_count); + }); + + it('should return notFound if no drivers are found', async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.drivers.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return null; + } + }); + allDbs.models.drivers.findAll = () => null; + }); + const resp = await server.inject({ + method: 'GET', + url: '/driver', + }); + + expect(resp.statusCode).toBe(404); + }); + it('should return badImplementation if findAllDrivers fails', async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.drivers.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return null; + } + }); + allDbs.models.drivers.findAll = () => + new Promise((resolve, reject) => { + reject(new Error()); + }); + }); + + const res = await server.inject({ + method: 'GET', + url: '/driver', + }); + + expect(res.statusCode).toBe(500); + }); +}); + +describe("/driver/{driverId} routes tests",()=> { + let server; + beforeEach(async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.drivers.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return drivers; + } + }); + }); + }); + it("should return 200",async () => { + const resp = await server.inject({ + method: "GET", + url: "/driver/1" + }); + expect(resp.statusCode).toBe(200); + }); + it("should return 404",async ()=> { + const res = await server.inject({ + method:"GET", + url: "/drivers/2" + }); + expect(res.statusCode).toBe(404); + expect(res.result.message).toEqual("Not Found") + }) +}) \ No newline at end of file diff --git a/lib/routes/ride/routes.js b/lib/routes/ride/routes.js new file mode 100644 index 0000000..34c4ecb --- /dev/null +++ b/lib/routes/ride/routes.js @@ -0,0 +1,61 @@ +import { createRide, getRideById } from '@daos/ridesDao'; +import Joi from 'joi'; + +export default [ + { + method: 'POST', + path: '/start', + options: { + description: 'driver starts the ride', + notes: 'driver starts the ride', + tags: ['api', 'driver-start-ride'], + cors: true, + validate: { + payload: Joi.object({ + userId: Joi.string(), + cabId: Joi.string(), + startPoint: Joi.object({ + latitude: Joi.number(), + longitude: Joi.number(), + }), + }), + }, + }, + + handler: async (request, h) => { + // circuit breaker, rate-limiter can be used here + const { userId, cabId, startPoint } = request.payload; + const rideData = { + userId, + cabId, + startPoint, + timeStart: new Date(), + }; + const createdRide = await createRide(rideData); + return h.response(createdRide).code(200); + }, + }, + { + method: 'GET', + path: '/{rideId}', + options: { + description: 'get one ride by ID', + notes: 'GET ride API', + tags: ['api', 'rides'], + cors: true, + validate: { + params: Joi.object({ + rideId: Joi.string().required(), + }), + }, + }, + handler: async (request, h) => { + const { rideId } = request.params; + const rideInfo = await getRideById(rideId); + if (rideInfo) { + return h.response(rideInfo).code(200); + } + return h.response(`No data found for ride ${rideId}`).code(404); + }, + }, +]; diff --git a/lib/routes/ride/tests/routes.test.js b/lib/routes/ride/tests/routes.test.js new file mode 100644 index 0000000..70cf7c3 --- /dev/null +++ b/lib/routes/ride/tests/routes.test.js @@ -0,0 +1,62 @@ +import { resetAndMockDB } from '@utils/testUtils'; + +describe('ride routes test', () => { + let server; + beforeEach(async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.rides.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return null; + } + }); + }); + }); + it('should return the created ride', async () => { + const payload = { + userId: '1', + cabId: '1', + startPoint: { + latitude: 1, + longitude: 1, + }, + }; + const res = await server.inject({ + method: 'POST', + url: '/ride/start', + payload, + }); + expect(res.statusCode).toBe(200); + expect(res.result.userId).toEqual('1'); + expect(res.result.cabId).toEqual('1'); + }); + it('should get one ride by id', async () => { + server = await resetAndMockDB(async (allDbs) => { + allDbs.models.rides.$queryInterface.$useHandler((query) => { + if (query === 'findById') { + return { + id: '1', + userId: '1', + cabId: '1', + startPoint: { latitude: 1, longitude: 1 }, + }; + } + }); + }); + const res = await server.inject({ + method: 'GET', + url: '/ride/1', + }); + expect(res.statusCode).toBe(200); + expect(res.result.id).toEqual('1'); + expect(res.result.user_id).toEqual('1'); + expect(res.result.cab_id).toEqual('1'); + expect(res.result.start_point).toEqual({ latitude: 1, longitude: 1 }); + }); + it('should return 404', async () => { + const res = await server.inject({ + method: 'GET', + url: '/ride/2', + }); + expect(res.statusCode).toBe(404); + }); +}); diff --git a/migrations/20240415072214-alter-ride.js b/migrations/20240415072214-alter-ride.js new file mode 100644 index 0000000..a423ddf --- /dev/null +++ b/migrations/20240415072214-alter-ride.js @@ -0,0 +1,45 @@ + + +module.exports = { + async up(queryInterface, Sequelize) { + await Promise.all([ + queryInterface.changeColumn('rides', 'end_point', { + type: Sequelize.DataTypes.GEOMETRY('POINT'), + allowNull: true, + }), + queryInterface.changeColumn('rides', 'ride_time', { + type: Sequelize.DataTypes.DECIMAL(10, 2), + allowNull: true, + }), + queryInterface.changeColumn('rides', 'fare', { + type: Sequelize.DataTypes.DECIMAL(10, 2), + allowNull: true, + }), + queryInterface.changeColumn('rides', 'time_end', { + type: Sequelize.DataTypes.DATE, + allowNull: true, + }), + ]); + }, + + async down(queryInterface, Sequelize) { + await Promise.all([ + queryInterface.changeColumn('rides', 'end_point', { + type: Sequelize.DataTypes.GEOMETRY('POINT'), + allowNull: false, + }), + queryInterface.changeColumn('rides', 'ride_time', { + type: Sequelize.DataTypes.DECIMAL(10, 2), + allowNull: false, + }), + queryInterface.changeColumn('rides', 'fare', { + type: Sequelize.DataTypes.DECIMAL(10, 2), + allowNull: false, + }), + queryInterface.changeColumn('rides', 'time_end', { + type: Sequelize.DataTypes.DATE, + allowNull: false, + }), + ]); + }, +}; diff --git a/server.js b/server.js index f80d375..e8c7f89 100644 --- a/server.js +++ b/server.js @@ -94,6 +94,18 @@ const initServer = async () => { name: 'fetch-cabs', description: 'Fetch nearest cabs', }, + { + name: 'driver', + description: 'User related endpoints', + }, + { + name: "driver-start-ride", + description : "User books ride" + }, + { + name : "rides", + description: "Fetch rides details" + }, { name: 'reset-cache', description: 'Cache invalidation endpoints', @@ -145,6 +157,7 @@ const initServer = async () => { await server.start(); + // eslint-disable-next-line func-names const onPreHandler = function (request, h) { const requestQueryParams = request.query; const requestPayload = request.payload; @@ -153,6 +166,7 @@ const initServer = async () => { return h.continue; }; + // eslint-disable-next-line func-names const onPreResponse = function (request, h) { const { response } = request; const responseSource = response.source; diff --git a/utils/testUtils.js b/utils/testUtils.js index aff97c9..05f673a 100644 --- a/utils/testUtils.js +++ b/utils/testUtils.js @@ -21,7 +21,9 @@ export function configDB(metadataOptions = DEFAULT_METADATA_OPTIONS) { 'car_details', mockData.MOCK_CAR_DETAILS, ); - const rides = DBConnectionMock.define('rides', mockData.MOCK_RIDES); + const ridesMock = DBConnectionMock.define('rides', mockData.MOCK_RIDES); + ridesMock.findByPk = (query) => ridesMock.findById(query); + ridesMock.count = () => 1; const oauthClientsMock = DBConnectionMock.define( 'oauthClients', @@ -65,7 +67,7 @@ export function configDB(metadataOptions = DEFAULT_METADATA_OPTIONS) { drivers: driversMock, cabs: cabsMock, carDetails: carDetailsMock, - rides: rides, + rides: ridesMock, }; }