diff --git a/src/__test__/subscribe.Test.ts b/src/__test__/subscribe.Test.ts new file mode 100644 index 0000000..cd33133 --- /dev/null +++ b/src/__test__/subscribe.Test.ts @@ -0,0 +1,97 @@ +import request from 'supertest'; +import app from '../app'; +import { afterAllHook, beforeAllHook } from './testSetup'; +import dbConnection from '../database'; +import Subscription from '../database/models/subscribe'; + +const subscribeRepository = dbConnection.getRepository(Subscription); + +beforeAll(async () => { + await beforeAllHook(); +}); + +afterAll(async () => { + await afterAllHook(); +}); + +describe('POST /api/v1/subscribe', () => { + beforeEach(async () => { + // Clear the table before each test + await subscribeRepository.clear(); + }); + + it('should subscribe successfully with a valid email', async () => { + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'test@example.com' }); + + expect(response.status).toBe(201); + expect(response.body.message).toBe('Subscribed successfully'); + expect(response.body.subscription.email).toBe('test@example.com'); + + const subscription = await subscribeRepository.findOne({ + where: { email: 'test@example.com' }, + }); + expect(subscription).toBeDefined(); + }); + + it('should return 400 if the email is already subscribed', async () => { + const subscription = new Subscription(); + subscription.email = 'test@example.com'; + await subscribeRepository.save(subscription); + + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'test@example.com' }); + + expect(response.status).toBe(400); + expect(response.body.message).toBe('Email is already subscribed'); + }); + + it('should return 400 for an invalid email format', async () => { + const response = await request(app) + .post('/api/v1/subscribe') + .send({ email: 'invalid-email' }); + + expect(response.status).toBe(400); + expect(response.body.errors[0].msg).toBe('Email is not valid'); + }); +}); + +describe('DELETE /api/v1/subscribe/delete/:id', () => { + beforeEach(async () => { + // Clear the table before each test + await subscribeRepository.clear(); + }); + + it('should remove a subscription successfully', async () => { + const subscription = new Subscription(); + subscription.email = 'test@example.com'; + await subscribeRepository.save(subscription); + + const response = await request(app) + .delete(`/api/v1/subscribe/delete/${subscription.id}`) + .send(); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Subscription removed successfully'); + }); + + it('should return 404 if the subscription does not exist', async () => { + const response = await request(app) + .delete('/api/v1/subscribe/delete/450') + .send(); + + expect(response.status).toBe(404); + expect(response.body.message).toBe('Subscription not found'); + }); + + it('should return 400 for invalid ID', async () => { + const response = await request(app) + .delete('/api/v1/subscribe/delete/noid') + .send(); + + expect(response.status).toBe(400); + expect(response.body.message).toBeUndefined(); + }); +}); diff --git a/src/controller/subscribeController.ts b/src/controller/subscribeController.ts new file mode 100644 index 0000000..358f1b0 --- /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;