From c49095fc7c4a30233a0167297a09f761bc4127fa Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 10:53:05 +0100 Subject: [PATCH 1/4] fix: handle case where an unsubscribe user tries to subscribe --- .../NewsLetterSubscriptionController.ts | 23 +++++++- src/models/newsLetterSubscription.ts | 3 + .../newsLetterSubscription.service.ts | 22 +++++-- src/test/newsLetterSubscription.spec.ts | 59 ++++++++++++------- 4 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index 8f23fd83..65f2961d 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -52,6 +52,23 @@ const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); * type: string * example: You are already subscribed to our newsletter. * + * 400: + * description: User is already subscribed but unsubscribe. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: boolean + * example: false + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You are already subscribed, please enable newsletter subscription to receive newsletter again + * * 500: * description: Internal server error. An error occurred while processing the subscription. * content: @@ -80,13 +97,15 @@ const subscribeToNewsletter = async ( throw new BadRequest("Email is missing in request body."); } const subscriber = await newsLetterSubscriptionService.subscribeUser(email); - res.status(!subscriber.isSubscribe ? 201 : 200).json({ + res.status(subscriber.isNewlySubscribe ? 201 : 200).json({ status: "success", - message: !subscriber.isSubscribe + message: subscriber.isNewlySubscribe ? "Subscriber subscription successful" : "You are already subscribed to our newsletter", }); } catch (error) { + console.log(error); + next(error); } }; diff --git a/src/models/newsLetterSubscription.ts b/src/models/newsLetterSubscription.ts index 8f83c605..a9a41829 100644 --- a/src/models/newsLetterSubscription.ts +++ b/src/models/newsLetterSubscription.ts @@ -12,4 +12,7 @@ export class NewsLetterSubscriber { @Column() email: string; + + @Column() + isSubscribe: boolean; } diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index be94bdd9..b1633c35 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -15,26 +15,36 @@ export class NewsLetterSubscriptionService } public async subscribeUser(email: string): Promise<{ - isSubscribe: boolean; + isNewlySubscribe: boolean; subscriber: NewsLetterSubscriber; }> { - let isSubscribe = false; + let isNewlySubscribe = true; + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ where: { email }, }); - if (isExistingSubscriber) { - isSubscribe = true; - return { isSubscribe, subscriber: isExistingSubscriber }; + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isNewlySubscribe = false; + return { isNewlySubscribe, subscriber: isExistingSubscriber }; + } + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === false) { + throw new BadRequest( + "You are already subscribed, please enable newsletter subscription to receive newsletter again", + ); } + const newSubscriber = new NewsLetterSubscriber(); newSubscriber.email = email; + newSubscriber.isSubscribe = true; + const subscriber = await this.newsLetterSubscriber.save(newSubscriber); + if (!subscriber) { throw new HttpError( 500, "An error occurred while processing your request", ); } - return { isSubscribe, subscriber }; + return { isNewlySubscribe, subscriber }; } } diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index d5384077..ee889657 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -2,6 +2,7 @@ import { Repository } from "typeorm"; import AppDataSource from "../data-source"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; +import { BadRequest } from "../middleware"; jest.mock("../data-source", () => ({ __esModule: true, @@ -22,9 +23,7 @@ jest.mock("../utils"); describe("NewsLetterSubscriptionService", () => { let newsLetterSubscriptionService: NewsLetterSubscriptionService; - let newsLetterRepositoryMock: jest.Mocked< - Repository - >; + let newsLetterRepositoryMock: jest.Mocked>; beforeEach(() => { newsLetterRepositoryMock = { @@ -45,12 +44,8 @@ describe("NewsLetterSubscriptionService", () => { describe("SubscribeToNewsLetter", () => { it("should subscribe a new user", async () => { - const user = new NewsLetterSubscriber(); - user.email = "test@example.com"; - - const payload = { - email: "test1@example.com", - }; + const newSubscriber = new NewsLetterSubscriber(); + newSubscriber.email = "test1@example.com"; (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null); (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( @@ -59,40 +54,60 @@ describe("NewsLetterSubscriptionService", () => { return Promise.resolve(user); }, ); + const result = await newsLetterSubscriptionService.subscribeUser("test1@example.com"); - expect(result.isSubscribe).toBe(false); + expect(result.isNewlySubscribe).toBe(true); expect(result.subscriber).toEqual({ id: "456", email: "test1@example.com", + isSubscribe: true, }); - expect(newsLetterRepositoryMock.save).toHaveBeenCalled(); + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + email: "test1@example.com", + isSubscribe: true, + }), + ); }); - it("should handle already subscribed user", async () => { - const user = new NewsLetterSubscriber(); - user.id = "123"; - user.email = "test@example.com"; + it("should handle an already subscribed user", async () => { + const existingSubscriber = new NewsLetterSubscriber(); + existingSubscriber.id = "123"; + existingSubscriber.email = "test@example.com"; + existingSubscriber.isSubscribe = true; - (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); - (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( - (user) => { - user.id = "456"; - return Promise.resolve(user); - }, + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + existingSubscriber, ); + const result = await newsLetterSubscriptionService.subscribeUser("test@example.com"); - expect(result.isSubscribe).toBe(true); + expect(result.isNewlySubscribe).toBe(false); expect(result.subscriber).toEqual({ id: "123", email: "test@example.com", + isSubscribe: true, }); expect(newsLetterRepositoryMock.save).not.toHaveBeenCalled(); }); + it("should throw a Conflict error if already subscribed but inactive", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + 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"), From 41e9feb7f2e5403f9a2962aff705110def9846da Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 15:47:18 +0100 Subject: [PATCH 2/4] feat: unsubscribe from newsletter - implemeted unsubscibe from new letter functionality - write unittest to test for unsubscribe service --- .../NewsLetterSubscriptionController.ts | 84 ++++++++++++++++++- src/routes/newsLetterSubscription.ts | 11 ++- .../newsLetterSubscription.service.ts | 20 ++++- src/test/newsLetterSubscription.spec.ts | 49 +++++++++++ src/types/index.d.ts | 1 + 5 files changed, 161 insertions(+), 4 deletions(-) diff --git a/src/controllers/NewsLetterSubscriptionController.ts b/src/controllers/NewsLetterSubscriptionController.ts index 65f2961d..325740f4 100644 --- a/src/controllers/NewsLetterSubscriptionController.ts +++ b/src/controllers/NewsLetterSubscriptionController.ts @@ -11,7 +11,7 @@ const newsLetterSubscriptionService = new NewsLetterSubscriptionService(); * summary: Subscribe to the newsletter * description: Allows a user to subscribe to the newsletter by providing an email address. * tags: - * - Newsletter Subscription + * - Newsletter * requestBody: * required: true * content: @@ -110,4 +110,84 @@ const subscribeToNewsletter = async ( } }; -export { subscribeToNewsletter }; +/** + * @swagger + * /newsletter/unsubscribe: + * post: + * summary: Unsubscribe from newsletter + * description: Allows a logedegin user to unsubscribe from the newsletter using their email address. + * tags: + * - Newsletter + * security: + * - bearerAuth: [] # Assumes you're using bearer token authentication + * responses: + * 200: + * description: Successfully unsubscribed from the newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: Successfully unsubscribed from newsletter + * 400: + * description: Bad request, missing or invalid email. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 400 + * message: + * type: string + * example: You already unsubscribed to newsletter. + * 404: + * description: User not subscribed ti newsletter. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: unsuccessful + * status_code: + * type: number + * example: 404 + * message: + * type: string + * example: You are not subscribed to newsletter. + */ +const unSubscribeToNewsletter = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { email } = req.user; + if (!email) { + throw new BadRequest("Email is missing in request body."); + } + const subscriber = + await newsLetterSubscriptionService.unSubcribeUser(email); + if (subscriber) { + res.status(200).json({ + status: "success", + message: "Successfully unsubscribed from newsletter", + }); + } + } catch (error) { + next(error); + } +}; + +export { subscribeToNewsletter, unSubscribeToNewsletter }; diff --git a/src/routes/newsLetterSubscription.ts b/src/routes/newsLetterSubscription.ts index 67e6b717..faa81de4 100644 --- a/src/routes/newsLetterSubscription.ts +++ b/src/routes/newsLetterSubscription.ts @@ -1,5 +1,8 @@ import { Router } from "express"; -import { subscribeToNewsletter } from "../controllers/NewsLetterSubscriptionController"; +import { + subscribeToNewsletter, + unSubscribeToNewsletter, +} from "../controllers/NewsLetterSubscriptionController"; import { authMiddleware } from "../middleware"; const newsLetterSubscriptionRoute = Router(); @@ -10,4 +13,10 @@ newsLetterSubscriptionRoute.post( subscribeToNewsletter, ); +newsLetterSubscriptionRoute.post( + "/newsletter-subscription/unsubscribe", + authMiddleware, + unSubscribeToNewsletter, +); + export { newsLetterSubscriptionRoute }; diff --git a/src/services/newsLetterSubscription.service.ts b/src/services/newsLetterSubscription.service.ts index b1633c35..e6c805ae 100644 --- a/src/services/newsLetterSubscription.service.ts +++ b/src/services/newsLetterSubscription.service.ts @@ -2,7 +2,7 @@ import { Repository } from "typeorm"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { INewsLetterSubscriptionService } from "../types"; import AppDataSource from "../data-source"; -import { BadRequest, HttpError } from "../middleware"; +import { BadRequest, HttpError, ResourceNotFound } from "../middleware"; export class NewsLetterSubscriptionService implements INewsLetterSubscriptionService @@ -47,4 +47,22 @@ export class NewsLetterSubscriptionService } return { isNewlySubscribe, subscriber }; } + + public async unSubcribeUser(email: string): Promise { + const isExistingSubscriber = await this.newsLetterSubscriber.findOne({ + where: { email }, + }); + + if (!isExistingSubscriber) { + throw new ResourceNotFound("You are not subscribed to newsletter"); + } + + if (isExistingSubscriber && isExistingSubscriber.isSubscribe === true) { + isExistingSubscriber.isSubscribe = false; + await this.newsLetterSubscriber.save(isExistingSubscriber); + return isExistingSubscriber; + } + + throw new BadRequest("You already unsubscribed to newsletter"); + } } diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index ee889657..4a2b8652 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -118,4 +118,53 @@ describe("NewsLetterSubscriptionService", () => { ).rejects.toThrow("An error occurred while processing your request"); }); }); + + describe("UnsubscribeFromNewsLetter", () => { + it("should successfully unsubscribe a logged-in user from the newsletter", async () => { + const user = new NewsLetterSubscriber(); + user.email = "test1@example.com"; + user.id = "5678"; + user.isSubscribe = true; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.isSubscribe = false; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.unSubcribeUser("test1@example.com"); + + expect(result).toEqual({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }); + + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }), + ); + }); + + it("should throw and error if user is not subscribed", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + }); }); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ab301391..4127a7ab 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -87,6 +87,7 @@ export interface GoogleUser { export interface INewsLetterSubscriptionService { subscribeUser(email: string): Promise; + unSubcribeUser(email: string): Promise; } export interface INewsLetterSubscription { From 89c30179ead96042783879e3c4cf2d5f204b9114 Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 16:14:42 +0100 Subject: [PATCH 3/4] test: fix test for unsubscribe service --- src/test/newsLetterSubscription.spec.ts | 51 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index 57607047..b4777a9c 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -2,7 +2,7 @@ import { Repository } from "typeorm"; import AppDataSource from "../data-source"; import { NewsLetterSubscriber } from "../models/newsLetterSubscription"; import { NewsLetterSubscriptionService } from "../services/newsLetterSubscription.service"; -import { BadRequest } from "../middleware"; +import { BadRequest, ResourceNotFound } from "../middleware"; jest.mock("../data-source", () => ({ __esModule: true, @@ -188,4 +188,53 @@ describe("NewsLetterSubscriptionService", () => { }); }); }); + + describe("UnsubscribeFromNewsLetter", () => { + it("should successfully unsubscribe a logged-in user from the newsletter", async () => { + const user = new NewsLetterSubscriber(); + user.email = "test1@example.com"; + user.id = "5678"; + user.isSubscribe = true; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(user); + + (newsLetterRepositoryMock.save as jest.Mock).mockImplementation( + (user) => { + user.isSubscribe = false; + return Promise.resolve(user); + }, + ); + + const result = + await newsLetterSubscriptionService.unSubcribeUser("test1@example.com"); + + expect(result).toEqual({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }); + + expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: "5678", + email: "test1@example.com", + isSubscribe: false, + }), + ); + }); + + it("should throw an error if user is not subscribed", async () => { + const inactiveSubscriber = new NewsLetterSubscriber(); + inactiveSubscriber.email = "test@example.com"; + inactiveSubscriber.isSubscribe = false; + + (newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue( + inactiveSubscriber, + ); + + await expect( + newsLetterSubscriptionService.subscribeUser("test@example.com"), + ).rejects.toThrow(BadRequest); + }); + }); }); From ed49ed6639c9ea4fc73defdfe853c2f6860dbf49 Mon Sep 17 00:00:00 2001 From: khingz Date: Thu, 8 Aug 2024 16:18:25 +0100 Subject: [PATCH 4/4] chore: remove comment --- src/test/newsLetterSubscription.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/newsLetterSubscription.spec.ts b/src/test/newsLetterSubscription.spec.ts index b4777a9c..6f5f3e09 100644 --- a/src/test/newsLetterSubscription.spec.ts +++ b/src/test/newsLetterSubscription.spec.ts @@ -87,7 +87,6 @@ describe("NewsLetterSubscriptionService", () => { const result = await newsLetterSubscriptionService.subscribeUser("test@example.com"); - console.log(result); expect(result.isNewlySubscribe).toBe(false); expect(result.subscriber).toEqual({