From 7f683ec669da488fae09c14ad3ecba5915ad146a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Gaczy=C5=84ski?= <30621967+krygacz@users.noreply.github.com> Date: Mon, 7 Mar 2022 14:52:23 +0100 Subject: [PATCH] Object endpoints (#33) * Add cross-env for seamless Windows support * Initial object endpoints * Add mongodb memory server for easier testing * Rename Object to Offer * Add flat package * bug fixes, flatten body before updating, better error formatting * surname -> lastName to pass tests * update roleCheck for easier usage * Add authorization * Add @types/jest package for jest suggestions in vscode * Improve error messages * Add tests * fix casing to pass github actions tests * fix auth * reenable tests * cleanup * apply suggestions --- .env.template | 1 - package-lock.json | 81 ++++-- package.json | 7 +- src/app.js | 4 +- src/auth/auth.router.js | 8 +- src/auth/auth.test.js | 6 +- src/auth/passport.js | 16 +- src/helpers/dbConnection.js | 26 +- src/helpers/productionDbConnection.js | 16 ++ src/helpers/validators.js | 33 +++ src/middlewares/roleCheck.js | 4 +- src/offer/offer.controller.js | 87 +++++++ .../object.model.js => offer/offer.model.js} | 9 +- src/offer/offer.router.js | 40 +++ src/offer/offer.test.js | 236 ++++++++++++++++++ src/server.js | 5 +- src/user/user.router.js | 14 +- 17 files changed, 527 insertions(+), 66 deletions(-) create mode 100644 src/helpers/productionDbConnection.js create mode 100644 src/offer/offer.controller.js rename src/{object/object.model.js => offer/offer.model.js} (81%) create mode 100644 src/offer/offer.router.js create mode 100644 src/offer/offer.test.js diff --git a/.env.template b/.env.template index d7078fd..cc05eb1 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,4 @@ MONGO_URL = -MONGO_TEST_URL = PORT = JWT_SECRET = JWT_EXP = diff --git a/package-lock.json b/package-lock.json index a31d42a..0db8628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "bcrypt": "^5.0.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.0.0", "express": "^4.17.3", "express-validator": "^6.14.0", + "flat": "^5.0.2", "jsonwebtoken": "^8.5.1", "mongoose": "^6.2.3", "morgan": "^1.10.0", @@ -24,6 +26,7 @@ "@babel/core": "^7.17.5", "@babel/eslint-parser": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@types/jest": "^27.4.1", "eslint": "^8.9.0", "eslint-config-prettier": "^8.4.0", "eslint-plugin-jest": "^26.1.1", @@ -2636,6 +2639,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "27.4.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", + "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", + "dev": true, + "dependencies": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -4130,11 +4143,27 @@ "node": ">= 0.10" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5391,6 +5420,14 @@ "node": ">=8" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -6206,8 +6243,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -9467,7 +9503,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -10176,7 +10211,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -10188,7 +10222,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -11275,7 +11308,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -13489,6 +13521,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "27.4.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", + "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", + "dev": true, + "requires": { + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -14615,11 +14657,18 @@ "vary": "^1" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -15557,6 +15606,11 @@ "path-exists": "^4.0.0" } }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -16143,8 +16197,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -18594,8 +18647,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { "version": "1.0.7", @@ -19134,7 +19186,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -19142,8 +19193,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { "version": "1.0.4", @@ -19953,7 +20003,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index 97576ca..4623d6f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "type": "module", "scripts": { "start": "node .", - "dev": "NODE_ENV=development nodemon .", - "test": "NODE_ENV=test jest", + "dev": "cross-env NODE_ENV=development nodemon .", + "test": "cross-env NODE_ENV=test jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "postinstall": "husky install", @@ -28,9 +28,11 @@ "dependencies": { "bcrypt": "^5.0.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "dotenv": "^16.0.0", "express": "^4.17.3", "express-validator": "^6.14.0", + "flat": "^5.0.2", "jsonwebtoken": "^8.5.1", "mongoose": "^6.2.3", "morgan": "^1.10.0", @@ -41,6 +43,7 @@ "@babel/core": "^7.17.5", "@babel/eslint-parser": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@types/jest": "^27.4.1", "eslint": "^8.9.0", "eslint-config-prettier": "^8.4.0", "eslint-plugin-jest": "^26.1.1", diff --git a/src/app.js b/src/app.js index 1950fad..e010d34 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ import 'dotenv/config'; import cors from 'cors'; import morgan from 'morgan'; import { StartRouter } from './routes/start.js'; +import { OfferRouter } from './offer/offer.router.js'; import passport from 'passport'; import { AuthRouter } from './auth/auth.router.js'; import { JwtConfig } from './auth/passport.js'; @@ -10,7 +11,7 @@ import { UserRouter } from './user/user.router.js'; export const app = express(); app.use(passport.initialize()); -JwtConfig(passport); +JwtConfig(); app.use(cors()); app.use(morgan('tiny')); app.use(express.json()); @@ -18,4 +19,5 @@ app.use(express.urlencoded({ extended: true })); app.use('/', StartRouter); app.use('/auth', AuthRouter); +app.use('/offer', OfferRouter); app.use('/user', UserRouter); diff --git a/src/auth/auth.router.js b/src/auth/auth.router.js index 27323c1..07bf21f 100644 --- a/src/auth/auth.router.js +++ b/src/auth/auth.router.js @@ -2,7 +2,7 @@ import { Router } from 'express'; import { signin, signup, protectedController } from './auth.controller.js'; import { verifyFieldsErrors } from '../middlewares/validation.middleware.js'; import { registerValidator, loginValidator } from '../helpers/validators.js'; -import passport from 'passport'; +import { requireAuth } from './passport.js'; export const AuthRouter = Router(); @@ -10,8 +10,4 @@ AuthRouter.post('/register', [registerValidator, verifyFieldsErrors], signup); AuthRouter.post('/login', [loginValidator, verifyFieldsErrors], signin); -AuthRouter.get( - '/protected', - passport.authenticate('jwt', { session: false }), - protectedController, -); +AuthRouter.get('/protected', requireAuth, protectedController); diff --git a/src/auth/auth.test.js b/src/auth/auth.test.js index b717c13..3aec6f4 100644 --- a/src/auth/auth.test.js +++ b/src/auth/auth.test.js @@ -1,6 +1,6 @@ import request from 'supertest'; import { app } from '../app.js'; -import { connect, disconnect } from '../helpers/testDbConnection.js'; +import dbConnection from '../helpers/dbConnection.js'; const userBody = { email: `example@test.pl`, @@ -22,10 +22,10 @@ let token; describe('auth endpoints', () => { beforeAll(async () => { - await connect(); + await dbConnection.connect(); }); afterAll(async () => { - await disconnect(); + await dbConnection.disconnect(); }); it('should allow a POST to /auth/register with correct data', async () => { diff --git a/src/auth/passport.js b/src/auth/passport.js index 6a207f1..dcde9f1 100644 --- a/src/auth/passport.js +++ b/src/auth/passport.js @@ -1,7 +1,8 @@ import { Strategy, ExtractJwt } from 'passport-jwt'; +import passport from 'passport'; import { User } from '../user/user.model.js'; -export const JwtConfig = (passport) => { +export const JwtConfig = () => { const options = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET, @@ -10,7 +11,7 @@ export const JwtConfig = (passport) => { const JwtStrategy = () => new Strategy(options, async (payload, done) => { try { - const user = await User.findOne({ id: payload.id }); + const user = await User.findOne({ _id: payload.id }); if (user) { return done(null, user); } else { @@ -23,3 +24,14 @@ export const JwtConfig = (passport) => { passport.use(JwtStrategy()); }; + +export const requireAuth = (req, res, next) => { + passport.authenticate('jwt', { session: false }, function (err, user, info) { + if (err || !user?.role) { + return res.status(401).json({ message: 'Unauthorized', errors: [info] }); + } else { + req.user = user; + return next(); + } + })(req, res, next); +}; diff --git a/src/helpers/dbConnection.js b/src/helpers/dbConnection.js index 2bcb6b3..a5e65ba 100644 --- a/src/helpers/dbConnection.js +++ b/src/helpers/dbConnection.js @@ -1,21 +1,9 @@ -import mongoose from 'mongoose'; +import * as productionDbConnection from './productionDbConnection.js'; +import * as testDbConnection from './testDbConnection.js'; -const dbUri = - process.env.NODE_ENV === 'test' - ? process.env.MONGO_TEST_URL - : process.env.MONGO_URL; +const dbConnection = + process.env.NODE_ENV === 'production' + ? productionDbConnection + : testDbConnection; -export const dbConnection = async () => { - try { - await mongoose.connect(dbUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - } catch (error) { - console.log(error.message); - } -}; - -export const closeConnection = () => { - return mongoose.disconnect(); -}; +export default dbConnection; diff --git a/src/helpers/productionDbConnection.js b/src/helpers/productionDbConnection.js new file mode 100644 index 0000000..cd5891b --- /dev/null +++ b/src/helpers/productionDbConnection.js @@ -0,0 +1,16 @@ +import mongoose from 'mongoose'; + +export const connect = async () => { + try { + await mongoose.connect(process.env.MONGO_URL, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + } catch (error) { + console.log(error.message); + } +}; + +export const disconnect = () => { + return mongoose.disconnect(); +}; diff --git a/src/helpers/validators.js b/src/helpers/validators.js index 5cf5fab..527ae34 100644 --- a/src/helpers/validators.js +++ b/src/helpers/validators.js @@ -61,3 +61,36 @@ export const loginValidator = [ .isLength({ min: 8 }) .isString(), ]; + +export const offerUpdateValidator = [ + body('title') + .optional() + .isLength({ min: 6, max: 255 }) + .withMessage('Invalid title length'), + body('accomodationType').optional().isString(), + body('description').optional().isString(), + body('localisation.address') + .optional() + .isString() + .isLength({ min: 2, max: 255 }), + body('localisation.latitude').optional().isNumeric(), + body('localisation.longitude').optional().isNumeric(), + body('amenities').optional().isArray(), + body('price').optional().isNumeric(), + body('oldPrice').optional().isNumeric(), + body('image').optional().isURL().withMessage('Invalid image URL format'), + body('images').optional().isArray(), + body('images.*').isURL().withMessage('Invalid image URL format'), +]; + +export const offerCreateValidator = [ + body('title').notEmpty().withMessage('Title is required'), + body('localisation.address').notEmpty().withMessage('Address is required'), + body('localisation.latitude').notEmpty().withMessage('Latitude is required'), + body('localisation.longitude') + .notEmpty() + .withMessage('Longitude is required'), + body('price').notEmpty().withMessage('Price is required'), + body('image').notEmpty().withMessage('Image URL is required'), + [...offerUpdateValidator], +]; diff --git a/src/middlewares/roleCheck.js b/src/middlewares/roleCheck.js index fe2646d..7a68d0e 100644 --- a/src/middlewares/roleCheck.js +++ b/src/middlewares/roleCheck.js @@ -1,6 +1,6 @@ -export const roleCheck = (req, res, next, roles) => { +export const roleCheck = (roles) => (req, res, next) => { if (!roles.includes(req.user.role)) - return res.status(401).send('Access denied.'); + return res.status(403).json({ message: 'Access denied.', errors: [] }); return next(); }; diff --git a/src/offer/offer.controller.js b/src/offer/offer.controller.js new file mode 100644 index 0000000..d6f8fea --- /dev/null +++ b/src/offer/offer.controller.js @@ -0,0 +1,87 @@ +import { Offer } from './offer.model.js'; +import flatten from 'flat'; +import { USER_ROLE } from '../constants.js'; + +export const getMany = async (req, res) => { + const offers = await Offer.find({}); + res.status(200).json({ data: offers }); +}; + +export const getOne = async (req, res) => { + if (!req.params.id) + return res + .status(400) + .json({ message: 'Bad request: no ID param', errors: [] }); + try { + const offer = await Offer.findById(req.params.id); + res.status(200).json({ data: offer }); + } catch (error) { + res.status(400).json({ + message: 'Could not find offer with the specified ID', + errors: [error], + }); + } +}; + +export const updateOne = async (req, res) => { + if (!req.params.id) + return res + .status(400) + .json({ message: 'Bad request: no ID param', errors: [] }); + + const offer = await Offer.findById(req.params.id); + + if (!offer.host.equals(req.user._id) && req.user.role !== USER_ROLE.ADMIN) + return res.status(403).json({ message: 'Access denied', errors: [] }); + + try { + const flattenedBody = flatten(req.body); + const updatedOffer = await Offer.findByIdAndUpdate( + req.params.id, + flattenedBody, + { returnDocument: 'after' }, + ); + res.status(200).json({ data: updatedOffer }); + } catch (error) { + res.status(400).json({ + message: 'Could not update offer with the specified ID', + errors: [error], + }); + } +}; + +export const removeOne = async (req, res) => { + if (!req.params.id) + return res + .status(400) + .json({ message: 'Bad request: no ID param', errors: [] }); + + const offer = await Offer.findById(req.params.id); + + if (!offer.host.equals(req.user._id) && req.user.role !== USER_ROLE.ADMIN) + return res.status(403).json({ message: 'Access denied', errors: [] }); + + try { + const removedOffer = await Offer.findByIdAndRemove(req.params.id); + res.status(200).json({ data: removedOffer }); + } catch (error) { + res.status(400).json({ + message: 'Could not remove offer with the specified ID', + errors: [error], + }); + } +}; + +export const createOne = async (req, res) => { + try { + const createdOffer = await Offer.create({ + ...req.body, + host: req.user._id, + }); + res.status(200).json({ data: createdOffer }); + } catch (error) { + res + .status(400) + .json({ message: 'Could not create offer', errors: [error] }); + } +}; diff --git a/src/object/object.model.js b/src/offer/offer.model.js similarity index 81% rename from src/object/object.model.js rename to src/offer/offer.model.js index 6d1b4ba..425235a 100644 --- a/src/object/object.model.js +++ b/src/offer/offer.model.js @@ -1,7 +1,12 @@ import mongoose from 'mongoose'; -const objectSchema = new mongoose.Schema( +const offerSchema = new mongoose.Schema( { + host: { + type: mongoose.SchemaTypes.ObjectId, + ref: 'user', + required: true, + }, title: { type: String, required: true, @@ -53,4 +58,4 @@ const objectSchema = new mongoose.Schema( { timestamps: true }, ); -export const Object = new mongoose.model('object', objectSchema); +export const Offer = new mongoose.model('offer', offerSchema); diff --git a/src/offer/offer.router.js b/src/offer/offer.router.js new file mode 100644 index 0000000..4ecd3a1 --- /dev/null +++ b/src/offer/offer.router.js @@ -0,0 +1,40 @@ +import { Router } from 'express'; +import { requireAuth } from '../auth/passport.js'; +import { roleCheck } from '../middlewares/roleCheck.js'; +import { USER_ROLE } from '../constants.js'; +import { + offerCreateValidator, + offerUpdateValidator, +} from '../helpers/validators.js'; +import { verifyFieldsErrors } from '../middlewares/validation.middleware.js'; +import { + getMany, + getOne, + updateOne, + removeOne, + createOne, +} from './offer.controller.js'; +export const OfferRouter = Router(); + +OfferRouter.get('/', getMany); +OfferRouter.post( + '/', + requireAuth, + roleCheck([USER_ROLE.ADMIN, USER_ROLE.HOST]), + [offerCreateValidator, verifyFieldsErrors], + createOne, +); +OfferRouter.get('/:id', getOne); +OfferRouter.patch( + '/:id', + requireAuth, + roleCheck([USER_ROLE.ADMIN, USER_ROLE.HOST]), + [offerUpdateValidator, verifyFieldsErrors], + updateOne, +); +OfferRouter.delete( + '/:id', + requireAuth, + roleCheck([USER_ROLE.ADMIN, USER_ROLE.HOST]), + removeOne, +); diff --git a/src/offer/offer.test.js b/src/offer/offer.test.js new file mode 100644 index 0000000..a7799f3 --- /dev/null +++ b/src/offer/offer.test.js @@ -0,0 +1,236 @@ +import request from 'supertest'; +import { app } from '../app.js'; +import { USER_ROLE } from '../constants.js'; +import dbConnection from '../helpers/dbConnection.js'; + +const offerBody = { + title: 'Super oferta', + localisation: { + address: 'ul. Hdueudg', + latitude: 2137, + longitude: 420, + }, + price: 1.23, + image: 'https://freepngimg.com/thumb/cat/22211-5-red-cat.png', +}; + +const incorrectOfferBody = { + title: 'Super oferta', + localisation: { + latitude: 2137, + longitude: 420, + }, + image: 'https//freepngimg.com/thumb/cat/22211-5-red-cat.png', +}; + +const userBody = { + email: `user@test.pl`, + password: 's3cur3@pass', + name: 'user', + lastName: 'userowski', + dob: '1994-05-23', + role: USER_ROLE.USER, +}; + +const host1Body = { + email: `host1@test.pl`, + password: 's3cur3@pass', + name: 'host1', + lastName: 'pierwszy', + dob: '1994-05-23', + role: USER_ROLE.HOST, +}; + +const host2Body = { + email: `host2@test.pl`, + password: 's3cur3@pass', + name: 'host2', + lastName: 'drugi', + dob: '1994-05-23', + role: USER_ROLE.HOST, +}; + +const adminBody = { + email: `admin@test.pl`, + password: 's3cur3@pass', + name: 'admin', + lastName: 'adminowski', + dob: '1994-05-23', + role: USER_ROLE.ADMIN, +}; + +let userToken, host1Token, host2Token, adminToken, offerID; + +const registerUsers = async () => { + host1Token = (await request(app).post('/auth/register').send(host1Body)).body + .token; + host2Token = (await request(app).post('/auth/register').send(host2Body)).body + .token; + userToken = (await request(app).post('/auth/register').send(userBody)).body + .token; + adminToken = (await request(app).post('/auth/register').send(adminBody)).body + .token; +}; + +describe('offer endpoints', () => { + beforeAll(async () => { + await dbConnection.connect(); + await registerUsers(); + }); + + afterAll(async () => { + await dbConnection.disconnect(); + }); + + it('should allow a POST to /offer when authorized', async () => { + const res = await request(app) + .post('/offer') + .set('Authorization', host1Token) + .send(offerBody); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data).toMatchObject(offerBody); + offerID = res.body.data._id; + }); + + it('should not allow a POST to /offer when unauthorized (no token)', async () => { + const res = await request(app).post('/offer').send(offerBody); + expect(res.status).toBe(401); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a POST to /offer when unauthorized (insufficient role)', async () => { + const res = await request(app) + .post('/offer') + .set('Authorization', userToken) + .send(offerBody); + expect(res.status).toBe(403); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a POST to /offer with incorrect payload', async () => { + const res = await request(app) + .post('/offer') + .set('Authorization', host1Token) + .send(incorrectOfferBody); + expect(res.status).toBe(400); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should allow a PATCH to /offer/:id when authorized (host - owner)', async () => { + const res = await request(app) + .patch(`/offer/${offerID}`) + .set('Authorization', host1Token) + .send({ title: 'testPatchTitle' }); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data).toMatchObject({ + ...offerBody, + title: 'testPatchTitle', + }); + }); + + it('should allow a PATCH to /offer/:id when authorized (admin)', async () => { + const res = await request(app) + .patch(`/offer/${offerID}`) + .set('Authorization', adminToken) + .send(offerBody); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data).toMatchObject(offerBody); + }); + + it('should not allow a PATCH to /offer/:id when unauthorized (no token)', async () => { + const res = await request(app).patch(`/offer/${offerID}`).send(offerBody); + expect(res.status).toBe(401); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a PATCH to /offer/:id when unauthorized (different host)', async () => { + const res = await request(app) + .patch(`/offer/${offerID}`) + .set('Authorization', host2Token) + .send(offerBody); + expect(res.status).toBe(403); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a PATCH to /offer/:id when unauthorized (different user)', async () => { + const res = await request(app) + .patch(`/offer/${offerID}`) + .set('Authorization', userToken) + .send(offerBody); + expect(res.status).toBe(403); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a PATCH to /offer/:id with incorrect payload', async () => { + const res = await request(app) + .patch(`/offer/${offerID}`) + .set('Authorization', host1Token) + .send(incorrectOfferBody); + expect(res.status).toBe(400); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should allow a GET to /offer', async () => { + const res = await request(app).get('/offer'); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data[0]).toMatchObject(offerBody); + }); + + it('should allow a GET to /offer/:id', async () => { + const res = await request(app).get(`/offer/${offerID}`); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data).toMatchObject(offerBody); + }); + + it('should not allow a DELETE to /offer when unauthorized', async () => { + const res = await request(app).delete(`/offer/${offerID}`); + expect(res.status).toBe(401); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a DELETE to /offer when unauthorized (different host)', async () => { + const res = await request(app) + .delete(`/offer/${offerID}`) + .set('Authorization', host2Token); + expect(res.status).toBe(403); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should not allow a DELETE to /offer when unauthorized (different user)', async () => { + const res = await request(app) + .delete(`/offer/${offerID}`) + .set('Authorization', userToken); + expect(res.status).toBe(403); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.errors).toBe('object'); + }); + + it('should allow a DELETE to /offer when authorized', async () => { + const res = await request(app) + .delete(`/offer/${offerID}`) + .set('Authorization', host1Token); + expect(res.status).toBe(200); + expect(typeof res.body).toBe('object'); + expect(typeof res.body.data).toBe('object'); + expect(res.body.data).toMatchObject(offerBody); + }); +}); diff --git a/src/server.js b/src/server.js index b185134..f822795 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,8 @@ import { app } from './app.js'; -import { dbConnection } from './helpers/dbConnection.js'; +import dbConnection from './helpers/dbConnection.js'; -dbConnection() +dbConnection + .connect() .then(() => app.listen(process.env.PORT, () => { console.log('Server is listing on port', process.env.PORT); diff --git a/src/user/user.router.js b/src/user/user.router.js index af8ee02..cf81426 100644 --- a/src/user/user.router.js +++ b/src/user/user.router.js @@ -1,22 +1,16 @@ import { Router } from 'express'; -import passport from 'passport'; +import { requireAuth } from '../auth/passport.js'; import { USER_ROLE } from '../constants.js'; import { roleCheck } from '../middlewares/roleCheck.js'; import { deleteMe, deleteUser } from './user.controller.js'; export const UserRouter = Router(); -UserRouter.delete( - '/me', - passport.authenticate('jwt', { session: false }), - deleteMe, -); +UserRouter.delete('/me', requireAuth, deleteMe); UserRouter.delete( '/:id', - passport.authenticate('jwt', { session: false }), - (req, res, next) => { - roleCheck(req, res, next, [USER_ROLE.ADMIN]); - }, + requireAuth, + roleCheck([USER_ROLE.ADMIN]), deleteUser, );