From b352136a52302fa8c8b7fa460f825a9c92a72eee Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 3 Sep 2024 20:38:45 +0530 Subject: [PATCH 01/26] feat: collect email for subscription --- controllers/subscription.ts | 40 +++++++++++++++++++++++++++++++++++++ routes/index.ts | 1 + routes/subscription.ts | 8 ++++++++ 3 files changed, 49 insertions(+) create mode 100644 controllers/subscription.ts create mode 100644 routes/subscription.ts diff --git a/controllers/subscription.ts b/controllers/subscription.ts new file mode 100644 index 000000000..dce3815c1 --- /dev/null +++ b/controllers/subscription.ts @@ -0,0 +1,40 @@ +import { CustomRequest, CustomResponse } from "../types/global"; +const { addOrUpdate } = require("../models/users"); +const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); + +export const subscribe = async (req: CustomRequest, res: CustomResponse) => { + try { + const { email, phoneNumber } = req.body; + const userId = req.userData.id; + await await addOrUpdate( + { + phoneNumber, + email, + isSubscribed: true, + }, + userId + ); + return res.status(201).json({ + message: "user subscribed successfully", + }); + } catch (error) { + res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + +export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { + try { + const userId = req.userData.id; + await await addOrUpdate( + { + isSubscribed: false, + }, + userId + ); + return res.status(200).json({ + message: "user unsubscribed successfully", + }); + } catch (error) { + res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; diff --git a/routes/index.ts b/routes/index.ts index 575797bc6..7395f9661 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -39,4 +39,5 @@ app.use("/v1/notifications", require("./notify")); app.use("/goals", require("./goals")); app.use("/invites", require("./invites")); app.use("/requests", require("./requests")); +app.use("/subscription", require("./subscription")); module.exports = app; diff --git a/routes/subscription.ts b/routes/subscription.ts new file mode 100644 index 000000000..1add28602 --- /dev/null +++ b/routes/subscription.ts @@ -0,0 +1,8 @@ +import express from "express"; +const router = express.Router(); +import authenticate from "../middlewares/authenticate"; +import { subscribe, unsubscribe } from "../controllers/subscription"; + +router.post('/', authenticate, subscribe); +router.put('/', authenticate, unsubscribe) +module.exports = router; \ No newline at end of file From 7e975f12eaac808f58af31d64820a250b1df6cad Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sat, 7 Sep 2024 02:22:48 +0530 Subject: [PATCH 02/26] feat: add API to send email & test locally --- config/custom-environment-variables.js | 5 ++++ config/default.js | 5 ++++ config/development.js | 5 ++++ controllers/subscription.ts | 36 ++++++++++++++++++++++++-- middlewares/validators/subscription.ts | 15 +++++++++++ package.json | 2 ++ routes/subscription.ts | 3 ++- test/config/test.js | 5 ++++ yarn.lock | 12 +++++++++ 9 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 middlewares/validators/subscription.ts diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index f22c5756e..f01c5c16a 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -55,6 +55,11 @@ module.exports = { }, }, + emailCredentials: { + user: "", + pass: "", + }, + userToken: { cookieName: "COOKIE_NAME", ttl: { diff --git a/config/default.js b/config/default.js index 71098835d..d648a2691 100644 --- a/config/default.js +++ b/config/default.js @@ -25,6 +25,11 @@ module.exports = { clientSecret: "", }, + emailCredentials: { + user: "", + pass: "", + }, + firestore: `{ "type": "service_account", "project_id": "", diff --git a/config/development.js b/config/development.js index 0e4d3bd44..19ffd4057 100644 --- a/config/development.js +++ b/config/development.js @@ -30,6 +30,11 @@ module.exports = { }, }, + emailCredentials: { + user: "", + pass: "", + }, + userToken: { publicKey: "-----BEGIN PUBLIC KEY-----\n" + diff --git a/controllers/subscription.ts b/controllers/subscription.ts index dce3815c1..5e102a752 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -1,12 +1,15 @@ import { CustomRequest, CustomResponse } from "../types/global"; const { addOrUpdate } = require("../models/users"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); +const nodemailer = require("nodemailer"); +const config = require("config"); +const emailCredentials = config.get("emailCredentials"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { try { const { email, phoneNumber } = req.body; const userId = req.userData.id; - await await addOrUpdate( + await addOrUpdate( { phoneNumber, email, @@ -25,7 +28,7 @@ export const subscribe = async (req: CustomRequest, res: CustomResponse) => { export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { try { const userId = req.userData.id; - await await addOrUpdate( + await addOrUpdate( { isSubscribed: false, }, @@ -38,3 +41,32 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; + +export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { + try { + const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 587, + secure: false, + auth: { + user: emailCredentials.user, + pass: emailCredentials.pass, + }, + }); + + const info = await transporter.sendMail({ + from: `"Real Dev Squad" <${emailCredentials.user}>`, + to: "dgandhrav@gmail.com", + subject: "Hello local, Testing in progress.", + text: "working for notification feature", + html: "Hello world!", + }); + + res.send({ message: "Email sent", info }); + } catch (error) { + console.error("Error occurred:", error); + res.status(500).send({ message: "Failed to send email", error }); + } + console.log(emailCredentials); + res.send(emailCredentials) +}; \ No newline at end of file diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts new file mode 100644 index 000000000..22396ead0 --- /dev/null +++ b/middlewares/validators/subscription.ts @@ -0,0 +1,15 @@ +import { NextFunction } from "express"; +import { CustomRequest, CustomResponse } from "../../types/global"; +const joi = require("joi"); + +export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + const subscribeSchema = Joi.object({ + phoneNumber: joi.string().required(), + email: joi.string().required() + }); + const { error } = subscribeSchema.validate(req.body); + if (error) { + return res.status(400).json({ error: error.details[0].message }); + } + next(); +}; diff --git a/package.json b/package.json index 7df8a48a8..46ccb8ffa 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "tdd:watch": "sh scripts/tests/tdd.sh" }, "dependencies": { + "@types/nodemailer": "^6.4.15", "axios": "1.7.2", "cloudinary": "2.0.3", "config": "3.3.7", @@ -34,6 +35,7 @@ "morgan": "1.10.0", "multer": "1.4.5-lts.1", "newrelic": "11.19.0", + "nodemailer": "^6.9.15", "passport": "0.7.0", "passport-github2": "0.1.12", "rate-limiter-flexible": "5.0.3", diff --git a/routes/subscription.ts b/routes/subscription.ts index 1add28602..55cd81a2a 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -1,8 +1,9 @@ import express from "express"; const router = express.Router(); import authenticate from "../middlewares/authenticate"; -import { subscribe, unsubscribe } from "../controllers/subscription"; +import { subscribe, unsubscribe, sendEmail } from "../controllers/subscription"; router.post('/', authenticate, subscribe); router.put('/', authenticate, unsubscribe) +router.get('/send-email', sendEmail) module.exports = router; \ No newline at end of file diff --git a/test/config/test.js b/test/config/test.js index a90dc3d78..da6a6c3e3 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -35,6 +35,11 @@ module.exports = { "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-hqc2v%40dev-rds.iam.gserviceaccount.com" }`, + + emailCredentials: { + user: "", + pass: "", + }, services: { rdsApi: { baseUrl: `http://localhost:${port}`, diff --git a/yarn.lock b/yarn.lock index 5781ddc3d..7683feeb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1076,6 +1076,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3" integrity sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw== +"@types/nodemailer@^6.4.15": + version "6.4.15" + resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.15.tgz#494be695e11c438f7f5df738fb4ab740312a6ed2" + integrity sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ== + dependencies: + "@types/node" "*" + "@types/qs@*", "@types/qs@^6.2.31": version "6.9.15" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" @@ -5879,6 +5886,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nodemailer@^6.9.15: + version "6.9.15" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04" + integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ== + nodemon@3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.3.tgz#dcce9ee0aa7d19cd4dcd576ae9a0456d9078b286" From c9b12d4d5c6708983f94b0e9c76b2583ed28c7b8 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 8 Sep 2024 22:22:30 +0530 Subject: [PATCH 03/26] feat: write test cases for subscription APIs --- middlewares/validators/subscription.ts | 6 +- package.json | 1 + routes/subscription.js | 12 +++ routes/subscription.ts | 9 -- test/fixtures/subscription/subscription.ts | 7 ++ test/integration/subscription.test.js | 100 ++++++++++++++++++ .../subscription-validator.test.js | 65 ++++++++++++ yarn.lock | 7 ++ 8 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 routes/subscription.js delete mode 100644 routes/subscription.ts create mode 100644 test/fixtures/subscription/subscription.ts create mode 100644 test/integration/subscription.test.js create mode 100644 test/unit/middlewares/subscription-validator.test.js diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 22396ead0..621e6c742 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -1,11 +1,11 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../../types/global"; -const joi = require("joi"); +import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { const subscribeSchema = Joi.object({ - phoneNumber: joi.string().required(), - email: joi.string().required() + phoneNumber: Joi.string().required(), + email: Joi.string().required() }); const { error } = subscribeSchema.validate(req.body); if (error) { diff --git a/package.json b/package.json index 46ccb8ffa..b01a32092 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "multer": "1.4.5-lts.1", "newrelic": "11.19.0", "nodemailer": "^6.9.15", + "nodemailer-mock": "^2.0.6", "passport": "0.7.0", "passport-github2": "0.1.12", "rate-limiter-flexible": "5.0.3", diff --git a/routes/subscription.js b/routes/subscription.js new file mode 100644 index 000000000..4cf7aa970 --- /dev/null +++ b/routes/subscription.js @@ -0,0 +1,12 @@ +import express from "express"; +import authenticate from "../middlewares/authenticate"; +import { subscribe, unsubscribe, sendEmail } from "../controllers/subscription"; +import { validateSubscribe } from "../middlewares/validators/subscription"; +import authorizeRoles from "../middlewares/authorizeRoles"; +const router = express.Router(); +const { SUPERUSER } = require("../constants/roles"); + +router.post("/", authenticate, validateSubscribe, subscribe); +router.put("/", authenticate, unsubscribe); +router.get("/send-email", authenticate, authorizeRoles([SUPERUSER]), sendEmail); +module.exports = router; diff --git a/routes/subscription.ts b/routes/subscription.ts deleted file mode 100644 index 55cd81a2a..000000000 --- a/routes/subscription.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from "express"; -const router = express.Router(); -import authenticate from "../middlewares/authenticate"; -import { subscribe, unsubscribe, sendEmail } from "../controllers/subscription"; - -router.post('/', authenticate, subscribe); -router.put('/', authenticate, unsubscribe) -router.get('/send-email', sendEmail) -module.exports = router; \ No newline at end of file diff --git a/test/fixtures/subscription/subscription.ts b/test/fixtures/subscription/subscription.ts new file mode 100644 index 000000000..cacf54940 --- /dev/null +++ b/test/fixtures/subscription/subscription.ts @@ -0,0 +1,7 @@ +export const subscribedMessage = "user subscribed successfully"; + +export const unSubscribedMessage = "user unsubscribed successfully"; +export const subscriptionData = { + phoneNumber: "+9199999999999", + email: "example@gmail.com", +}; diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js new file mode 100644 index 000000000..5d13f920c --- /dev/null +++ b/test/integration/subscription.test.js @@ -0,0 +1,100 @@ +const chai = require("chai"); +const sinon = require("sinon"); +const app = require("../../server"); +const cookieName = config.get("userToken.cookieName"); +const { subscribedMessage, unSubscribedMessage, subscriptionData } = require("../fixtures/subscription/subscription"); +const addUser = require("../utils/addUser"); +const authService = require("../../services/authService"); +const chaiHttp = require("chai-http"); +chai.use(chaiHttp); +const nodemailer = require("nodemailer"); +const nodemailerMock = require("nodemailer-mock"); + +const { expect } = chai; +let userId = ""; + +describe("/subscription email notifications", function () { + let jwt; + + beforeEach(async function () { + userId = await addUser(); + jwt = authService.generateAuthToken({ userId }); + }); + + it("Should return 401 if the user is not logged in", function (done) { + chai + .request(app) + .post("/subscription") + .end((err, res) => { + if (err) { + return done(); + } + expect(res).to.have.status(401); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Unauthenticated User"); + return done(); + }); + }); + + it("should add user's data and make them subscribe to us.", function (done) { + chai + .request(app) + .post(`/subscription`) + .set("cookie", `${cookieName}=${jwt}`) + .send(subscriptionData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(201); + expect(res.body).to.have.keys(["message"]); + expect(res.body.message).to.equal(subscribedMessage); + return done(); + }); + }); + + it("should unsubscribe the user", function (done) { + chai + .request(app) + .put(`/subscription`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res.body).to.have.keys(["message"]); + expect(res.body.message).to.equal(unSubscribedMessage); + return done(); + }); + }); + + describe("/send-email endpoint", function () { + beforeEach(function () { + sinon.stub(nodemailerMock, "createTransport").callsFake(nodemailerMock.createTransport); + }); + + afterEach(function () { + sinon.restore(); + nodemailerMock.mock.reset(); + }); + + it("should handle errors if sending email fails", function (done) { + sinon.stub(nodemailer, "createTransport").callsFake(() => { + throw new Error("Transport error"); + }); + + chai + .request(app) + .get("/subscription/send-email") + .end((err, res) => { + if (err) return done(err); + + expect(res).to.have.status(500); + expect(res.body).to.have.property("message", "Failed to send email"); + expect(res.body).to.have.property("error"); + return done(); + }); + }); + }); +}); diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js new file mode 100644 index 000000000..f3fd5c4ea --- /dev/null +++ b/test/unit/middlewares/subscription-validator.test.js @@ -0,0 +1,65 @@ +const Sinon = require("sinon"); +const { expect } = require("chai"); +const { validateSubscribe } = require("../../../middlewares/validators/subscription"); + +describe("Middleware | Validators | Subscription", function () { + let req, res, nextSpy; + + beforeEach(function () { + req = { body: {} }; + res = { + status: Sinon.stub().returnsThis(), + json: Sinon.stub(), + }; + nextSpy = Sinon.spy(); + }); + + it("should call next function when a valid request body is passed", async function () { + req.body = { + phoneNumber: "1234567890", + email: "test@example.com", + }; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.calledOnce).to.be.equal(true); + expect(res.status.called).to.be.equal(false); + }); + + it("should return a 400 error when phoneNumber is missing", async function () { + req.body = { + email: "test@example.com", + }; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"phoneNumber" is required'); + }); + + it("should return a 400 error when email is missing", async function () { + req.body = { + phoneNumber: "1234567890", + }; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"email" is required'); + }); + + it("should return a 400 error when both phoneNumber and email are missing", async function () { + req.body = {}; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"phoneNumber" is required'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 7683feeb0..95acf1406 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5886,6 +5886,13 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nodemailer-mock@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/nodemailer-mock/-/nodemailer-mock-2.0.6.tgz#0dd3e522df73682d47f4f2b1ee905aacd22e2c8e" + integrity sha512-9x/QN1AbKy4PJ7yIQnToly3c7gUCSGABeB10/c5jgO986fAOMghzVedbZe8UDsu2PEStCoOd+MayX09CduYSHQ== + dependencies: + debug "^4.3.4" + nodemailer@^6.9.15: version "6.9.15" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.15.tgz#57b79dc522be27e0e47ac16cc860aa0673e62e04" From 80a1c5a91f392719a5f5cabf6c6c33d7da340938 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 8 Sep 2024 22:25:52 +0530 Subject: [PATCH 04/26] refactor: add comment --- controllers/subscription.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 5e102a752..e82144f6d 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -56,6 +56,7 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { const info = await transporter.sendMail({ from: `"Real Dev Squad" <${emailCredentials.user}>`, + // TODO: after approving this PR we need to send email to TEJAS sir via this API as a POC. to: "dgandhrav@gmail.com", subject: "Hello local, Testing in progress.", text: "working for notification feature", From fa46bf87761141b5f6928b8d30cedf7529f17346 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 8 Sep 2024 23:11:36 +0530 Subject: [PATCH 05/26] feat: add test for send-email API --- test/integration/subscription.test.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index 5d13f920c..1847b521d 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -9,10 +9,11 @@ const chaiHttp = require("chai-http"); chai.use(chaiHttp); const nodemailer = require("nodemailer"); const nodemailerMock = require("nodemailer-mock"); - +const userData = require("../fixtures/user/user")(); const { expect } = chai; let userId = ""; - +const superUser = userData[4]; +let superUserAuthToken = ""; describe("/subscription email notifications", function () { let jwt; @@ -70,7 +71,9 @@ describe("/subscription email notifications", function () { }); describe("/send-email endpoint", function () { - beforeEach(function () { + beforeEach(async function () { + const superUserId = await addUser(superUser); + superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); sinon.stub(nodemailerMock, "createTransport").callsFake(nodemailerMock.createTransport); }); @@ -79,6 +82,21 @@ describe("/subscription email notifications", function () { nodemailerMock.mock.reset(); }); + it("Should return 401 if the super user is not logged in", function (done) { + chai + .request(app) + .post("/subscription") + .end((err, res) => { + if (err) { + return done(); + } + expect(res).to.have.status(401); + expect(res.body).to.be.a("object"); + expect(res.body.message).to.equal("Unauthenticated User"); + return done(); + }); + }); + it("should handle errors if sending email fails", function (done) { sinon.stub(nodemailer, "createTransport").callsFake(() => { throw new Error("Transport error"); @@ -87,9 +105,9 @@ describe("/subscription email notifications", function () { chai .request(app) .get("/subscription/send-email") + .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) return done(err); - expect(res).to.have.status(500); expect(res.body).to.have.property("message", "Failed to send email"); expect(res.body).to.have.property("error"); From eac79a9237cdbc97b23e3c2cfeb3a1e13440611b Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 10 Sep 2024 21:29:52 +0530 Subject: [PATCH 06/26] feat: validating email and add contants --- config/custom-environment-variables.js | 4 +++- config/default.js | 4 +++- config/development.js | 4 +++- controllers/subscription.ts | 15 +++++++-------- middlewares/validators/subscription.ts | 3 ++- test/config/test.js | 4 +++- .../middlewares/subscription-validator.test.js | 1 - 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index f01c5c16a..81f998126 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -55,9 +55,11 @@ module.exports = { }, }, - emailCredentials: { + emailSubscriptionCredentials: { user: "", pass: "", + host: "", + port: "", }, userToken: { diff --git a/config/default.js b/config/default.js index d648a2691..4a98960f5 100644 --- a/config/default.js +++ b/config/default.js @@ -25,9 +25,11 @@ module.exports = { clientSecret: "", }, - emailCredentials: { + emailSubscriptionCredentials: { user: "", pass: "", + host: "", + port: "", }, firestore: `{ diff --git a/config/development.js b/config/development.js index 19ffd4057..fe6202f9b 100644 --- a/config/development.js +++ b/config/development.js @@ -30,9 +30,11 @@ module.exports = { }, }, - emailCredentials: { + emailSubscriptionCredentials: { user: "", pass: "", + host: "", + port: "", }, userToken: { diff --git a/controllers/subscription.ts b/controllers/subscription.ts index e82144f6d..e1ab5bb20 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -3,7 +3,7 @@ const { addOrUpdate } = require("../models/users"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const nodemailer = require("nodemailer"); const config = require("config"); -const emailCredentials = config.get("emailCredentials"); +const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { try { @@ -45,17 +45,17 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { try { const transporter = nodemailer.createTransport({ - host: "smtp.gmail.com", - port: 587, + host: emailSubscriptionCredentials.host, + port: emailSubscriptionCredentials.port, secure: false, auth: { - user: emailCredentials.user, - pass: emailCredentials.pass, + user: emailSubscriptionCredentials.user, + pass: emailSubscriptionCredentials.pass, }, }); const info = await transporter.sendMail({ - from: `"Real Dev Squad" <${emailCredentials.user}>`, + from: `"Real Dev Squad" <${emailSubscriptionCredentials.user}>`, // TODO: after approving this PR we need to send email to TEJAS sir via this API as a POC. to: "dgandhrav@gmail.com", subject: "Hello local, Testing in progress.", @@ -68,6 +68,5 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { console.error("Error occurred:", error); res.status(500).send({ message: "Failed to send email", error }); } - console.log(emailCredentials); - res.send(emailCredentials) + res.send(emailSubscriptionCredentials) }; \ No newline at end of file diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 621e6c742..66dd0f971 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -3,9 +3,10 @@ import { CustomRequest, CustomResponse } from "../../types/global"; import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const subscribeSchema = Joi.object({ phoneNumber: Joi.string().required(), - email: Joi.string().required() + email: Joi.string().required().regex(emailRegex) }); const { error } = subscribeSchema.validate(req.body); if (error) { diff --git a/test/config/test.js b/test/config/test.js index da6a6c3e3..a74e06c96 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -36,9 +36,11 @@ module.exports = { "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-hqc2v%40dev-rds.iam.gserviceaccount.com" }`, - emailCredentials: { + emailSubscriptionCredentials: { user: "", pass: "", + host: "", + port: "", }, services: { rdsApi: { diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index f3fd5c4ea..11647e581 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -56,7 +56,6 @@ describe("Middleware | Validators | Subscription", function () { req.body = {}; await validateSubscribe(req, res, nextSpy); - expect(nextSpy.called).to.be.equal(false); expect(res.status.calledOnceWith(400)).to.be.equal(true); expect(res.json.calledOnce).to.be.equal(true); From b172b39500b7fed41f1b662e222ecbfdaf43327a Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 10 Sep 2024 21:43:24 +0530 Subject: [PATCH 07/26] refactor: change file extension --- routes/{subscription.js => subscription.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename routes/{subscription.js => subscription.ts} (89%) diff --git a/routes/subscription.js b/routes/subscription.ts similarity index 89% rename from routes/subscription.js rename to routes/subscription.ts index 4cf7aa970..f16c7a316 100644 --- a/routes/subscription.js +++ b/routes/subscription.ts @@ -2,7 +2,7 @@ import express from "express"; import authenticate from "../middlewares/authenticate"; import { subscribe, unsubscribe, sendEmail } from "../controllers/subscription"; import { validateSubscribe } from "../middlewares/validators/subscription"; -import authorizeRoles from "../middlewares/authorizeRoles"; +const authorizeRoles = require("../middlewares/authorizeRoles"); const router = express.Router(); const { SUPERUSER } = require("../constants/roles"); From d4abe57e6c4dce9cbaf07cf18ddff46b9cd251ec Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Thu, 12 Sep 2024 18:46:41 +0530 Subject: [PATCH 08/26] feat: add test for invalid email --- config/custom-environment-variables.js | 2 +- config/default.js | 2 +- config/development.js | 2 +- test/config/test.js | 2 +- .../middlewares/subscription-validator.test.js | 16 ++++++++++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 81f998126..b1c6f73eb 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -57,7 +57,7 @@ module.exports = { emailSubscriptionCredentials: { user: "", - pass: "", + pass: "", host: "", port: "", }, diff --git a/config/default.js b/config/default.js index 4a98960f5..5f3bbad61 100644 --- a/config/default.js +++ b/config/default.js @@ -27,7 +27,7 @@ module.exports = { emailSubscriptionCredentials: { user: "", - pass: "", + pass: "", host: "", port: "", }, diff --git a/config/development.js b/config/development.js index fe6202f9b..7892ef1ca 100644 --- a/config/development.js +++ b/config/development.js @@ -32,7 +32,7 @@ module.exports = { emailSubscriptionCredentials: { user: "", - pass: "", + pass: "", host: "", port: "", }, diff --git a/test/config/test.js b/test/config/test.js index a74e06c96..a5de4124d 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -38,7 +38,7 @@ module.exports = { emailSubscriptionCredentials: { user: "", - pass: "", + pass: "", host: "", port: "", }, diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index 11647e581..015033c70 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -61,4 +61,20 @@ describe("Middleware | Validators | Subscription", function () { expect(res.json.calledOnce).to.be.equal(true); expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"phoneNumber" is required'); }); + + it("should return a 400 error when email is not in correct format", async function () { + req.body = { + phoneNumber: "1234567890", + email: "invalid-email", + }; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]) + .to.have.property("error") + .that.includes('"email" with value "invalid-email" fails to match the required pattern'); + }); }); From f2e12262ba1de8e575e2cdf5b258ffb12aa31f53 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Fri, 13 Sep 2024 00:09:13 +0530 Subject: [PATCH 09/26] feat: add feature flag --- controllers/subscription.ts | 13 ++++++ test/integration/subscription.test.js | 58 ++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/controllers/subscription.ts b/controllers/subscription.ts index e1ab5bb20..f5829eb95 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -9,6 +9,11 @@ export const subscribe = async (req: CustomRequest, res: CustomResponse) => { try { const { email, phoneNumber } = req.body; const userId = req.userData.id; + const dev = req.query.dev === "true"; + if (!dev) { + return res.boom.notFound("Route not found"); + } + await addOrUpdate( { phoneNumber, @@ -28,6 +33,10 @@ export const subscribe = async (req: CustomRequest, res: CustomResponse) => { export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { try { const userId = req.userData.id; + const dev = req.query.dev === "true"; + if (!dev) { + return res.boom.notFound("Route not found"); + } await addOrUpdate( { isSubscribed: false, @@ -44,6 +53,10 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { try { + const dev = req.query.dev === "true"; + if (!dev) { + return res.boom.notFound("Route not found"); + } const transporter = nodemailer.createTransport({ host: emailSubscriptionCredentials.host, port: emailSubscriptionCredentials.port, diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index 1847b521d..bdaadcc87 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -25,7 +25,7 @@ describe("/subscription email notifications", function () { it("Should return 401 if the user is not logged in", function (done) { chai .request(app) - .post("/subscription") + .post("/subscription?dev=true") .end((err, res) => { if (err) { return done(); @@ -40,7 +40,7 @@ describe("/subscription email notifications", function () { it("should add user's data and make them subscribe to us.", function (done) { chai .request(app) - .post(`/subscription`) + .post(`/subscription?dev=true`) .set("cookie", `${cookieName}=${jwt}`) .send(subscriptionData) .end((err, res) => { @@ -54,10 +54,26 @@ describe("/subscription email notifications", function () { }); }); + it("shouldn't add user's data and return 404 when dev is not equal to true", function (done) { + chai + .request(app) + .post(`/subscription`) + .set("cookie", `${cookieName}=${jwt}`) + .send(subscriptionData) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(404); + expect(res.body).to.have.property("message", "Route not found"); + return done(); + }); + }); + it("should unsubscribe the user", function (done) { chai .request(app) - .put(`/subscription`) + .put(`/subscription?dev=true`) .set("cookie", `${cookieName}=${jwt}`) .end((err, res) => { if (err) { @@ -70,6 +86,21 @@ describe("/subscription email notifications", function () { }); }); + it("shouldn't unsubscribe the user return 404 when dev is not equal to true", function (done) { + chai + .request(app) + .put(`/subscription`) + .set("cookie", `${cookieName}=${jwt}`) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(404); + expect(res.body).to.have.property("message", "Route not found"); + return done(); + }); + }); + describe("/send-email endpoint", function () { beforeEach(async function () { const superUserId = await addUser(superUser); @@ -85,7 +116,7 @@ describe("/subscription email notifications", function () { it("Should return 401 if the super user is not logged in", function (done) { chai .request(app) - .post("/subscription") + .post("/subscription?dev=true") .end((err, res) => { if (err) { return done(); @@ -104,7 +135,7 @@ describe("/subscription email notifications", function () { chai .request(app) - .get("/subscription/send-email") + .get("/subscription/send-email?dev=true") .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) return done(err); @@ -114,5 +145,22 @@ describe("/subscription email notifications", function () { return done(); }); }); + + it("Sending mail should return 404 if dev is not equal to true", function (done) { + sinon.stub(nodemailer, "createTransport").callsFake(() => { + throw new Error("Transport error"); + }); + + chai + .request(app) + .get("/subscription/send-email") + .set("Cookie", `${cookieName}=${superUserAuthToken}`) + .end((err, res) => { + if (err) return done(err); + expect(res).to.have.status(404); + expect(res.body).to.have.property("message", "Route not found"); + return done(); + }); + }); }); }); From e58f5ab85cf9c8dc430ce922ce6c121c1411f104 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Fri, 13 Sep 2024 02:13:40 +0530 Subject: [PATCH 10/26] refactor: change config details --- config/custom-environment-variables.js | 4 ++-- config/default.js | 4 ++-- config/development.js | 4 ++-- controllers/subscription.ts | 8 +++++--- test/config/test.js | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index b1c6f73eb..83044e4f0 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -56,8 +56,8 @@ module.exports = { }, emailSubscriptionCredentials: { - user: "", - pass: "", + email: "", + password: "", host: "", port: "", }, diff --git a/config/default.js b/config/default.js index 5f3bbad61..5d1c0cbe8 100644 --- a/config/default.js +++ b/config/default.js @@ -26,8 +26,8 @@ module.exports = { }, emailSubscriptionCredentials: { - user: "", - pass: "", + email: "", + password: "", host: "", port: "", }, diff --git a/config/development.js b/config/development.js index 7892ef1ca..8b6f0b809 100644 --- a/config/development.js +++ b/config/development.js @@ -31,8 +31,8 @@ module.exports = { }, emailSubscriptionCredentials: { - user: "", - pass: "", + email: "", + password: "", host: "", port: "", }, diff --git a/controllers/subscription.ts b/controllers/subscription.ts index f5829eb95..31004709e 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -57,18 +57,20 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { if (!dev) { return res.boom.notFound("Route not found"); } + const transporter = nodemailer.createTransport({ host: emailSubscriptionCredentials.host, port: emailSubscriptionCredentials.port, secure: false, + auth: { - user: emailSubscriptionCredentials.user, - pass: emailSubscriptionCredentials.pass, + user: emailSubscriptionCredentials.email, + pass: emailSubscriptionCredentials.password, }, }); const info = await transporter.sendMail({ - from: `"Real Dev Squad" <${emailSubscriptionCredentials.user}>`, + from: `"Real Dev Squad" <${emailSubscriptionCredentials.email}>`, // TODO: after approving this PR we need to send email to TEJAS sir via this API as a POC. to: "dgandhrav@gmail.com", subject: "Hello local, Testing in progress.", diff --git a/test/config/test.js b/test/config/test.js index a5de4124d..e637108bd 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -37,8 +37,8 @@ module.exports = { }`, emailSubscriptionCredentials: { - user: "", - pass: "", + email: "", + password: "", host: "", port: "", }, From 5f8be59d02693eaa9e2b713d9f5ad528209a254d Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Fri, 13 Sep 2024 12:22:54 +0530 Subject: [PATCH 11/26] refactor: change test email --- controllers/subscription.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 31004709e..f3d96accf 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -71,8 +71,7 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { const info = await transporter.sendMail({ from: `"Real Dev Squad" <${emailSubscriptionCredentials.email}>`, - // TODO: after approving this PR we need to send email to TEJAS sir via this API as a POC. - to: "dgandhrav@gmail.com", + to: "tejasatrds@gmail.com ", subject: "Hello local, Testing in progress.", text: "working for notification feature", html: "Hello world!", From c0fe63698754b44c67ffb5dc73cc255643516a14 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 8 Oct 2024 22:30:10 +0530 Subject: [PATCH 12/26] fix: change phone number to optional --- middlewares/validators/subscription.ts | 3 +- .../subscription-validator.test.js | 50 ++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 66dd0f971..d66c34bad 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -4,8 +4,9 @@ import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const phoneNumberRegex = /^\+91\d{10}$/; const subscribeSchema = Joi.object({ - phoneNumber: Joi.string().required(), + phoneNumber: Joi.string().optional().regex(phoneNumberRegex), email: Joi.string().required().regex(emailRegex) }); const { error } = subscribeSchema.validate(req.body); diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index 015033c70..b8abd7359 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -2,7 +2,7 @@ const Sinon = require("sinon"); const { expect } = require("chai"); const { validateSubscribe } = require("../../../middlewares/validators/subscription"); -describe("Middleware | Validators | Subscription", function () { +describe.only("Middleware | Validators | Subscription", function () { let req, res, nextSpy; beforeEach(function () { @@ -16,7 +16,7 @@ describe("Middleware | Validators | Subscription", function () { it("should call next function when a valid request body is passed", async function () { req.body = { - phoneNumber: "1234567890", + phoneNumber: "+911234567890", // Valid format email: "test@example.com", }; @@ -24,24 +24,22 @@ describe("Middleware | Validators | Subscription", function () { expect(nextSpy.calledOnce).to.be.equal(true); expect(res.status.called).to.be.equal(false); + expect(res.json.called).to.be.equal(false); }); - it("should return a 400 error when phoneNumber is missing", async function () { + it("should not return an error when phoneNumber is missing", async function () { req.body = { email: "test@example.com", }; await validateSubscribe(req, res, nextSpy); - - expect(nextSpy.called).to.be.equal(false); - expect(res.status.calledOnceWith(400)).to.be.equal(true); - expect(res.json.calledOnce).to.be.equal(true); - expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"phoneNumber" is required'); + expect(nextSpy.calledOnce).to.be.equal(true); + expect(res.status.called).to.be.equal(false); + expect(res.json.called).to.be.equal(false); }); - it("should return a 400 error when email is missing", async function () { req.body = { - phoneNumber: "1234567890", + phoneNumber: "+911234567890", }; await validateSubscribe(req, res, nextSpy); @@ -59,12 +57,28 @@ describe("Middleware | Validators | Subscription", function () { expect(nextSpy.called).to.be.equal(false); expect(res.status.calledOnceWith(400)).to.be.equal(true); expect(res.json.calledOnce).to.be.equal(true); - expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"phoneNumber" is required'); + expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"email" is required'); }); - it("should return a 400 error when email is not in correct format", async function () { + it("should return a 400 error when phoneNumber is not in correct format", async function () { req.body = { phoneNumber: "1234567890", + email: "test@example.com", + }; + + await validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]) + .to.have.property("error") + .that.includes('"phoneNumber" with value "1234567890" fails to match the required pattern'); + }); + + it("should return a 400 error when email is not in correct format", async function () { + req.body = { + phoneNumber: "+911234567890", // Valid format email: "invalid-email", }; @@ -77,4 +91,16 @@ describe("Middleware | Validators | Subscription", function () { .to.have.property("error") .that.includes('"email" with value "invalid-email" fails to match the required pattern'); }); + + it("should not return an error when phoneNumber is in correct format", async function () { + req.body = { + phoneNumber: "+911234567890", // Valid format + email: "test@example.com", + }; + + await validateSubscribe(req, res, nextSpy); + expect(nextSpy.calledOnce).to.be.equal(true); + expect(res.status.called).to.be.equal(false); + expect(res.json.called).to.be.equal(false); + }); }); From 47897cbb2d1beb8bd013ffe0da3e878c45016ce9 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Wed, 9 Oct 2024 17:20:31 +0530 Subject: [PATCH 13/26] fix: make phoneNumber optional --- constants/subscription-validator.ts | 2 ++ controllers/subscription.ts | 13 ++++--------- middlewares/validators/subscription.ts | 5 ++--- test/fixtures/subscription/subscription.ts | 3 ++- .../unit/middlewares/subscription-validator.test.js | 9 +++++---- 5 files changed, 15 insertions(+), 17 deletions(-) create mode 100644 constants/subscription-validator.ts diff --git a/constants/subscription-validator.ts b/constants/subscription-validator.ts new file mode 100644 index 000000000..d078bd138 --- /dev/null +++ b/constants/subscription-validator.ts @@ -0,0 +1,2 @@ +export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +export const phoneNumberRegex = /^\+91\d{10}$/; \ No newline at end of file diff --git a/controllers/subscription.ts b/controllers/subscription.ts index f3d96accf..a17264624 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -7,21 +7,16 @@ const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { try { - const { email, phoneNumber } = req.body; + const { email } = req.body; + const phoneNumber = req.body.phoneNumber || null; const userId = req.userData.id; const dev = req.query.dev === "true"; if (!dev) { return res.boom.notFound("Route not found"); } - await addOrUpdate( - { - phoneNumber, - email, - isSubscribed: true, - }, - userId - ); + const data = { email, isSubscribed: true, phoneNumber }; + await addOrUpdate(data, userId); return res.status(201).json({ message: "user subscribed successfully", }); diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index d66c34bad..3cc562950 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -1,12 +1,11 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../../types/global"; +import { phoneNumberRegex, emailRegex } from "../../constants/subscription-validator"; import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - const phoneNumberRegex = /^\+91\d{10}$/; const subscribeSchema = Joi.object({ - phoneNumber: Joi.string().optional().regex(phoneNumberRegex), + phoneNumber: Joi.string().allow('').optional().regex(phoneNumberRegex), email: Joi.string().required().regex(emailRegex) }); const { error } = subscribeSchema.validate(req.body); diff --git a/test/fixtures/subscription/subscription.ts b/test/fixtures/subscription/subscription.ts index cacf54940..512e6e55a 100644 --- a/test/fixtures/subscription/subscription.ts +++ b/test/fixtures/subscription/subscription.ts @@ -2,6 +2,7 @@ export const subscribedMessage = "user subscribed successfully"; export const unSubscribedMessage = "user unsubscribed successfully"; export const subscriptionData = { - phoneNumber: "+9199999999999", + phoneNumber: "+911234567890", email: "example@gmail.com", }; + diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index b8abd7359..7069c8748 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -2,7 +2,7 @@ const Sinon = require("sinon"); const { expect } = require("chai"); const { validateSubscribe } = require("../../../middlewares/validators/subscription"); -describe.only("Middleware | Validators | Subscription", function () { +describe("Middleware | Validators | Subscription", function () { let req, res, nextSpy; beforeEach(function () { @@ -16,7 +16,7 @@ describe.only("Middleware | Validators | Subscription", function () { it("should call next function when a valid request body is passed", async function () { req.body = { - phoneNumber: "+911234567890", // Valid format + phoneNumber: "+911234567890", email: "test@example.com", }; @@ -37,6 +37,7 @@ describe.only("Middleware | Validators | Subscription", function () { expect(res.status.called).to.be.equal(false); expect(res.json.called).to.be.equal(false); }); + it("should return a 400 error when email is missing", async function () { req.body = { phoneNumber: "+911234567890", @@ -78,7 +79,7 @@ describe.only("Middleware | Validators | Subscription", function () { it("should return a 400 error when email is not in correct format", async function () { req.body = { - phoneNumber: "+911234567890", // Valid format + phoneNumber: "+911234567890", email: "invalid-email", }; @@ -94,7 +95,7 @@ describe.only("Middleware | Validators | Subscription", function () { it("should not return an error when phoneNumber is in correct format", async function () { req.body = { - phoneNumber: "+911234567890", // Valid format + phoneNumber: "+911234567890", email: "test@example.com", }; From 82f9585914426638a56f6fd686fd33b0463cb16a Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Thu, 10 Oct 2024 01:24:34 +0530 Subject: [PATCH 14/26] fix: change API name --- routes/subscription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/subscription.ts b/routes/subscription.ts index f16c7a316..74db6e3d9 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -8,5 +8,5 @@ const { SUPERUSER } = require("../constants/roles"); router.post("/", authenticate, validateSubscribe, subscribe); router.put("/", authenticate, unsubscribe); -router.get("/send-email", authenticate, authorizeRoles([SUPERUSER]), sendEmail); +router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; From 5ef628fc5aaf8792b44624d99d160c31994a8b1e Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Thu, 10 Oct 2024 01:39:21 +0530 Subject: [PATCH 15/26] fix: change API name --- routes/subscription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/subscription.ts b/routes/subscription.ts index 74db6e3d9..f16c7a316 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -8,5 +8,5 @@ const { SUPERUSER } = require("../constants/roles"); router.post("/", authenticate, validateSubscribe, subscribe); router.put("/", authenticate, unsubscribe); -router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); +router.get("/send-email", authenticate, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; From 6f0a148f029e054f111b31486bc0392374734608 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sat, 12 Oct 2024 21:40:12 +0530 Subject: [PATCH 16/26] fix: try-catch Indain phone numbers controllers --- constants/subscription-validator.ts | 3 +- controllers/subscription.ts | 36 +++++++++++-------- middlewares/validators/subscription.ts | 4 +-- routes/subscription.ts | 2 +- test/integration/subscription.test.js | 8 ++--- .../subscription-validator.test.js | 16 --------- types/global.d.ts | 3 ++ 7 files changed, 33 insertions(+), 39 deletions(-) diff --git a/constants/subscription-validator.ts b/constants/subscription-validator.ts index d078bd138..0854d7ac9 100644 --- a/constants/subscription-validator.ts +++ b/constants/subscription-validator.ts @@ -1,2 +1 @@ -export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; -export const phoneNumberRegex = /^\+91\d{10}$/; \ No newline at end of file +export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; \ No newline at end of file diff --git a/controllers/subscription.ts b/controllers/subscription.ts index a17264624..9d639a9d5 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -6,16 +6,19 @@ const config = require("config"); const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { - try { - const { email } = req.body; - const phoneNumber = req.body.phoneNumber || null; - const userId = req.userData.id; - const dev = req.query.dev === "true"; + const dev = req.query.dev === "true"; if (!dev) { return res.boom.notFound("Route not found"); } - + const { email } = req.body; + const phoneNumber = req.body.phoneNumber || null; + const userId = req.userData.id; const data = { email, isSubscribed: true, phoneNumber }; + const userAlreadySubscribed = req.userData.isSubscribed; + try { + if (userAlreadySubscribed) { + return res.boom.badRequest({message: "User is already subscribed"}); + } await addOrUpdate(data, userId); return res.status(201).json({ message: "user subscribed successfully", @@ -26,12 +29,16 @@ export const subscribe = async (req: CustomRequest, res: CustomResponse) => { }; export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { - try { - const userId = req.userData.id; - const dev = req.query.dev === "true"; + const dev = req.query.dev === "true"; if (!dev) { return res.boom.notFound("Route not found"); } + const userId = req.userData.id; + const userAlreadySubscribed = req.userData.isSubscribed; + try { + if (!userAlreadySubscribed) { + return res.boom.badRequest({message: "User is already unsubscribed"}); + } await addOrUpdate( { isSubscribed: false, @@ -46,13 +53,14 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { } }; +// TODO: currently we are sending test email to a user only (i.e., Tejas sir as decided) +// later we need to make service which send email to all subscribed user export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { + const dev = req.query.dev === "true"; + if (!dev) { + return res.boom.notFound("Route not found"); + } try { - const dev = req.query.dev === "true"; - if (!dev) { - return res.boom.notFound("Route not found"); - } - const transporter = nodemailer.createTransport({ host: emailSubscriptionCredentials.host, port: emailSubscriptionCredentials.port, diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 3cc562950..4ac12576f 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -1,11 +1,11 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../../types/global"; -import { phoneNumberRegex, emailRegex } from "../../constants/subscription-validator"; +import { emailRegex } from "../../constants/subscription-validator"; import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { const subscribeSchema = Joi.object({ - phoneNumber: Joi.string().allow('').optional().regex(phoneNumberRegex), + phoneNumber: Joi.string().allow('').optional(), email: Joi.string().required().regex(emailRegex) }); const { error } = subscribeSchema.validate(req.body); diff --git a/routes/subscription.ts b/routes/subscription.ts index f16c7a316..74db6e3d9 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -8,5 +8,5 @@ const { SUPERUSER } = require("../constants/roles"); router.post("/", authenticate, validateSubscribe, subscribe); router.put("/", authenticate, unsubscribe); -router.get("/send-email", authenticate, authorizeRoles([SUPERUSER]), sendEmail); +router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index bdaadcc87..b62844295 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -101,7 +101,7 @@ describe("/subscription email notifications", function () { }); }); - describe("/send-email endpoint", function () { + describe("/notify endpoint", function () { beforeEach(async function () { const superUserId = await addUser(superUser); superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); @@ -116,7 +116,7 @@ describe("/subscription email notifications", function () { it("Should return 401 if the super user is not logged in", function (done) { chai .request(app) - .post("/subscription?dev=true") + .get("/subscription/notify?dev=true") .end((err, res) => { if (err) { return done(); @@ -135,7 +135,7 @@ describe("/subscription email notifications", function () { chai .request(app) - .get("/subscription/send-email?dev=true") + .get("/subscription/notify?dev=true") .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) return done(err); @@ -153,7 +153,7 @@ describe("/subscription email notifications", function () { chai .request(app) - .get("/subscription/send-email") + .get("/subscription/notify") .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) return done(err); diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index 7069c8748..8a2c5c79e 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -61,22 +61,6 @@ describe("Middleware | Validators | Subscription", function () { expect(res.json.firstCall.args[0]).to.have.property("error").that.includes('"email" is required'); }); - it("should return a 400 error when phoneNumber is not in correct format", async function () { - req.body = { - phoneNumber: "1234567890", - email: "test@example.com", - }; - - await validateSubscribe(req, res, nextSpy); - - expect(nextSpy.called).to.be.equal(false); - expect(res.status.calledOnceWith(400)).to.be.equal(true); - expect(res.json.calledOnce).to.be.equal(true); - expect(res.json.firstCall.args[0]) - .to.have.property("error") - .that.includes('"phoneNumber" with value "1234567890" fails to match the required pattern'); - }); - it("should return a 400 error when email is not in correct format", async function () { req.body = { phoneNumber: "+911234567890", diff --git a/types/global.d.ts b/types/global.d.ts index 6c6afa057..8f1046ada 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -36,6 +36,9 @@ export type userData = { status: string; username: string; updated_at: number; + isSubscribed: boolean; + phoneNumber: string | null; + email: string; }; export type CustomResponse = Response & { boom: Boom }; From 28e7a1466453c20226e998f96b89b6c9d3c1d54d Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sat, 12 Oct 2024 21:59:32 +0530 Subject: [PATCH 17/26] fix: put to patch --- routes/subscription.ts | 2 +- test/integration/subscription.test.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/subscription.ts b/routes/subscription.ts index 74db6e3d9..828682fab 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -7,6 +7,6 @@ const router = express.Router(); const { SUPERUSER } = require("../constants/roles"); router.post("/", authenticate, validateSubscribe, subscribe); -router.put("/", authenticate, unsubscribe); +router.patch("/", authenticate, unsubscribe); router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index b62844295..9b526fc52 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -73,7 +73,7 @@ describe("/subscription email notifications", function () { it("should unsubscribe the user", function (done) { chai .request(app) - .put(`/subscription?dev=true`) + .patch(`/subscription?dev=true`) .set("cookie", `${cookieName}=${jwt}`) .end((err, res) => { if (err) { @@ -89,7 +89,7 @@ describe("/subscription email notifications", function () { it("shouldn't unsubscribe the user return 404 when dev is not equal to true", function (done) { chai .request(app) - .put(`/subscription`) + .patch(`/subscription`) .set("cookie", `${cookieName}=${jwt}`) .end((err, res) => { if (err) { @@ -135,7 +135,7 @@ describe("/subscription email notifications", function () { chai .request(app) - .get("/subscription/notify?dev=true") + .get("/subscription/notify?dev=true") .set("Cookie", `${cookieName}=${superUserAuthToken}`) .end((err, res) => { if (err) return done(err); From 620b15c390d327f67c21fa6571b3b6152a728506 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sat, 12 Oct 2024 23:53:26 +0530 Subject: [PATCH 18/26] feat: add devFlagMiddleware --- controllers/subscription.ts | 37 +++++------------ middlewares/devFlag.ts | 14 +++++++ routes/subscription.ts | 7 ++-- test/unit/middlewares/devFlag.test.js | 58 +++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 middlewares/devFlag.ts create mode 100644 test/unit/middlewares/devFlag.test.js diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 9d639a9d5..15befff84 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -6,10 +6,6 @@ const config = require("config"); const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { - const dev = req.query.dev === "true"; - if (!dev) { - return res.boom.notFound("Route not found"); - } const { email } = req.body; const phoneNumber = req.body.phoneNumber || null; const userId = req.userData.id; @@ -17,27 +13,22 @@ export const subscribe = async (req: CustomRequest, res: CustomResponse) => { const userAlreadySubscribed = req.userData.isSubscribed; try { if (userAlreadySubscribed) { - return res.boom.badRequest({message: "User is already subscribed"}); + return res.boom.badRequest("User already subscribed"); } await addOrUpdate(data, userId); - return res.status(201).json({ - message: "user subscribed successfully", - }); + return res.status(201).json("User subscribed successfully"); } catch (error) { - res.boom.badImplementation(INTERNAL_SERVER_ERROR); + logger.error(`Error occurred while subscribing: ${error.message}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { - const dev = req.query.dev === "true"; - if (!dev) { - return res.boom.notFound("Route not found"); - } const userId = req.userData.id; const userAlreadySubscribed = req.userData.isSubscribed; try { if (!userAlreadySubscribed) { - return res.boom.badRequest({message: "User is already unsubscribed"}); + return res.boom.badRequest("User is already unsubscribed"); } await addOrUpdate( { @@ -45,21 +36,16 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { }, userId ); - return res.status(200).json({ - message: "user unsubscribed successfully", - }); + return res.status(200).json("User unsubscribed successfully"); } catch (error) { - res.boom.badImplementation(INTERNAL_SERVER_ERROR); + logger.error(`Error occurred while unsubscribing: ${error.message}`); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; // TODO: currently we are sending test email to a user only (i.e., Tejas sir as decided) // later we need to make service which send email to all subscribed user export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { - const dev = req.query.dev === "true"; - if (!dev) { - return res.boom.notFound("Route not found"); - } try { const transporter = nodemailer.createTransport({ host: emailSubscriptionCredentials.host, @@ -80,10 +66,9 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { html: "Hello world!", }); - res.send({ message: "Email sent", info }); + return res.send({ message: "Email sent successfully", info }); } catch (error) { - console.error("Error occurred:", error); - res.status(500).send({ message: "Failed to send email", error }); + logger.error("Error occurred while sending email:", error.message); + return res.status(500).send({ message: "Failed to send email", error }); } - res.send(emailSubscriptionCredentials) }; \ No newline at end of file diff --git a/middlewares/devFlag.ts b/middlewares/devFlag.ts new file mode 100644 index 000000000..83c454f6d --- /dev/null +++ b/middlewares/devFlag.ts @@ -0,0 +1,14 @@ +import { NextFunction, Request, Response } from "express"; +import { CustomRequest, CustomResponse } from "../types/global"; // Assuming these types extend the default ones + +export const devFlagMiddleware = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + try { + const dev = req.query.dev === "true"; + if (!dev) { + return res.boom.notFound("Route not found"); + } + next(); + } catch (err) { + next(err); + } +}; diff --git a/routes/subscription.ts b/routes/subscription.ts index 828682fab..ae1c4add3 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -5,8 +5,9 @@ import { validateSubscribe } from "../middlewares/validators/subscription"; const authorizeRoles = require("../middlewares/authorizeRoles"); const router = express.Router(); const { SUPERUSER } = require("../constants/roles"); +import { devFlagMiddleware } from "../middlewares/devFlag"; -router.post("/", authenticate, validateSubscribe, subscribe); -router.patch("/", authenticate, unsubscribe); -router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); +router.post("/", authenticate, devFlagMiddleware, validateSubscribe, subscribe); +router.patch("/", authenticate,devFlagMiddleware, unsubscribe); +router.get("/notify", authenticate, devFlagMiddleware, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; diff --git a/test/unit/middlewares/devFlag.test.js b/test/unit/middlewares/devFlag.test.js new file mode 100644 index 000000000..bd692ec1b --- /dev/null +++ b/test/unit/middlewares/devFlag.test.js @@ -0,0 +1,58 @@ +const { expect } = require("chai"); +const { devFlagMiddleware } = require("../../../middlewares/devFlag"); +const sinon = require("sinon"); + +describe("devFlagMiddleware", function () { + let req; + let res; + let next; + + beforeEach(function () { + req = { + query: {}, + }; + res = { + boom: { + notFound: sinon.spy((message) => { + res.status = 404; + res.message = message; + }), + }, + }; + next = sinon.spy(); + }); + + it("should call next() if dev query parameter is true", function () { + req.query.dev = "true"; + devFlagMiddleware(req, res, next); + return expect(next.calledOnce).to.be.equal(true); + }); + + it("should return 404 if dev query parameter is not true", function () { + req.query.dev = "false"; + + devFlagMiddleware(req, res, next); + + expect(res.status).to.equal(404); + expect(res.message).to.equal("Route not found"); + return expect(next.notCalled).to.be.equal(true); + }); + + it("should return 404 if dev query parameter is missing", function () { + devFlagMiddleware(req, res, next); + + expect(res.status).to.equal(404); + expect(res.message).to.equal("Route not found"); + return expect(next.notCalled).to.be.equal(true); + }); + + it("should call next(err) if an error occurs", function () { + res.boom.notFound = sinon.stub().throws(new Error("Test error")); + + devFlagMiddleware(req, res, next); + + expect(next.calledOnce).to.be.equal(true); + expect(next.args[0][0]).to.be.instanceOf(Error); + return expect(next.args[0][0].message).to.equal("Test error"); + }); +}); From 783f96d3ba15f7ac55b4b60bd82bced5afe8ba3b Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 13 Oct 2024 02:10:15 +0530 Subject: [PATCH 19/26] fix: comments --- controllers/subscription.ts | 2 +- routes/index.ts | 3 +- routes/subscription.ts | 7 ++- test/fixtures/subscription/subscription.ts | 5 +- test/integration/subscription.test.js | 54 +--------------------- 5 files changed, 10 insertions(+), 61 deletions(-) diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 15befff84..4a1bc29b2 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -60,7 +60,7 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { const info = await transporter.sendMail({ from: `"Real Dev Squad" <${emailSubscriptionCredentials.email}>`, - to: "tejasatrds@gmail.com ", + to: "tejasatrds@gmail.com", subject: "Hello local, Testing in progress.", text: "working for notification feature", html: "Hello world!", diff --git a/routes/index.ts b/routes/index.ts index 7395f9661..8cd97bd3e 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -1,5 +1,6 @@ import express from "express"; const app = express.Router(); +import { devFlagMiddleware } from "../middlewares/devFlag"; app.use("/answers", require("./answers")); app.use("/auctions", require("./auctions")); @@ -39,5 +40,5 @@ app.use("/v1/notifications", require("./notify")); app.use("/goals", require("./goals")); app.use("/invites", require("./invites")); app.use("/requests", require("./requests")); -app.use("/subscription", require("./subscription")); +app.use("/subscription", devFlagMiddleware, require("./subscription")); module.exports = app; diff --git a/routes/subscription.ts b/routes/subscription.ts index ae1c4add3..5827a3fd4 100644 --- a/routes/subscription.ts +++ b/routes/subscription.ts @@ -5,9 +5,8 @@ import { validateSubscribe } from "../middlewares/validators/subscription"; const authorizeRoles = require("../middlewares/authorizeRoles"); const router = express.Router(); const { SUPERUSER } = require("../constants/roles"); -import { devFlagMiddleware } from "../middlewares/devFlag"; -router.post("/", authenticate, devFlagMiddleware, validateSubscribe, subscribe); -router.patch("/", authenticate,devFlagMiddleware, unsubscribe); -router.get("/notify", authenticate, devFlagMiddleware, authorizeRoles([SUPERUSER]), sendEmail); +router.post("/", authenticate, validateSubscribe, subscribe); +router.patch("/", authenticate, unsubscribe); +router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); module.exports = router; diff --git a/test/fixtures/subscription/subscription.ts b/test/fixtures/subscription/subscription.ts index 512e6e55a..c39463123 100644 --- a/test/fixtures/subscription/subscription.ts +++ b/test/fixtures/subscription/subscription.ts @@ -1,6 +1,5 @@ -export const subscribedMessage = "user subscribed successfully"; - -export const unSubscribedMessage = "user unsubscribed successfully"; +export const subscribedMessage = "User subscribed successfully"; +export const unSubscribedMessage = "User unsubscribed successfully"; export const subscriptionData = { phoneNumber: "+911234567890", email: "example@gmail.com", diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index 9b526fc52..2cefcce6f 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -48,24 +48,7 @@ describe("/subscription email notifications", function () { return done(err); } expect(res).to.have.status(201); - expect(res.body).to.have.keys(["message"]); - expect(res.body.message).to.equal(subscribedMessage); - return done(); - }); - }); - - it("shouldn't add user's data and return 404 when dev is not equal to true", function (done) { - chai - .request(app) - .post(`/subscription`) - .set("cookie", `${cookieName}=${jwt}`) - .send(subscriptionData) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(404); - expect(res.body).to.have.property("message", "Route not found"); + expect(res.body).to.equal(subscribedMessage); return done(); }); }); @@ -80,23 +63,7 @@ describe("/subscription email notifications", function () { return done(err); } expect(res).to.have.status(200); - expect(res.body).to.have.keys(["message"]); - expect(res.body.message).to.equal(unSubscribedMessage); - return done(); - }); - }); - - it("shouldn't unsubscribe the user return 404 when dev is not equal to true", function (done) { - chai - .request(app) - .patch(`/subscription`) - .set("cookie", `${cookieName}=${jwt}`) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(404); - expect(res.body).to.have.property("message", "Route not found"); + expect(res.body).to.equal(unSubscribedMessage); return done(); }); }); @@ -145,22 +112,5 @@ describe("/subscription email notifications", function () { return done(); }); }); - - it("Sending mail should return 404 if dev is not equal to true", function (done) { - sinon.stub(nodemailer, "createTransport").callsFake(() => { - throw new Error("Transport error"); - }); - - chai - .request(app) - .get("/subscription/notify") - .set("Cookie", `${cookieName}=${superUserAuthToken}`) - .end((err, res) => { - if (err) return done(err); - expect(res).to.have.status(404); - expect(res.body).to.have.property("message", "Route not found"); - return done(); - }); - }); }); }); From e472233773d818e1c7e6bbf747758d91950146d1 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 13 Oct 2024 02:19:07 +0530 Subject: [PATCH 20/26] fix: return message --- controllers/subscription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 4a1bc29b2..24d10b83f 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -69,6 +69,6 @@ export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { return res.send({ message: "Email sent successfully", info }); } catch (error) { logger.error("Error occurred while sending email:", error.message); - return res.status(500).send({ message: "Failed to send email", error }); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); } }; \ No newline at end of file From 7a6c4ff425cff17fc18e7c83f7908d3a7a621501 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 13 Oct 2024 02:51:59 +0530 Subject: [PATCH 21/26] remove: comments --- middlewares/devFlag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/devFlag.ts b/middlewares/devFlag.ts index 83c454f6d..5887925af 100644 --- a/middlewares/devFlag.ts +++ b/middlewares/devFlag.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import { CustomRequest, CustomResponse } from "../types/global"; // Assuming these types extend the default ones +import { CustomRequest, CustomResponse } from "../types/global"; export const devFlagMiddleware = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { try { From 1ffe7f19bce10ef970a99d609e8d72e5bf9ba272 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Sun, 13 Oct 2024 03:58:20 +0530 Subject: [PATCH 22/26] fix: failing test case --- test/integration/subscription.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/subscription.test.js b/test/integration/subscription.test.js index 2cefcce6f..66d652287 100644 --- a/test/integration/subscription.test.js +++ b/test/integration/subscription.test.js @@ -107,8 +107,7 @@ describe("/subscription email notifications", function () { .end((err, res) => { if (err) return done(err); expect(res).to.have.status(500); - expect(res.body).to.have.property("message", "Failed to send email"); - expect(res.body).to.have.property("error"); + expect(res.body).to.have.property("message", "An internal server error occurred"); return done(); }); }); From 75e80f497e32e2329207a8f4e170e8ebe890d768 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Mon, 14 Oct 2024 00:27:39 +0530 Subject: [PATCH 23/26] fix: add test cases --- constants/subscription-validator.ts | 3 +- middlewares/devFlag.ts | 3 +- middlewares/validators/subscription.ts | 11 ++++- .../subscription-validator.test.js | 42 ++++++++++++++++--- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/constants/subscription-validator.ts b/constants/subscription-validator.ts index 0854d7ac9..78b05a88a 100644 --- a/constants/subscription-validator.ts +++ b/constants/subscription-validator.ts @@ -1 +1,2 @@ -export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; \ No newline at end of file +export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +export const phoneNumberRegex = /^[+]{1}(?:[0-9\-\\(\\)\\/.]\s?){6,15}[0-9]{1}$/; \ No newline at end of file diff --git a/middlewares/devFlag.ts b/middlewares/devFlag.ts index 5887925af..643208cfb 100644 --- a/middlewares/devFlag.ts +++ b/middlewares/devFlag.ts @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../types/global"; export const devFlagMiddleware = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { @@ -9,6 +9,7 @@ export const devFlagMiddleware = (req: CustomRequest, res: CustomResponse, next: } next(); } catch (err) { + logger.error("Error occurred in devFlagMiddleware:", err.message); next(err); } }; diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 4ac12576f..7a3ebcfdf 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -1,11 +1,18 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../../types/global"; -import { emailRegex } from "../../constants/subscription-validator"; +import { emailRegex, phoneNumberRegex } from "../../constants/subscription-validator"; // Removed phoneNumberRegex import import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { + + if(req.body.email){ + req.body.email = req.body.email.trim(); + } + if (req.body.phoneNumber) { + req.body.phoneNumber = req.body.phoneNumber.trim(); + } const subscribeSchema = Joi.object({ - phoneNumber: Joi.string().allow('').optional(), + phoneNumber: Joi.string().allow('').optional().regex(phoneNumberRegex), email: Joi.string().required().regex(emailRegex) }); const { error } = subscribeSchema.validate(req.body); diff --git a/test/unit/middlewares/subscription-validator.test.js b/test/unit/middlewares/subscription-validator.test.js index 8a2c5c79e..74d2ed9df 100644 --- a/test/unit/middlewares/subscription-validator.test.js +++ b/test/unit/middlewares/subscription-validator.test.js @@ -20,7 +20,7 @@ describe("Middleware | Validators | Subscription", function () { email: "test@example.com", }; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.calledOnce).to.be.equal(true); expect(res.status.called).to.be.equal(false); @@ -32,7 +32,7 @@ describe("Middleware | Validators | Subscription", function () { email: "test@example.com", }; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.calledOnce).to.be.equal(true); expect(res.status.called).to.be.equal(false); expect(res.json.called).to.be.equal(false); @@ -43,7 +43,7 @@ describe("Middleware | Validators | Subscription", function () { phoneNumber: "+911234567890", }; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.called).to.be.equal(false); expect(res.status.calledOnceWith(400)).to.be.equal(true); @@ -54,7 +54,7 @@ describe("Middleware | Validators | Subscription", function () { it("should return a 400 error when both phoneNumber and email are missing", async function () { req.body = {}; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.called).to.be.equal(false); expect(res.status.calledOnceWith(400)).to.be.equal(true); expect(res.json.calledOnce).to.be.equal(true); @@ -67,7 +67,7 @@ describe("Middleware | Validators | Subscription", function () { email: "invalid-email", }; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.called).to.be.equal(false); expect(res.status.calledOnceWith(400)).to.be.equal(true); @@ -83,9 +83,39 @@ describe("Middleware | Validators | Subscription", function () { email: "test@example.com", }; - await validateSubscribe(req, res, nextSpy); + validateSubscribe(req, res, nextSpy); expect(nextSpy.calledOnce).to.be.equal(true); expect(res.status.called).to.be.equal(false); expect(res.json.called).to.be.equal(false); }); + + it("should trim and validate phoneNumber if it contains leading or trailing spaces", async function () { + req.body = { + phoneNumber: " +911234567890 ", + email: "test@example.com", + }; + + validateSubscribe(req, res, nextSpy); + + expect(nextSpy.calledOnce).to.be.equal(true); + expect(res.status.called).to.be.equal(false); + expect(res.json.called).to.be.equal(false); + expect(req.body.phoneNumber).to.equal("+911234567890"); + }); + + it("should return a 400 error when phoneNumber is in incorrect format", async function () { + req.body = { + phoneNumber: "invalid-number", + email: "test@example.com", + }; + + validateSubscribe(req, res, nextSpy); + + expect(nextSpy.called).to.be.equal(false); + expect(res.status.calledOnceWith(400)).to.be.equal(true); + expect(res.json.calledOnce).to.be.equal(true); + expect(res.json.firstCall.args[0]) + .to.have.property("error") + .that.includes('"phoneNumber" with value "invalid-number" fails to match the required pattern'); + }); }); From a36a05c86efd3c6b877ec3b3fafb3e6f9fb8c9c1 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Mon, 14 Oct 2024 01:57:27 +0530 Subject: [PATCH 24/26] fix: remove comment --- middlewares/validators/subscription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middlewares/validators/subscription.ts b/middlewares/validators/subscription.ts index 7a3ebcfdf..dc129e797 100644 --- a/middlewares/validators/subscription.ts +++ b/middlewares/validators/subscription.ts @@ -1,6 +1,6 @@ import { NextFunction } from "express"; import { CustomRequest, CustomResponse } from "../../types/global"; -import { emailRegex, phoneNumberRegex } from "../../constants/subscription-validator"; // Removed phoneNumberRegex import +import { emailRegex, phoneNumberRegex } from "../../constants/subscription-validator"; import Joi from 'joi'; export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => { From f79bb5a4714444e1c8cebeb8daa3598b85824ced Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 15 Oct 2024 03:08:40 +0530 Subject: [PATCH 25/26] fix: custom environment variables --- config/custom-environment-variables.js | 11 +++++++---- config/development.js | 7 ------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 83044e4f0..d7a123ca4 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -56,10 +56,13 @@ module.exports = { }, emailSubscriptionCredentials: { - email: "", - password: "", - host: "", - port: "", + email: "RDS_EMAIL", + password: "RDS_EMAIL_PASSWORD", + host: "SMTP_HOST", + port: { + __name: "SMTP_PORT", + __format: "number", + }, }, userToken: { diff --git a/config/development.js b/config/development.js index 8b6f0b809..0e4d3bd44 100644 --- a/config/development.js +++ b/config/development.js @@ -30,13 +30,6 @@ module.exports = { }, }, - emailSubscriptionCredentials: { - email: "", - password: "", - host: "", - port: "", - }, - userToken: { publicKey: "-----BEGIN PUBLIC KEY-----\n" + From 9f870db5f67c61d8b5732f68b7d49d00bf111cd5 Mon Sep 17 00:00:00 2001 From: tejaskh3 Date: Tue, 15 Oct 2024 03:29:28 +0530 Subject: [PATCH 26/26] rename: emailSubscriptonCrenderials --- config/custom-environment-variables.js | 2 +- config/default.js | 2 +- controllers/subscription.ts | 12 ++++++------ test/config/test.js | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index d7a123ca4..b251ac00a 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -55,7 +55,7 @@ module.exports = { }, }, - emailSubscriptionCredentials: { + emailServiceConfig: { email: "RDS_EMAIL", password: "RDS_EMAIL_PASSWORD", host: "SMTP_HOST", diff --git a/config/default.js b/config/default.js index 5d1c0cbe8..6e5f9cee9 100644 --- a/config/default.js +++ b/config/default.js @@ -25,7 +25,7 @@ module.exports = { clientSecret: "", }, - emailSubscriptionCredentials: { + emailServiceConfig: { email: "", password: "", host: "", diff --git a/controllers/subscription.ts b/controllers/subscription.ts index 24d10b83f..f3621987e 100644 --- a/controllers/subscription.ts +++ b/controllers/subscription.ts @@ -3,7 +3,7 @@ const { addOrUpdate } = require("../models/users"); const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); const nodemailer = require("nodemailer"); const config = require("config"); -const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials"); +const emailServiceConfig = config.get("emailServiceConfig"); export const subscribe = async (req: CustomRequest, res: CustomResponse) => { const { email } = req.body; @@ -48,18 +48,18 @@ export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => { export const sendEmail = async (req: CustomRequest, res: CustomResponse) => { try { const transporter = nodemailer.createTransport({ - host: emailSubscriptionCredentials.host, - port: emailSubscriptionCredentials.port, + host: emailServiceConfig.host, + port: emailServiceConfig.port, secure: false, auth: { - user: emailSubscriptionCredentials.email, - pass: emailSubscriptionCredentials.password, + user: emailServiceConfig.email, + pass: emailServiceConfig.password, }, }); const info = await transporter.sendMail({ - from: `"Real Dev Squad" <${emailSubscriptionCredentials.email}>`, + from: `"Real Dev Squad" <${emailServiceConfig.email}>`, to: "tejasatrds@gmail.com", subject: "Hello local, Testing in progress.", text: "working for notification feature", diff --git a/test/config/test.js b/test/config/test.js index e637108bd..688b40c26 100644 --- a/test/config/test.js +++ b/test/config/test.js @@ -36,7 +36,7 @@ module.exports = { "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-hqc2v%40dev-rds.iam.gserviceaccount.com" }`, - emailSubscriptionCredentials: { + emailServiceConfig: { email: "", password: "", host: "",