Skip to content

Commit

Permalink
Merge pull request hngprojects#516 from Khingz/feat/subscription
Browse files Browse the repository at this point in the history
feat: unsubscribe from newsletter
  • Loading branch information
AdeGneus authored Aug 8, 2024
2 parents f503716 + ed49ed6 commit 2b6230c
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 23 deletions.
107 changes: 103 additions & 4 deletions src/controllers/NewsLetterSubscriptionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -80,12 +97,94 @@ 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);
}
};

/**
* @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);
}
Expand Down Expand Up @@ -205,4 +304,4 @@ const getAllNewsletter = async (
}
};

export { getAllNewsletter, subscribeToNewsletter };
export { getAllNewsletter, subscribeToNewsletter, unSubscribeToNewsletter };
3 changes: 3 additions & 0 deletions src/models/newsLetterSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export class NewsLetterSubscriber {

@Column()
email: string;

@Column()
isSubscribe: boolean;
}
7 changes: 7 additions & 0 deletions src/routes/newsLetterSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Router } from "express";
import {
getAllNewsletter,
subscribeToNewsletter,
unSubscribeToNewsletter,
} from "../controllers/NewsLetterSubscriptionController";
import { UserRole } from "../enums/userRoles";
import { authMiddleware, checkPermissions } from "../middleware";
Expand All @@ -14,6 +15,12 @@ newsLetterSubscriptionRoute.post(
subscribeToNewsletter,
);

newsLetterSubscriptionRoute.post(
"/newsletter-subscription/unsubscribe",
authMiddleware,
unSubscribeToNewsletter,
);

newsLetterSubscriptionRoute.get(
"/newsletter-subscription",
authMiddleware,
Expand Down
43 changes: 36 additions & 7 deletions src/services/newsLetterSubscription.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Repository } from "typeorm";
import AppDataSource from "../data-source";
import { HttpError } from "../middleware";
import { NewsLetterSubscriber } from "../models/newsLetterSubscription";
import { INewsLetterSubscriptionService } from "../types";
import { BadRequest, HttpError, ResourceNotFound } from "../middleware";

export class NewsLetterSubscriptionService
implements INewsLetterSubscriptionService
Expand All @@ -15,27 +15,56 @@ 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 };
}

public async unSubcribeUser(email: string): Promise<any> {
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");
}

public async fetchAllNewsletter({
Expand Down
92 changes: 80 additions & 12 deletions src/test/newsLetterSubscription.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, ResourceNotFound } from "../middleware";

jest.mock("../data-source", () => ({
__esModule: true,
Expand All @@ -22,9 +23,7 @@ jest.mock("../utils");

describe("NewsLetterSubscriptionService", () => {
let newsLetterSubscriptionService: NewsLetterSubscriptionService;
let newsLetterRepositoryMock: jest.Mocked<
Repository<NewsLetterSubscriptionService>
>;
let newsLetterRepositoryMock: jest.Mocked<Repository<NewsLetterSubscriber>>;

beforeEach(() => {
newsLetterRepositoryMock = {
Expand All @@ -45,12 +44,8 @@ describe("NewsLetterSubscriptionService", () => {

describe("SubscribeToNewsLetter", () => {
it("should subscribe a new user", async () => {
const user = new NewsLetterSubscriber();
user.email = "[email protected]";

const payload = {
email: "[email protected]",
};
const newSubscriber = new NewsLetterSubscriber();
newSubscriber.email = "[email protected]";

(newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(null);
(newsLetterRepositoryMock.save as jest.Mock).mockImplementation(
Expand All @@ -59,39 +54,63 @@ describe("NewsLetterSubscriptionService", () => {
return Promise.resolve(user);
},
);

const result =
await newsLetterSubscriptionService.subscribeUser("[email protected]");

expect(result.isSubscribe).toBe(false);
expect(result.isNewlySubscribe).toBe(true);
expect(result.subscriber).toEqual({
id: "456",
email: "[email protected]",
isSubscribe: true,
});
expect(newsLetterRepositoryMock.save).toHaveBeenCalled();
expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith(
expect.objectContaining({
email: "[email protected]",
isSubscribe: true,
}),
);
});

it("should handle already subscribed user", async () => {
const user = new NewsLetterSubscriber();
user.id = "123";
user.email = "[email protected]";
user.isSubscribe = true;
(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("[email protected]");

expect(result.isSubscribe).toBe(true);
expect(result.isNewlySubscribe).toBe(false);
expect(result.subscriber).toEqual({
id: "123",
email: "[email protected]",
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 = "[email protected]";
inactiveSubscriber.isSubscribe = false;

(newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(
inactiveSubscriber,
);

await expect(
newsLetterSubscriptionService.subscribeUser("[email protected]"),
).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"),
Expand Down Expand Up @@ -168,4 +187,53 @@ describe("NewsLetterSubscriptionService", () => {
});
});
});

describe("UnsubscribeFromNewsLetter", () => {
it("should successfully unsubscribe a logged-in user from the newsletter", async () => {
const user = new NewsLetterSubscriber();
user.email = "[email protected]";
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("[email protected]");

expect(result).toEqual({
id: "5678",
email: "[email protected]",
isSubscribe: false,
});

expect(newsLetterRepositoryMock.save).toHaveBeenCalledWith(
expect.objectContaining({
id: "5678",
email: "[email protected]",
isSubscribe: false,
}),
);
});

it("should throw an error if user is not subscribed", async () => {
const inactiveSubscriber = new NewsLetterSubscriber();
inactiveSubscriber.email = "[email protected]";
inactiveSubscriber.isSubscribe = false;

(newsLetterRepositoryMock.findOne as jest.Mock).mockResolvedValue(
inactiveSubscriber,
);

await expect(
newsLetterSubscriptionService.subscribeUser("[email protected]"),
).rejects.toThrow(BadRequest);
});
});
});
Loading

0 comments on commit 2b6230c

Please sign in to comment.