diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9e6d9b9..9ecbe25 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -1,11 +1,9 @@ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Node.js CI +name: Build-Test-Deploy Pipeline on: - push: - branches: [ develop ] pull_request: branches: [ develop ] @@ -36,15 +34,15 @@ jobs: cd server npm ci npm run build --if-present - - name: Server - run tests - run: | - cd server - npm test - name: UI - run build run: | cd ui npm ci npm run build --if-present + - name: Server - run tests + run: | + cd server + npm test - name: UI - run tests run: | cd ui @@ -52,9 +50,10 @@ jobs: - name: Code Coverage uses: romeovs/lcov-reporter-action@v0.2.16 with: - lcov-file: ./coverage/lcov.info + lcov-file: ./server/coverage/lcov.info + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Deploy uses: JamesIves/github-pages-deploy-action@v4.2.3 with: branch: gh-pages - folder: develop \ No newline at end of file + folder: ./ui/dist/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 382f01b..6ae5f2a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules /dist server/.nyc_output/*.json server/.nyc_output/processinfo/*.json +server/coverage/lcov-report # local env files .env.local @@ -22,5 +23,4 @@ pnpm-debug.log* *.ntvs* *.njsproj *.sln -*.sw? -*.env +*.sw? \ No newline at end of file diff --git a/server/.env b/server/.env new file mode 100644 index 0000000..3fff465 --- /dev/null +++ b/server/.env @@ -0,0 +1 @@ +TOKEN_SECRET=test \ No newline at end of file diff --git a/server/.nyc_output/processinfo/index.json b/server/.nyc_output/processinfo/index.json index 83b4e73..e2f59c3 100644 --- a/server/.nyc_output/processinfo/index.json +++ b/server/.nyc_output/processinfo/index.json @@ -1 +1 @@ -{"processes":{"ecd4285b-d9dc-4104-b850-fcf81a763c89":{"parent":null,"children":[]}},"files":{"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\app.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\database\\index.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\config\\db.config.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\models\\user.model.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\routes\\auth.routes.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\controllers\\auth.controller.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\business\\user.business.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"],"C:\\Users\\Jarrod\\Documents\\AAF\\CSSD\\server\\datalayer\\mongo.js":["ecd4285b-d9dc-4104-b850-fcf81a763c89"]},"externalIds":{}} \ No newline at end of file +{"processes":{"4d150775-f277-4c3e-a138-a2031d24611b":{"parent":null,"children":[]}},"files":{"C:\\Projects\\CSSD-Assignment\\server\\app.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\database\\index.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\config\\db.config.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\models\\journey.model.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\models\\bill.model.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\utilities.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\models\\location.model.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\models\\user.model.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\routes\\auth.routes.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\controllers\\auth.controller.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\business\\user.business.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\datalayer\\mongo.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\routes\\bill.routes.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\controllers\\bill.controller.js":["4d150775-f277-4c3e-a138-a2031d24611b"],"C:\\Projects\\CSSD-Assignment\\server\\business\\bill.business.js":["4d150775-f277-4c3e-a138-a2031d24611b"]},"externalIds":{}} \ No newline at end of file diff --git a/server/app.js b/server/app.js index a8ef318..f4b6b27 100644 --- a/server/app.js +++ b/server/app.js @@ -8,7 +8,7 @@ const cookieParser = require("cookie-parser"); require("./database"); require("dotenv").config(); -var app = express(); +const app = express(); app.use( cookieSession({ @@ -24,8 +24,8 @@ app.use(cors({ origin: "http://localhost:8080", credentials: true })); /** * Router setup */ -var authRouter = require("./routes/auth.routes"); - +const authRouter = require("./routes/auth.routes"); +const billRouter = require("./routes/bill.routes"); /** * View Engine setup */ @@ -40,5 +40,6 @@ app.use(cookieParser()); // Configuring the main routes app.use("/auth", authRouter); +app.use("/bill", billRouter) module.exports = app; diff --git a/server/business/bill.business.js b/server/business/bill.business.js new file mode 100644 index 0000000..e284c5c --- /dev/null +++ b/server/business/bill.business.js @@ -0,0 +1,39 @@ +const DataLayer = require("../datalayer/mongo"); +const Utilities = require("../utilities") +const model = require("../database").getModel("bill"); +const httpError = require("http-errors"); + +module.exports = class BillBusiness { + constructor() { + // Create an instance of the data layer. + this.dataLayer = new DataLayer(model); + } + + /** + * Get all bills. + */ + async getAllBills(queryString) { + const filter = { + driver: queryString.driver, + paid: queryString.paid, + limit: queryString.limit ?? 10, + offset: queryString.offset ?? 0 + } + return this.dataLayer + .findAllAndPopulate(filter, [{ path: 'journey', populate: { path: 'entryLocation exitLocation' }}, {path: 'driver', select: 'username type email'}]) + .catch((error) => { + throw httpError(500, error.message)}) + } + + /** + * Get a bill by ID. + */ + async payBill(id) { + const record = { + paid: true + } + return this.dataLayer.update(id, record) + .catch((error) => {throw httpError(404, error.message) + }) + } +} \ No newline at end of file diff --git a/server/business/user.business.js b/server/business/user.business.js index 7111eaa..51ac094 100644 --- a/server/business/user.business.js +++ b/server/business/user.business.js @@ -4,7 +4,7 @@ const httpError = require("http-errors"); const jwt = require("jsonwebtoken"); const bcrypt = require("bcryptjs"); -class UserBusiness { +module.exports = class UserBusiness { constructor() { // Create an instance of the data layer. this.dataLayer = new DataLayer(model); @@ -47,8 +47,7 @@ class UserBusiness { id: user._id, }; }) - .catch(() => { - throw httpError(400, "Your email or password is incorrect."); + .catch(() => {throw httpError(400, "Your email or password is incorrect."); }); } @@ -104,15 +103,10 @@ class UserBusiness { }); } } -module.exports = UserBusiness; /** * Validates the data in a User. */ function isUserDataValid(user) { - if (!user || !user.username || !user.email || !user.password) { - return false; - } else { - return true; - } + return !(!user || !user.username || !user.email || !user.password); } diff --git a/server/controllers/bill.controller.js b/server/controllers/bill.controller.js new file mode 100644 index 0000000..07f2c67 --- /dev/null +++ b/server/controllers/bill.controller.js @@ -0,0 +1,23 @@ +const BillBusiness = require("../business/bill.business"); +const billBusiness = new BillBusiness(); + +/** + * Get all bills + */ +exports.getAllBills = async (req, res) => { + billBusiness.getAllBills(req.query) + .then((data) => {return res.status(200).send(data)}) + .catch((error) => { + res.status(error.status).send({message: error.message}) + }) + +} + +/** + * Pay bill + */ +exports.payBill = async (req, res) => { + billBusiness.payBill(req.params.id) + .then(() => {res.status(200).send({message: "Bill paid."})}) + .catch((error) => {res.status(error.status).send({message: error.message})}) +} \ No newline at end of file diff --git a/server/coverage/lcov.info b/server/coverage/lcov.info new file mode 100644 index 0000000..d615551 --- /dev/null +++ b/server/coverage/lcov.info @@ -0,0 +1,408 @@ +TN: +SF:app.js +FNF:0 +FNH:0 +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:6,1 +DA:8,1 +DA:9,1 +DA:11,1 +DA:13,1 +DA:22,1 +DA:27,1 +DA:28,1 +DA:32,1 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,1 +DA:42,1 +DA:43,1 +DA:45,1 +LF:24 +LH:24 +BRF:0 +BRH:0 +end_of_record +TN: +SF:utilities.js +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:1,(anonymous_0) +DA:1,1 +DA:3,1 +DA:5,1 +DA:8,1 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record +TN: +SF:business\bill.business.js +FN:7,(anonymous_0) +FN:15,(anonymous_1) +FN:24,(anonymous_2) +FN:31,(anonymous_3) +FN:36,(anonymous_4) +FNF:5 +FNH:4 +FNDA:1,(anonymous_0) +FNDA:5,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:2,(anonymous_3) +FNDA:1,(anonymous_4) +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:6,1 +DA:9,1 +DA:16,5 +DA:22,5 +DA:25,0 +DA:32,2 +DA:35,2 +DA:36,1 +LF:12 +LH:11 +BRDA:19,0,0,5 +BRDA:19,0,1,4 +BRDA:20,1,0,5 +BRDA:20,1,1,4 +BRF:4 +BRH:4 +end_of_record +TN: +SF:business\user.business.js +FN:8,(anonymous_0) +FN:16,(anonymous_1) +FN:18,(anonymous_2) +FN:50,(anonymous_3) +FN:57,(anonymous_4) +FN:63,(anonymous_5) +FN:71,(anonymous_6) +FN:82,(anonymous_7) +FN:94,(anonymous_8) +FN:97,(anonymous_9) +FN:101,(anonymous_10) +FN:110,isUserDataValid +FNF:12 +FNH:11 +FNDA:1,(anonymous_0) +FNDA:2,(anonymous_1) +FNDA:2,(anonymous_2) +FNDA:1,(anonymous_3) +FNDA:3,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:3,(anonymous_6) +FNDA:2,(anonymous_7) +FNDA:2,(anonymous_8) +FNDA:2,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:3,isUserDataValid +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:7,1 +DA:10,1 +DA:17,2 +DA:19,2 +DA:24,2 +DA:25,1 +DA:31,1 +DA:43,1 +DA:50,1 +DA:58,3 +DA:64,2 +DA:72,3 +DA:73,0 +DA:75,3 +DA:82,3 +DA:83,2 +DA:84,1 +DA:85,1 +DA:86,1 +DA:87,0 +DA:95,2 +DA:99,2 +DA:102,0 +DA:111,3 +LF:29 +LH:26 +BRDA:24,0,0,1 +BRDA:24,0,1,1 +BRDA:72,1,0,0 +BRDA:72,1,1,3 +BRDA:83,2,0,1 +BRDA:83,2,1,1 +BRDA:85,3,0,1 +BRDA:85,3,1,0 +BRDA:111,4,0,3 +BRDA:111,4,1,3 +BRDA:111,4,2,3 +BRDA:111,4,3,3 +BRF:12 +BRH:10 +end_of_record +TN: +SF:config\db.config.js +FNF:0 +FNH:0 +DA:1,1 +LF:1 +LH:1 +BRF:0 +BRH:0 +end_of_record +TN: +SF:controllers\auth.controller.js +FN:7,(anonymous_0) +FN:10,(anonymous_1) +FN:23,(anonymous_2) +FN:31,(anonymous_3) +FN:34,(anonymous_4) +FN:39,(anonymous_5) +FN:47,(anonymous_6) +FNF:7 +FNH:6 +FNDA:2,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:3,(anonymous_3) +FNDA:1,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:0,(anonymous_6) +DA:1,1 +DA:2,1 +DA:7,1 +DA:8,2 +DA:11,1 +DA:12,1 +DA:13,1 +DA:14,1 +DA:16,1 +DA:24,1 +DA:31,1 +DA:32,3 +DA:35,1 +DA:40,2 +DA:47,1 +DA:48,0 +DA:49,0 +LF:17 +LH:15 +BRF:0 +BRH:0 +end_of_record +TN: +SF:controllers\bill.controller.js +FN:7,(anonymous_0) +FN:9,(anonymous_1) +FN:10,(anonymous_2) +FN:19,(anonymous_3) +FN:21,(anonymous_4) +FN:22,(anonymous_5) +FNF:6 +FNH:5 +FNDA:5,(anonymous_0) +FNDA:5,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:2,(anonymous_3) +FNDA:1,(anonymous_4) +FNDA:1,(anonymous_5) +DA:1,1 +DA:2,1 +DA:7,1 +DA:8,5 +DA:9,5 +DA:11,0 +DA:19,1 +DA:20,2 +DA:21,1 +DA:22,1 +LF:10 +LH:9 +BRF:0 +BRH:0 +end_of_record +TN: +SF:database\index.js +FN:25,(anonymous_0) +FN:29,setRunValidators +FN:39,(anonymous_2) +FN:42,(anonymous_3) +FN:50,(anonymous_4) +FNF:5 +FNH:2 +FNDA:0,(anonymous_0) +FNDA:0,setRunValidators +FNDA:1,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:2,(anonymous_4) +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:7,1 +DA:8,1 +DA:9,1 +DA:10,1 +DA:13,1 +DA:14,1 +DA:17,1 +DA:18,0 +DA:23,1 +DA:25,1 +DA:26,0 +DA:27,0 +DA:30,0 +DA:34,1 +DA:40,1 +DA:43,0 +DA:44,0 +DA:47,1 +DA:49,1 +DA:51,2 +LF:24 +LH:18 +BRDA:17,0,0,0 +BRDA:17,0,1,1 +BRF:2 +BRH:1 +end_of_record +TN: +SF:datalayer\mongo.js +FN:2,(anonymous_0) +FN:10,(anonymous_1) +FN:20,(anonymous_2) +FN:27,(anonymous_3) +FN:34,(anonymous_4) +FN:37,(anonymous_5) +FNF:6 +FNH:6 +FNDA:2,(anonymous_0) +FNDA:5,(anonymous_1) +FNDA:2,(anonymous_2) +FNDA:3,(anonymous_3) +FNDA:2,(anonymous_4) +FNDA:1,(anonymous_5) +DA:4,2 +DA:11,5 +DA:21,2 +DA:28,3 +DA:35,2 +DA:37,1 +DA:42,1 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models\bill.model.js +FN:2,(anonymous_0) +FN:27,(anonymous_1) +FNF:2 +FNH:1 +FNDA:1,(anonymous_0) +FNDA:0,(anonymous_1) +DA:1,1 +DA:2,1 +DA:3,1 +DA:27,1 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:37,1 +LF:11 +LH:5 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models\journey.model.js +FN:1,(anonymous_0) +FNF:1 +FNH:1 +FNDA:1,(anonymous_0) +DA:1,1 +DA:2,1 +DA:24,1 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models\location.model.js +FN:1,(anonymous_0) +FNF:1 +FNH:1 +FNDA:1,(anonymous_0) +DA:1,1 +DA:2,1 +DA:21,1 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models\user.model.js +FN:2,(anonymous_0) +FNF:1 +FNH:1 +FNDA:1,(anonymous_0) +DA:2,1 +DA:3,1 +DA:30,1 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:routes\auth.routes.js +FNF:0 +FNH:0 +DA:1,1 +DA:2,1 +DA:5,1 +DA:8,1 +DA:11,1 +DA:14,1 +DA:16,1 +LF:7 +LH:7 +BRF:0 +BRH:0 +end_of_record +TN: +SF:routes\bill.routes.js +FNF:0 +FNH:0 +DA:1,1 +DA:2,1 +DA:5,1 +DA:8,1 +DA:11,1 +DA:13,1 +LF:6 +LH:6 +BRF:0 +BRH:0 +end_of_record diff --git a/server/database/index.js b/server/database/index.js index f3cff22..eaea80b 100644 --- a/server/database/index.js +++ b/server/database/index.js @@ -4,9 +4,9 @@ mongoose.Promise = global.Promise; const environment = process.env.NODE_ENV; let dbConfig = require("../config/db.config.js")[environment]; -const journey = require("../models/journey")(mongoose); -const bill = require("../models/bill")(mongoose); -const location = require("../models/location")(mongoose); +const journey = require("../models/journey.model.js")(mongoose); +const bill = require("../models/bill.model.js")(mongoose); +const location = require("../models/location.model.js")(mongoose); const user = require("../models/user.model.js")(mongoose); // Create mongoose and read in config diff --git a/server/database/seed.js b/server/database/seed.js index 200149a..3e1d0a3 100644 --- a/server/database/seed.js +++ b/server/database/seed.js @@ -1,9 +1,14 @@ const mongoose = require("mongoose"); mongoose.users = require("../models/user.model")(mongoose); +mongoose.locations = require("../models/location.model")(mongoose); +mongoose.journeys = require("../models/journey.model")(mongoose); +mongoose.bills = require("../models/bill.model")(mongoose); const bcrypt = require("bcryptjs"); +const environment = process.env.NODE_ENV; +let dbConfig = require("../config/db.config.js")[environment] ?? { url: "mongodb://localhost:27017/highwaytrackerdb_testing" }; mongoose - .connect("mongodb://localhost:27017/highwaytrackerdb_testing", { + .connect(dbConfig.url, { useNewUrlParser: true, useUnifiedTopology: true, }) @@ -16,15 +21,83 @@ mongoose const users = [ { + _id: "123456789107", username: "test_username", email: "test@email.com", - password: bcrypt.hashSync("admin", 8), + password: bcrypt.hashSync("test1", 8), + type: "Driver", + }, + { + _id: "123456789108", + username: "test_username2", + email: "test2@email.com", + password: bcrypt.hashSync("test1", 8), type: "Driver", }, ]; + +const locations = [ + { + _id: "123456789101", + name: "test_location_1", + coordinates: { + longitude: 50, + latitude: 50 + } + }, + { + _id: "123456789102", + name: "test_location_2", + coordinates: { + longitude: 0, + latitude: 0 + } + } +] + +const journeys = [ + { + _id: "123456789103", + regNumber: "test_reg_number", + entryLocation: "123456789101", + exitLocation: "123456789102", + journeyDateTime: "2022-02-01T15:50:51.039Z" + }, + { + _id: "123456789104", + regNumber: "test_reg_number2", + entryLocation: "123456789101", + exitLocation: "123456789102", + journeyDateTime: "2022-02-01T15:50:51.038Z" + }, +] + +const bills = [ + { + _id: "123456789105", + journey: "123456789103", + driver: "123456789107", + cost: 5, + paid: false + }, + { + _id: "123456789106", + journey: "123456789104", + driver: "123456789108", + cost: 5, + paid: true + } +] + const seedDB = async () => { - await mongoose.users.deleteMany(); - await mongoose.users.insertMany(users); + for(const collection in mongoose.connection.collections){ + await mongoose.connection.collections[collection].deleteMany() + } + + await mongoose.users.insertMany(users) + await mongoose.locations.insertMany(locations) + await mongoose.journeys.insertMany(journeys) + await mongoose.bills.insertMany(bills) }; seedDB() diff --git a/server/datalayer/mongo.js b/server/datalayer/mongo.js index 693ca84..a6138ce 100644 --- a/server/datalayer/mongo.js +++ b/server/datalayer/mongo.js @@ -4,6 +4,23 @@ class DataLayer { this.model = model; } + /** + * Find all records in the database. + */ + async findAllAndPopulate(filter, populateFilter) { + return this.model.find(JSON.parse(JSON.stringify(filter))) + .limit(filter.limit) + .skip(filter.offset * filter.limit) + .populate(JSON.parse(JSON.stringify(populateFilter))) + } + + /** + * Find a record by property in the database. + */ + async findByProperty(propertyToFind) { + return this.model.find(propertyToFind); + } + /** * Create and save the record to the database. */ @@ -12,10 +29,13 @@ class DataLayer { } /** - * Find a record by property in the database. + * Update and save the record to the database. */ - async findByProperty(propertyToFind) { - return this.model.find(propertyToFind); + async update(recordId, recordToUpdate) { + return this.model.findByIdAndUpdate(recordId, recordToUpdate) + .orFail(new Error("Bill can't be found in the database.")) + .catch(error => {throw new Error(error.message)}); + } } diff --git a/server/middleware/auth/authJwt.js b/server/middleware/auth/authJwt.js index 45f9f37..09faa78 100644 --- a/server/middleware/auth/authJwt.js +++ b/server/middleware/auth/authJwt.js @@ -1,7 +1,7 @@ const jwt = require("jsonwebtoken"); // Check if token is valid -checkJwtToken = (req, res, next) => { +const checkJwtToken = (req, res, next) => { if (!req.session || !req.session.token) { return res.status(401).send({ message: "Unauthorized: No token provided.", @@ -24,10 +24,10 @@ checkJwtToken = (req, res, next) => { }); }; -isOperator = (req, res, next) => { +const isOperator = (req, res, next) => { const type = req.type; - if (type != "Toll Operator") { + if (type !== "Toll Operator") { return res.status(403).send({ message: "Unauthorized: You not do have permission to view this page.", diff --git a/server/models/bill.js b/server/models/bill.js deleted file mode 100644 index 474f5ed..0000000 --- a/server/models/bill.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = mongoose => { - const billSchema = mongoose.Schema( - { - journey: { - type: mongoose.Schema.Types.ObjectId, - ref: "journey", - required: [true, "A journey must be attached to a bill."], - unique: true - }, - driver: { - type: String, - required: [true, "A driver must be assigned to a bill."] - }, - cost: { - type: Number, - required: [true, "A cost for the bill is required."] - } - }, - { - toJSON: {virtuals: true}, - toObject: {virtuals: true} - } - ) - - billSchema.virtual('billReferenceNumber', { - billReferenceNumber: this._id - }) - - return mongoose.model("bill", billSchema) -} \ No newline at end of file diff --git a/server/models/bill.model.js b/server/models/bill.model.js new file mode 100644 index 0000000..1413b84 --- /dev/null +++ b/server/models/bill.model.js @@ -0,0 +1,38 @@ +const Utilities = require("../utilities") +module.exports = mongoose => { + const billSchema = mongoose.Schema( + { + journey: { + type: mongoose.Schema.Types.ObjectId, + ref: "journey", + required: [true, "A journey must be attached to a bill."], + unique: true + }, + driver: { + type: mongoose.Schema.Types.ObjectId, + ref: "user", + required: [true, "A driver must be assigned to a bill."] + }, + cost: { + type: Number, + required: [true, "A cost for the bill is required."] + }, + paid: { + type: Boolean, + default: false + } + } + ) + + billSchema.pre('insertMany', async function (next, docs) { + for(const index in docs){ + const bill = docs[index] + const journey = await mongoose.model('journey').findById(bill.journey).populate({path: 'entryLocation exitLocation'}) + bill.cost = Utilities.calculateCost(journey) + await mongoose.model('bill').findByIdAndUpdate(bill._id, bill) + } + next() + }) + + return mongoose.model("bill", billSchema) +} \ No newline at end of file diff --git a/server/models/journey.js b/server/models/journey.model.js similarity index 70% rename from server/models/journey.js rename to server/models/journey.model.js index 481b3f1..d2a69b2 100644 --- a/server/models/journey.js +++ b/server/models/journey.model.js @@ -1,6 +1,4 @@ -const haversine = require('haversine-distance') - -module.exports = mongoose => { +module.exports = mongoose => { const journeySchema = mongoose.Schema( { regNumber: { @@ -23,9 +21,5 @@ module.exports = mongoose => { } ) - journeySchema.methods.getJourneyDistance = function() { - return haversine(this.entryLocation.coordinates, this.exitLocation.coordinates) - } - return mongoose.model("journey", journeySchema) } \ No newline at end of file diff --git a/server/models/location.js b/server/models/location.model.js similarity index 100% rename from server/models/location.js rename to server/models/location.model.js diff --git a/server/package-lock.json b/server/package-lock.json index f0524c0..470d780 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,7 +15,7 @@ "dotenv": "^15.0.0", "eslint": "^8.8.0", "express": "^4.17.2", - "haversine-distance": "^1.2.1", + "haversine": "^1.1.1", "http-errors": "^2.0.0", "jshint": "^2.13.4", "jsonwebtoken": "^8.5.1", @@ -2361,10 +2361,10 @@ "node": ">=8" } }, - "node_modules/haversine-distance": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/haversine-distance/-/haversine-distance-1.2.1.tgz", - "integrity": "sha512-rQpG89d6NlAis0eqOSFXDqNU/GZcMPlHNVMqTSzD16niD9s1fDK8T6kwrK0WJ7OMU+iRNy3cgGYnNQihMqmaHg==" + "node_modules/haversine": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/haversine/-/haversine-1.1.1.tgz", + "integrity": "sha512-KW4MS8+krLIeiw8bF5z532CptG0ZyGGFj0UbKMxx25lKnnJ1hMUbuzQl+PXQjNiDLnl1bOyz23U6hSK10r4guw==" }, "node_modules/he": { "version": "1.2.0", @@ -6793,10 +6793,10 @@ } } }, - "haversine-distance": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/haversine-distance/-/haversine-distance-1.2.1.tgz", - "integrity": "sha512-rQpG89d6NlAis0eqOSFXDqNU/GZcMPlHNVMqTSzD16niD9s1fDK8T6kwrK0WJ7OMU+iRNy3cgGYnNQihMqmaHg==" + "haversine": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/haversine/-/haversine-1.1.1.tgz", + "integrity": "sha512-KW4MS8+krLIeiw8bF5z532CptG0ZyGGFj0UbKMxx25lKnnJ1hMUbuzQl+PXQjNiDLnl1bOyz23U6hSK10r4guw==" }, "he": { "version": "1.2.0", diff --git a/server/package.json b/server/package.json index 8a85949..4dab44b 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,7 @@ { "scripts": { - "start": "set NODE_ENV=dev&& node ./bin/www", - "test": "set NODE_ENV=test&& node ./database/seed.js && nyc mocha --recursive --timeout 5000 --exit" + "start": "set NODE_ENV=dev&& node ./database/seed.js && node ./bin/www", + "test": "set NODE_ENV=test&& node ./database/seed.js && nyc --reporter lcov mocha --recursive --timeout 5000 --exit" }, "dependencies": { "bcryptjs": "^2.4.3", @@ -14,8 +14,8 @@ "dotenv": "^15.0.0", "eslint": "^8.8.0", "express": "^4.17.2", + "haversine": "^1.1.1", "http-errors": "^2.0.0", - "haversine-distance": "^1.2.1", "jshint": "^2.13.4", "jsonwebtoken": "^8.5.1", "mocha": "^9.2.0", diff --git a/server/routes/auth.routes.js b/server/routes/auth.routes.js index a664b36..129ccfd 100644 --- a/server/routes/auth.routes.js +++ b/server/routes/auth.routes.js @@ -1,8 +1,8 @@ -var express = require("express"); -var router = express.Router(); +const express = require("express"); +const router = express.Router(); // Get the Auth controller -var authController = require("../controllers/auth.controller"); +const authController = require("../controllers/auth.controller"); // Log the user in router.post("/login/", authController.login); diff --git a/server/routes/bill.routes.js b/server/routes/bill.routes.js new file mode 100644 index 0000000..46e3986 --- /dev/null +++ b/server/routes/bill.routes.js @@ -0,0 +1,13 @@ +const express = require("express"); +const router = express.Router(); + +// Get the Bill controller +const billController = require("../controllers/bill.controller"); + +// Get All Bills +router.get("/", billController.getAllBills); + +// Pay for bill +router.put("/:id", billController.payBill); + +module.exports = router; diff --git a/server/test/integration/auth.controller.test.js b/server/test/integration/auth.controller.test.js index 1369ef4..dc87a1b 100644 --- a/server/test/integration/auth.controller.test.js +++ b/server/test/integration/auth.controller.test.js @@ -33,7 +33,7 @@ describe("Testing /auth paths", () => { it("user should be able to login", (done) => { // Arrange const request = { - email: "test@test.com", + email: "test@email.com", password: "test1", }; @@ -48,7 +48,7 @@ describe("Testing /auth paths", () => { res.body.should.have.property("message"); res.body.message.should.be.eql("Successfully logged in."); res.should.have.cookie("highwayTracker-token"); - + done(); }); }); @@ -56,7 +56,7 @@ describe("Testing /auth paths", () => { it("user shouldn't be able to login with invalid credentials", (done) => { // Act const request = { - email: "test@test.com", + email: "test@email.com", password: "test2", }; diff --git a/server/test/integration/bill.controller.test.js b/server/test/integration/bill.controller.test.js new file mode 100644 index 0000000..10e1765 --- /dev/null +++ b/server/test/integration/bill.controller.test.js @@ -0,0 +1,199 @@ +let chai = require("chai"); +let chaiHttp = require("chai-http"); +let server = require("../../app"); +let should = chai.should(); +chai.use(chaiHttp); + +describe("Testing /bill paths", () => { + it("Should get all bills", (done) => { + // Arrange + const url = "/bill/" + + // Act + chai.request(server) + .get(url) + .send() + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.should.have.lengthOf(2); + res.body[0].should.haveOwnProperty('cost', 72.93887106726764) + res.body[0].should.haveOwnProperty('paid', false) + res.body[0].driver.should.haveOwnProperty('username', 'test_username') + res.body[0].driver.should.haveOwnProperty('email', 'test@email.com') + res.body[0].driver.should.haveOwnProperty('type', 'Driver') + res.body[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') + res.body[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') + res.body[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) + res.body[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) + + done(); + }) + }) + + it("Should get all bills which match the driver ID", (done) => { + // Arrange + const driverId = "123456789107" + const url = `/bill?driver=${driverId}` + + // Act + chai.request(server) + .get(url) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.should.have.lengthOf(1); + res.body[0].should.haveOwnProperty('cost', 72.93887106726764) + res.body[0].should.haveOwnProperty('paid', false) + res.body[0].driver.should.haveOwnProperty('username', 'test_username') + res.body[0].driver.should.haveOwnProperty('email', 'test@email.com') + res.body[0].driver.should.haveOwnProperty('type', 'Driver') + res.body[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') + res.body[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') + res.body[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) + res.body[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) + + done(); + }) + }) + + it("Should get all bills which match the paid", (done) => { + // Arrange + const paid = true + const url = `/bill?paid=${paid}` + + // Act + chai.request(server) + .get(url) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.should.have.lengthOf(1); + res.body[0].should.haveOwnProperty('cost', 72.93887106726764) + res.body[0].driver.should.haveOwnProperty('username', 'test_username2') + res.body[0].driver.should.haveOwnProperty('email', 'test2@email.com') + res.body[0].driver.should.haveOwnProperty('type', 'Driver') + res.body[0].should.haveOwnProperty('paid', true) + res.body[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number2') + res.body[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.038Z') + res.body[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) + res.body[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) + + done(); + }) + }) + + it("Should get one bill when pagination limit is one", (done) => { + // Arrange + const limit = 1 + const url = `/bill?limit=${limit}` + + // Act + chai.request(server) + .get(url) + .send() + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.should.have.lengthOf(1); + res.body[0].should.haveOwnProperty('cost', 72.93887106726764) + res.body[0].driver.should.haveOwnProperty('username', 'test_username') + res.body[0].driver.should.haveOwnProperty('email', 'test@email.com') + res.body[0].driver.should.haveOwnProperty('type', 'Driver') + res.body[0].should.haveOwnProperty('paid', false) + res.body[0].journey.should.haveOwnProperty('regNumber', 'test_reg_number') + res.body[0].journey.should.haveOwnProperty('journeyDateTime', '2022-02-01T15:50:51.039Z') + res.body[0].journey.entryLocation.should.haveOwnProperty('name', 'test_location_1') + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('longitude', 50) + res.body[0].journey.entryLocation.coordinates.should.haveOwnProperty('latitude', 50) + res.body[0].journey.exitLocation.should.haveOwnProperty('name', 'test_location_2') + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('longitude', 0) + res.body[0].journey.exitLocation.coordinates.should.haveOwnProperty('latitude', 0) + + done(); + }) + }) + + it("Should get no bills when pagination offset is one", (done) => { + // Arrange + const offset = 1 + const url = `/bill?offset=${offset}` + + // Act + chai.request(server) + .get(url) + .send() + .end((err, res) => { + // Assert + + res.should.have.status(200); + res.should.be.a("object"); + res.body.should.have.lengthOf(0); + + done(); + }) + }) + + it("Should update paid to true", (done) => { + // Arrange + const requestBody = { + paid: true + } + const url = `/bill/123456789105` + + // Act + chai.request(server) + .put(url) + .send(requestBody) + .end((err, res) => { + // Assert + res.should.have.status(200); + res.should.be.a("object"); + res.body.message.should.be.eql("Bill paid."); + + done(); + }) + }) + + it("Should throw error if bill doesnt exist", (done) => { + // Arrange + const fakeId = '111111111111' + const requestBody = { + paid: true + } + const url = `/bill/${fakeId}` + + // Act + chai.request(server) + .put(url) + .send(requestBody) + .end((err, res) => { + // Assert + res.should.have.status(404); + res.body.message.should.be.eql("Bill can't be found in the database."); + + done(); + }) + }) + +}) \ No newline at end of file diff --git a/server/test/unit/utilities.test.js b/server/test/unit/utilities.test.js new file mode 100644 index 0000000..d994c08 --- /dev/null +++ b/server/test/unit/utilities.test.js @@ -0,0 +1,34 @@ +let chai = require("chai"); +let should = chai.should(); +const Utilities = require("../../utilities") + +describe("Testing utilities functions", () => { + it("Calculate cost based on distance travelled", (done) => { + // Arrange + const location1 = { + name: "test_location1", + coordinates: { + longitude: 0, + latitude: 0 + } + } + const location2 = { + name: "test_location2", + coordinates: { + longitude: 50, + latitude: 50 + } + } + const journey = { + entryLocation: location1, + exitLocation: location2 + } + + // Act + const cost = Utilities.calculateCost(journey) + + // Assert + cost.should.equal(72.93887106726764) + done(); + }) +}) \ No newline at end of file diff --git a/server/utilities.js b/server/utilities.js new file mode 100644 index 0000000..2f1a78f --- /dev/null +++ b/server/utilities.js @@ -0,0 +1,9 @@ +const haversine = require("haversine") + +module.exports = class Utilities { + static calculateCost(journey) { + return (haversine(journey.entryLocation.coordinates, journey.exitLocation.coordinates) * this.costPerMile) + } + + static costPerMile = 0.01 +} \ No newline at end of file