From 42c273d91d5a7dce08e014c5b652fa46bb41fca3 Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 08:34:29 +0100 Subject: [PATCH 1/3] feat: implement news letter subcription --- .../NewsLetterSubscriptionController.ts | 27 ++++++++++++++++ src/index.ts | 2 ++ src/models/newsLetterSubscription.ts | 15 +++++++++ src/routes/index.ts | 1 + src/routes/newsLetterSubscription.ts | 11 +++++++ .../newsLetterSubscription.service.ts | 32 +++++++++++++++++++ src/types/index.d.ts | 8 +++++ 7 files changed, 96 insertions(+) create mode 100644 src/controllers/NewsLetterSubscriptionController.ts create mode 100644 src/models/newsLetterSubscription.ts create mode 100644 src/routes/newsLetterSubscription.ts create mode 100644 src/services/newsLetterSubscription.service.ts diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts new file mode 100644 index 00000000..ab32abdd --- /dev/null +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -0,0 +1,27 @@ +import { Request, Response, NextFunction } from "express"; +import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; +import { BadRequest } from "../middleware"; + +const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); + +const subscribeToNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const { email } = req.body; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + await newsLetterSubscriptionService.subscribeUser(email); + res.status(201).json({ + status: "success", + message: "Subscriber subscription successful", + }); + } catch (error) { + next(error); + } +}; + +export { subscribeToNewsletter }; diff --git a/src/index.ts b/src/index.ts index d47b4e51..b63ae272 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { faqRouter, helpRouter, jobRouter, + newsLetterSubscriptionRoute, notificationRouter, paymentFlutterwaveRouter, paymentRouter, @@ -87,6 +88,7 @@ server.use("/api/v1", blogRouter); server.use("/api/v1", contactRouter); server.use("/api/v1", jobRouter); server.use("/api/v1", roleRouter); +server.use("/api/v1", newsLetterSubscriptionRoute); server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); diff --git a/src/models/newsLetterSubscription.ts b/src/models/newsLetterSubscription.ts new file mode 100644 index 00000000..8f83c605 --- /dev/null +++ b/src/models/newsLetterSubscription.ts @@ -0,0 +1,15 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, +} from "typeorm"; + +@Entity() +export class NewsLetterSubscriber { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column() + email: string; +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 2d1594e1..842b13d5 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -17,3 +17,4 @@ export * from "./testimonial"; export * from "./user"; export * from "./faq"; export * from "./run-test"; +export * from "./newsLetterSubscription"; diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts new file mode 100644 index 00000000..4ad1ab50 --- /dev/null +++ b/src/routes/newsLetterSubscription.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { subscribeToNewsletter } from "../controllers/NewsLetterSubscriptionController"; + +const newsLetterSubscriptionRoute = Router(); + +newsLetterSubscriptionRoute.post( + "/newsletter-subscription", + subscribeToNewsletter, +); + +export { newsLetterSubscriptionRoute }; diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts new file mode 100644 index 00000000..7f9d9806 --- /dev/null +++ b/src/services/newsLetterSubscription.service.ts @@ -0,0 +1,32 @@ +import { Repository } from "typeorm"; +import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; +import { INewsLetterSubscriptionService } from "../types"; +import AppDataSource from "../data-source"; +import { BadRequest, HttpError } from "../middleware"; + +export class NewsLetterSubscriptionService + implements INewsLetterSubscriptionService +{ + private newsLetterSubscriber: Repository; + + constructor() { + this.newsLetterSubscriber = + AppDataSource.getRepository(NewsLetterSubscriber); + } + + public async subscribeUser(email: string): Promise { + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ + where: { email }, + }); + if (isExistingSubscriber) { + throw new BadRequest("This email already subscribed"); + } + const newSubscriber = new NewsLetterSubscriber(); + newSubscriber.email = email; + const subscriber = await this.newsLetterSubscriber.save(newSubscriber); + if (!subscriber) { + throw new HttpError(500, "Internal server error"); + } + return newSubscriber; + } +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 0692d6d6..ab301391 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -84,3 +84,11 @@ export interface GoogleUser { picture: string; sub: string; } + +export interface INewsLetterSubscriptionService { + subscribeUser(email: string): Promise; +} + +export interface INewsLetterSubscription { + email: string; +} From aa22dd70f42c63d29161ce2cd21f8002446304b0 Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 09:56:52 +0100 Subject: [PATCH 2/3] test: add unittest to subcribe user authservice --- .../NewsLetterSubscriptionController.ts | 73 +++++++++++- src/routes/newsLetterSubscription.ts | 2 + .../newsLetterSubscription.service.ts | 16 ++- src/test/newsLetterSubscription.spec.ts | 106 ++++++++++++++++++ 4 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 src/test/newsLetterSubscription.spec.ts diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index ab32abdd..8f23fd83 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -4,6 +4,71 @@ import { BadRequest } from "../middleware"; const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); +/** + * @swagger + * /api/v1/newsletter-subscription: + * post: + * summary: Subscribe to the newsletter + * description: Allows a user to subscribe to the newsletter by providing an email address. + * tags: + * - Newsletter Subscription + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * format: email + * example: user@example.com + * description: The user's email address for subscribing to the newsletter. + * responses: + * 201: + * description: Subscription successful. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Subscriber subscription successful + * 200: + * description: User is already subscribed to the newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: You are already subscribed to our newsletter. + * + * 500: + * description: Internal server error. An error occurred while processing the subscription. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * status_code: + * type: number + * example: 500 + * message: + * type: string + * example: An error occurred while processing your request. + */ const subscribeToNewsletter = async ( req: Request, res: Response, @@ -14,10 +79,12 @@ const subscribeToNewsletter = async ( if (!email) { throw new BadRequest("Email is missing in request body."); } - await newsLetterSubscriptionService.subscribeUser(email); - res.status(201).json({ + const subscriber = await newsLetterSubscriptionService.subscribeUser(email); + res.status(!subscriber.isSubscribe ? 201 : 200).json({ status: "success", - message: "Subscriber subscription successful", + message: !subscriber.isSubscribe + ? "Subscriber subscription successful" + : "You are already subscribed to our newsletter", }); } catch (error) { next(error); diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index 4ad1ab50..9c16f946 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -1,10 +1,12 @@ import { Router } from "express"; import { subscribeToNewsletter } from "../controllers/NewsLetterSubscriptionController"; +import { authMiddleware } from "../middleware"; const newsLetterSubscriptionRoute = Router(); newsLetterSubscriptionRoute.post( "/newsletter-subscription", + // authMiddleware, subscribeToNewsletter, ); diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index 7f9d9806..be94bdd9 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -14,19 +14,27 @@ export class NewsLetterSubscriptionService AppDataSource.getRepository(NewsLetterSubscriber); } - public async subscribeUser(email: string): Promise { + public async subscribeUser(email: string): Promise<{ + isSubscribe: boolean; + subscriber: NewsLetterSubscriber; + }> { + let isSubscribe = false; const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ where: { email }, }); if (isExistingSubscriber) { - throw new BadRequest("This email already subscribed"); + isSubscribe = true; + return { isSubscribe, subscriber: isExistingSubscriber }; } const newSubscriber = new NewsLetterSubscriber(); newSubscriber.email = email; const subscriber = await this.newsLetterSubscriber.save(newSubscriber); if (!subscriber) { - throw new HttpError(500, "Internal server error"); + throw new HttpError( + 500, + "An error occurred while processing your request", + ); } - return newSubscriber; + return { isSubscribe, subscriber }; } } diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts new file mode 100644 index 00000000..d5384077 --- /dev/null +++ b/src/test/newsLetterSubscription.spec.ts @@ -0,0 +1,106 @@ +import { Repository } from "typeorm"; +import AppDataSource from "../data-source"; +import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; +import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; + +jest.mock("../data-source", () => ({ + __esModule: true, + default: { + getRepository: jest.fn(), + initialize: jest.fn(), + isInitialized: false, + }, +})); + +jest.mock("../utils", () => ({ + ...jest.requireActual("../utils"), + verifyToken: jest.fn(), +})); + +jest.mock("../models"); +jest.mock("../utils"); + +describe("NewsLetterSubscriptionService", () => { + let newsLetterSubscriptionService: NewsLetterSubscriptionService; + let newsLetterRepositoryMock: jest.Mocked< + Repository + >; + + beforeEach(() => { + newsLetterRepositoryMock = { + findOne: jest.fn(), + save: jest.fn(), + } as any; + + (AppDataSource.getRepository as jest.Mock).mockImplementation((entity) => { + if (entity === NewsLetterSubscriber) return newsLetterRepositoryMock; + }); + + newsLetterSubscriptionService = new NewsLetterSubscriptionService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("SubscribeToNewsLetter", () => { + it("should subscribe a new user", async () => { + const user = new NewsLetterSubscriber(); + user.email = "test@example.com"; + + const payload = { + email: "test1@example.com", + }; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.id = "456"; + return Promise.resolve(user); + }, + ); + const result = + await newsLetterSubscriptionService.subscribeUser("test1@example.com"); + + expect(result.isSubscribe).toBe(false); + expect(result.subscriber).toEqual({ + id: "456", + email: "test1@example.com", + }); + expect(newsLetterRepositoryMock.save).toHaveBeenCalled(); + }); + + it("should handle already subscribed user", async () => { + const user = new NewsLetterSubscriber(); + user.id = "123"; + user.email = "test@example.com"; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.id = "456"; + return Promise.resolve(user); + }, + ); + const result = + await newsLetterSubscriptionService.subscribeUser("test@example.com"); + + expect(result.isSubscribe).toBe(true); + expect(result.subscriber).toEqual({ + id: "123", + email: "test@example.com", + }); + expect(newsLetterRepositoryMock.save).not.toHaveBeenCalled(); + }); + + it("should throw an error if something goes wrong", async () => { + (newsLetterRepositoryMock.findOne as jest.Mock).mockRejectedValue( + new Error("An error occurred while processing your request"), + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow("An error occurred while processing your request"); + }); + }); +}); From 091638e5ed429c77d92bd97082fc2e3320d587da Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 09:58:21 +0100 Subject: [PATCH 3/3] fix: protect subscribe to newsletter route --- src/routes/newsLetterSubscription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index 9c16f946..67e6b717 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -6,7 +6,7 @@ const newsLetterSubscriptionRoute = Router(); newsLetterSubscriptionRoute.post( "/newsletter-subscription", - // authMiddleware, + authMiddleware, subscribeToNewsletter, );