├── .gitignore
├── Procfile
├── README.md
├── access.log
├── emails
├── confirm_email.ejs
└── reset_password.ejs
├── env_requirement
├── jest.config.js
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── src
├── app.js
├── constants
│ └── constant.js
├── controllers
│ ├── auth
│ │ ├── auth.js
│ │ └── auth_utils.js
│ ├── category
│ │ └── category.js
│ ├── item
│ │ └── item.js
│ └── utils.js
├── db
│ └── mongoose_connect.js
├── index.js
├── middlewares
│ ├── accept_language.js
│ ├── async.js
│ ├── check_permissions.js
│ ├── cors_header.js
│ ├── is_auth.js
│ ├── is_intl.js
│ ├── log_errors.js
│ ├── middlewares.js
│ ├── param_validator.js
│ ├── requests_limiter.js
│ └── uploader
│ │ ├── image_uploader.js
│ │ └── multer_storage_imgur.js
├── models
│ ├── category.js
│ ├── image.js
│ ├── item.js
│ └── user.js
├── routes
│ ├── auth.js
│ ├── category.js
│ ├── helper
│ │ ├── 404.js
│ │ └── error_handler.js
│ ├── item.js
│ └── routes.js
├── services
│ └── emails
│ │ ├── email_sender.js
│ │ └── emails.js
└── utils
│ ├── error_thrower.js
│ ├── image_util.js
│ ├── is_mongo_id.js
│ ├── json_success_fail.js
│ ├── jwt_promise.js
│ └── param_validation.js
└── test
├── fixtures
└── keys_bg.png
├── integration_test
├── 404.test.js
├── auth
│ ├── login.test.js
│ ├── profile.test.js
│ ├── refreshFcmToken.test.js
│ ├── register.test.js
│ ├── resetPassword.test.js
│ ├── updateProfile.test.js
│ └── verifyEmail.test.js
├── category
│ ├── addCategory.test.js
│ ├── addCategoryUploadImage.test.js
│ ├── addItemToCategory.test.js
│ └── getCategories.test.js
└── item
│ ├── addItem.test.js
│ └── getItems.test.js
└── uitils
└── tokenUtils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Node ###
2 |
3 | #jest test coverage
4 | coverage/
5 | # Logs
6 | logs
7 | npm-debug.log*
8 |
9 | # Optional npm cache directory
10 | .npm
11 |
12 | # Dependency directories
13 | node_modules/
14 | /node_modules
15 | /jspm_packages
16 | /bower_components
17 |
18 |
19 | # Optional eslint cache
20 | .eslintcache
21 |
22 |
23 | #Build generated
24 | dist/
25 | build/
26 | public/bundle.js
27 | public/bundle.js.map
28 |
29 | # Serverless generated files
30 | .serverless/
31 |
32 | ### VisualStudioCode ###
33 | .vscode/*
34 | !.vscode/settings.json
35 | !.vscode/tasks.json
36 | !.vscode/launch.json
37 | !.vscode/extensions.json
38 |
39 | ### Vim ###
40 | *.sw[a-p]
41 |
42 | ### WebStorm/IntelliJ ###
43 | /.idea
44 | .idea/
45 | modules.xml
46 | *.ipr
47 |
48 |
49 | ### System Files ###
50 | *.DS_Store
51 |
52 | # Windows thumbnail cache files
53 | Thumbs.db
54 | ehthumbs.db
55 | ehthumbs_vista.db
56 |
57 | # Folder config file
58 | Desktop.ini
59 |
60 | # Recycle Bin used on file shares
61 | $RECYCLE.BIN/
62 |
63 | # Thumbnails
64 | ._*
65 |
66 | # Files that might appear in the root of a volume
67 | .DocumentRevisions-V100
68 | .fseventsd
69 | .Spotlight-V100
70 | .TemporaryItems
71 | .Trashes
72 | .VolumeIcon.icns
73 | .com.apple.timemachine.donotpresent
74 |
75 | #istanbul reports
76 | .nyc_output/
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node src/index.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # e-commerce-backend-nodejs (under development)
2 | ### After I end the MVP, I will write a good doc so anyone will have the ability to use at or contributor to it, after that, I will make a mobile app to use this backend
3 |
--------------------------------------------------------------------------------
/access.log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/access.log
--------------------------------------------------------------------------------
/emails/confirm_email.ejs:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 | E-Commerce
9 |
38 |
39 |
40 |
54 |
64 |
65 |
--------------------------------------------------------------------------------
/emails/reset_password.ejs:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 | E-Commerce
9 |
38 |
39 |
40 |
54 |
64 |
65 |
--------------------------------------------------------------------------------
/env_requirement:
--------------------------------------------------------------------------------
1 | PORT
2 | MONGO_URL
3 | PRIVATE_SERVER_KEY
4 | IMGUR_CLIENT_ID
5 | EMAIL
6 | EMAIL_PASSWORD
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node'
3 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e-commerce-backend-nodejs",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "homepage": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs/blob/master/README.md",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/AbdOoSaed/e-commerce-backend-nodejs/issues"
13 | },
14 | "scripts": {
15 | "start:pm2": "pm2 kill & pm2 start -i max src/index.js",
16 | "stop:pm2": "pm2 kill",
17 | "start": "node src/index.js",
18 | "start:dev": "nodemon src/index.js",
19 | "test": "jest --runInBand --verbose --forceExit",
20 | "test:watch": "jest --watchAll --runInBand --verbose --forceExit",
21 | "test:coverage": "jest --runInBand --coverage --forceExit"
22 | },
23 | "keywords": [
24 | "JS",
25 | "NodeJs",
26 | "backend",
27 | "e-commerce"
28 | ],
29 | "author": "",
30 | "license": "ISC",
31 | "dependencies": {
32 | "@types/express-status-monitor": "^1.2.3",
33 | "accesscontrol": "^2.2.1",
34 | "bcrypt": "^5.0.1",
35 | "blurhash": "^1.1.3",
36 | "compression": "^1.7.4",
37 | "concat-stream": "^2.0.0",
38 | "ejs": "^3.1.6",
39 | "express": "^4.17.1",
40 | "express-mongo-sanitize": "^2.1.0",
41 | "express-rate-limit": "^5.2.6",
42 | "express-status-monitor": "^1.3.3",
43 | "express-validator": "^6.12.0",
44 | "helmet": "^4.6.0",
45 | "http-auth": "^4.1.5",
46 | "http-auth-connect": "^1.0.4",
47 | "http-status-codes": "^2.1.4",
48 | "imgur": "^0.3.2",
49 | "jsonwebtoken": "^8.5.1",
50 | "lodash": "^4.17.21",
51 | "mongoose": "^5.12.11",
52 | "mongoose-intl": "^3.2.0",
53 | "morgan": "^1.10.0",
54 | "multer": "^1.4.2",
55 | "nodemailer": "^6.6.0",
56 | "request-ip": "^2.1.3",
57 | "sharp": "^0.28.2",
58 | "xss-clean": "^0.1.1"
59 | },
60 | "devDependencies": {
61 | "@types/node": "^15.6.0",
62 | "jest": "^27.0.4",
63 | "supertest": "^6.1.3"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/public/favicon.ico
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | const errorHandler = require("./routes/helper/error_handler");
2 | const express = require("express");
3 | const routers = require("./routes/routes");
4 |
5 | const app = express();
6 | require("./middlewares/middlewares")(app);
7 |
8 | app.use(routers);
9 | app.use(errorHandler);
10 |
11 | require("./db/mongoose_connect")()
12 | .then((value) => {
13 | if (process.env.NODE_ENV !== "test") console.log(value);
14 | })
15 | .catch((reason) => console.log(reason));
16 | //connect to DB first then run the app
17 | module.exports = app;
18 |
--------------------------------------------------------------------------------
/src/constants/constant.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.freeze({
2 | USER_ENUM: {
3 | USER: "user",
4 | MODERATOR: "moderator",
5 | ADMIN: "admin",
6 | OWNER: "owner",
7 | },
8 | USER_TYPE: ["user", "moderator", "admin", "owner"],
9 | PERMISSIONS: {
10 | addCategory: ["moderator", "admin", "owner"],
11 | addItem: ["moderator", "admin", "owner"],
12 | addItemToCategory: ["moderator", "admin", "owner"],
13 | },
14 | PHONE_LOCAL: ["ar-EG", "ar-SA"],
15 | LANGUAGES: ["en", "ar"],
16 | DEFAULT_LANGUAGE: "en",
17 | NAME_MIN_LENGTH: 3,
18 | NAME_MAX_LENGTH: 25,
19 | PASSWORD_MIN_LENGTH: 6,
20 | PASSWORD_MAX_LENGTH: 25,
21 | RANDOM_CODE_LENGTH: 6,
22 | MAX_IMAGES_IN_ITEM: 4,
23 | });
24 |
--------------------------------------------------------------------------------
/src/controllers/auth/auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 | const _ = require("lodash");
3 | const User = require("../../models/user");
4 | const Image = require("../../models/image");
5 | const errorThrower = require("../../utils/error_thrower");
6 | const emails = require("../../services/emails/emails");
7 | const CONSTANT = require("../../constants/constant");
8 | const {
9 | findByEmailOrThrowIfUserExist,
10 | findByEmailOrThrowIfUserNotExist,
11 | findByIdOrThrowIfUserNotExist,
12 | creatCredentialsTO,
13 | verifyTokenTo,
14 | getImageById,
15 | } = require("./auth_utils");
16 | const { deleteImageById } = require("../../utils/image_util.js");
17 |
18 | exports.login = async (req, res, next) => {
19 | try {
20 | const { email, password, fcmToken } = req.body;
21 | const user = await findByEmailOrThrowIfUserNotExist({ email: email });
22 | const isEqual = await bcrypt.compare(password, user.password);
23 | if (!isEqual) {
24 | return errorThrower("Email or password incorrect!", 422);
25 | }
26 | if (fcmToken) {
27 | user.fcmTokens.push(fcmToken);
28 | user.save();
29 | }
30 | const token = await user.generateJWT();
31 |
32 | return res.jsonSuccess({
33 | user: {
34 | ..._.pick(user, [
35 | "_id",
36 | "name",
37 | "email",
38 | "phone",
39 | "emailVerified",
40 | "userType",
41 | ]),
42 | image: await getImageById({ id: user.image }),
43 | },
44 | token,
45 | });
46 | } catch (e) {
47 | next(e);
48 | }
49 | };
50 | exports.register = async (req, res, next) => {
51 | try {
52 | const { name, email, phone, password, fcmToken } = req.body;
53 | await findByEmailOrThrowIfUserExist({ email: email });
54 | const fcmTokens = fcmToken ? [fcmToken] : [];
55 | const user = new User({ name, email, phone, password, fcmTokens });
56 | await user.save();
57 |
58 | const jwtToken = await user.generateJWT();
59 | const { token, code } = await creatCredentialsTO({
60 | to: "verifyEmail",
61 | email: user.email,
62 | userID: user._id,
63 | });
64 | res.jsonSuccess({
65 | user: {
66 | ..._.pick(user, ["_id", "name", "email", "phone", "emailVerified"]),
67 | },
68 | token: jwtToken,
69 | confirmEmailToken: token,
70 | });
71 | await emails.sendConfirmEmail({
72 | to: email,
73 | name,
74 | ConfirmEmailToken: token,
75 | code,
76 | });
77 | } catch (e) {
78 | next(e);
79 | }
80 | };
81 | exports.profile = async (req, res, next) => {
82 | try {
83 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId });
84 | return res.jsonSuccess({
85 | user: {
86 | ..._.pick(user, [
87 | "_id",
88 | "name",
89 | "email",
90 | "phone",
91 | "emailVerified",
92 | "userType",
93 | ]),
94 | image: await getImageById({ id: user.image }),
95 | },
96 | });
97 | } catch (e) {
98 | next(e);
99 | }
100 | };
101 | exports.refreshFcmToken = async (req, res, next) => {
102 | try {
103 | const { fcmToken } = req.body;
104 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId });
105 |
106 | user.fcmTokens.push(fcmToken);
107 | user.save();
108 |
109 | return res.jsonSuccess();
110 | } catch (e) {
111 | next(e);
112 | }
113 | };
114 | exports.reSendConfirmEmail = async (req, res, next) => {
115 | try {
116 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId });
117 |
118 | const { token, code } = await creatCredentialsTO({
119 | to: "verifyEmail",
120 | email: user.email,
121 | userID: user._id,
122 | });
123 | res.jsonSuccess({
124 | confirmEmailToken: token,
125 | });
126 | await emails.sendConfirmEmail({
127 | to: user.email,
128 | name: user.name,
129 | confirmEmailToken: token,
130 | code,
131 | });
132 | } catch (e) {
133 | next(e);
134 | }
135 | };
136 | exports.verifyEmail = async (req, res, next) => {
137 | try {
138 | const { code, confirmEmailToken } = req.query;
139 |
140 | const { userID, email } = await verifyTokenTo({
141 | to: "verifyEmail",
142 | code: code,
143 | token: confirmEmailToken,
144 | });
145 | const user = await findByEmailOrThrowIfUserNotExist({ email: email });
146 | if (!user.emailVerified) {
147 | user.emailVerified = true;
148 | await user.save();
149 | }
150 | return res.jsonSuccess({
151 | user: _.pick(user, ["_id", "email", "emailVerified"]),
152 | });
153 | } catch (e) {
154 | next(e);
155 | }
156 | };
157 | exports.resetPassword = async (req, res, next) => {
158 | try {
159 | const { email } = req.body;
160 | const user = await findByEmailOrThrowIfUserNotExist({ email: email });
161 | const { token, code } = await creatCredentialsTO({
162 | to: "resetPassword",
163 | email: user.email,
164 | userID: user._id,
165 | });
166 | res.jsonSuccess({
167 | message: `Email sent to ${user.email}`,
168 | resetPasswordToken: token,
169 | });
170 | await emails.sendResetPassword({
171 | to: email,
172 | resetPasswordToken: token,
173 | code,
174 | });
175 | } catch (e) {
176 | next(e);
177 | }
178 | };
179 | exports.verifyResetPassword = async (req, res, next) => {
180 | try {
181 | const { code, resetPasswordToken } = req.query;
182 |
183 | const { userID, email } = await verifyTokenTo({
184 | to: "resetPassword",
185 | code: code,
186 | token: resetPasswordToken,
187 | });
188 | const user = await findByEmailOrThrowIfUserNotExist({ email: email });
189 |
190 | const tokenJWT = await user.generateJWT();
191 | return res.jsonSuccess({ token: tokenJWT });
192 | } catch (e) {
193 | next(e);
194 | }
195 | };
196 | exports.updateProfile = async (req, res, next) => {
197 | try {
198 | let image;
199 | const allowedUpdates = ["name", "email", "phone"];
200 | const updatesReq = Object.keys(req.body); // ["email", "password"];
201 | const updates = updatesReq.filter((update) =>
202 | allowedUpdates.includes(update)
203 | );
204 |
205 | const user = await findByIdOrThrowIfUserNotExist({ id: req.userId });
206 |
207 | const file = req.file;
208 | if (file) {
209 | image = new Image({
210 | ..._.pick(file, ["deletehash", "link", "imageHash"]),
211 | });
212 | await image.save();
213 | req.body.image = image._id;
214 | if (image) {
215 | deleteImageById({ id: user.image });
216 | updates.push("image");
217 | }
218 | }
219 |
220 | updates.forEach((update) => (user[update] = req.body[update]));
221 |
222 | await user.save();
223 | if (!image) {
224 | image = await getImageById({ id: user.image });
225 | }
226 |
227 | return res.jsonSuccess({
228 | user: {
229 | ..._.pick(user, ["_id", "name", "email", "phone", "emailVerified"]),
230 | image: image,
231 | },
232 | });
233 | } catch (e) {
234 | next(e);
235 | }
236 | };
237 |
--------------------------------------------------------------------------------
/src/controllers/auth/auth_utils.js:
--------------------------------------------------------------------------------
1 | const User = require("../../models/user");
2 | const Image = require("../../models/image");
3 | const { jwtSign, jwtVerify } = require("../../utils/jwt_promise");
4 | const errorThrower = require("../../utils/error_thrower");
5 |
6 | exports.findByEmailOrThrowIfUserExist = async ({ email }) => {
7 | const user = await User.findOne({ email: email });
8 | if (user) {
9 | return errorThrower("Email address already exist!", 422);
10 | }
11 | return user;
12 | };
13 | exports.findByEmailOrThrowIfUserNotExist = async ({ email }) => {
14 | const user = await User.findOne({ email: email });
15 | if (!user) {
16 | return errorThrower("Email address not exist!", 422);
17 | }
18 | return user;
19 | };
20 | exports.findByIdOrThrowIfUserNotExist = async ({ id }) => {
21 | const user = await User.findById(id);
22 | if (!user) {
23 | return errorThrower("User not found!", 400);
24 | }
25 | return user;
26 | };
27 | exports.creatCredentialsTO = async ({ to, userID, email }) => {
28 | const code = random6Digits();
29 | const token = await jwtSign({
30 | payload: { userID, email },
31 | additionalSecret: `${code}${to}`,
32 | });
33 |
34 | return { token, code };
35 | };
36 | exports.verifyTokenTo = async ({ to, code, token }) => {
37 | const { userID, email } = await jwtVerify(token, `${code}${to}`);
38 | return { userID, email };
39 | };
40 | exports.getImageById = async ({ id }) => {
41 | if (id) {
42 | const image = await Image.findById(id).select("-_id -deletehash");
43 | if (image) {
44 | return image;
45 | }
46 | }
47 | return null;
48 | };
49 |
50 | const random6Digits = () => Math.floor(100000 + Math.random() * 900000);
51 |
--------------------------------------------------------------------------------
/src/controllers/category/category.js:
--------------------------------------------------------------------------------
1 | const _ = require("lodash");
2 | const Category = require("../../models/category");
3 | const Image = require("../../models/image");
4 | const { deleteImageById } = require("../../utils/image_util");
5 | const { addItemToCategory } = require("../utils");
6 | const errorThrower = require("../../utils/error_thrower");
7 |
8 | exports.getCategories = async (req, res, next) => {
9 | try {
10 | const categories = await Category.find()
11 | .populate({
12 | path: "image",
13 | select: "-_id -deletehash -__v",
14 | })
15 | .select("-__v -items");
16 |
17 | return res.jsonSuccess({ categories: categories });
18 | } catch (e) {
19 | next(e);
20 | }
21 | };
22 | exports.addCategory = async (req, res, next) => {
23 | let imageId;
24 | try {
25 | const { name } = req.body;
26 | const file = req.file;
27 | if (file) {
28 | const image = new Image({
29 | ..._.pick(file, ["deletehash", "link", "imageHash"]),
30 | });
31 | await image.save();
32 | imageId = image._id;
33 | }
34 | const category = new Category({ name, image: imageId });
35 | await category.save();
36 |
37 | return res.jsonSuccess();
38 | } catch (e) {
39 | deleteImageById({ id: imageId });
40 | next(e);
41 | }
42 | };
43 |
44 | exports.addItemToCategory = async (req, res, next) => {
45 | try {
46 | const { categoryId } = req.params;
47 | const { itemId } = req.body;
48 | const isAdded = await addItemToCategory({ categoryId, itemID: itemId });
49 | if (isAdded) return res.jsonSuccess();
50 | return errorThrower("Category not found", 404);
51 | } catch (e) {
52 | next(e);
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/src/controllers/item/item.js:
--------------------------------------------------------------------------------
1 | const _ = require("lodash");
2 | const Category = require("../../models/category");
3 | const Item = require("../../models/item");
4 | const Image = require("../../models/image");
5 | const { deleteImageById } = require("../../utils/image_util");
6 | const errorThrower = require("../../utils/error_thrower");
7 | const { addItemToCategory } = require("../utils");
8 |
9 | exports.addItem = async (req, res, next) => {
10 | try {
11 | const files = req.files;
12 | let images = [];
13 | if (files && files.length > 0) {
14 | images = files.map(
15 | (image) =>
16 | new Image({ ..._.pick(image, ["deletehash", "link", "imageHash"]) })
17 | );
18 | images.forEach((image) => image.save()); //save images to DB
19 | }
20 |
21 | const {
22 | title,
23 | disc,
24 | unitName,
25 | price,
26 | discount,
27 | maxQuantityInOrder,
28 | categoryId,
29 | } = req.body;
30 |
31 | const imagesIDs = images.map((image) => image._id);
32 | const item = new Item({
33 | title,
34 | disc,
35 | units: [
36 | {
37 | name: unitName,
38 | price,
39 | discount,
40 | maxQuantityInOrder,
41 | images: imagesIDs,
42 | },
43 | ],
44 | });
45 |
46 | await addItemToCategory({ categoryId: categoryId, itemID: item._id });
47 | await item.save();
48 |
49 | return res.jsonSuccess();
50 | } catch (e) {
51 | next(e);
52 | }
53 | };
54 | exports.getAllItems = async (req, res, next) => {
55 | try {
56 | const categoryId = req.params.id;
57 | const categories = await Category.findById(categoryId)
58 | .populate({
59 | path: "items",
60 | populate: {
61 | path: "units.images",
62 | model: "Image",
63 | select: "-_id -deletehash -__v",
64 | },
65 | })
66 | .select("items");
67 | if (!categories) {
68 | return errorThrower("Category not found", 404);
69 | }
70 | return res.jsonSuccess({ items: categories?.items ?? [] });
71 | } catch (e) {
72 | next(e);
73 | }
74 | };
75 |
--------------------------------------------------------------------------------
/src/controllers/utils.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user");
2 | const Item = require("../models/item");
3 | const Category = require("../models/category");
4 | const Image = require("../models/image");
5 | const { jwtSign, jwtVerify } = require("../utils/jwt_promise");
6 | const errorThrower = require("../utils/error_thrower");
7 |
8 | exports.addItemToCategory = async ({ categoryId, itemID }) => {
9 | if (!categoryId || !itemID) return false;
10 |
11 | const category = await Category.findById(categoryId);
12 | if (!category) return false;
13 |
14 | // I am not sure we need validation for itemId
15 | // to be sure the item exists or not, for now
16 | // I insert itemId without validation
17 | //if u read this comment let me know ur opinion, Good Day
18 |
19 | category.items.push(itemID);
20 | category.save();
21 | return true;
22 | };
23 |
--------------------------------------------------------------------------------
/src/db/mongoose_connect.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | module.exports = () => {
4 | return new Promise((resolutionFunc, rejectionFunc) => {
5 | const mongooseOptions = {
6 | useNewUrlParser: true,
7 | useCreateIndex: true,
8 | autoIndex: true,
9 | keepAlive: true,
10 | poolSize: 50, //50 people can connect at the same time
11 | bufferMaxEntries: 0,
12 | connectTimeoutMS: 10000,
13 | socketTimeoutMS: 45000,
14 | family: 4, // Use IPv4, skip trying IPv6
15 | useFindAndModify: false,
16 | useUnifiedTopology: true,
17 | };
18 | mongoose
19 | .connect(process.env.MONGO_URL, mongooseOptions)
20 | .then((_) =>
21 | resolutionFunc("mongo connected to " + process.env.MONGO_URL)
22 | )
23 | .catch((error) => rejectionFunc("mongo not connected", error));
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const app = require("./app");
2 | const fs = require("fs");
3 | const path = require("path");
4 |
5 | const port = process.env.PORT || 3000;
6 |
7 | app.listen(port, () => console.log("start listen on port:" + port));
8 |
9 | process.on("uncaughtException", (err) => {
10 | console.error(err.stack); // either logs on console or send to other server via api call.
11 | fs.appendFileSync(path.join(__dirname, "./../", "access.log"), err.stack);
12 | // process.exit(1);
13 | });
14 |
--------------------------------------------------------------------------------
/src/middlewares/accept_language.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const CONSTANT = require("../constants/constant");
3 |
4 | const acceptLanguage = (req, res, next) => {
5 | const userLanguage = req.get("accept-language");
6 | const acceptedLanguage = CONSTANT.LANGUAGES.includes(userLanguage);
7 |
8 | req.userLanguage = acceptedLanguage
9 | ? userLanguage
10 | : CONSTANT.DEFAULT_LANGUAGE;
11 |
12 | mongoose.setDefaultLanguage(req.userLanguage);
13 |
14 | return next();
15 | };
16 | module.exports = acceptLanguage;
17 |
--------------------------------------------------------------------------------
/src/middlewares/async.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = (fn) => {
2 | return async (req, res, next) => {
3 | try {
4 | await fn(req, res, next);
5 | } catch (error) {
6 | next(error);
7 | }
8 | };
9 | };
10 |
11 | module.exports = asyncWrapper;
--------------------------------------------------------------------------------
/src/middlewares/check_permissions.js:
--------------------------------------------------------------------------------
1 | const isAuth = require("../middlewares/is_auth");
2 | const errorThrower = require("../utils/error_thrower");
3 | const constant = require("../constants/constant");
4 | const { jwtVerify } = require("../utils/jwt_promise");
5 | module.exports = (action) => {
6 | return async (req, res, next) => {
7 | try {
8 | const authHeader = req.get("Authorization");
9 |
10 | if (!authHeader) {
11 | errorThrower("Not authenticated", 401);
12 | }
13 | const token = authHeader.replace("Bearer ", "");
14 | const decodeToken = await jwtVerify(token);
15 |
16 | const userType = decodeToken.userType;
17 |
18 | if (constant.PERMISSIONS[action].includes(userType)) {
19 | next();
20 | } else {
21 | return errorThrower(
22 | "Token valid, but you don't have the right permission:)",
23 | 403
24 | );
25 | }
26 | } catch (error) {
27 | next(error);
28 | }
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/middlewares/cors_header.js:
--------------------------------------------------------------------------------
1 | const corsHeader = (req, res, next) => {
2 | //to accept connect to server from other domain (CORS)
3 | res.setHeader("Access-Control-Allow-Origin", "*"); //accept all domain
4 | res.setHeader("Access-Control-Allow-Methods", "GET, POST");
5 | res.setHeader("Access-Control-Allow-Headers", "*"); //accept all headers
6 |
7 | if (req.method.toString().toLowerCase() === "options") {
8 | //fix issue with modern browser
9 | //first browser options request to check server status
10 | return res.sendStatus(200);
11 | }
12 |
13 | return next();
14 | };
15 | module.exports = corsHeader;
16 |
--------------------------------------------------------------------------------
/src/middlewares/is_auth.js:
--------------------------------------------------------------------------------
1 | const { jwtVerify } = require("../utils/jwt_promise");
2 | const errorThrower = require("../utils/error_thrower");
3 |
4 | module.exports = async (req, res, next) => {
5 | const authHeader = req.get("Authorization");
6 |
7 | if (!authHeader) {
8 | errorThrower("Not authenticated", 401);
9 | }
10 | const token = authHeader.replace("Bearer ", "");
11 |
12 | try {
13 | const decodeToken = await jwtVerify(token);
14 |
15 | req.userId = decodeToken._id;
16 | req.userEmail = decodeToken.email;
17 | req.userType = decodeToken.userType;
18 | next();
19 | } catch (e) {
20 | next(e);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/middlewares/is_intl.js:
--------------------------------------------------------------------------------
1 | const CONSTANT = require("../constants/constant");
2 | const errorThrower = require("../utils/error_thrower");
3 | module.exports = (field) => {
4 | return async (req, res, next) => {
5 | try {
6 | //in some time when i use form-data
7 | //fields come as String for this i need to parse it
8 |
9 | let input = {};
10 | if (typeof req.body[field] === "object") {
11 | input = req.body[field];
12 | }
13 | if (typeof req.body[field] === "string") {
14 | input = JSON.parse(req.body[field]);
15 | }
16 | // input = { "en": "test", "ar": "تجربة" }
17 |
18 | req.body[field] = input;
19 | for (let i = 0; i < CONSTANT.LANGUAGES.length; i++) {
20 | if (!input[CONSTANT.LANGUAGES[i]]) {
21 | return errorThrower(
22 | `one of ${field} languages not found=>'${CONSTANT.LANGUAGES[i]}' you must send [${CONSTANT.LANGUAGES}]`,
23 | 422
24 | );
25 | }
26 | }
27 | next();
28 | } catch (error) {
29 | next(error);
30 | }
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/src/middlewares/log_errors.js:
--------------------------------------------------------------------------------
1 | const logErrors = (err, req, res, next) => {
2 | console.error("Logger => " + err.stack);
3 | return next(err);
4 | };
5 | module.exports = logErrors;
6 |
--------------------------------------------------------------------------------
/src/middlewares/middlewares.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const auth = require("http-auth");
3 | const authConnect = require("http-auth-connect");
4 | const statusMonitor = require("express-status-monitor")({ path: "" });
5 | const path = require("path");
6 | const helmet = require("helmet");
7 | const morgan = require("morgan");
8 | const fs = require("fs");
9 | const corsHeader = require("../middlewares/cors_header");
10 | const acceptLanguage = require("../middlewares/accept_language");
11 | const mongoSanitize = require("express-mongo-sanitize");
12 | const xss = require("xss-clean");
13 | const compression = require("compression");
14 | const limiter = require("../middlewares/requests_limiter");
15 | const requestIp = require("request-ip");
16 |
17 | module.exports = (app) => {
18 | app.set("trust proxy", 1);
19 | app.use(statusMonitor.middleware);
20 | app.get(
21 | "/admin/statusMonitor",
22 | authConnect(
23 | auth.basic({}, (user, pass, callback) =>
24 | //TODO make real auth
25 | callback(user === "user" && pass === "pass")
26 | )
27 | ),
28 | statusMonitor.pageRoute
29 | );
30 | app.use(limiter);
31 | app.use(compression());
32 | app.use(mongoSanitize());
33 | app.use(xss());
34 | app.use(helmet());
35 | app.use(requestIp.mw()); //req.clientIp
36 | app.use(express.urlencoded({ extended: true }));
37 | app.use(express.json({ limit: "10kb" }));
38 | app.use(express.static(path.join(__dirname, "../../", "public")));
39 | app.use(corsHeader);
40 | app.use(acceptLanguage);
41 | require("../utils/json_success_fail")(app);
42 | if (process.env.NODE_ENV !== "test") {
43 | //not log error in test environment
44 | const accessLogStream = fs.createWriteStream(
45 | path.join(__dirname, "./../../", "access.log")
46 | );
47 |
48 | app.use(
49 | morgan(
50 | ":remote-addr __ :method __ HTTP/:http-version __ :url __ :status __ :res[content-length] __ :req[header] __ :response-time ms __ :date[iso]",
51 | { stream: accessLogStream }
52 | )
53 | );
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/src/middlewares/param_validator.js:
--------------------------------------------------------------------------------
1 | const { body, param, query, check } = require("express-validator");
2 | const isMongoId = require("../utils/is_mongo_id");
3 | const CONSTANT = require("../constants/constant");
4 | const bodyPrice = body("price")
5 | .isInt({ gt: 0 })
6 | .withMessage("must be a more than 0.");
7 | const bodyEmail = body("email")
8 | .trim()
9 | .isEmail()
10 | .normalizeEmail()
11 | .withMessage("must be a valid email.");
12 |
13 | const bodyPassword = body("password")
14 | .trim()
15 | .isLength({
16 | min: CONSTANT.PASSWORD_MIN_LENGTH,
17 | max: CONSTANT.PASSWORD_MAX_LENGTH,
18 | })
19 | .withMessage(
20 | `length must be between ${CONSTANT.PASSWORD_MIN_LENGTH},${CONSTANT.PASSWORD_MAX_LENGTH}`
21 | );
22 | const bodyFcmToken = body("fcmToken")
23 | .isString()
24 | .isLength({ min: 8 })
25 | .withMessage("must be a valid.");
26 | const bodyName = body("name")
27 | .trim()
28 | .isLength({
29 | min: CONSTANT.NAME_MIN_LENGTH,
30 | max: CONSTANT.NAME_MAX_LENGTH,
31 | })
32 | .withMessage(
33 | `length must be between ${CONSTANT.NAME_MIN_LENGTH},${CONSTANT.NAME_MAX_LENGTH}`
34 | );
35 | const bodyPhone = body("phone")
36 | .trim()
37 | .isMobilePhone(CONSTANT.PHONE_LOCAL)
38 | .withMessage(`must be ${CONSTANT.PHONE_LOCAL}`);
39 | const bodyUserType = body("userType")
40 | .trim()
41 | .notEmpty()
42 | .custom((user) => CONSTANT.USER_TYPE.includes(user));
43 | const queryRandomCode = query("code")
44 | .isLength({
45 | max: CONSTANT.RANDOM_CODE_LENGTH,
46 | min: CONSTANT.RANDOM_CODE_LENGTH,
47 | })
48 | .withMessage(`length must be ${CONSTANT.RANDOM_CODE_LENGTH} digits`)
49 | .trim()
50 | .isInt();
51 |
52 | const queryToken = query("token").isJWT().withMessage(`is not valid`);
53 | const queryConfirmEmailToken = query("confirmEmailToken")
54 | .isJWT()
55 | .withMessage(`is not valid`);
56 | const queryResetPasswordToken = query("resetPasswordToken")
57 | .isJWT()
58 | .withMessage(`is not valid`);
59 | const paramId = param("id").trim().custom(isMongoId);
60 | const paramMongoId = (fieldName) => param(fieldName).trim().custom(isMongoId);
61 | const bodyMongoId = (fieldName) => body(fieldName).trim().custom(isMongoId);
62 | const queryMongoId = (fieldName) => query(fieldName).trim().custom(isMongoId);
63 |
64 | const paramValidator = {
65 | bodyName,
66 | bodyFcmToken,
67 | bodyEmail,
68 | bodyPrice,
69 | bodyPassword,
70 | bodyPhone,
71 | bodyUserType,
72 | queryRandomCode,
73 | queryToken,
74 | queryConfirmEmailToken,
75 | paramId,
76 | paramMongoId,
77 | queryResetPasswordToken,
78 | bodyMongoId,
79 | queryMongoId,
80 | };
81 | module.exports = paramValidator;
82 |
--------------------------------------------------------------------------------
/src/middlewares/requests_limiter.js:
--------------------------------------------------------------------------------
1 | const rateLimit = require("express-rate-limit");
2 |
3 | const limiter = rateLimit({
4 | // each user only make 100 req in 15 MIN
5 | windowMs: 2 * 60 * 1000, // 2 minutes
6 | max: 20, // limit each IP to 100 requests per windowMs
7 | message: {
8 | status: "error",
9 | message: "Too many requests, please try again later after 2 minutes.",
10 | },
11 | });
12 | module.exports = limiter;
--------------------------------------------------------------------------------
/src/middlewares/uploader/image_uploader.js:
--------------------------------------------------------------------------------
1 | const CONSTANT = require("../../constants/constant");
2 | const _multer = require("multer");
3 | const acceptedMimetype = ["image/png", "image/jpeg", "image/jpg"];
4 |
5 | const multerStorageImgur = require("./multer_storage_imgur");
6 |
7 | const _fileFilter = (req, file, callback) => {
8 | callback(null, acceptedMimetype.includes(file.mimetype));
9 | };
10 |
11 | const upload = _multer({
12 | storage: multerStorageImgur({ clientId: process.env.IMGUR_CLIENT_ID }),
13 | fileFilter: _fileFilter,
14 | });
15 |
16 | exports.single = upload.single("image");
17 | exports.array = upload.array("image", CONSTANT.MAX_IMAGES_IN_ITEM);
18 |
--------------------------------------------------------------------------------
/src/middlewares/uploader/multer_storage_imgur.js:
--------------------------------------------------------------------------------
1 | const imgur = require("imgur");
2 | const concat = require("concat-stream");
3 | const sharp = require("sharp");
4 | const { encodeImageToBlurHash } = require("../../utils/image_util");
5 |
6 | function setupImgurStorage(opts = {}) {
7 | if (!opts.clientId) throw new Error("Missing client id");
8 | imgur.setClientId(opts.clientId);
9 |
10 | async function _handleFile(req, file, cb) {
11 | if (!file.mimetype || !file.mimetype.match(/image/gi)) {
12 | return cb(new Error("File is not of image type"));
13 | }
14 | const resize = sharp()
15 | .resize({ width: 500 })
16 | .jpeg({ quality: 60, palette: true });
17 | file.stream.pipe(resize).pipe(
18 | concat((data) => {
19 | imgur
20 | ._imgurRequest("upload", data, {})
21 | .then((json) => {
22 | const jsonLink = json.link;
23 | if (!(json && jsonLink)) {
24 | return cb(new Error("File is not uploaded"));
25 | }
26 |
27 | const factor = json.width / json.height;
28 |
29 | encodeImageToBlurHash(data, factor)
30 | .then((imageHash) => {
31 | cb(null, { ...json, imageHash: imageHash ?? "" });
32 | })
33 | .catch(cb);
34 | })
35 | .catch(cb);
36 | })
37 | );
38 | }
39 |
40 | function _removeFile() {}
41 |
42 | return { _handleFile, _removeFile };
43 | }
44 |
45 | module.exports = function (opts) {
46 | return setupImgurStorage(opts);
47 | };
48 |
--------------------------------------------------------------------------------
/src/models/category.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const { Schema, Types, model } = require("mongoose");
3 | const _ = require("lodash");
4 | const CONSTANT = require("../constants/constant");
5 |
6 | const mongooseIntl = require("mongoose-intl");
7 |
8 | // r we need to know who creates the category? it depends. HaHaHa
9 | // I need to stop over-engineering and end this project ASAP
10 | const categorySchema = new Schema(
11 | {
12 | name: {
13 | type: String,
14 | intl: true,
15 | required: true,
16 | requiredAll: true,
17 | trim: true,
18 | unique: true,
19 | },
20 | image: {
21 | type: Types.ObjectId,
22 | ref: "Image",
23 | default: null,
24 | required: false,
25 | },
26 | items: {
27 | type: [
28 | {
29 | type: Types.ObjectId,
30 | ref: "Item",
31 | },
32 | ],
33 | default: [],
34 | required: false,
35 | },
36 | },
37 | {
38 | timestamps: false,
39 | toJSON: {
40 | virtuals: true,
41 | },
42 | }
43 | );
44 | categorySchema.plugin(mongooseIntl, {
45 | languages: CONSTANT.LANGUAGES,
46 | defaultLanguage: CONSTANT.DEFAULT_LANGUAGE,
47 | });
48 | categorySchema.pre("save", async function (next) {
49 | const category = this;
50 | // i used uniqBy instead of uniq
51 | // because ObjectId("foo") == ObjectId("foo"); => false
52 | //the solve for this problem is to convert to string
53 | category.items = _.uniqBy(category.items, (id) => id.toString());
54 | next();
55 | });
56 | module.exports = mongoose.model("Category", categorySchema);
57 |
--------------------------------------------------------------------------------
/src/models/image.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 | const imageSchema = new Schema(
4 | {
5 | link: {
6 | type: String,
7 | required: true,
8 | index: true,
9 | },
10 | deletehash: {
11 | type: String,
12 | required: true,
13 | },
14 | imageHash: {
15 | type: String,
16 | default: "",
17 | },
18 | },
19 | {
20 | timestamps: false,
21 | }
22 | );
23 | module.exports = mongoose.model("Image", imageSchema);
24 |
--------------------------------------------------------------------------------
/src/models/item.js:
--------------------------------------------------------------------------------
1 | const CONSTANT = require("../constants/constant");
2 | const { Schema, Types, model } = require("mongoose");
3 | const mongooseIntl = require("mongoose-intl");
4 |
5 | //add a new field called deletedAt to make a soft delete
6 |
7 | const itemSchema = new Schema(
8 | {
9 | title: {
10 | type: String,
11 | intl: true,
12 | requiredAll: true,
13 | },
14 | disc: {
15 | type: String,
16 | intl: true,
17 | requiredAll: true,
18 | },
19 | units: [
20 | //https://www.fakahany.com/ar/item/view/228
21 | {
22 | _id: {
23 | type: Types.ObjectId,
24 | required: true,
25 | auto: true,
26 | },
27 | name: {
28 | type: String,
29 | intl: true,
30 | requiredAll: true,
31 | },
32 | images: {
33 | type: [
34 | {
35 | type: Types.ObjectId,
36 | ref: "Image",
37 | },
38 | ],
39 | default: [],
40 | required: false,
41 | },
42 | price: {
43 | type: Number,
44 | required: true,
45 | },
46 | discount: {
47 | type: Number,
48 | required: false,
49 | default: 0,
50 | },
51 | minQuantityInOrder: {
52 | type: Number,
53 | default: 1,
54 | // min Quantity user can Order
55 | },
56 | maxQuantityInOrder: {
57 | type: Number,
58 | default: null,
59 | // max Quantity user can add to Order
60 | },
61 | },
62 | ],
63 | },
64 | {
65 | timestamps: false,
66 | toJSON: {
67 | virtuals: true,
68 | },
69 | }
70 | );
71 | itemSchema.plugin(mongooseIntl, {
72 | languages: CONSTANT.LANGUAGES,
73 | defaultLanguage: CONSTANT.DEFAULT_LANGUAGE,
74 | });
75 | module.exports = model("Item", itemSchema);
76 |
--------------------------------------------------------------------------------
/src/models/user.js:
--------------------------------------------------------------------------------
1 | const { Schema, Types, model } = require("mongoose");
2 | const { jwtSign } = require("../utils/jwt_promise");
3 | const bcrypt = require("bcrypt");
4 | const CONSTANT = require("../constants/constant");
5 | const _ = require("lodash");
6 | const { deleteImageById } = require("../utils/image_util");
7 |
8 | const UserSchema = new Schema(
9 | {
10 | name: {
11 | type: String,
12 | required: [true, "must provide name"],
13 | maxlength: [20, "name can not be more than 20 characters"],
14 | trim: true,
15 | },
16 | email: {
17 | type: String,
18 | required: true,
19 | unique: true,
20 | index: true,
21 | trim: true,
22 | lowercase: true,
23 | match: [
24 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
25 | "Please provide a valid email",
26 | ],
27 | },
28 | emailVerified: {
29 | type: Boolean,
30 | default: false,
31 | },
32 | phone: {
33 | type: String,
34 | required: true,
35 | trim: true,
36 | },
37 | password: {
38 | type: String,
39 | required: true,
40 | trim: true,
41 | },
42 |
43 | userType: {
44 | type: String,
45 | enum: {
46 | values: CONSTANT.USER_TYPE,
47 | message: `wrong user type try ${CONSTANT.USER_ENUM.USER}`,
48 | },
49 | default: CONSTANT.USER_ENUM.USER,
50 | },
51 | image: {
52 | type: Types.ObjectId,
53 | ref: "Image",
54 | },
55 | fcmTokens: {
56 | //TODO should be [{fcmToken, deviceId}]
57 | type: [String],
58 | default: [],
59 | required: false,
60 | },
61 | },
62 | {
63 | timestamps: {
64 | createdAt: true,
65 | updatedAt: false,
66 | },
67 | }
68 | );
69 | UserSchema.methods.generateJWT = function () {
70 | // add user ip in token
71 | return jwtSign({
72 | payload: {
73 | _id: this._id.toString(),
74 | email: this.email,
75 | userType: this.userType,
76 | },
77 | });
78 | };
79 |
80 | UserSchema.pre("save", async function (next) {
81 | const user = this;
82 | if (user.isModified("password")) {
83 | // Hash the plain text password before saving
84 | user.password = await bcrypt.hash(user.password, 6);
85 | }
86 | if (user.isModified("email")) {
87 | // set emailVerified if user changes his email
88 | user.emailVerified = false;
89 | }
90 | user.fcmTokens = _.union(user.fcmTokens);
91 | next();
92 | });
93 | // Delete user Image when user is removed
94 | UserSchema.pre("remove", async function (next) {
95 | const user = this;
96 | deleteImageById({ id: user.image });
97 | next();
98 | });
99 | module.exports = model("User", UserSchema);
100 |
--------------------------------------------------------------------------------
/src/routes/auth.js:
--------------------------------------------------------------------------------
1 | const { Router } = require("express");
2 | const router = Router();
3 | const {
4 | register,
5 | login,
6 | resetPassword,
7 | verifyResetPassword,
8 | verifyEmail,
9 | profile,
10 | updateProfile,
11 | reSendConfirmEmail,
12 | refreshFcmToken,
13 | } = require("../controllers/auth/auth");
14 | const uploader = require("../middlewares/uploader/image_uploader");
15 | const isAuth = require("../middlewares/is_auth");
16 | const paramValidator = require("../middlewares/param_validator");
17 | const paramValidation = require("../utils/param_validation");
18 |
19 | router.post(
20 | "/register",
21 | [
22 | paramValidator.bodyName,
23 | paramValidator.bodyEmail,
24 | paramValidator.bodyPhone,
25 | paramValidator.bodyPassword,
26 | ],
27 | paramValidation,
28 | register
29 | );
30 | router.post(
31 | "/login",
32 | [paramValidator.bodyEmail, paramValidator.bodyPassword],
33 | paramValidation,
34 | login
35 | );
36 |
37 | router.put("/updateProfile", isAuth, uploader.single, updateProfile);
38 | router.post("/profile", isAuth, profile);
39 | router.post("/reSendConfirmEmail", isAuth, reSendConfirmEmail);
40 | router.get(
41 | "/verifyEmail",
42 | [paramValidator.queryRandomCode, paramValidator.queryConfirmEmailToken],
43 | paramValidation,
44 | verifyEmail
45 | );
46 | router.post(
47 | "/resetPassword",
48 | [paramValidator.bodyEmail],
49 | paramValidation,
50 | resetPassword
51 | );
52 | router.get(
53 | "/verifyResetPassword",
54 | [paramValidator.queryRandomCode, paramValidator.queryResetPasswordToken],
55 | paramValidation,
56 | verifyResetPassword
57 | );
58 | router.post(
59 | "/refreshFcmToken",
60 | isAuth,
61 | [paramValidator.bodyFcmToken],
62 | paramValidation,
63 | refreshFcmToken
64 | );
65 |
66 | module.exports = router;
67 |
--------------------------------------------------------------------------------
/src/routes/category.js:
--------------------------------------------------------------------------------
1 | const { Router } = require("express");
2 | const checkPermissions = require("../middlewares/check_permissions");
3 | const isIntl = require("../middlewares/is_intl");
4 | const imageUploader = require("../middlewares/uploader/image_uploader");
5 | const {
6 | addCategory,
7 | addItemToCategory,
8 | getCategories,
9 | } = require("../controllers/category/category");
10 | const paramValidator = require("../middlewares/param_validator");
11 | const paramValidation = require("../utils/param_validation");
12 |
13 | const router = Router();
14 |
15 | //create category
16 | router.post(
17 | "/add",
18 | checkPermissions("addCategory"),
19 | imageUploader.single,
20 | isIntl("name"),
21 | paramValidation,
22 | addCategory
23 | );
24 | //add an item to an existing category
25 | router.post(
26 | "/:categoryId/addItem",
27 | checkPermissions("addItemToCategory"),
28 | paramValidator.paramMongoId("categoryId"),
29 | paramValidator.bodyMongoId("itemId"),
30 | paramValidation,
31 | addItemToCategory
32 | );
33 | router.get("/getAll", getCategories);
34 |
35 | module.exports = router;
36 |
--------------------------------------------------------------------------------
/src/routes/helper/404.js:
--------------------------------------------------------------------------------
1 | const notFound = (__, res) => {
2 | return res.jsonFail(
3 | {
4 | message: "api not found",
5 | },
6 | 404
7 | );
8 | };
9 | module.exports = notFound;
10 |
--------------------------------------------------------------------------------
/src/routes/helper/error_handler.js:
--------------------------------------------------------------------------------
1 | const errorHandler = (err, __, res, _) => {
2 | const {statusCode, message} = err;
3 | return res.status(statusCode || 500).jsonFail({message});
4 | };
5 | module.exports = errorHandler;
6 |
--------------------------------------------------------------------------------
/src/routes/item.js:
--------------------------------------------------------------------------------
1 | const { Router } = require("express");
2 | const checkPermissions = require("../middlewares/check_permissions");
3 | const isIntl = require("../middlewares/is_intl");
4 | const paramValidator = require("../middlewares/param_validator");
5 | const paramValidation = require("../utils/param_validation");
6 | const imageUploader = require("../middlewares/uploader/image_uploader");
7 | const { addItem, getAllItems } = require("../controllers/item/item");
8 |
9 | const router = Router();
10 |
11 | router.post(
12 | "/add",
13 | checkPermissions("addItem"),
14 | imageUploader.array,
15 | isIntl("title"),
16 | isIntl("disc"),
17 | isIntl("unitName"),
18 | paramValidator.bodyPrice,
19 | paramValidation,
20 | addItem
21 | );
22 |
23 | router.get("/getAll/:id", paramValidator.paramId, paramValidation, getAllItems);
24 |
25 | module.exports = router;
26 |
--------------------------------------------------------------------------------
/src/routes/routes.js:
--------------------------------------------------------------------------------
1 | const authRouter = require("./auth");
2 | const categoryRouter = require("./category");
3 | const itemRouter = require("./item");
4 | const errorHandler = require("./helper/error_handler");
5 | const notFound = require("./helper/404");
6 | const logErrors = require("../middlewares/log_errors");
7 | const {Router} = require("express");
8 | const routers = Router();
9 |
10 | routers.use("/api/auth", authRouter);
11 | routers.use("/api/category", categoryRouter);
12 | routers.use("/api/items", itemRouter);
13 |
14 | if (process.env.NODE_ENV !== "test") {
15 | //not log error in test environment
16 | routers.use(logErrors);
17 | }
18 | routers.use(errorHandler);
19 |
20 | routers.use(notFound);
21 |
22 | module.exports = routers;
23 |
--------------------------------------------------------------------------------
/src/services/emails/email_sender.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require("nodemailer");
2 |
3 | module.exports = ({ to, subject, html }) => {
4 | return new Promise(async (resolutionFunc, rejectionFunc) => {
5 | const transport = nodemailer.createTransport({
6 | service: "gmail",
7 | auth: {
8 | user: process.env.EMAIL,
9 | pass: process.env.EMAIL_PASSWORD,
10 | },
11 | });
12 |
13 | const mailOptions = {
14 | from: `Tkamul SA <${process.env.EMAIL}>`,
15 | to,
16 | sender: "Tkamul SA",
17 | subject,
18 | html,
19 | };
20 |
21 | transport.sendMail(mailOptions, (err, info) => {
22 | if (err) {
23 | rejectionFunc(err);
24 | }
25 | resolutionFunc(info);
26 | });
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/src/services/emails/emails.js:
--------------------------------------------------------------------------------
1 | const ejs = require("ejs");
2 | const path = require("path");
3 |
4 | const emailSender = require("./email_sender");
5 |
6 | exports.sendConfirmEmail = async ({ to, name, confirmEmailToken, code }) => {
7 | const hostname = ""; //req.hostname
8 | const subject = "Confirm your email";
9 | const confirmEmailLink = `${hostname}/api/auth/verifyEmail?token=${confirmEmailToken}&code=${code}`;
10 | const renderedFile = await ejs.renderFile(
11 | path.join(__dirname, "../../../", "emails/confirm_email.ejs"),
12 | { name, confirmEmailLink, code }
13 | );
14 | await emailSender({ to, subject, html: renderedFile });
15 | };
16 | exports.sendResetPassword = async ({ to, resetPasswordToken, code }) => {
17 | const hostname = ""; //req.hostname
18 | const subject = "Reset password";
19 | const resetPasswordLink = `${hostname}/api/auth/verifyResetPassword?token=${resetPasswordToken}&code=${code}`;
20 | const renderedFile = await ejs.renderFile(
21 | path.join(__dirname, "../../../", "emails/reset_password.ejs"),
22 | { resetPasswordLink, code }
23 | );
24 | await emailSender({ to, subject, html: renderedFile });
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils/error_thrower.js:
--------------------------------------------------------------------------------
1 | const errorThrower = (err, statusCode) => {
2 | if (err) {
3 | const error = new Error(err.toString());
4 | error.statusCode = statusCode;
5 | throw error;
6 | }
7 | };
8 | module.exports = errorThrower;
9 |
--------------------------------------------------------------------------------
/src/utils/image_util.js:
--------------------------------------------------------------------------------
1 | const Imgur = require("imgur");
2 | const sharp = require("sharp");
3 | const blurHash = require("blurhash");
4 | const Image = require("../models/image");
5 |
6 | // factor=width/height
7 | // factor=500/600=0.83
8 | exports.encodeImageToBlurHash = (data, factor = 1) =>
9 | new Promise((resolve, reject) => {
10 | sharp(data)
11 | .raw()
12 | .ensureAlpha()
13 | .resize(Math.round(44 * factor), 44, { fit: "inside" })
14 | .toBuffer((err, buffer, { width, height }) => {
15 | if (err) return reject(err);
16 | resolve(
17 | blurHash.encode(new Uint8ClampedArray(buffer), width, height, 4, 4)
18 | );
19 | });
20 | });
21 | exports.deleteImageById = async ({ id }) => {
22 | if (id) {
23 | const image = await Image.findByIdAndDelete(id);
24 | if (image) {
25 | Imgur.deleteImage(image.deletehash);
26 | }
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/src/utils/is_mongo_id.js:
--------------------------------------------------------------------------------
1 | const ObjectId = require("mongoose").Types.ObjectId;
2 | const isMongoID = (id) =>
3 | ObjectId.isValid(id) && new ObjectId(id).toString() === id;
4 | module.exports = isMongoID;
5 |
--------------------------------------------------------------------------------
/src/utils/json_success_fail.js:
--------------------------------------------------------------------------------
1 | module.exports = (app) => {
2 | app.response.jsonSuccess = function (obj, statusCode) {
3 | return this.status(statusCode ?? 200).json({
4 | status: true,
5 | ...obj,
6 | });
7 | };
8 | app.response.jsonFail = function (obj, statusCode) {
9 | return this.status(statusCode ?? 400).json({
10 | status: false,
11 | ...obj,
12 | });
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/jwt_promise.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | exports.jwtSign = ({
3 | payload = {},
4 | additionalSecret = "",
5 | expiresIn = "30 days",
6 | }) => {
7 | return new Promise((resolve, reject) => {
8 | jwt.sign(
9 | payload,
10 | `${process.env.PRIVATE_SERVER_KEY}${additionalSecret}`,
11 | { expiresIn: expiresIn },
12 | (err, token) => {
13 | if (err) {
14 | return reject(err);
15 | }
16 | resolve(token);
17 | }
18 | );
19 | });
20 | };
21 | exports.jwtVerify = (token, additionalSecret = "") => {
22 | return new Promise((resolve, reject) => {
23 | jwt.verify(
24 | token,
25 | `${process.env.PRIVATE_SERVER_KEY}${additionalSecret}`,
26 | {},
27 | (err, decodeToken) => {
28 | if (err || !decodeToken) {
29 | const error = new Error("Not authenticated");
30 | error.statusCode = 401;
31 | return reject(error);
32 | }
33 | resolve(decodeToken);
34 | }
35 | );
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/param_validation.js:
--------------------------------------------------------------------------------
1 | const { validationResult } = require("express-validator");
2 | const errorThrower = require("../utils/error_thrower");
3 |
4 | module.exports = (req, res, next) => {
5 | const errors = validationResult(req)
6 | .array({ onlyFirstError: true })
7 | .map((error) => `param ${error.param} ${error.msg}`)
8 | .join(" && ");
9 | if (errors.length > 0) {
10 | errorThrower(errors, 422);
11 | }
12 | next();
13 | };
14 |
--------------------------------------------------------------------------------
/test/fixtures/keys_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binSaed/e-commerce-backend-nodejs/c54c503b26c418501c7059cbb74a5922a2df0125/test/fixtures/keys_bg.png
--------------------------------------------------------------------------------
/test/integration_test/404.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../src/app");
3 |
4 | const server = request(app);
5 |
6 | describe("404", () => {
7 | test("should get 404 api not found", async (done) => {
8 | const response = await server
9 | .get("/randomRoute")
10 | .expect("Content-Type", /json/)
11 | .expect(404);
12 |
13 | const body = response.body;
14 | expect(body).toMatchObject({ status: false });
15 | done();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/integration_test/auth/login.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const User = require("../../../src/models/user");
4 |
5 | const server = request(app);
6 |
7 | let userData = {
8 | email: "me@abdosaed.ml",
9 | password: "abdo1234",
10 | name: "abdelrahman",
11 | phone: "01151034858",
12 | };
13 | beforeEach(() => {
14 | userData = {
15 | email: "me@abdosaed.ml",
16 | password: "abdo1234",
17 | name: "abdelrahman",
18 | phone: "01151034858",
19 | };
20 | });
21 | beforeAll(async () => {
22 | await User.deleteMany();
23 | const user = await new User(userData).save();
24 | });
25 | describe("login", () => {
26 | test("login should be fail when body not send", async () => {
27 | const response = await server
28 | .post("/api/auth/login")
29 | .expect("Content-Type", /json/)
30 | .expect(422);
31 | const { body } = response;
32 | expect(body.status).toBe(false);
33 | });
34 | test("login should be fail when body with not valid pram", async () => {
35 | userData.email = "abdo.ml";
36 | userData.password = "1234";
37 | const response = await server
38 | .post("/api/auth/login")
39 | .send(userData)
40 | .expect("Content-Type", /json/)
41 | .expect(422);
42 |
43 | const { body } = response;
44 |
45 | expect(body.status).toBe(false);
46 |
47 | expect(body.message).toContain("email");
48 | expect(body.message).toContain("param password");
49 | });
50 | test("login should be fail when email not found", async () => {
51 | userData.email = "wrongEmail";
52 | const response = await server
53 | .post("/api/auth/login")
54 | .send(userData)
55 | .expect("Content-Type", /json/)
56 | .expect(422);
57 |
58 | const { body } = response;
59 |
60 | expect(body.status).toBe(false);
61 | });
62 | test("login should be fail when wrong password", async () => {
63 | userData.password = "wrongPassword";
64 | const response = await server
65 | .post("/api/auth/login")
66 | .send(userData)
67 | .expect("Content-Type", /json/)
68 | .expect(422);
69 |
70 | const { body } = response;
71 |
72 | expect(body.status).toBe(false);
73 | });
74 | test("should login", async () => {
75 | const response = await server
76 | .post("/api/auth/login")
77 | .send(userData)
78 | .expect("Content-Type", /json/)
79 | .expect(200);
80 |
81 | const { body } = response;
82 |
83 | expect(body.status).toBe(true);
84 |
85 | expect(userData.name).toBe(body.user.name);
86 | expect(userData.email).toBe(body.user.email);
87 | expect(body.token.toString().split(".").length).toBe(3);
88 | });
89 |
90 | test("should save fcmToken when login", async () => {
91 | const user = { ...userData, fcmToken: "token" };
92 |
93 | const response = await server
94 | .post("/api/auth/login")
95 | .send(user)
96 | .expect("Content-Type", /json/)
97 | .expect(200);
98 |
99 | const { body } = response;
100 |
101 | expect(body.status).toBe(true);
102 |
103 | const userInDB = await User.findById(body.user._id);
104 |
105 | expect(body.status).toBe(true);
106 |
107 | expect(userInDB.fcmTokens.length).toBe(1);
108 | expect(userInDB.fcmTokens[0]).toEqual(user.fcmToken);
109 | });
110 | test("should save fcmToken when login without duplicate", async () => {
111 | const user = { ...userData, fcmToken: "token" };
112 | await server.post("/api/auth/login").send(user);
113 | const response = await server
114 | .post("/api/auth/login")
115 | .send(user)
116 | .expect("Content-Type", /json/)
117 | .expect(200);
118 |
119 | const { body } = response;
120 |
121 | expect(body.status).toBe(true);
122 |
123 | const userInDB = await User.findById(body.user._id);
124 |
125 | expect(body.status).toBe(true);
126 |
127 | expect(userInDB.fcmTokens.length).toBe(1);
128 | });
129 | });
130 |
--------------------------------------------------------------------------------
/test/integration_test/auth/profile.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const User = require("../../../src/models/user");
4 |
5 | const server = request(app);
6 |
7 | let userData = {
8 | email: "me@abdosaed.ml",
9 | password: "abdo1234",
10 | name: "abdelrahman",
11 | phone: "01151034858",
12 | };
13 | let token;
14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token
15 | beforeEach(() => {
16 | userData = {
17 | email: "me@abdosaed.ml",
18 | password: "abdo1234",
19 | name: "abdelrahman",
20 | phone: "01151034858",
21 | };
22 | });
23 | beforeAll(async () => {
24 | await User.deleteMany();
25 | const user = await new User(userData).save();
26 | token = await user.generateJWT();
27 | });
28 |
29 | describe("profile", () => {
30 | test("should return user profile", async () => {
31 | const response = await server
32 | .post("/api/auth/profile")
33 | .set("Authorization", `Bearer ${token}`)
34 | .expect("Content-Type", /json/)
35 | .expect(200);
36 |
37 | const body = response.body;
38 |
39 | expect(body.status).toBe(true);
40 |
41 | expect(userData.name).toBe(body.user.name);
42 | expect(userData.email).toBe(body.user.email);
43 | });
44 | test("should fail not valid token", async (done) => {
45 | const response = await server
46 | .post("/api/auth/profile")
47 | .set("Authorization", `Bearer ${wrongToken()}`)
48 | .expect("Content-Type", /json/)
49 | .expect(401);
50 | done();
51 |
52 | const { body } = response;
53 |
54 | expect(body.status).toBe(false);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/integration_test/auth/refreshFcmToken.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const User = require("../../../src/models/user");
4 |
5 | const server = request(app);
6 |
7 | let userData = {
8 | email: "me@abdosaed.ml",
9 | password: "abdo1234",
10 | name: "abdelrahman",
11 | phone: "01151034858",
12 | };
13 | let token;
14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token
15 | beforeAll(async () => {
16 | await User.deleteMany();
17 | const user = await new User(userData).save();
18 | token = await user.generateJWT();
19 | });
20 |
21 | describe("refreshFcmToken", () => {
22 | test("should fail not valid token", async () => {
23 | const response = await server
24 | .post("/api/auth/refreshFcmToken")
25 | .set("Authorization", `Bearer ${wrongToken()}`)
26 | .expect("Content-Type", /json/)
27 | .expect(401);
28 |
29 | const { body } = response;
30 |
31 | expect(body.status).toBe(false);
32 | });
33 |
34 | test("should success valid fcmToken", async () => {
35 | const response = await server
36 | .post("/api/auth/refreshFcmToken")
37 | .send({ fcmToken: "fcmToken1234" })
38 | .set("Authorization", `Bearer ${token}`)
39 | .expect("Content-Type", /json/)
40 | .expect(200);
41 |
42 | const { body } = response;
43 |
44 | expect(body.status).toBe(true);
45 | });
46 |
47 | test("should fail not valid fcmToken", async () => {
48 | //valid fcmToken Length up to 8
49 | const response = await server
50 | .post("/api/auth/refreshFcmToken")
51 | .set("Authorization", `Bearer ${token}`)
52 | .expect("Content-Type", /json/)
53 | .expect(422);
54 |
55 | const { body } = response;
56 |
57 | expect(body.status).toBe(false);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/test/integration_test/auth/register.test.js:
--------------------------------------------------------------------------------
1 | const app = require("../../../src/app");
2 | const User = require("../../../src/models/user");
3 | const request = require("supertest");
4 |
5 | const server = request(app);
6 |
7 | let userData = {
8 | email: "me@abdosaed.ml",
9 | password: "abdo1234",
10 | name: "abdelrahman",
11 | phone: "01151034858",
12 | };
13 | beforeEach(() => {
14 | userData = {
15 | email: "me@abdosaed.ml",
16 | password: "abdo1234",
17 | name: "abdelrahman",
18 | phone: "01151034858",
19 | };
20 | });
21 | beforeAll(async () => {
22 | await User.deleteMany();
23 | });
24 | afterEach(async () => {
25 | await User.deleteMany();
26 | });
27 |
28 | describe("register", () => {
29 | test("should fail register when body not send", async () => {
30 | const response = await server
31 | .post("/api/auth/register")
32 | .expect("Content-Type", /json/)
33 |
34 | .expect(422);
35 |
36 | const { body } = response;
37 |
38 | expect(body.status).toBe(false);
39 |
40 | const emailNotValid = body.message
41 | .toString()
42 | .toLowerCase()
43 | .includes("email");
44 | expect(emailNotValid).toBe(true);
45 | });
46 | test("should fail register when body with not valid pram", async () => {
47 | userData.email = "abdo.ml";
48 | userData.password = "1234";
49 | userData.name = "al";
50 | const response = await server
51 | .post("/api/auth/register")
52 | .send(userData)
53 | .expect("Content-Type", /json/)
54 |
55 | .expect(422);
56 |
57 | const { body } = response;
58 |
59 | expect(body.status).toBe(false);
60 |
61 | const emailNotValid = body.message
62 | .toString()
63 | .toLowerCase()
64 | .includes("email");
65 | const nameNotValid = body.message
66 | .toString()
67 | .toLowerCase()
68 | .includes("param name");
69 | const passwordNotValid = body.message
70 | .toString()
71 | .toLowerCase()
72 | .includes("param password");
73 | expect(emailNotValid).toBe(true);
74 | expect(nameNotValid).toBe(true);
75 | expect(passwordNotValid).toBe(true);
76 | });
77 | test("should register new user", async () => {
78 | const response = await server
79 | .post("/api/auth/register")
80 | .send(userData)
81 | .expect("Content-Type", /json/)
82 | .expect(200);
83 |
84 | const body = response.body;
85 |
86 | expect(body.status).toBe(true);
87 |
88 | expect(userData.name).toBe(body.user.name);
89 | expect(userData.email).toBe(body.user.email);
90 | expect(body.token.toString().split(".").length).toBe(3);
91 | });
92 | test("should fail register when email used before", async () => {
93 | await server.post("/api/auth/register").send(userData);
94 | const response = await server
95 | .post("/api/auth/register")
96 | .send(userData)
97 | .expect("Content-Type", /json/)
98 |
99 | .expect(422);
100 |
101 | const { body } = response;
102 |
103 | expect(body.status).toBe(false);
104 |
105 | const emailNotValid = body.message
106 | .toString()
107 | .toLowerCase()
108 | .includes("email");
109 | expect(emailNotValid).toBe(true);
110 | });
111 | test("should not save fcmToken if it empty", async () => {
112 | const user = { ...userData };
113 | const response = await server
114 | .post("/api/auth/register")
115 | .send(user)
116 | .expect("Content-Type", /json/)
117 | .expect(200);
118 | const { body } = response;
119 | const userInDB = await User.findById(body.user._id);
120 |
121 | expect(body.status).toBe(true);
122 |
123 | expect(userInDB.fcmTokens.length).toBe(0);
124 | });
125 | test("should save fcmToken if it not empty", async () => {
126 | const user = { ...userData, fcmToken: "token" };
127 | const response = await server
128 | .post("/api/auth/register")
129 | .send(user)
130 | .expect("Content-Type", /json/)
131 | .expect(200);
132 | const { body } = response;
133 | const userInDB = await User.findById(body.user._id);
134 |
135 | expect(body.status).toBe(true);
136 |
137 | expect(userInDB.fcmTokens.length).toBe(1);
138 | expect(userInDB.fcmTokens[0]).toEqual(user.fcmToken);
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/test/integration_test/auth/resetPassword.test.js:
--------------------------------------------------------------------------------
1 | const app = require("../../../src/app");
2 | const User = require("../../../src/models/user");
3 | const request = require("supertest");
4 | const emails = require("../../../src/services/emails/emails");
5 |
6 | const server = request(app);
7 |
8 | let userData = {
9 | email: "me@abdosaed.ml",
10 | password: "abdo1234",
11 | name: "abdelrahman",
12 | phone: "01151034858",
13 | };
14 | let token;
15 | const wrongToken = () => token.replace("a", "b"); //to be wrong token
16 | beforeEach(() => {
17 | userData = {
18 | email: "me@abdosaed.ml",
19 | password: "abdo1234",
20 | name: "abdelrahman",
21 | phone: "01151034858",
22 | };
23 | });
24 | beforeAll(async () => {
25 | await User.deleteMany();
26 | const user = await new User(userData).save();
27 | token = await user.generateJWT();
28 | });
29 |
30 | describe("resetPassword", () => {
31 | test("should send resetPassword to email", async () => {
32 | emails.sendResetPassword = jest.fn();
33 | const response = await server
34 | .post("/api/auth/resetPassword")
35 | .send(userData)
36 | .expect("Content-Type", /json/)
37 | .expect(200);
38 |
39 | const { body } = response;
40 |
41 | expect(body.status).toBe(true);
42 |
43 | expect(emails.sendResetPassword).toHaveBeenCalled();
44 | });
45 | test("should fail and not send resetPassword if email not found", async () => {
46 | userData.email = "notfound@gmail.com";
47 | emails.sendResetPassword = jest.fn();
48 | const response = await server
49 | .post("/api/auth/resetPassword")
50 | .send(userData)
51 | .expect("Content-Type", /json/)
52 | .expect(422);
53 |
54 | const { body } = response;
55 |
56 | expect(body.status).toBe(false);
57 |
58 | expect(emails.sendResetPassword).toHaveBeenCalledTimes(0);
59 | });
60 | });
61 | describe("verifyResetPassword", () => {
62 | test("should get user token if resetPasswordToken is valid", async () => {
63 | emails.sendResetPassword = jest.fn();
64 | await server.post("/api/auth/resetPassword").send(userData);
65 | const { resetPasswordToken, code } =
66 | emails.sendResetPassword.mock.calls[0][0];
67 |
68 | const response = await server
69 | .get(
70 | `/api/auth/verifyResetPassword?resetPasswordToken=${resetPasswordToken}&code=${code}`
71 | )
72 | .expect("Content-Type", /json/)
73 | .expect(200);
74 | const { body } = response;
75 | expect(emails.sendResetPassword).toBeCalled();
76 | expect(body.status).toBe(true);
77 | expect(body.token).not.toBeNull();
78 | });
79 | test("should fail with wrong email", async () => {});
80 | test("should fail with wrong token and code", async () => {
81 | emails.sendResetPassword = jest.fn();
82 | await server.post("/api/auth/resetPassword").send(userData);
83 | const { resetPasswordToken, code } =
84 | emails.sendResetPassword.mock.calls[0][0];
85 |
86 | const response = await server
87 | .get(
88 | `/api/auth/verifyResetPassword?resetPasswordToken=${resetPasswordToken.replace(
89 | "a",
90 | "b"
91 | )}&code=${code}`
92 | )
93 | .expect("Content-Type", /json/)
94 | .expect(401);
95 | const { body } = response;
96 |
97 | expect(body.status).toBe(false);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/test/integration_test/auth/updateProfile.test.js:
--------------------------------------------------------------------------------
1 | const app = require("../../../src/app");
2 | const User = require("../../../src/models/user");
3 | const request = require("supertest");
4 |
5 | const server = request(app);
6 |
7 | let userData = {
8 | email: "me@abdosaed.ml",
9 | password: "abdo1234",
10 | name: "abdelrahman",
11 | phone: "01151034858",
12 | };
13 | let token;
14 | const wrongToken = () => token.replace("a", "b"); //to be wrong token
15 | beforeEach(() => {
16 | userData = {
17 | email: "me@abdosaed.ml",
18 | password: "abdo1234",
19 | name: "abdelrahman",
20 | phone: "01151034858",
21 | };
22 | });
23 | beforeAll(async () => {
24 | await User.deleteMany();
25 | const user = await new User(userData).save();
26 | token = await user.generateJWT();
27 | });
28 |
29 | describe("updateProfile", () => {
30 | test("should update the user with valid data", async () => {
31 | const newName = "newname";
32 | const newEmail = "newemail";
33 | const newPhone = "newphone";
34 | const response = await server
35 | .put("/api/auth/updateProfile")
36 | .set("Authorization", `Bearer ${token}`)
37 | .field("name", newName)
38 | .field("email", newEmail)
39 | .field("phone", newPhone)
40 | .expect("Content-Type", /json/)
41 | .expect(200);
42 |
43 | const { body } = response;
44 | expect(body.status).toBe(true);
45 |
46 | const { user } = body;
47 | expect(user.email).toEqual(newEmail);
48 | expect(user.name).toEqual(newName);
49 | expect(user.phone).toEqual(newPhone);
50 | });
51 | test("should be fail update the user with wrong token", async () => {
52 | const response = await server
53 | .put("/api/auth/updateProfile")
54 | .set("Authorization", `Bearer ${wrongToken()}`)
55 | .expect("Content-Type", /json/)
56 | .expect(401);
57 |
58 | const { body } = response;
59 | expect(body.status).toBe(false);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/test/integration_test/auth/verifyEmail.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const nodemailer = require("nodemailer");
3 | const app = require("../../../src/app");
4 | const User = require("../../../src/models/user");
5 | const emails = require("../../../src/services/emails/emails");
6 |
7 | const server = request(app);
8 |
9 | let userData = {
10 | email: "me@abdosaed.ml",
11 | password: "abdo1234",
12 | name: "abdelrahman",
13 | phone: "01151034858",
14 | };
15 | let token;
16 | const wrongToken = () => token.replace("a", "b"); //to be wrong token
17 |
18 | beforeEach(() => {
19 | userData = {
20 | email: "me@abdosaed.ml",
21 | password: "abdo1234",
22 | name: "abdelrahman",
23 | phone: "01151034858",
24 | };
25 | });
26 | beforeAll(async () => {
27 | await User.deleteMany();
28 | const user = await new User(userData).save();
29 | token = await user.generateJWT();
30 | });
31 |
32 | describe("reSendConfirmEmail", () => {
33 | test("should send email", async () => {
34 | const sendMail = jest.fn().mockResolvedValue("done");
35 | nodemailer.createTransport = jest.fn().mockReturnValue({ sendMail });
36 | const response = await server
37 | .post("/api/auth/reSendConfirmEmail")
38 | .set("Authorization", `Bearer ${token}`)
39 | .expect("Content-Type", /json/)
40 | .expect(200);
41 |
42 | const { body } = response;
43 |
44 | expect(body.status).toBe(true);
45 | expect(nodemailer.createTransport).toHaveBeenCalled();
46 | });
47 | test("should fail to send email not valid token", async () => {
48 | nodemailer.createTransport = jest.fn();
49 | const response = await server
50 | .post("/api/auth/reSendConfirmEmail")
51 | .set("Authorization", `Bearer ${wrongToken}`)
52 | .expect("Content-Type", /json/)
53 | .expect(401);
54 |
55 | const { body } = response;
56 |
57 | expect(body.status).toBe(false);
58 | expect(nodemailer.createTransport).toBeCalledTimes(0);
59 | });
60 | });
61 | describe("verifyEmail", () => {
62 | test("should set emailVerified to true", async () => {
63 | emails.sendConfirmEmail = jest.fn();
64 | await server
65 | .post("/api/auth/reSendConfirmEmail")
66 | .set("Authorization", `Bearer ${token}`);
67 | const { confirmEmailToken, code } =
68 | emails.sendConfirmEmail.mock.calls[0][0];
69 |
70 | const response = await server
71 | .get(
72 | `/api/auth/verifyEmail?confirmEmailToken=${confirmEmailToken}&code=${code}`
73 | )
74 | .expect("Content-Type", /json/)
75 | .expect(200);
76 | const { body } = response;
77 |
78 | expect(body.status).toBe(true);
79 | expect(body.user.emailVerified).toBe(true);
80 | });
81 | test("should fail with wrong token", async () => {
82 | emails.sendConfirmEmail = jest.fn();
83 | await server
84 | .post("/api/auth/reSendConfirmEmail")
85 | .set("Authorization", `Bearer ${wrongToken()}`)
86 | .expect(401);
87 |
88 | expect(emails.sendConfirmEmail).toBeCalledTimes(0);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/test/integration_test/category/addCategory.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 | const Item = require("../../../src/models/item");
5 | const TokenUtils = require("../../uitils/tokenUtils");
6 |
7 | const server = request(app);
8 | const category = {
9 | name: { en: "categoryName", ar: "categoryName" },
10 | };
11 | const categoryWithoutArIntel = { name: { en: "categoryName" } };
12 | const categoryWithoutEnIntel = { name: { ar: "categoryName" } };
13 |
14 | beforeEach(async () => {
15 | await Category.deleteMany();
16 | await Item.deleteMany();
17 | });
18 |
19 | beforeAll(async () => {});
20 | afterAll(async () => {
21 | jest.resetAllMocks();
22 | });
23 |
24 | describe("addCategory", () => {
25 | test("should create category when user has access", async () => {
26 | const response = await server
27 | .post(`/api/category/add`)
28 | .send(category)
29 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
30 | .expect("Content-Type", /json/)
31 | .expect(200);
32 | const body = response.body;
33 |
34 | expect(body.status).toBeTruthy();
35 |
36 | const categories = await Category.find();
37 |
38 | expect(
39 | JSON.stringify(category.name).includes(categories[0].name)
40 | ).toBeTruthy();
41 | });
42 |
43 | test("should not create category when token expired (Unauthorized)", async () => {
44 | const response = await server
45 | .post(`/api/category/add`)
46 | .send(category)
47 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`)
48 | .expect("Content-Type", /json/)
49 | .expect(401);
50 | const body = response.body;
51 |
52 | expect(body.status).toBeFalsy();
53 | expect(body).toHaveProperty("message");
54 |
55 | const categories = await Category.find();
56 | expect(categories.length).toBe(0);
57 | });
58 | test("should not create category when user hasn't access (Forbidden)", async () => {
59 | const response = await server
60 | .post(`/api/category/add`)
61 | .send(category)
62 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`)
63 | .expect("Content-Type", /json/)
64 | .expect(403);
65 | const body = response.body;
66 |
67 | expect(body.status).toBeFalsy();
68 | expect(body).toHaveProperty("message");
69 |
70 | const categories = await Category.find();
71 | expect(categories.length).toBe(0);
72 | });
73 | test("should not create category when user not send data", async () => {
74 | const response = await server
75 | .post(`/api/category/add`)
76 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
77 | .expect("Content-Type", /json/)
78 | .expect(422);
79 | const body = response.body;
80 |
81 | expect(body.status).toBeFalsy();
82 | expect(body).toHaveProperty("message");
83 |
84 | const categories = await Category.find();
85 | expect(categories.length).toBe(0);
86 | });
87 | test("should not create category when data not valid", async () => {
88 | const response1 = await server
89 | .post(`/api/category/add`)
90 | .send(categoryWithoutArIntel)
91 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
92 | .expect("Content-Type", /json/)
93 | .expect(422);
94 | const body1 = response1.body;
95 |
96 | expect(body1.status).toBeFalsy();
97 | expect(body1).toHaveProperty("message");
98 |
99 | const categories1 = await Category.find();
100 | expect(categories1.length).toBe(0);
101 |
102 | const response2 = await server
103 | .post(`/api/category/add`)
104 | .send(categoryWithoutEnIntel)
105 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
106 | .expect("Content-Type", /json/)
107 | .expect(422);
108 | const body2 = response2.body;
109 |
110 | expect(body2.status).toBeFalsy();
111 | expect(body2).toHaveProperty("message");
112 |
113 | const categories2 = await Category.find();
114 | expect(categories2.length).toBe(0);
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/test/integration_test/category/addCategoryUploadImage.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 | const Item = require("../../../src/models/item");
5 | const TokenUtils = require("../../uitils/tokenUtils");
6 |
7 | jest.mock("multer", () => () => {
8 | return {
9 | array: jest.fn(() => (req, res, next) => next()),
10 | single: jest.fn(() => {
11 | return (req, res, next) => {
12 | req.body = {
13 | name: { en: "categoryName", ar: "categoryName" },
14 | };
15 | req.file = {
16 | imageHash: "sample.name",
17 | link: "sample.type",
18 | deletehash: "sample.url",
19 | };
20 | return next();
21 | };
22 | }),
23 | };
24 | });
25 |
26 | const server = request(app);
27 | const category = {
28 | name: { en: "categoryName", ar: "categoryName" },
29 | };
30 |
31 | beforeEach(async () => {
32 | await Category.deleteMany();
33 | await Item.deleteMany();
34 | });
35 |
36 | beforeAll(async () => {});
37 | afterAll(async () => {
38 | jest.resetAllMocks();
39 | });
40 |
41 | describe("addCategoryUploadImage", () => {
42 | test("should create category with image if user upload image", async () => {
43 | const response = await server
44 | .post(`/api/category/add`)
45 | .field("name", JSON.stringify(category.name))
46 | .attach("image", "test/fixtures/keys_bg.png")
47 | .set("Connection", "keep-alive")
48 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
49 | .expect("Content-Type", /json/)
50 | .expect(200);
51 |
52 | const body = response.body;
53 |
54 | expect(body.status).toBeTruthy();
55 | });
56 |
57 | test("should not create category when token expired (Unauthorized)", async () => {
58 | const response = await server
59 | .post(`/api/category/add`)
60 | .field("name", JSON.stringify(category.name))
61 | .attach("image", "test/fixtures/keys_bg.png")
62 | .set("Connection", "keep-alive")
63 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`)
64 | .expect("Content-Type", /json/)
65 | .expect(401);
66 | const body = response.body;
67 |
68 | expect(body.status).toBeFalsy();
69 | expect(body).toHaveProperty("message");
70 |
71 | const categories = await Category.find();
72 | expect(categories.length).toBe(0);
73 | });
74 | test("should not create category when user hasn't access (Forbidden)", async () => {
75 | const response = await server
76 | .post(`/api/category/add`)
77 | .field("name", JSON.stringify(category.name))
78 | .attach("image", "test/fixtures/keys_bg.png")
79 | .set("Connection", "keep-alive")
80 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`)
81 | .expect("Content-Type", /json/)
82 | .expect(403);
83 | const body = response.body;
84 |
85 | expect(body.status).toBeFalsy();
86 | expect(body).toHaveProperty("message");
87 |
88 | const categories = await Category.find();
89 | expect(categories.length).toBe(0);
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/test/integration_test/category/addItemToCategory.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 | const Item = require("../../../src/models/item");
5 | const TokenUtils = require("../../uitils/tokenUtils");
6 |
7 | const server = request(app);
8 |
9 | beforeEach(async () => {
10 | await Category.deleteMany();
11 | await Item.deleteMany();
12 | });
13 | afterAll(async () => jest.resetAllMocks());
14 |
15 | describe("addItemToCategory", () => {
16 | test("should not add item to category when token expired (Unauthorized)", async () => {
17 | const category = new Category({
18 | name: { en: "categoryName", ar: "categoryName" },
19 | });
20 | await category.save();
21 |
22 | const item = new Item({
23 | title: { en: "itemTitle", ar: "itemTitle" },
24 | disc: { en: "itemDisc", ar: "itemDisc" },
25 | });
26 | await item.save();
27 |
28 | const response = await server
29 | .post(`/api/category/${category._id}/addItem`)
30 | .send({ itemId: item._id })
31 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`)
32 | .expect("Content-Type", /json/)
33 | .expect(401);
34 |
35 | const body = response.body;
36 | expect(body.status).toBe(false);
37 | expect(body).toHaveProperty("message");
38 |
39 | const categoryAfterInsert = await Category.findById(category._id);
40 |
41 | expect(categoryAfterInsert.items.includes(item._id)).toBe(false);
42 | });
43 | test("should not add item to category when user hasn't access (Forbidden)", async () => {
44 | const category = new Category({
45 | name: { en: "categoryName", ar: "categoryName" },
46 | });
47 | await category.save();
48 |
49 | const item = new Item({
50 | title: { en: "itemTitle", ar: "itemTitle" },
51 | disc: { en: "itemDisc", ar: "itemDisc" },
52 | });
53 | await item.save();
54 |
55 | const response = await server
56 | .post(`/api/category/${category._id}/addItem`)
57 | .send({ itemId: item._id })
58 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`)
59 | .expect("Content-Type", /json/)
60 | .expect(403);
61 |
62 | const body = response.body;
63 | expect(body.status).toBe(false);
64 | expect(body).toHaveProperty("message");
65 |
66 | const categoryAfterInsert = await Category.findById(category._id);
67 |
68 | expect(categoryAfterInsert.items.includes(item._id)).toBe(false);
69 | });
70 | test("should add item to category when user has access", async () => {
71 | const category = new Category({
72 | name: { en: "categoryName", ar: "categoryName" },
73 | });
74 | await category.save();
75 |
76 | const item = new Item({
77 | title: { en: "itemTitle", ar: "itemTitle" },
78 | disc: { en: "itemDisc", ar: "itemDisc" },
79 | });
80 | await item.save();
81 |
82 | const response = await server
83 | .post(`/api/category/${category._id}/addItem`)
84 | .send({ itemId: item._id })
85 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
86 | .expect("Content-Type", /json/)
87 | .expect(200);
88 | const body = response.body;
89 |
90 | expect(body.status).toBe(true);
91 |
92 | const categoryAfterInsert = await Category.findById(category._id);
93 |
94 | expect(categoryAfterInsert.items.includes(item._id)).toBe(true);
95 | });
96 | test("should return 404 with message if wrong categoryId", async () => {
97 | const item = new Item({
98 | title: { en: "itemTitle", ar: "itemTitle" },
99 | disc: { en: "itemDisc", ar: "itemDisc" },
100 | });
101 | await item.save();
102 |
103 | const wrongCategoryId = "60a9deeb06496a08843fa4a2";
104 |
105 | const response = await server
106 | .post(`/api/category/${wrongCategoryId}/addItem`)
107 | .send({ itemId: item._id })
108 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
109 | .expect("Content-Type", /json/)
110 | .expect(404);
111 |
112 | const body = response.body;
113 |
114 | expect(body.status).toBe(false);
115 | expect(body).toHaveProperty("message");
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/test/integration_test/category/getCategories.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 |
5 | const server = request(app);
6 |
7 | beforeEach(() => {});
8 | beforeAll(async () => {
9 | await Category.deleteMany();
10 | });
11 |
12 | describe("getCategories", () => {
13 | test("should return empty list if no Categories", async () => {
14 | const response = await server
15 | .get("/api/category/getAll")
16 | .expect("Content-Type", /json/)
17 | .expect(200);
18 |
19 | const body = response.body;
20 |
21 | expect(body.status).toBe(true);
22 | expect(body.categories).toBeInstanceOf(Array);
23 | expect(body.categories.length).toBe(0);
24 | });
25 | test("should return list of Categories if it has", async () => {
26 | const category = new Category({
27 | name: { en: "categoryName", ar: "categoryName" },
28 | });
29 | await category.save();
30 |
31 | const response = await server
32 | .get("/api/category/getAll")
33 | .expect("Content-Type", /json/)
34 | .expect(200);
35 |
36 | const { body } = response;
37 |
38 | expect(body.status).toBe(true);
39 | expect(body.categories).toBeInstanceOf(Array);
40 | expect(body.categories.length).toBe(1);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/integration_test/item/addItem.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 | const Item = require("../../../src/models/item");
5 | const TokenUtils = require("../../uitils/tokenUtils");
6 |
7 | const server = request(app);
8 | const item = {
9 | title: { en: "sdfsdf", ar: " vfddfdf" },
10 | disc: { en: "sdfsdf", ar: " vfddfdf" },
11 | unitName: { en: "sdfsdf", ar: " vfddfdf" },
12 | price: 1,
13 | categoryId: "60a94a17bddc6f1838b0e1d1",
14 | discount: "4",
15 | maxQuantityInOrder: "5",
16 | };
17 | const itemWithoutTitleArIntel = () => {
18 | const newItem = { ...item };
19 | newItem.title = { en: "sdfsdf" };
20 | return newItem;
21 | };
22 | const itemWithoutTitleEnIntel = () => {
23 | const newItem = { ...item };
24 | newItem.title = { ar: "sdfsdf" };
25 | return newItem;
26 | };
27 | const itemWithoutDiscArIntel = () => {
28 | const newItem = { ...item };
29 | newItem.disc = { en: "sdfsdf" };
30 | return newItem;
31 | };
32 | const itemWithoutDiscEnIntel = () => {
33 | const newItem = { ...item };
34 | newItem.disc = { ar: "sdfsdf" };
35 | return newItem;
36 | };
37 | const itemWithoutUnitNameArIntel = () => {
38 | const newItem = { ...item };
39 | newItem.unitName = { en: "sdfsdf" };
40 | return newItem;
41 | };
42 | const itemWithoutUnitNameEnIntel = () => {
43 | const newItem = { ...item };
44 | newItem.unitName = { ar: "sdfsdf" };
45 | return newItem;
46 | };
47 | const itemWithoutPrice = () => {
48 | const newItem = { ...item };
49 | delete newItem.price;
50 | return newItem;
51 | };
52 |
53 | const itemWithNotValidPrice = () => {
54 | const newItem = { ...item };
55 | newItem.price = "ss";
56 | return newItem;
57 | };
58 | const itemWithPriceUnderOne = () => {
59 | const newItem = { ...item };
60 | newItem.price = 0;
61 | return newItem;
62 | };
63 |
64 | beforeEach(async () => {
65 | await Category.deleteMany();
66 | await Item.deleteMany();
67 | });
68 |
69 | beforeAll(async () => {
70 | jest.resetAllMocks();
71 | });
72 | afterAll(async () => {});
73 |
74 | describe("addCategory", () => {
75 | test("should create item when user has access", async () => {
76 | const response = await server
77 | .post(`/api/items/add`)
78 | .send(item)
79 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
80 | .expect("Content-Type", /json/)
81 | .expect(200);
82 | const body = response.body;
83 |
84 | expect(body.status).toBeTruthy();
85 |
86 | const items = await Item.find();
87 |
88 | expect(JSON.stringify(item.title).includes(items[0].title)).toBeTruthy();
89 | expect(JSON.stringify(item.disc).includes(items[0].disc)).toBeTruthy();
90 | expect(
91 | JSON.stringify(item.unitName).includes(items[0].units[0].name)
92 | ).toBeTruthy();
93 | expect(item.price).toEqual(items[0].units[0].price);
94 | });
95 | test("should create item and add to category when user has access", async () => {
96 | const category = new Category({
97 | name: { en: "categoryName", ar: "categoryName" },
98 | });
99 | await category.save();
100 | const categoryId = category._id;
101 |
102 | const response = await server
103 | .post(`/api/items/add`)
104 | .send({ ...item, categoryId })
105 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
106 | .expect("Content-Type", /json/)
107 | .expect(200);
108 | const body = response.body;
109 |
110 | expect(body.status).toBeTruthy();
111 |
112 | const items = await Item.find();
113 | const itemDB = items[0];
114 | expect(JSON.stringify(item.title).includes(itemDB.title)).toBeTruthy();
115 | expect(JSON.stringify(item.disc).includes(itemDB.disc)).toBeTruthy();
116 | expect(
117 | JSON.stringify(item.unitName).includes(itemDB.units[0].name)
118 | ).toBeTruthy();
119 | expect(item.price).toEqual(itemDB.units[0].price);
120 |
121 | const category2 = await Category.findById(categoryId);
122 | expect(category2.items[0]).toEqual(itemDB._id);
123 | });
124 | test("should not create item when token expired (Unauthorized)", async () => {
125 | const response = await server
126 | .post(`/api/items/add`)
127 | .send(item)
128 | .set("Authorization", `Bearer ${await TokenUtils.expiredOwnerToken()}`)
129 | .expect("Content-Type", /json/)
130 | .expect(401);
131 | const body = response.body;
132 |
133 | expect(body.status).toBeFalsy();
134 | expect(body).toHaveProperty("message");
135 |
136 | const items = await Item.find();
137 | expect(items).toHaveLength(0);
138 | });
139 | test("should not create item when user hasn't access (Forbidden)", async () => {
140 | const response = await server
141 | .post(`/api/items/add`)
142 | .send(item)
143 | .set("Authorization", `Bearer ${await TokenUtils.userToken()}`)
144 | .expect("Content-Type", /json/)
145 | .expect(403);
146 | const body = response.body;
147 |
148 | expect(body.status).toBeFalsy();
149 | expect(body).toHaveProperty("message");
150 |
151 | const items = await Item.find();
152 | expect(items.length).toBe(0);
153 | });
154 | test("should not create item when user not send data", async () => {
155 | const response = await server
156 | .post(`/api/items/add`)
157 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
158 | .expect("Content-Type", /json/)
159 | .expect(422);
160 | const body = response.body;
161 |
162 | expect(body.status).toBeFalsy();
163 | expect(body).toHaveProperty("message");
164 |
165 | const items = await Item.find();
166 | expect(items.length).toBe(0);
167 | });
168 | test("should not create item when data not valid", async () => {
169 | const response1 = await server
170 | .post(`/api/items/add`)
171 | .send(itemWithoutTitleArIntel())
172 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
173 | .expect("Content-Type", /json/)
174 | .expect(422);
175 | const body1 = response1.body;
176 |
177 | expect(body1.status).toBeFalsy();
178 | expect(body1).toHaveProperty("message");
179 | const items1 = await Item.find();
180 | expect(items1).toHaveLength(0);
181 |
182 | const response2 = await server
183 | .post(`/api/items/add`)
184 | .send(itemWithoutTitleEnIntel())
185 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
186 | .expect("Content-Type", /json/)
187 | .expect(422);
188 | const body2 = response2.body;
189 |
190 | expect(body2.status).toBeFalsy();
191 | expect(body2).toHaveProperty("message");
192 | const items2 = await Item.find();
193 | expect(items2).toHaveLength(0);
194 |
195 | const response3 = await server
196 | .post(`/api/items/add`)
197 | .send(itemWithoutDiscArIntel())
198 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
199 | .expect("Content-Type", /json/)
200 | .expect(422);
201 | const body3 = response3.body;
202 |
203 | expect(body3.status).toBeFalsy();
204 | expect(body3).toHaveProperty("message");
205 | const items3 = await Item.find();
206 | expect(items3).toHaveLength(0);
207 |
208 | const response4 = await server
209 | .post(`/api/items/add`)
210 | .send(itemWithoutDiscEnIntel())
211 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
212 | .expect("Content-Type", /json/)
213 | .expect(422);
214 | const body4 = response4.body;
215 |
216 | expect(body4.status).toBeFalsy();
217 | expect(body4).toHaveProperty("message");
218 | const items4 = await Item.find();
219 | expect(items4).toHaveLength(0);
220 |
221 | const response5 = await server
222 | .post(`/api/items/add`)
223 | .send(itemWithoutUnitNameArIntel())
224 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
225 | .expect("Content-Type", /json/)
226 | .expect(422);
227 | const body5 = response5.body;
228 |
229 | expect(body5.status).toBeFalsy();
230 | expect(body5).toHaveProperty("message");
231 | const items5 = await Item.find();
232 | expect(items5).toHaveLength(0);
233 |
234 | const response6 = await server
235 | .post(`/api/items/add`)
236 | .send(itemWithoutUnitNameArIntel())
237 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
238 | .expect("Content-Type", /json/)
239 | .expect(422);
240 | const body6 = response6.body;
241 |
242 | expect(body6.status).toBeFalsy();
243 | expect(body6).toHaveProperty("message");
244 | const items6 = await Item.find();
245 | expect(items6).toHaveLength(0);
246 |
247 | const response7 = await server
248 | .post(`/api/items/add`)
249 | .send(itemWithoutUnitNameEnIntel())
250 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
251 | .expect("Content-Type", /json/)
252 | .expect(422);
253 | const body7 = response7.body;
254 |
255 | expect(body7.status).toBeFalsy();
256 | expect(body7).toHaveProperty("message");
257 | const items7 = await Item.find();
258 | expect(items7).toHaveLength(0);
259 |
260 | const response8 = await server
261 | .post(`/api/items/add`)
262 | .send(itemWithoutPrice())
263 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
264 | .expect("Content-Type", /json/)
265 | .expect(422);
266 | const body8 = response8.body;
267 |
268 | expect(body8.status).toBeFalsy();
269 | expect(body8).toHaveProperty("message");
270 | const items8 = await Item.find();
271 | expect(items8).toHaveLength(0);
272 |
273 | const response9 = await server
274 | .post(`/api/items/add`)
275 | .send(itemWithNotValidPrice())
276 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
277 | .expect("Content-Type", /json/)
278 | .expect(422);
279 | const body9 = response9.body;
280 |
281 | expect(body9.status).toBeFalsy();
282 | expect(body9).toHaveProperty("message");
283 | const items9 = await Item.find();
284 | expect(items9).toHaveLength(0);
285 |
286 | const response10 = await server
287 | .post(`/api/items/add`)
288 | .send(itemWithPriceUnderOne())
289 | .set("Authorization", `Bearer ${await TokenUtils.ownerUserToken()}`)
290 | .expect("Content-Type", /json/)
291 | .expect(422);
292 | const body10 = response10.body;
293 |
294 | expect(body10.status).toBeFalsy();
295 | expect(body10).toHaveProperty("message");
296 | const items10 = await Item.find();
297 | expect(items10).toHaveLength(0);
298 | });
299 | });
300 |
--------------------------------------------------------------------------------
/test/integration_test/item/getItems.test.js:
--------------------------------------------------------------------------------
1 | const request = require("supertest");
2 | const app = require("../../../src/app");
3 | const Category = require("../../../src/models/category");
4 | const Item = require("../../../src/models/item");
5 |
6 | const server = request(app);
7 |
8 | beforeEach(async () => {
9 | await Category.deleteMany();
10 | await Item.deleteMany();
11 | });
12 | beforeAll(async () => {
13 | jest.resetAllMocks();
14 | });
15 |
16 | describe("getItems", () => {
17 | test("should return 422 with message if wrongMongoId", async () => {
18 | const wrongMongoId = "60a9fbad6c449219ggc9ec37";
19 | const response = await server
20 | .get(`/api/items/getAll/${wrongMongoId}`)
21 | .expect("Content-Type", /json/)
22 | .expect(422);
23 |
24 | const body = response.body;
25 |
26 | expect(body.status).toBeFalsy();
27 | expect(body).toHaveProperty("message");
28 | });
29 | test("should return 404 with message if notFoundCategoryID", async () => {
30 | const notFoundCategoryID = "60283da17a9e931be015420d";
31 | const response = await server
32 | .get(`/api/items/getAll/${notFoundCategoryID}`)
33 | .expect("Content-Type", /json/)
34 | .expect(404);
35 |
36 | const body = response.body;
37 |
38 | expect(body.status).toBeFalsy();
39 | expect(body).toHaveProperty("message");
40 | });
41 | test("should return list of items if category has", async () => {
42 | const category = new Category({
43 | name: { en: "categoryName", ar: "categoryName" },
44 | });
45 |
46 | const item = new Item({
47 | title: { en: "aa", ar: " bb" },
48 | disc: { en: "aa", ar: " bb" },
49 | units: [
50 | {
51 | name: { en: "aa", ar: " bb" },
52 | price: 1,
53 | },
54 | ],
55 | });
56 |
57 | const categoryID = category._id;
58 | const itemID = item._id;
59 | category.items.push(itemID);
60 | await item.save();
61 | await category.save();
62 | const response = await server
63 | .get(`/api/items/getAll/${categoryID}`)
64 | .expect("Content-Type", /json/)
65 | .expect(200);
66 |
67 | const { body } = response;
68 |
69 | expect(body.status).toBe(true);
70 | expect(body.items).toBeInstanceOf(Array);
71 | expect(body.items.length).toBe(1);
72 | });
73 | test("should return empty list if no items", async () => {
74 | const category = new Category({
75 | name: { en: "categoryName", ar: "categoryName" },
76 | });
77 | const categoryID = category._id;
78 | await category.save();
79 |
80 | const response = await server
81 | .get(`/api/items/getAll/${categoryID}`)
82 | .expect("Content-Type", /json/)
83 | .expect(200);
84 |
85 | const { body } = response;
86 |
87 | expect(body.status).toBe(true);
88 | expect(body.items).toBeInstanceOf(Array);
89 | expect(body.items.length).toBe(0);
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/test/uitils/tokenUtils.js:
--------------------------------------------------------------------------------
1 | const { jwtSign } = require("../../src/utils/jwt_promise");
2 |
3 | let _expiredUserToken;
4 | let _expiredOwnerToken;
5 | let _userToken;
6 | let _wrongUserToken;
7 | let _ownerUserToken;
8 |
9 | exports.expiredUserToken = async () => {
10 | if (_expiredUserToken) return _expiredUserToken;
11 |
12 | _expiredUserToken = await jwtSign({
13 | payload: {
14 | _id: "id",
15 | email: "me@abdosaed.ml",
16 | userType: "user",
17 | },
18 | expiresIn: "0",
19 | });
20 | return _expiredUserToken;
21 | };
22 | exports.expiredOwnerToken = async () => {
23 | if (_expiredOwnerToken) return _expiredOwnerToken;
24 |
25 | _expiredOwnerToken = await jwtSign({
26 | payload: {
27 | _id: "id",
28 | email: "me@abdosaed.ml",
29 | userType: "owner",
30 | },
31 | expiresIn: "0",
32 | });
33 | return _expiredOwnerToken;
34 | };
35 | exports.userToken = async () => {
36 | if (_userToken) return _userToken;
37 |
38 | _userToken = await jwtSign({
39 | payload: {
40 | _id: "id",
41 | email: "me@abdosaed.ml",
42 | userType: "user",
43 | },
44 | });
45 | return _userToken;
46 | };
47 | exports.wrongUserToken = async () => {
48 | if (_wrongUserToken) return _wrongUserToken;
49 |
50 | const token = await jwtSign({
51 | payload: {
52 | _id: "id",
53 | email: "me@abdosaed.ml",
54 | userType: "user",
55 | },
56 | });
57 | _wrongUserToken = token.replace("a", "b"); //to be wrong token
58 | return _wrongUserToken;
59 | };
60 | exports.ownerUserToken = async () => {
61 | if (_ownerUserToken) return _ownerUserToken;
62 |
63 | _ownerUserToken = await jwtSign({
64 | payload: {
65 | _id: "id",
66 | email: "me@abdosaed.ml",
67 | userType: "owner",
68 | },
69 | });
70 |
71 | return _ownerUserToken;
72 | };
73 |
--------------------------------------------------------------------------------