generated from Real-Dev-Squad/website-template
-
Notifications
You must be signed in to change notification settings - Fork 255
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: collect email for subscription (#2124)
* feat: collect email for subscription * feat: add API to send email & test locally * feat: write test cases for subscription APIs * refactor: add comment * feat: add test for send-email API * feat: validating email and add contants * refactor: change file extension * feat: add test for invalid email * feat: add feature flag * refactor: change config details * refactor: change test email * fix: change phone number to optional * fix: make phoneNumber optional * fix: change API name * fix: change API name * fix: try-catch Indain phone numbers controllers * fix: put to patch * feat: add devFlagMiddleware * fix: comments * fix: return message * remove: comments * fix: failing test case * fix: add test cases * fix: remove comment * fix: custom environment variables * rename: emailSubscriptonCrenderials --------- Co-authored-by: Vinit khandal <[email protected]>
- Loading branch information
Showing
16 changed files
with
478 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
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}$/; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
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 emailServiceConfig = config.get("emailServiceConfig"); | ||
|
||
export const subscribe = async (req: CustomRequest, res: CustomResponse) => { | ||
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("User already subscribed"); | ||
} | ||
await addOrUpdate(data, userId); | ||
return res.status(201).json("User subscribed successfully"); | ||
} catch (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 userId = req.userData.id; | ||
const userAlreadySubscribed = req.userData.isSubscribed; | ||
try { | ||
if (!userAlreadySubscribed) { | ||
return res.boom.badRequest("User is already unsubscribed"); | ||
} | ||
await addOrUpdate( | ||
{ | ||
isSubscribed: false, | ||
}, | ||
userId | ||
); | ||
return res.status(200).json("User unsubscribed successfully"); | ||
} catch (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) => { | ||
try { | ||
const transporter = nodemailer.createTransport({ | ||
host: emailServiceConfig.host, | ||
port: emailServiceConfig.port, | ||
secure: false, | ||
|
||
auth: { | ||
user: emailServiceConfig.email, | ||
pass: emailServiceConfig.password, | ||
}, | ||
}); | ||
|
||
const info = await transporter.sendMail({ | ||
from: `"Real Dev Squad" <${emailServiceConfig.email}>`, | ||
to: "[email protected]", | ||
subject: "Hello local, Testing in progress.", | ||
text: "working for notification feature", | ||
html: "<b>Hello world!</b>", | ||
}); | ||
|
||
return res.send({ message: "Email sent successfully", info }); | ||
} catch (error) { | ||
logger.error("Error occurred while sending email:", error.message); | ||
return res.boom.badImplementation(INTERNAL_SERVER_ERROR); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { NextFunction } from "express"; | ||
import { CustomRequest, CustomResponse } from "../types/global"; | ||
|
||
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) { | ||
logger.error("Error occurred in devFlagMiddleware:", err.message); | ||
next(err); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { NextFunction } from "express"; | ||
import { CustomRequest, CustomResponse } from "../../types/global"; | ||
import { emailRegex, phoneNumberRegex } from "../../constants/subscription-validator"; | ||
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().regex(phoneNumberRegex), | ||
email: Joi.string().required().regex(emailRegex) | ||
}); | ||
const { error } = subscribeSchema.validate(req.body); | ||
if (error) { | ||
return res.status(400).json({ error: error.details[0].message }); | ||
} | ||
next(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"; | ||
const authorizeRoles = require("../middlewares/authorizeRoles"); | ||
const router = express.Router(); | ||
const { SUPERUSER } = require("../constants/roles"); | ||
|
||
router.post("/", authenticate, validateSubscribe, subscribe); | ||
router.patch("/", authenticate, unsubscribe); | ||
router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail); | ||
module.exports = router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const subscribedMessage = "User subscribed successfully"; | ||
export const unSubscribedMessage = "User unsubscribed successfully"; | ||
export const subscriptionData = { | ||
phoneNumber: "+911234567890", | ||
email: "[email protected]", | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
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 userData = require("../fixtures/user/user")(); | ||
const { expect } = chai; | ||
let userId = ""; | ||
const superUser = userData[4]; | ||
let superUserAuthToken = ""; | ||
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?dev=true") | ||
.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?dev=true`) | ||
.set("cookie", `${cookieName}=${jwt}`) | ||
.send(subscriptionData) | ||
.end((err, res) => { | ||
if (err) { | ||
return done(err); | ||
} | ||
expect(res).to.have.status(201); | ||
expect(res.body).to.equal(subscribedMessage); | ||
return done(); | ||
}); | ||
}); | ||
|
||
it("should unsubscribe the user", function (done) { | ||
chai | ||
.request(app) | ||
.patch(`/subscription?dev=true`) | ||
.set("cookie", `${cookieName}=${jwt}`) | ||
.end((err, res) => { | ||
if (err) { | ||
return done(err); | ||
} | ||
expect(res).to.have.status(200); | ||
expect(res.body).to.equal(unSubscribedMessage); | ||
return done(); | ||
}); | ||
}); | ||
|
||
describe("/notify endpoint", function () { | ||
beforeEach(async function () { | ||
const superUserId = await addUser(superUser); | ||
superUserAuthToken = authService.generateAuthToken({ userId: superUserId }); | ||
sinon.stub(nodemailerMock, "createTransport").callsFake(nodemailerMock.createTransport); | ||
}); | ||
|
||
afterEach(function () { | ||
sinon.restore(); | ||
nodemailerMock.mock.reset(); | ||
}); | ||
|
||
it("Should return 401 if the super user is not logged in", function (done) { | ||
chai | ||
.request(app) | ||
.get("/subscription/notify?dev=true") | ||
.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"); | ||
}); | ||
|
||
chai | ||
.request(app) | ||
.get("/subscription/notify?dev=true") | ||
.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", "An internal server error occurred"); | ||
return done(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
}); | ||
}); |
Oops, something went wrong.