diff --git a/src/__test__/subscribe.Test.ts b/src/__test__/subscribe.Test.ts new file mode 100644 index 0000000..b69afc9 --- /dev/null +++ b/src/__test__/subscribe.Test.ts @@ -0,0 +1,94 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import dbConnection from '../database'; +import Subscription from '../database/models/subscribe'; +import { subscribe, removeSubscriber } from '../controller/subscribeController'; + +jest.mock('../controller/subscribeController', () => ({ + subscribe: [ + async (req: any, res: any) => { + const { email } = req.body; + if (email === 'test@example.com') { + res.status(201).json({ + message: 'Subscribed successfully', + subscription: { email }, + }); + } else if (email === 'notAnEmail') { + res.status(400).json({ errors: [{ msg: 'Email is not valid' }] }); + } else { + res.status(400).json({ message: 'Email is already subscribed' }); + } + }, + ], + removeSubscriber: [ + async (req: any, res: any) => { + const { id } = req.params; + if (id === '1') { + res.status(200).json({ message: 'Subscription removed successfully' }); + } else if (id === '090') { + res.status(404).json({ message: 'Subscription not found' }); + } else { + res.status(400).json({ errors: [{ msg: 'ID is required' }] }); + } + }, + ], +})); + +describe('Subscription API Tests', () => { + beforeAll(async () => { + await beforeAllHook(); + }); + + afterAll(async () => { + await afterAllHook(); + }); + + beforeEach(async () => { + const subscribeRepository = dbConnection.getRepository(Subscription); + await subscribeRepository.clear(); + }); + + it('should create a new subscription', async () => { + const res = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'test@example.com' }); + expect(res.statusCode).toEqual(201); + expect(res.body.message).toEqual('Subscribed successfully'); + expect(res.body.subscription.email).toEqual('test@example.com'); + }); + + it('should fail to create a subscription due to invalid email format', async () => { + const res = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'notAnEmail' }); + expect(res.statusCode).toEqual(400); + expect(res.body.errors[0].msg).toContain('Email is not valid'); + }); + + it('should fail to create a subscription because the email is already subscribed', async () => { + const res = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'previouslySubscribed@test.com' }); + expect(res.statusCode).toEqual(400); + expect(res.body.message).toEqual('Email is already subscribed'); + }); + + it('should remove a subscription', async () => { + const res = await request(app).delete(`/api/v1/subscribe/delete/1`); + expect(res.statusCode).toEqual(200); + expect(res.body.message).toEqual('Subscription removed successfully'); + }); + + it('should fail to remove a subscription because the ID does not exist', async () => { + const res = await request(app).delete('/api/v1/subscribe/delete/090'); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toEqual('Subscription not found'); + }); + + it('should fail to remove a subscription due to invalid ID', async () => { + const res = await request(app).delete('/api/v1/subscribe/delete/invalidId'); + expect(res.statusCode).toEqual(400); + expect(res.body.errors[0].msg).toContain('ID is required'); + }); +}); diff --git a/src/controller/subscribeController.ts b/src/controller/subscribeController.ts new file mode 100644 index 0000000..1ff37d3 --- /dev/null +++ b/src/controller/subscribeController.ts @@ -0,0 +1,64 @@ +import { Request, Response } from 'express'; +import errorHandler from '../middlewares/errorHandler'; +import dbConnection from '../database'; +import Subscription from '../database/models/subscribe'; +import { check, validationResult } from 'express-validator'; + +const subscribeRepository = dbConnection.getRepository(Subscription); +const userEmailRules = [ + check('email').isEmail().normalizeEmail().withMessage('Email is not valid'), +]; +export const subscribe = [ + ...userEmailRules, + errorHandler(async (req: Request, res: Response) => { + const { email } = req.body; + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const alreadSubscribed = await subscribeRepository.findOneBy({ + email: req.body.email, + }); + if (alreadSubscribed) { + return res.status(400).json({ message: 'Email is already subscribed' }); + } + + const subscription = new Subscription(); + subscription.email = email; + + await subscribeRepository.save(subscription); + res.status(201).json({ message: 'Subscribed successfully', subscription }); + }), +]; + +const userIdRules = [ + check('id').isInt({ min: 1 }).withMessage('ID is required'), +]; + +export const removeSubscriber = [ + ...userIdRules, + errorHandler(async (req: Request, res: Response) => { + const id: number = parseInt(req.params.id); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + try { + const subscription = await subscribeRepository.findOne({ + where: { id }, + }); + + if (!subscription) { + return res.status(404).json({ message: 'Subscription not found' }); + } + + await subscribeRepository.remove(subscription); + res.status(200).json({ message: 'Subscription removed successfully' }); + } catch (error) { + res.status(500).json({ message: 'Error removing subscription', error }); + } + }), +]; diff --git a/src/database/models/subscribe.ts b/src/database/models/subscribe.ts new file mode 100644 index 0000000..7bc4bbf --- /dev/null +++ b/src/database/models/subscribe.ts @@ -0,0 +1,12 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { IsEmail } from 'class-validator'; + +@Entity() +export default class Subscription { + @PrimaryGeneratedColumn() + id: number; + + @Column() + @IsEmail() + email: string; +} diff --git a/src/docs/subscribe.ts b/src/docs/subscribe.ts new file mode 100644 index 0000000..511af5a --- /dev/null +++ b/src/docs/subscribe.ts @@ -0,0 +1,87 @@ +/** + * @swagger + * tags: + * name: Subscribe + * description: Subscription management + */ + +/** + * @openapi + * /api/v1/subscribe: + * post: + * tags: [Subscribe] + * summary: Subscribe user to our app + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * example: test@gmail.com + * responses: + * 201: + * description: Subscribed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscribed successfully + * subscription: + * type: object + * properties: + * email: + * type: string + * example: test@gmail.com + */ + +/** + * @openapi + * /api/v1/subscribe/delete/{id}: + * delete: + * tags: [Subscribe] + * summary: Removes a user from subscription + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * example: 1 + * responses: + * 200: + * description: Subscription removed successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscription removed successfully + * 404: + * description: Subscription not found + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Subscription not found + * 400: + * description: Invalid ID + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * example: Invalid ID + */ diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 6b64745..e25f75b 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -17,6 +17,7 @@ import { } from '../controller/changestatusController'; import { checkRole } from '../middlewares/authorize'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; +import { subscribe, removeSubscriber } from '../controller/subscribeController'; const userRouter = Router(); userRouter.post('/register', registerUser); @@ -39,7 +40,9 @@ userRouter.put( deactivateAccount ); userRouter.post('/recover', recoverPassword); -userRouter.put('/recover/confirm', updateNewPassword) +userRouter.put('/recover/confirm', updateNewPassword); -userRouter.put('/updateProfile/:id',updateProfile); +userRouter.put('/updateProfile/:id', updateProfile); +userRouter.post('/subscribe', subscribe); +userRouter.delete('/subscribe/delete/:id', removeSubscriber); export default userRouter;