diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b8aec..9388397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ env: TEST_SAMPLE_BUYER_EMAIL: ${{secrets.TEST_SAMPLE_BUYER_EMAIL}} TEST_VENDOR2_EMAIL: ${{secrets.TEST_VENDOR2_EMAIL}} - STRIPE_SECRET_KEY: ${{secrets.STRIPE_SECRET_KEYT}} + STRIPE_SECRET_KEY: ${{secrets.STRIPE_SECRET_KEY}} jobs: build-lint-test-coverage: diff --git a/.gitignore b/.gitignore index 829a739..b130fe4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,13 +6,3 @@ coverage/ dist /src/logs .DS_Store - - src/controllers/notificationControllers.ts - src/entities/Notification.ts - src/entities/NotificationItem.ts - src/routes/NoficationRoutes.ts - src/services/notificationServices/deleteNotification.ts - src/services/notificationServices/getNotifications.ts - src/services/notificationServices/updateNotification.ts - src/utils/getNotifications.ts - src/utils/sendNotification.ts \ No newline at end of file diff --git a/package.json b/package.json index 06430f2..ddbee4e 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,12 @@ "express-session": "^1.18.0", "express-winston": "^4.2.0", "highlight.js": "^11.9.0", - "joi": "^17.13.1", "jsend": "^1.1.0", "jsonwebtoken": "^9.0.2", "mailgen": "^2.0.28", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", - "node-nlp": "^4.27.0", + "node-nlp": "^3.10.2", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "passport": "^0.7.0", @@ -96,6 +95,7 @@ "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "jest-mock-extended": "^3.0.6", + "joi": "^17.13.1", "prettier": "^3.2.5", "supertest": "^7.0.0", "ts-jest": "^29.1.2", diff --git a/src/__test__/cart.test.ts b/src/__test__/cart.test.ts index 81a1412..40321f1 100644 --- a/src/__test__/cart.test.ts +++ b/src/__test__/cart.test.ts @@ -11,38 +11,44 @@ import { Category } from '../entities/Category'; import { Cart } from '../entities/Cart'; import { CartItem } from '../entities/CartItem'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { updateOrderService } from '../services/orderServices/updateOrderService'; const vendor1Id = uuid(); + const buyer1Id = uuid(); const buyer2Id = uuid(); const buyer3Id = uuid(); +const buyer4Id = uuid(); + const product1Id = uuid(); const product2Id = uuid(); + const catId = uuid(); const cart1Id = uuid(); const cartItemId = uuid(); + const sampleCartId = uuid(); const sampleCartItemId = uuid(); const samplecartItem3Id = uuid(); -const feedbackID = uuid(); -const feedbackID2 = uuid(); const sampleAdminId = uuid(); let returnedCartId: string; +let returnedCartItemId: string; +let returnedCartItem2Id: string; const jwtSecretKey = process.env.JWT_SECRET || ''; if (!process.env.TEST_USER_EMAIL || !process.env.TEST_USER_PASS) throw new Error('TEST_USER_PASS or TEST_USER_EMAIL not set in .env'); const sampleAdmin: UserInterface = { - id: vendor1Id, - firstName: 'vendor1', + id: sampleAdminId, + firstName: 'admin!', lastName: 'user', - email: process.env.TEST_USER_EMAIL, - password: process.env.TEST_USER_PASS, - userType: 'Vendor', + email: 'admin@gmail.com', + password: 'admin', + userType: 'Admin', gender: 'Male', - phoneNumber: '10026380996347', + phoneNumber: '10026386347', photoUrl: 'https://example.com/photo.jpg', role: 'ADMIN', }; @@ -86,7 +92,7 @@ const sampleBuyer1: UserInterface = { const sampleBuyer2: UserInterface = { id: buyer2Id, - firstName: 'buyer1', + firstName: 'buyer2', lastName: 'user', email: 'elijahladdiedv@example.com', password: 'password', @@ -98,15 +104,27 @@ const sampleBuyer2: UserInterface = { }; const sampleBuyer3: UserInterface = { id: buyer3Id, - firstName: 'buyer1', + firstName: 'buyer3', lastName: 'user', - email: 'elhladdiedv@example.com', + email: 'buyer3@example.com', password: 'password', - userType: 'Admin', + userType: 'Buyer', gender: 'Male', - phoneNumber: '121163800', + phoneNumber: '1211ddf3800', photoUrl: 'https://example.com/photo.jpg', - role: 'ADMIN', + role: 'BUYER', +}; +const sampleBuyer4: UserInterface = { + id: buyer4Id, + firstName: 'buyer4', + lastName: 'user', + email: 'buyer4@example.com', + password: 'password', + userType: 'Buyer', + gender: 'Male', + phoneNumber: '1211ddsdff3800', + photoUrl: 'https://example.com/photo.jpg', + role: 'BUYER', }; const sampleCat = { @@ -120,7 +138,7 @@ const sampleProduct1 = { description: 'amazing product', images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], newPrice: 200, - quantity: 10, + quantity: 20, vendor: sampleVendor1, categories: [sampleCat], }; @@ -174,7 +192,7 @@ const sampleCartItem3 = { total: 400, }; -const bodyTosend = { +const bodyToSend = { productId: product1Id, quantity: 2, }; @@ -189,6 +207,9 @@ beforeAll(async () => { await userRepository?.save({ ...sampleVendor1 }); await userRepository?.save({ ...sampleBuyer1 }); await userRepository?.save({ ...sampleBuyer2 }); + await userRepository?.save({ ...sampleBuyer3 }); + await userRepository?.save({ ...sampleBuyer4 }); + await userRepository?.save({ ...sampleAdmin }); const productRepository = connection?.getRepository(Product); await productRepository?.save({ ...sampleProduct1 }); @@ -211,98 +232,41 @@ afterAll(async () => { }); describe('Cart| Order management for guest/buyer', () => { - describe('Creating new product', () => { - it('should create new product', async () => { - const response = await request(app) - .post('/product') - .field('name', 'test product3') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('expirationDate', '10-2-2023') - .field('categories', 'technology') - .field('categories', 'sample') - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(201); - expect(response.body.data.product).toBeDefined; - }, 60000); - - it('return an error if the number of product images exceeds 6', async () => { - const response = await request(app) - .post(`/product/`) - .field('name', 'test-product-images') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('expirationDate', '10-2-2023') - .field('categories', 'technology') - .field('categories', 'sample') - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(400); - expect(response.body.error).toBe('Product cannot have more than 6 images'); - }); - it('should not create new product it already exist', async () => { - const response = await request(app) - .post('/product') - .field('name', 'test product3') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('categories', sampleCat.name) - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - - expect(response.status).toBe(409); - }); - - it('should not create new product, if there are missing field data', async () => { + describe('Adding product to cart on guest/buyer', () => { + it('should add product to cart as authenticated buyer', async () => { const response = await request(app) - .post('/product') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('categories', sampleCat.name) - .attach('images', `${__dirname}/test-assets/photo1.png`) - .attach('images', `${__dirname}/test-assets/photo2.webp`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + .post(`/cart`) + .send(bodyToSend) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(400); + expect(response.status).toBe(201); + expect(response.body.data.message).toBe('cart updated successfully'); + expect(response.body.data.cart).toBeDefined; }); - it('should not create new product, images are not at least more than 1', async () => { + it('should add product to cart as authenticated buyer2', async () => { const response = await request(app) - .post('/product') - .field('name', 'test-product-image') - .field('description', 'amazing product3') - .field('newPrice', 200) - .field('quantity', 10) - .field('categories', sampleCat.name) - .attach('images', `${__dirname}/test-assets/photo1.png`) - .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + .post(`/cart`) + .send({ + productId: product2Id, + quantity: 200, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - expect(response.status).toBe(400); + expect(response.status).toBe(201); + expect(response.body.data.message).toBe('cart updated successfully'); + expect(response.body.data.cart).toBeDefined; }); - }); - describe('Adding product to cart on guest/buyer', () => { - it('should add product to cart as authenticated buyer', async () => { + it('should add product to cart as authenticated buyer (buyer4)', async () => { const response = await request(app) .post(`/cart`) - .send(bodyTosend) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + .send({ + productId: product2Id, + quantity: 1, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer4Id, sampleBuyer4.email)}`); expect(response.status).toBe(201); expect(response.body.data.message).toBe('cart updated successfully'); @@ -310,16 +274,18 @@ describe('Cart| Order management for guest/buyer', () => { }); it('should add product to cart as guest', async () => { - const response = await request(app).post(`/cart`).send(bodyTosend); + const response = await request(app).post(`/cart`).send(bodyToSend); expect(response.status).toBe(201); expect(response.body.data.message).toBe('cart updated successfully'); expect(response.body.data.cart).toBeDefined; returnedCartId = response.body.data.cart.id; + + returnedCartItemId = response.body.data.cart.items[0].id; }); - it('should add second product to cart as guest', async () => { + it('should update quantity of product, if it is already in cart of guest', async () => { const response = await request(app) .post(`/cart`) .set('Cookie', [`cartId=${returnedCartId}`]) @@ -333,6 +299,21 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.cart).toBeDefined; }); + it('should add second product to cart of guest', async () => { + const response = await request(app) + .post(`/cart`) + .set('Cookie', [`cartId=${returnedCartId}`]) + .send({ + productId: product2Id, + quantity: 3, + }); + + returnedCartItem2Id = response.body.data.cart.items[1].id; + expect(response.status).toBe(201); + expect(response.body.data.message).toBe('cart updated successfully'); + expect(response.body.data.cart).toBeDefined; + }); + it('should return 400 for incorrect Id syntax (IDs not in uuid form), when add product to cart', async () => { const response = await request(app) .post(`/cart`) @@ -397,7 +378,7 @@ describe('Cart| Order management for guest/buyer', () => { it('should get Empty cart items of authenticated user', async () => { const response = await request(app) .get('/cart') - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); expect(response.status).toBe(200); expect(response.body.data.message).toBe('Cart is empty'); @@ -443,12 +424,13 @@ describe('Cart| Order management for guest/buyer', () => { describe('Order management tests', () => { let orderId: any; + let order2Id: any; let productId: any; let feedbackId: any; let feedback2Id: any; describe('Create order', () => { - it('should return 201 when user is found', async () => { + it('should create order for authenticated user', async () => { const response = await request(app) .post('/product/orders') .send({ @@ -462,46 +444,92 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.status).toBe(201); }); - it('should return orders for the buyer', async () => { + it('create order for another authenticated user', async () => { const response = await request(app) - .get('/product/client/orders') - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(200); - orderId = response.body.data.orders[0]?.id; - productId = response.body.data.orders[0]?.orderItems[0]?.product?.id; - }); + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer4Id, sampleBuyer4.email)}`); + expect(response.status).toBe(201); + order2Id = response.body.data.id; + }); - it('should get single order', async () => { + it('should not create an order if user has an empty cart or his carts has already been checked out', async () => { const response = await request(app) - .get(`/product/client/orders/${orderId}`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - - expect(response.status).toBe(404); + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); + expect(response.status).toBe(400); }); - it('should not return data for single order, if order doesn\'t exist', async () => { + it('should not create an order, if there are not enough product in stock', async () => { const response = await request(app) - .get(`/product/client/orders/${uuid()}`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - - expect(response.status).toBe(404); + .post('/product/orders') + .send({ + address: { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }, + }) + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + expect(response.status).toBe(400); }); + describe('Retriving Info about oreder Test', () => { + it('should return orders for the buyer', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + orderId = response.body.data.orders[0]?.id; + productId = response.body.data.orders[0]?.orderItems[0]?.product?.id; + }); - it('should not return data for single order, for an incorrect id syntax', async () => { - const response = await request(app) - .get(`/product/client/orders/incorrectId`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(404); - }); + it('should get single order', async () => { + const response = await request(app) + .get(`/product/client/orders/${orderId}`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - it('should return 404 if the buyer has no orders', async () => { - const response = await request(app) - .get('/product/client/orders') - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - expect(response.status).toBe(404); - expect(response.body.message).toBeUndefined; + expect(response.status).toBe(200); + expect(response.body.data.order).toBeDefined(); + }); + + it('should not return data for single order, if order doesn\'t exist', async () => { + const response = await request(app) + .get(`/product/client/orders/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + + expect(response.status).toBe(404); + }); + + it('should not return data for single order, for an incorrect id syntax', async () => { + const response = await request(app) + .get(`/product/client/orders/incorrectId`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + + expect(response.status).toBe(400); + }); + + it('should return 404 if the buyer has no orders', async () => { + const response = await request(app) + .get('/product/client/orders') + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + expect(response.status).toBe(404); + expect(response.body.message).toBeUndefined; + }); }); it('should return transaction history for the buyer', async () => { @@ -528,6 +556,22 @@ describe('Cart| Order management for guest/buyer', () => { .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(200); }); + + it('should return 404, if order doesn\'t exist', async () => { + const response = await request(app) + .put(`/product/client/orders/${uuid()}`) + .send({ orderStatus: 'completed' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(404); + }); + + it('should return 200 and make refund to buyer when order is cancelled or returned', async () => { + const response = await request(app) + .put(`/product/client/orders/${order2Id}`) + .send({ orderStatus: 'cancelled' }) + .set('Authorization', `Bearer ${getAccessToken(buyer4Id, sampleBuyer4.email)}`); + expect(response.status).toBe(200); + }); }); describe('Add feedback to the product with order', () => { it('should create new feedback to the ordered product', async () => { @@ -536,9 +580,11 @@ describe('Cart| Order management for guest/buyer', () => { .send({ orderId, comment: 'Well this product looks so fantastic' }) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(201); - feedbackId = response.body.data.id + feedbackId = response.body.data.id; }); - it('should create new feedback to the ordered product', async () => { + + + it('should create second feedback to the ordered product', async () => { const response = await request(app) .post(`/feedback/${productId}/new`) .send({ orderId, comment: 'Murigalike this product looks so fantastic' }) @@ -560,36 +606,8 @@ describe('Cart| Order management for guest/buyer', () => { .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(200); }); - it('should remove recorderd feedback as admin ', async () => { - const response = await request(app) - .delete(`/feedback/admin/delete/${feedback2Id}`) - .send({ orderId, comment: 'Well this product looks so lovely' }) - .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); - expect(response.status).toBe(401); - }); - it('should remove recorder feedback as admin ', async () => { - const response = await request(app) - .delete(`/feedback/admin/delete/${feedback2Id}`) - .send({ orderId, comment: 'Well this product looks so lovely' }) - .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); - expect(response.status).toBe(401); - }); - - it('should return 404 if feedback not found', async () => { - const response = await request(app) - .post(`/feedback/admin/delete/${feedbackID}`) - .set('Authorization', `Bearer ${getAccessToken(sampleAdminId, sampleAdmin.email)}`); - expect(response.status).toBe(404); - }) - - it('should handle server error by returning 500 ', async () => { - const response = await request(app) - .delete(`/feedback/admin/delete/ghkjh - *****`) - .set('Authorization', `Bearer ${getAccessToken(sampleAdminId, sampleAdmin.email)}`); - expect(response.status).toBe(401); - }); }); - + describe('Feedback API', () => { describe('Add feedback to the product with order', () => { @@ -669,25 +687,12 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.status).toBe(200); }); - it('should not allow a different user (admin) to remove feedback', async () => { - const response = await request(app) - .delete(`/feedback/admin/delete/${feedback2Id}`) - .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); - expect(response.status).toBe(401); - }); - it('should fail to delete feedback with invalid feedbackId', async () => { const response = await request(app) .delete(`/feedback/delete/invalidFeedbackId`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(500); }); - - it('should fail to delete feedback without authorization', async () => { - const response = await request(app) - .delete(`/feedback/delete/${feedback2Id}`); - expect(response.status).toBe(401); - }); }); describe('Edge Cases', () => { @@ -709,19 +714,38 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.status).toBe(200); }); }); - }); - }); + describe('Delete feedback by Admin', () => { + it('should delete feedback, if user is authenticated as admin', async () => { + const response = await request(app) + .delete(`/feedback/admin/delete/${feedback2Id}`) + .set('Authorization', `Bearer ${getAccessToken(sampleAdminId, sampleAdmin.email)}`); - describe('Deleting product from cart', () => { - it('should return 400 if product id is not provided', async () => { - const response = await request(app) - .delete(`/cart/`) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('Feedback successfully removed'); + }); - expect(response.status).toBe(200); + it('should return 404, if feedback doesn\'t exist', async () => { + const response = await request(app) + .delete(`/feedback/admin/delete/${uuid()}`) + .set('Authorization', `Bearer ${getAccessToken(sampleAdminId, sampleAdmin.email)}`); + + expect(response.status).toBe(404); + }); + + it('should return 500, for incorrect feedback id syntax (invalid uuid) doesn\'t exist', async () => { + const response = await request(app) + .delete(`/feedback/admin/delete/invalid-uuid`) + .set('Authorization', `Bearer ${getAccessToken(sampleAdminId, sampleAdmin.email)}`); + + expect(response.status).toBe(500); + }); + }); }); + }); + + describe('Deleting product from cart', () => { it('should return 404 if product does not exist in cart', async () => { const response = await request(app) .delete(`/cart/${uuid()}`) @@ -759,39 +783,31 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.message).toBe('cart removed successfully'); }); - it('should add product to cart as authenticated buyer', async () => { - const response = await request(app) - .post(`/cart`) - .send(bodyTosend) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + it('should delete product in guest cart', async () => { + const response = await request(app).delete(`/cart/${returnedCartItemId}`); - expect(response.status).toBe(201); - expect(response.body.data.message).toBe('cart updated successfully'); - expect(response.body.data.cart).toBeDefined; + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('Product removed from cart successfully'); }); - it('should add product to cart as authenticated buyer', async () => { - const response = await request(app) - .post(`/cart`) - .send({ productId: product2Id, quantity: 2 }) - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + it('should delete guest cart if there are no products remaining there', async () => { + const response = await request(app).delete(`/cart/${returnedCartItem2Id}`); - expect(response.status).toBe(201); - expect(response.body.data.message).toBe('cart updated successfully'); - expect(response.body.data.cart).toBeDefined; + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('cart removed successfully'); }); - it('should return 404 if product does not exist in guest cart', async () => { + it('should return 404 if Cart item (product) does not exist in guest cart', async () => { const response = await request(app).delete(`/cart/${uuid()}`); expect(response.status).toBe(404); expect(response.body.message).toBe('Cart item not found'); }); - it('should return 404 if product does not exist in guest cart', async () => { - const response = await request(app).delete(`/cart/${samplecartItem3Id}`); + it('should return 400, for incorrect Cart item (product) id syntax (invalid uuid)', async () => { + const response = await request(app).delete(`/cart/invalid-uuid`); - expect(response.status).toBe(200); + expect(response.status).toBe(400); }); }); @@ -799,24 +815,13 @@ describe('Cart| Order management for guest/buyer', () => { it('should return 200 as authenticated buyer does not have a cart', async () => { const response = await request(app) .delete(`/cart`) - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + .set('Authorization', `Bearer ${getAccessToken(buyer3Id, sampleBuyer3.email)}`); expect(response.status).toBe(200); expect(response.body.data.message).toBe('Cart is empty'); expect(response.body.data.cart).toBeDefined; }); - it('should add product to cart as authenticated buyer', async () => { - const response = await request(app) - .post(`/cart`) - .send(bodyTosend) - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); - - expect(response.status).toBe(201); - expect(response.body.data.message).toBe('cart updated successfully'); - expect(response.body.data.cart).toBeDefined; - }); - it('should clear cart as authenticated buyer', async () => { const response = await request(app) .delete(`/cart`) @@ -835,17 +840,17 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.cart).toBeDefined; }); - it('should get cart items of guest user as empty with wrong cartId', async () => { + it('should clear cart items of guest user', async () => { const response = await request(app) - .get('/cart') - .set('Cookie', [`cartId=${uuid()}`]); + .delete('/cart') + .set('Cookie', [`cartId=${sampleCartId}`]); expect(response.status).toBe(200); - expect(response.body.data.message).toBe('Cart is empty'); + expect(response.body.data.message).toBe('Cart cleared successfully'); expect(response.body.data.cart).toBeDefined; }); - it('should delete cart items of guest user as empty with wrong cartId', async () => { + it('should return empty cart for guest user, if he/she doesn\'t have cart', async () => { const response = await request(app) .delete('/cart') .set('Cookie', [`cartId=${sampleCartId}`]); @@ -854,5 +859,12 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.message).toBe('Cart is empty'); expect(response.body.data.cart).toBeDefined; }); + + it('should return 400, for incorrect cart id syntax (invalid uuid) for guest user', async () => { + const response = await request(app).delete(`/cart`) + .set('Cookie', [`cartId=invalid-uuid`]); + + expect(response.status).toBe(400); + }); }); }); \ No newline at end of file diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index 79eda0f..942942f 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -1,4 +1,3 @@ - import request from 'supertest'; import jwt from 'jsonwebtoken'; import { app, server } from '../index'; @@ -241,7 +240,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(201); expect(response.body.status).toBe('success'); - }, 10000); + }); it('should return 400 for invalid coupon data', async () => { const response = await request(app) @@ -257,7 +256,7 @@ describe('Coupon Management System', () => { .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); expect(response.status).toBe(400); - }, 10000); + }); it('should return 403 if product not found', async () => { const response = await request(app) @@ -317,7 +316,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('success'); expect(response.body.data).toBeInstanceOf(Object); - }, 10000); + }); it('should return 404 if no coupons found', async () => { const newVendorId = uuid(); @@ -326,7 +325,7 @@ describe('Coupon Management System', () => { .set('Authorization', `Bearer ${getAccessToken(newVendorId, 'newvendor@example.com')}`); expect(response.status).toBe(401); - }, 10000); + }); }); describe('Vendor access all Coupon', () => { @@ -337,7 +336,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('success'); - }, 10000); + }); it('should return 404 for invalid vendor id', async () => { const invalidVendorId = uuid(); @@ -347,7 +346,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(404); expect(response.body.message).toBe('User not found'); - }, 10000); + }); it('should return 404 if no coupon found for VENDOR', async () => { const response = await request(app) @@ -373,7 +372,7 @@ describe('Coupon Management System', () => { .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); expect(response.status).toBe(200); - }, 10000); + }); it('should return 404 for invalid coupon code', async () => { const response = await request(app) @@ -383,7 +382,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(404); expect(response.body.status).toBe('error'); expect(response.body.message).toBe('Invalid coupon'); - }, 10000); + }); }); describe('Update Coupon', () => { @@ -397,7 +396,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('success'); - }, 10000); + }); it('should validate coupon update input', async () => { const response = await request(app) @@ -417,43 +416,79 @@ describe('Coupon Management System', () => { .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); expect(response.status).toBe(404); - }, 10000); + }); it('should return 200 for updating a discount of coupon', async () => { const response = await request(app) .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) .send({ discountRate: 25 }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - + expect(response.status).toBe(200); - }, 10000); + }); it('should return 200 for updating a expirationDate of coupon', async () => { const response = await request(app) .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) .send({ expirationDate: '2025-12-31' }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - + expect(response.status).toBe(200); - }, 10000); + }); it('should return 200 for updating a maxUsageLimit of coupon', async () => { const response = await request(app) .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) .send({ maxUsageLimit: 40 }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - + expect(response.status).toBe(200); - }, 10000); + }); it('should return 200 for updating a discountType of coupon', async () => { const response = await request(app) .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) .send({ discountType: 'MONEY' }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - + + expect(response.status).toBe(200); + }); + + it('should return 200 for updating a product of coupon', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) + .send({ product: product1Id }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + expect(response.status).toBe(200); - }, 10000); + }); + + it('should return 404, if product doesn\'t exist', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) + .send({ product: uuid() }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + }); + + it('should return 404 for coupon not found', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/===__8899jjhh`) + .send({ product: uuid() }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + }); + + it('should return 500, incorrect product ids (invalid uuid)', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode}`) + .send({ product: 'invalid-uuid' }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(500); + }); }); describe('Delete Coupon', () => { @@ -467,7 +502,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(200); expect(response.body.status).toBe('success'); - }, 10000); + }); it('should return 404 for deleting a non-existent coupon', async () => { const response = await request(app) @@ -480,7 +515,7 @@ describe('Coupon Management System', () => { expect(response.status).toBe(404); expect(response.body.status).toBe('error'); expect(response.body.message).toBe('Invalid coupon'); - }, 10000); + }); }); }); @@ -554,7 +589,7 @@ describe('Buyer Coupon Application', () => { `Coupon Code successfully activated discount on product: ${sampleProduct1.name}` ); }); - it('Should give discont when discount-type is money', async () => { + it('Should give discount when discount-type is money', async () => { const response = await request(app) .post(`/coupons/apply`) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`) @@ -568,4 +603,4 @@ describe('Buyer Coupon Application', () => { ); }); }); -}); +}); \ No newline at end of file diff --git a/src/__test__/getProduct.test.ts b/src/__test__/getProduct.test.ts index dc9ac8b..94a1b0d 100644 --- a/src/__test__/getProduct.test.ts +++ b/src/__test__/getProduct.test.ts @@ -50,6 +50,7 @@ const sampleBuyer1: UserInterface = { phoneNumber: '000380996348', photoUrl: 'https://example.com/photo.jpg', role: 'BUYER', + }; const sampleCat = { @@ -67,7 +68,12 @@ const sampleProduct1 = { vendor: sampleVendor1, categories: [sampleCat], }; -let cardID : string; +const bodyTosend = { + productId: product1Id, + quantity: 2, +}; + +let cardID: string; beforeAll(async () => { const connection = await dbConnection(); @@ -75,7 +81,7 @@ beforeAll(async () => { await categoryRepository?.save({ ...sampleCat }); const userRepository = connection?.getRepository(User); - await userRepository?.save({ ...sampleVendor1}); + await userRepository?.save({ ...sampleVendor1 }); await userRepository?.save({ ...sampleBuyer1 }); const productRepository = connection?.getRepository(Product); @@ -104,7 +110,7 @@ describe('Creating new product', () => { expect(response.status).toBe(201); expect(response.body.data.product).toBeDefined; - }, 60000); + }, 20000); }); describe('Get single product', () => { it('should get a single product', async () => { @@ -136,23 +142,92 @@ describe('Get single product', () => { expect(response.body.message).toBe('Product not found'); }, 10000); }); -describe('Cart Order and payment functionalities', () => { - it('should create a cart for a product', async () => { - const productId = product1Id; - const quantity = 8; - - const token = getAccessToken(BuyerID, sampleBuyer1.email); +describe('POST /confirm-payment', () => { + it('should add product to cart as authenticated buyer', async () => { const response = await request(app) - .post('/cart') - .set('Authorization', `Bearer ${token}`) - .send({ productId, quantity }); + .post(`/cart`) + .send(bodyTosend) + .set('Authorization', `Bearer ${getAccessToken(BuyerID, sampleBuyer1.email)}`); + + expect(response.status).toBe(201); + expect(response.body.data.message).toBe('cart updated successfully'); + expect(response.body.data.cart).toBeDefined; - - expect(response.status).toBe(201); - expect(response.body.data.cart).toBeDefined(); cardID = JSON.stringify(response.body.data.cart.id) }); + it('should create an order successfully', async () => { + const address = { + country: 'Test Country', + city: 'Test City', + street: 'Test Street', + }; + + + const response = await request(app) + .post('/product/orders') + .set('Authorization', `Bearer ${getAccessToken(BuyerID, sampleBuyer1.email)}`) + .send({ address }); + + console.log(response.body.message) + expect(response.status).toBe(201); + expect(response.body.message).toBe('Order created successfully'); + expect(response.body.data).toBeDefined(); + + }); + it('should confirm payment successfully', async () => { + const token = 'your_valid_access_token_here'; + + + const response = await request(app) + .post(`/product/payment/${cardID}`) + .set('Authorization', `Bearer ${getAccessToken(BuyerID, sampleBuyer1.email)}`) + .send({ payment_method: "pm_card_visa" }); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Payment successful!'); + }); + + it('should handle cart not found', async () => { + + const response = await request(app) + .post(`/product/payment/wkowkokfowkf`) + .set('Authorization', `Bearer ${getAccessToken(BuyerID, sampleBuyer1.email)}`) + .send({ payment_method: "pm_card_visa" }); + + expect(response.status).toBe(200); + + }); } -) \ No newline at end of file +) +describe('GET / product search', () => { + + it('should return a 400 error if no name is provided', async () => { + const response = await request(app) + .get(`/product/search/`) + .query({ name: '' }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('Please provide a search term'); + }, 10000); + + it('should return products if name is provided', async () => { + const response = await request(app) + .get('/product/search') + .query({ name: 'test product3' }); + + expect(response.status).toBe(200); + expect(response.body.data).toBeDefined(); + expect(response.body.pagination).toBeDefined(); + }); + + it('should return a 404 error if no products are found', async () => { + const response = await request(app) + .get('/product/search') + .query({ name: 'nonexistentproduct' }); + + expect(response.status).toBe(404); + expect(response.body.error).toBe('No products found'); + }); +}) \ No newline at end of file diff --git a/src/__test__/productStatus.test.ts b/src/__test__/productStatus.test.ts index 8e8b42a..ec692f3 100644 --- a/src/__test__/productStatus.test.ts +++ b/src/__test__/productStatus.test.ts @@ -160,7 +160,15 @@ describe('Vendor product availability status management tests', () => { expect(response.statusCode).toBe(200); expect(response.body.data.message).toBe('Product status updated successfully'); - }, 10000); + }); + + it('Should return 400 if isAvailable field is not provided', async () => { + const response = await request(app) + .put(`/product/availability/${product1Id}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.statusCode).toBe(400); + }); it('should auto update product status to false if product is expired', async () => { const response = await request(app) @@ -170,7 +178,7 @@ describe('Vendor product availability status management tests', () => { }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - expect(response.statusCode).toBe(201); + expect(response.statusCode).toBe(200); expect(response.body.data.message).toBe('Product status is set to false because it is expired.'); }); @@ -182,7 +190,7 @@ describe('Vendor product availability status management tests', () => { }) .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - expect(response.statusCode).toBe(202); + expect(response.statusCode).toBe(200); expect(response.body.data.message).toBe('Product status is set to false because it is out of stock.'); }); @@ -209,7 +217,7 @@ describe('Vendor product availability status management tests', () => { expect(response.body.message).toBe('Product not found'); }); - it('should not update product which is not in VENDOR s stock', async () => { + it('should not update product which is not in vendor`s stock', async () => { const response = await request(app) .put(`/product/availability/${product3Id}`) .send({ @@ -220,6 +228,17 @@ describe('Vendor product availability status management tests', () => { expect(response.statusCode).toBe(404); expect(response.body.message).toBe('Product not found in your stock'); }); + + it('should return error response, for incorrect id syntax (invalid uuid) ', async () => { + const response = await request(app) + .put(`/product/availability/invalid-uuid`) + .send({ + isAvailable: true, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.statusCode).toBe(500); + }); }); describe('search product by name availability tests', () => { diff --git a/src/__test__/roleCheck.test.ts b/src/__test__/roleCheck.test.ts index b17df32..1ad0467 100644 --- a/src/__test__/roleCheck.test.ts +++ b/src/__test__/roleCheck.test.ts @@ -6,7 +6,7 @@ import { dbConnection } from '../startups/dbConnection'; import { v4 as uuid } from 'uuid'; import { getConnection } from 'typeorm'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; -import { server } from '..'; + let reqMock: Partial; let resMock: Partial; @@ -37,7 +37,6 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase(); - server.close(); }); describe('hasRole MiddleWare Test', () => { diff --git a/src/__test__/route.test.ts b/src/__test__/route.test.ts index ac704b5..12eaaaa 100644 --- a/src/__test__/route.test.ts +++ b/src/__test__/route.test.ts @@ -4,6 +4,7 @@ import { createConnection, getConnection, getConnectionOptions, getRepository } import { User } from '../entities/User'; import { response } from 'express'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { v4 as uuid } from 'uuid'; beforeAll(async () => { await createConnection(); @@ -60,6 +61,25 @@ describe('POST /user/register', () => { }); }); describe('POST /user/verify/:id', () => { + + it('should not verify user, for incorrect id (invalid uuid)', async () => { + const response = await request(app) + .get(`/user/verify/invalid-uuid`); + + // Assert + expect(response.status).toBe(400); + + }); + + it('should not verify email for non existing user', async () => { + const response = await request(app) + .get(`/user/verify/${uuid()}`); + + // Assert + expect(response.status).toBe(404); + + }); + it('should verify a user', async () => { // Arrange const newUser = { @@ -188,43 +208,3 @@ describe('Password Reset Service', () => { } }); }); -describe('PUT/user/update', () => { - it('should return 401 if user is not authenticated', async () => { - const newUser = { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe23@example.com', - password: 'password', - gender: 'Male', - phoneNumber: '12345678900', - userType: 'Buyer', - photoUrl: 'https://example.com/photo.jpg', - }; - - // Create a new user - const res = await request(app).post('/user/register').send(newUser); - const userRepository = getRepository(User); - - const user = await userRepository.findOne({ where: { email: newUser.email } }); - if (user) { - const updateUser = { - id: user.id, - firstName: 'Biguseers2399', - lastName: '1', - email: 'john.doe23@example.com', - gender: 'Male', - phoneNumber: '0790easdas7dsdfd76175', - photoUrl: 'photo', - }; - const res = await request(app).put('/user/update').send(updateUser); - expect(res.status).toBe(201); - expect(res.body).toEqual({ - status: 'success', - data: { - code: 201, - message: 'User Profile has successfully been updated', - }, - }); - } - }); -}); diff --git a/src/__test__/searchProduct.test.ts b/src/__test__/searchProduct.test.ts index 081427c..f217b26 100644 --- a/src/__test__/searchProduct.test.ts +++ b/src/__test__/searchProduct.test.ts @@ -1,20 +1,31 @@ -import { Product } from '../entities/Product'; +import request from 'supertest'; +import jwt from 'jsonwebtoken'; import { app, server } from '../index'; +import { getConnection } from 'typeorm'; import { dbConnection } from '../startups/dbConnection'; import { User, UserInterface } from '../entities/User'; import { v4 as uuid } from 'uuid'; +import { Product } from '../entities/Product'; import { Category } from '../entities/Category'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; -import { searchProductService } from '../services/productServices/searchProduct'; - const vendor1Id = uuid(); -const vendor2Id = uuid(); -const buyerId = uuid(); const product1Id = uuid(); const product2Id = uuid(); -const product3Id = uuid(); +const InvalidID = uuid(); +const expiredProductId = uuid(); const catId = uuid(); +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; const sampleVendor1: UserInterface = { id: vendor1Id, @@ -29,85 +40,58 @@ const sampleVendor1: UserInterface = { role: 'VENDOR', }; -const sampleVendor2: UserInterface = { - id: vendor2Id, - firstName: 'vendor2o', - lastName: 'user', - email: 'vendor20@example.com', - password: 'password', - userType: 'Vendor', - gender: 'Female', - phoneNumber: '1234567890', - photoUrl: 'https://example.com/photo.jpg', - role: 'VENDOR', -}; - -const sampleBuyer1: UserInterface = { - id: buyerId, - firstName: 'buyer1o', - lastName: 'user', - email: 'buyer10@example.com', - password: 'password', - userType: 'Buyer', - gender: 'Male', - phoneNumber: '000380996348', - photoUrl: 'https://example.com/photo.jpg', - role: 'BUYER', -}; - -const sampleCat: Category = { +const sampleCat = { id: catId, name: 'accessories', -} as Category; +}; -const sampleProduct1: Product = { +const sampleProduct1 = { id: product1Id, - name: 'Product A', - description: 'Amazing product A', + name: 'test product single', + description: 'amazing product', images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], - newPrice: 100, + newPrice: 200, quantity: 10, vendor: sampleVendor1, categories: [sampleCat], -} as Product; +}; -const sampleProduct2: Product = { +const sampleProduct2 = { id: product2Id, - name: 'Product B', - description: 'Amazing product B', - images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], - newPrice: 200, - quantity: 20, + name: 'another test product', + description: 'another amazing product', + images: ['photo4.jpg', 'photo5.jpg'], + newPrice: 150, + quantity: 5, vendor: sampleVendor1, categories: [sampleCat], -} as Product; +}; -const sampleProduct3: Product = { - id: product3Id, - name: 'Product C', - description: 'Amazing product C', - images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], - newPrice: 300, - quantity: 30, - vendor: sampleVendor2, +const expiredProduct = { + id: expiredProductId, + name: 'expired product', + description: 'this product is expired', + images: ['photo6.jpg'], + newPrice: 100, + quantity: 3, + vendor: sampleVendor1, categories: [sampleCat], -} as Product; + expirationDate: new Date('2023-01-01'), +}; beforeAll(async () => { const connection = await dbConnection(); const categoryRepository = connection?.getRepository(Category); - await categoryRepository?.save(sampleCat); + await categoryRepository?.save({ ...sampleCat }); const userRepository = connection?.getRepository(User); - await userRepository?.save(sampleVendor1); - await userRepository?.save(sampleVendor2); - await userRepository?.save(sampleBuyer1); + await userRepository?.save({ ...sampleVendor1 }); const productRepository = connection?.getRepository(Product); - await productRepository?.save(sampleProduct1); - await productRepository?.save(sampleProduct2); - await productRepository?.save(sampleProduct3); + await productRepository?.save({ ...sampleProduct1 }); + await productRepository?.save({ ...sampleProduct2 }); + await productRepository?.save({ ...expiredProduct }); }); afterAll(async () => { @@ -115,49 +99,73 @@ afterAll(async () => { server.close(); }); -describe('searchProductService', () => { - it('should return all products without filters', async () => { - const result = await searchProductService({}); - expect(result.data.length).toBe(3); - expect(result.pagination.totalItems).toBe(3); - expect(result.pagination.totalPages).toBe(1); +describe('Get single product', () => { + it('should get a single product', async () => { + const response = await request(app) + .get(`/product/${product1Id}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + expect(response.body.product).toBeDefined(); + expect(response.body.product.id).toBe(product1Id); }); - it('should return products matching the name filter', async () => { - const result = await searchProductService({ name: 'Product A' }); - expect(result.data.length).toBe(1); - expect(result.data[0].name).toBe('Product A'); - expect(result.pagination.totalItems).toBe(1); - expect(result.pagination.totalPages).toBe(1); + it('should return 400 if product is expired', async () => { + const response = await request(app) + .get(`/product/${expiredProductId}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(400); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Product expired'); }); - it('should return sorted products by price in descending order', async () => { - const result = await searchProductService({ sortBy: 'newPrice', sortOrder: 'DESC' }); - expect(result.data.length).toBe(3); - expect(result.data[0].newPrice).toBe("300"); - expect(result.data[1].newPrice).toBe("200"); - expect(result.data[2].newPrice).toBe("100"); + it('should return 400 for invalid product id', async () => { + const response = await request(app) + .get(`/product/${InvalidID}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + expect(response.body.status).toBe('error'); + expect(response.body.message).toBe('Product not found'); + }); + + it('should return 404 if product not found', async () => { + const response = await request(app) + .get(`/product/${InvalidID}`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(404); + }); +}); + +describe('GET / product search', () => { + it('should sort products by newPrice in descending order', async () => { + const response = await request(app) + .get(`/product/search/`) + .query({ name: 'test', sortBy: 'newPrice', sortOrder: 'DESC' }); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + expect(response.body.data).toHaveLength(2); }); - it('should return paginated results', async () => { - const result = await searchProductService({ page: 1, limit: 2 }); - expect(result.data.length).toBe(2); - expect(result.pagination.totalItems).toBe(3); - expect(result.pagination.totalPages).toBe(2); + it('should return 400 if no name is provided', async () => { + const response = await request(app) + .get(`/product/search/`) + .query({ name: '' }); - const resultPage2 = await searchProductService({ page: 2, limit: 2 }); - expect(resultPage2.data.length).toBe(1); - expect(resultPage2.pagination.currentPage).toBe(2); + expect(response.status).toBe(400); + expect(response.body.error).toBe('Please provide a search term'); }); - it('should handle sorting and pagination together', async () => { - const result = await searchProductService({ sortBy: 'newPrice', sortOrder: 'ASC', page: 1, limit: 2 }); - expect(result.data.length).toBe(2); - expect(result.data[0].newPrice).toBe("100"); - expect(result.data[1].newPrice).toBe("200"); + it('should return a 404 error if no products are found', async () => { + const response = await request(app) + .get('/product/search') + .query({ name: 'nonexistentproduct' }); - const resultPage2 = await searchProductService({ sortBy: 'newPrice', sortOrder: 'ASC', page: 2, limit: 2 }); - expect(resultPage2.data.length).toBe(1); - expect(resultPage2.data[0].newPrice).toBe("300"); + expect(response.status).toBe(404); + expect(response.body.error).toBe('No products found'); }); }); diff --git a/src/__test__/test-assets/DatabaseCleanup.ts b/src/__test__/test-assets/DatabaseCleanup.ts index 1e86ca0..b5fbb58 100644 --- a/src/__test__/test-assets/DatabaseCleanup.ts +++ b/src/__test__/test-assets/DatabaseCleanup.ts @@ -13,6 +13,8 @@ import { server } from '../..'; import { VendorOrderItem } from '../../entities/VendorOrderItem'; import { VendorOrders } from '../../entities/vendorOrders'; import { Feedback } from '../../entities/Feedback'; +import { NotificationItem } from '../../entities/NotificationItem'; +import { Notification } from '../../entities/Notification'; export const cleanDatabase = async () => { const connection = getConnection(); @@ -28,6 +30,8 @@ export const cleanDatabase = async () => { await connection.getRepository(CartItem).delete({}); await connection.getRepository(Cart).delete({}); await connection.getRepository(wishList).delete({}); + await connection.getRepository(NotificationItem).delete({}); + await connection.getRepository(Notification).delete({}); // Many-to-Many relations // Clear junction table entries before deleting products and categories @@ -49,4 +53,4 @@ export const cleanDatabase = async () => { // console.log('Database cleaned'); // }).catch(error => { // console.error('Error cleaning database:', error); -// }); \ No newline at end of file +// }); diff --git a/src/__test__/userServices.test.ts b/src/__test__/userServices.test.ts index 29a2e7c..a58dff7 100644 --- a/src/__test__/userServices.test.ts +++ b/src/__test__/userServices.test.ts @@ -1,11 +1,102 @@ import request from 'supertest'; import { app, server } from '../index'; import { createConnection, getRepository } from 'typeorm'; -import { User } from '../entities/User'; +import { User, UserInterface } from '../entities/User'; + import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { v4 as uuid } from 'uuid'; +import { dbConnection } from '../startups/dbConnection'; + +import bcrypt from 'bcrypt' +import jwt from 'jsonwebtoken'; + +const userId = uuid(); +const user1Id = uuid(); +const user2Id = uuid(); +const user3Id = uuid(); + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + process.env.JWT_SECRET || '' + ); +}; + +const sampleUser: UserInterface = { + id: userId, + firstName: 'user', + lastName: 'user', + email: 'user@example.com', + password: '', + userType: 'Vendor', + verified: true, + gender: 'Male', + phoneNumber: '12638096347', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; + +const sampleUser1: UserInterface = { + id: user1Id, + firstName: 'user1', + lastName: 'user', + email: 'user1@example.com', + password: 'password', + userType: 'Vendor', + verified: true, + status: 'suspended', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; + +const sampleUser2: UserInterface = { + id: user2Id, + firstName: 'user2', + lastName: 'user', + email: 'user2@example.com', + password: '', + userType: 'Vendor', + verified: true, + twoFactorEnabled: true, + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; + +const sampleUser3: UserInterface = { + id: user3Id, + firstName: 'user3', + lastName: 'user', + email: 'user3@example.com', + password: '', + userType: 'Vendor', + verified: true, + twoFactorEnabled: true, + twoFactorCode: '123456', + twoFactorCodeExpiresAt: new Date(Date.now() + 10 * 60 * 1000), + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; beforeAll(async () => { - await createConnection(); + const connection = await dbConnection(); + sampleUser.password = await bcrypt.hash('password', 10); + sampleUser2.password = await bcrypt.hash('password', 10); + sampleUser3.password = await bcrypt.hash('password', 10); + + const userRepository = connection?.getRepository(User); + await userRepository?.save({ ...sampleUser }); + await userRepository?.save({ ...sampleUser1 }); + await userRepository?.save({ ...sampleUser2 }); + await userRepository?.save({ ...sampleUser3 }); }); afterAll(async () => { @@ -13,218 +104,429 @@ afterAll(async () => { server.close(); }); -describe('start2FAProcess', () => { - it('should register a new user', async () => { - // Arrange - const newUser = { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe1@example.com', - password: 'password', - gender: 'Male', - phoneNumber: '0789412421', - userType: 'Buyer', - }; - - // Act - const res = await request(app).post('/user/register').send(newUser); - // Assert - expect(res.status).toBe(201); - expect(res.body).toEqual({ - status: 'success', - data: { - code: 201, - message: 'User registered successfully', - }, +describe('User service Test', () => { + describe('User Authentication', () => { + it('should register a new user', async () => { + // Arrange + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe1@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '0789412421', + userType: 'Buyer', + }; + + // Act + const res = await request(app).post('/user/register').send(newUser); + // Assert + expect(res.status).toBe(201); + expect(res.body).toEqual({ + status: 'success', + data: { + code: 201, + message: 'User registered successfully', + }, + }); }); - }); - it('should return 400 if not sent email in body on enabling 2fa', async () => { - const data = {}; + it('should Login a user, with valid credentials', async () => { + const res = await request(app) + .post('/user/login') + .send({ + email: 'user@example.com', + password: 'password', + }); + // Assert + expect(res.status).toBe(200); + expect(res.body.data.token).toBeDefined(); + }); - const res = await request(app).post('/user/enable-2fa').send(data); + it('should send OTP, if 2FA is enabled', async () => { + const res = await request(app) + .post('/user/login') + .send({ + email: 'user2@example.com', + password: 'password', + }); + // Assert + expect(res.status).toBe(200); + expect(res.body.data.message).toBe('Please provide the OTP sent to your email or phone'); + }); - expect(res.status).toBe(400); - expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); - }); + it('should not Login a user, with invalid credentials', async () => { + const res = await request(app) + .post('/user/login') + .send({ + email: 'user@example.com', + password: 'passwo', + }); + // Assert + expect(res.status).toBe(401); + }); - it('should return 404 if user not exist on enabling 2fa', async () => { - const data = { - email: 'example@gmail.com', - }; + it('should not login User if user email is not verified', async () => { + const res = await request(app) + .post('/user/login') + .send({ + email: 'john.doe1@example.com', + password: 'password', + }); - const res = await request(app).post('/user/enable-2fa').send(data); + expect(res.status).toBe(400); + expect(res.body.message).toBe("Please verify your account"); + }); - expect(res.status).toBe(404); - expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + it('should not login User if user is currently suspended', async () => { + const res = await request(app) + .post('/user/login') + .send({ + email: sampleUser1.email, + password: sampleUser1.password, + }); + + expect(res.status).toBe(400); + expect(res.body.message).toBe("Your account has been suspended"); + }); }); - it('should enable two-factor authentication', async () => { - const data = { - email: 'john.doe1@example.com', - }; + describe('User Profile update', () => { + it('should update user profile', async () => { + const res = await request(app) + .put('/user/update') + .send({ + firstName: 'John', + lastName: 'Doe', + gender: 'Male', + phoneNumber: '0789412421', + photoUrl: 'photo.jpg', + }) + .set('Authorization', `Bearer ${getAccessToken(userId, sampleUser.email)}`); + + expect(res.status).toBe(200); + }); + + it('should not update user profile, if there is no request body sent', async () => { + const res = await request(app) + .put('/user/update') + .set('Authorization', `Bearer ${getAccessToken(userId, sampleUser.email)}`); + + expect(res.status).toBe(400); + }); - const res = await request(app).post('/user/enable-2fa').send(data); + it('should not update user profile, when some required fields are not provided', async () => { + const res = await request(app) + .put('/user/update') + .send({ + firstName: 'firstName updated', + lastName: 'lastName updated' + }) + .set('Authorization', `Bearer ${getAccessToken(userId, sampleUser.email)}`); - expect(res.status).toBe(200); - expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication enabled successfully' }); + expect(res.status).toBe(400); + }); }); - it('should return 400 if not sent email in body on disabling 2fa', async () => { - const data = {}; + describe('User Reset Password', () => { + it('should return response error, if no email and userID provided', async () => { + const respond = await request(app) + .post('/user/password/reset'); - const res = await request(app).post('/user/disable-2fa').send(data); + expect(respond.status).toBe(400); + }); - expect(res.status).toBe(400); - expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); - }); + it('should not reset password, for no existing Users', async () => { + const respond = await request(app) + .post('/user/password/reset') + .query({ + email: 'example@gmail.com', + userid: uuid() + }); - it('should return 404 if user not exist on disabling 2fa', async () => { - const data = { - email: 'example@gmail.com', - }; + expect(respond.status).toBe(404); + }); - const res = await request(app).post('/user/disable-2fa').send(data); + it('should not reset password, if no new password sent', async () => { + const respond = await request(app) + .post('/user/password/reset') + .query({ + email: sampleUser.email, + userid: sampleUser.id + }); - expect(res.status).toBe(404); - expect(res.body).toEqual({ status: 'error', message: 'User not found' }); - }); + expect(respond.status).toBe(400); + expect(respond.body.message).toBe('Please provide all required fields'); + }); - it('should disable two-factor authentication', async () => { - const data = { - email: 'john.doe1@example.com', - }; + it('should not reset password, if new password doesn\'t match password in confirmPassword field', async () => { + const respond = await request(app) + .post('/user/password/reset') + .query({ + email: sampleUser.email, + userid: sampleUser.id + }) + .send({ + newPassword: 'new-password', + confirmPassword: 'password' + }); + + expect(respond.status).toBe(400); + expect(respond.body.message).toBe('new password must match confirm password'); + }); - const res = await request(app).post('/user/disable-2fa').send(data); + it('should not reset password, for incorrect user id syntax (invalid uuid)', async () => { + const respond = await request(app) + .post('/user/password/reset') + .query({ + email: sampleUser.email, + userid: 'invalid-=uuid' + }); - expect(res.status).toBe(200); - expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication disabled successfully' }); + expect(respond.status).toBe(500); + }); + it('should reset password for the user', async () => { + const respond = await request(app) + .post('/user/password/reset') + .query({ + email: sampleUser.email, + userid: sampleUser.id + }) + .send({ + newPassword: 'new-password', + confirmPassword: 'new-password' + }); + + expect(respond.status).toBe(200); + expect(respond.body.data.message).toBe('Password updated successfully'); + }); }); - it('should return 400 if not sent email and otp in body on verifying OTP', async () => { - const data = {}; + describe('User password reset link', () => { + it('should send link to reset password', async () => { + const response = await request(app) + .post('/user/password/reset/link') + .query({ email: sampleUser.email }); - const res = await request(app).post('/user/verify-otp').send(data); + expect(response.status).toBe(200); + }); - expect(res.status).toBe(400); - expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and OTP code' }); - }); + it('should not send link to reset password, if no email provided', async () => { + const response = await request(app) + .post('/user/password/reset/link'); - it('should return 403 if OTP is invalid', async () => { - const email = 'john.doe1@example.com'; - const user = await getRepository(User).findOneBy({ email }); - if (user) { - user.twoFactorEnabled = true; - user.twoFactorCode = '123456'; - await getRepository(User).save(user); - } - - const data = { - email: 'john.doe1@example.com', - otp: '123457', - }; - - const res = await request(app).post('/user/verify-otp').send(data); - expect(res.status).toBe(403); - expect(res.body).toEqual({ status: 'error', message: 'Invalid authentication code' }); - }); + expect(response.status).toBe(400); + expect(response.body.message).toBe('Missing required field'); + }); - it('should return 403 if user not exist on verifying OTP', async () => { - const data = { - email: 'john.doe10@example.com', - otp: '123457', - }; + it('should not send link to reset password, if email doesn\'t exist in DB', async () => { + const response = await request(app) + .post('/user/password/reset/link') + .query({ email: 'nonexistingemail@gmail.com' }); - const res = await request(app).post('/user/verify-otp').send(data); - expect(res.status).toBe(403); - expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + expect(response.status).toBe(404); + expect(response.body.message).toBe('User not found'); + }); }); - it('should return 403 if OTP is expired', async () => { - const email = 'john.doe1@example.com'; - const userRepository = getRepository(User); - const user = await userRepository.findOneBy({ email }); - if (user) { - user.twoFactorEnabled = true; - user.twoFactorCode = '123456'; - user.twoFactorCodeExpiresAt = new Date(Date.now() - 10 * 60 * 1000); - await getRepository(User).save(user); - } - - const data = { - email: email, - otp: '123456', - }; - - const res = await request(app).post('/user/verify-otp').send(data); - expect(res.status).toBe(403); - expect(res.body).toEqual({ status: 'error', message: 'Authentication code expired' }); - if (user) { - await userRepository.remove(user); - } - }); + describe('Start@FAProcess', () => { - it('should return 400 if not sent email in body on resending OTP', async () => { - const data = {}; + it('should return 400 if not sent email in body on enabling 2fa', async () => { + const data = {}; - const res = await request(app).post('/user/resend-otp').send(data); + const res = await request(app).post('/user/enable-2fa').send(data); - expect(res.status).toBe(400); - expect(res.body).toEqual({ status: 'error', message: 'Please provide an email' }); - }); + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); + }); - it('should return 404 if user not exist on resending OTP', async () => { - const data = { - email: 'john.doe10@example.com', - }; + it('should return 404 if user not exist on enabling 2fa', async () => { + const data = { + email: 'example@gmail.com', + }; - const res = await request(app).post('/user/resend-otp').send(data); - expect(res.status).toBe(404); - expect(res.body).toEqual({ status: 'error', message: 'Incorrect email' }); - }); + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should enable two-factor authentication', async () => { + const data = { + email: 'john.doe1@example.com', + }; + + const res = await request(app).post('/user/enable-2fa').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication enabled successfully' }); + }); + + it('should return 400 if not sent email in body on disabling 2fa', async () => { + const data = {}; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide your email' }); + }); + + it('should return 404 if user not exist on disabling 2fa', async () => { + const data = { + email: 'example@gmail.com', + }; + + const res = await request(app).post('/user/disable-2fa').send(data); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should disable two-factor authentication', async () => { + const data = { + email: 'john.doe1@example.com', + }; + + const res = await request(app).post('/user/disable-2fa').send(data); - it('should resend OTP', async () => { - const newUser = { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe187@example.com', - password: 'password', - gender: 'Male', - phoneNumber: '0785044398', - userType: 'Buyer', - }; - - // Act - const resp = await request(app).post('/user/register').send(newUser); - if (!resp) { - console.log('Error creating user in resend otp test case'); - } - const data = { - email: 'john.doe187@example.com', - }; - - const res = await request(app).post('/user/resend-otp').send(data); - expect(res.status).toBe(200); - expect(res.body).toEqual({ status: 'success', data: { message: 'OTP sent successfully' } }); - }, 20000); - - it('should return 400 if not sent email in body on login', async () => { - const data = {}; - - const res = await request(app).post('/user/login').send(data); - - expect(res.status).toBe(400); - expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and password' }); - }, 1000); - - it('should return 404 if user not exist on login', async () => { - const data = { - email: 'john.doe10@example.com', - password: 'password', - }; - - const res = await request(app).post('/user/login').send(data); - expect(res.status).toBe(404); - expect(res.body).toEqual({ status: 'error', message: 'Incorrect email or password' }); - }, 10000); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', message: 'Two factor authentication disabled successfully' }); + }); + + it('should return 400 if not sent email and otp in body on verifying OTP', async () => { + const data = {}; + + const res = await request(app).post('/user/verify-otp').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and OTP code' }); + }); + + it('should return 403 if OTP is invalid', async () => { + const email = 'john.doe1@example.com'; + const user = await getRepository(User).findOneBy({ email }); + if (user) { + user.twoFactorEnabled = true; + user.twoFactorCode = '123456'; + await getRepository(User).save(user); + } + + const data = { + email: 'john.doe1@example.com', + otp: '123457', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'Invalid authentication code' }); + }); + + it('should return 403 if user not exist on verifying OTP', async () => { + const data = { + email: 'john.doe10@example.com', + otp: '123457', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'User not found' }); + }); + + it('should return 403 if OTP is expired', async () => { + const email = 'john.doe1@example.com'; + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ email }); + if (user) { + user.twoFactorEnabled = true; + user.twoFactorCode = '123456'; + user.twoFactorCodeExpiresAt = new Date(Date.now() - 10 * 60 * 1000); + await getRepository(User).save(user); + } + + const data = { + email: email, + otp: '123456', + }; + + const res = await request(app).post('/user/verify-otp').send(data); + expect(res.status).toBe(403); + expect(res.body).toEqual({ status: 'error', message: 'Authentication code expired' }); + }); + + it('should login user, if OTP provided is valid', async () => { + const res = await request(app) + .post('/user/verify-otp') + .send({ + email: sampleUser3.email, + otp: '123456', + }); + + expect(res.status).toBe(200); + expect(res.body.data.token).toBeDefined(); + }); + + it('should return 400 if not sent email in body on resending OTP', async () => { + const data = {}; + + const res = await request(app).post('/user/resend-otp').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email' }); + }); + + it('should return 404 if user not exist on resending OTP', async () => { + const data = { + email: 'john.doe10@example.com', + }; + + const res = await request(app).post('/user/resend-otp').send(data); + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'Incorrect email' }); + }); + + it('should resend OTP', async () => { + const newUser = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe187@example.com', + password: 'password', + gender: 'Male', + phoneNumber: '0785044398', + userType: 'Buyer', + }; + + // Act + const resp = await request(app).post('/user/register').send(newUser); + if (!resp) { + console.log('Error creating user in resend otp test case'); + } + const data = { + email: 'john.doe187@example.com', + }; + + const res = await request(app).post('/user/resend-otp').send(data); + expect(res.status).toBe(200); + expect(res.body).toEqual({ status: 'success', data: { message: 'OTP sent successfully' } }); + }); + + it('should return 400 if not sent email in body on login', async () => { + const data = {}; + + const res = await request(app).post('/user/login').send(data); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ status: 'error', message: 'Please provide an email and password' }); + }, 1000); + + it('should return 404 if user not exist on login', async () => { + const data = { + email: 'john.doe10@example.com', + password: 'password', + }; + + const res = await request(app).post('/user/login').send(data); + expect(res.status).toBe(404); + expect(res.body).toEqual({ status: 'error', message: 'Incorrect email or password' }); + }, 10000); + }); }); diff --git a/src/__test__/vendorProduct.test.ts b/src/__test__/vendorProduct.test.ts index f0a1450..54fec95 100644 --- a/src/__test__/vendorProduct.test.ts +++ b/src/__test__/vendorProduct.test.ts @@ -133,7 +133,24 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(201); expect(response.body.data.product).toBeDefined; - }, 120000); + }); + + it('should create new product. Test when provided one category and currently doesn\'t exist in DB ', async () => { + const response = await request(app) + .post('/product') + .field('name', 'test product4') + .field('description', 'amazing product4') + .field('newPrice', 200) + .field('quantity', 50) + .field('expirationDate', '10-2-2023') + .field('categories', 'new-category') + .attach('images', `${__dirname}/test-assets/photo1.png`) + .attach('images', `${__dirname}/test-assets/photo2.webp`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(201); + expect(response.body.data.product).toBeDefined; + }); it('return an error if the number of product images exceeds 6', async () => { const response = await request(app) @@ -203,7 +220,7 @@ describe('Vendor product management tests', () => { }); describe('Updating existing product', () => { - it('return error, if there are missing field data', async () => { + it('return response error, if there are missing field data', async () => { const response = await request(app) .put(`/product/${sampleProduct2.id}`) .field('newPrice', 200) @@ -218,7 +235,7 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(400); }); - it('return error, if product do not exist', async () => { + it('return response error, if product do not exist', async () => { const response = await request(app) .put(`/product/${uuid()}`) .field('name', 'test product3') @@ -235,8 +252,23 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(404); expect(response.body.error).toBe('Product not found'); }); + + it('return response error, for incorrect product id syntax (invalid uuid)', async () => { + const response = await request(app) + .put(`/product/invalid uuid`) + .field('name', 'test product3') + .field('description', 'amazing product3') + .field('newPrice', 200) + .field('quantity', 10) + .field('expirationDate', '10-2-2023') + .field('categories', 'technology') + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); - it('return an error if the number of product images exceeds 6', async () => { + expect(response.status).toBe(400); + expect(response.body.message).toBe('invalid input syntax for type uuid: "invalid uuid"'); + }); + + it('return response error if the number of product images exceeds 6', async () => { const response = await request(app) .put(`/product/${sampleProduct2.id}`) .field('name', 'test product3') @@ -255,6 +287,21 @@ describe('Vendor product management tests', () => { expect(response.body.error).toBe('Product cannot have more than 6 images'); }); + it('should update the product. Test when provided one category and currently doesn\'t exist in DB ', async () => { + const response = await request(app) + .put(`/product/${sampleProduct2.id}`) + .field('name', 'test product3 updated') + .field('description', 'amazing product3') + .field('newPrice', 200) + .field('quantity', 50) + .field('expirationDate', '10-2-2023') + .field('categories', 'new-category-update') + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('Product updated successfully'); + }); + it('should update the product', async () => { const response = await request(app) .put(`/product/${sampleProduct2.id}`) @@ -425,14 +472,30 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(200); expect(response.body.data).toBeDefined; }); + + it('should retrieve products, according to categories and vendor', async () => { + const response = await request(app) + .get('/product/recommended') + .query({ + categories: sampleCat.id, + vendor: sampleVendor1.id + }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + expect(response.body.data.products).toBeDefined; + }); it('should not return any product for a vendor with zero product in stock', async () => { const response = await request(app) - .get(`/product/recommended`) + .get('/product/recommended') + .query({ + categories: sampleCat.id, + vendor: sampleVendor2.id + }) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(200); - expect(response.body.products).toBeUndefined; + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('No products found for the specified vendor'); }); it('should not return any product for incorrect syntax of input', async () => { @@ -470,4 +533,4 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(400); }); }); -}); \ No newline at end of file +}); diff --git a/src/__test__/wishList.test.ts b/src/__test__/wishList.test.ts index 6658853..23f2609 100644 --- a/src/__test__/wishList.test.ts +++ b/src/__test__/wishList.test.ts @@ -201,4 +201,4 @@ describe('Wish list management tests', () => { expect(response.body.message).toBe('All products removed successfully'); }); }); -}); +}); \ No newline at end of file diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts index c68e16b..d24e1e5 100644 --- a/src/controllers/productController.ts +++ b/src/controllers/productController.ts @@ -52,24 +52,8 @@ export const singleProduct = async (req: Request, res: Response) => { await viewSingleProduct(req, res); }; export const searchProduct = async (req: Request, res: Response) => { - const { name, sortBy, sortOrder, page, limit } = req.query; + await searchProductService (req, res); - try { - const searchParams = { - name: name as string, - sortBy: sortBy as string, - sortOrder: sortOrder as 'ASC' | 'DESC', - page: parseInt(page as string, 10) || 1, - limit: parseInt(limit as string, 10) || 10, - }; - - const result = await searchProductService(searchParams); - - res.json(result); - } catch (error) { - console.error('Error searching products:', error); - res.status(500).json({ error: 'Internal Server Error' }); - } }; export const Payment = async (req: Request, res: Response) => { await confirmPayment(req, res); diff --git a/src/docs/notifications.yml b/src/docs/notifications.yml new file mode 100644 index 0000000..61e240f --- /dev/null +++ b/src/docs/notifications.yml @@ -0,0 +1,126 @@ +/notification: + get: + tags: + - Notification Management + summary: Get all user notifications + description: Return all user notifications of an authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: Return all user notifications for a user + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '500': + description: Internal server error + + delete: + tags: + - Notification Management + summary: Delete selected notifications + description: Delete all selected notification for an authenticated buyer + security: + - bearerAuth: [] + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + notificationIds: + example: "[]" + responses: + '200': + description: Notifications deleted successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Notification not found + '500': + description: Internal server error + + put: + tags: + - Notification Management + summary: Update selected notifications + description: Update selected notifications for an authenticated buyer + security: + - bearerAuth: [] + consumes: + - application/json + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + notificationIds: + example: "[]" + responses: + '200': + description: Notification updated successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Notification not found + '500': + description: Internal server error + +/notification/all: + delete: + tags: + - Notification Management + summary: Delete all notifications + description: Delete all notification for an authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: All notifications deleted successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Notification not found + '500': + description: Internal server error + + put: + tags: + - Notification Management + summary: Update all notifications + description: Update all notifications for an authenticated buyer + security: + - bearerAuth: [] + responses: + '200': + description: Notification updated successfully + '400': + description: Bad Request (syntax error, incorrect input format, etc..) + '401': + description: Unauthorized + '403': + description: Forbidden (Unauthorized action) + '404': + description: Notification not found + '500': + description: Internal server error \ No newline at end of file diff --git a/src/docs/orderDocs.yml b/src/docs/orderDocs.yml index fcb620e..c9f692a 100644 --- a/src/docs/orderDocs.yml +++ b/src/docs/orderDocs.yml @@ -106,3 +106,32 @@ paths: description: Order not found '500': description: Internal Server Error + + /product/client/orders/{orderId}: + get: + tags: + - Order + summary: Get a single order + description: Retrieve an order for the authenticated user + security: + - bearerAuth: [] + parameters: + - in: path + name: orderId + schema: + type: string + required: true + description: The ID of the order + responses: + '200': + description: Order Retrived successfully + '400': + description: Bad Request + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Order not found + '500': + description: Internal Server Error diff --git a/src/entities/Cart.ts b/src/entities/Cart.ts index cf354a5..fda1e15 100644 --- a/src/entities/Cart.ts +++ b/src/entities/Cart.ts @@ -47,4 +47,4 @@ export class Cart { this.totalAmount = 0; } } -} \ No newline at end of file +} diff --git a/src/entities/CartItem.ts b/src/entities/CartItem.ts index da110d6..d651adf 100644 --- a/src/entities/CartItem.ts +++ b/src/entities/CartItem.ts @@ -50,4 +50,4 @@ export class CartItem { updateTotal (): void { this.total = this.newPrice * this.quantity; } -} \ No newline at end of file +} diff --git a/src/entities/Category.ts b/src/entities/Category.ts index 9b4f856..9152553 100644 --- a/src/entities/Category.ts +++ b/src/entities/Category.ts @@ -17,4 +17,4 @@ export class Category { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/Feedback.ts b/src/entities/Feedback.ts index b64554e..6de9058 100644 --- a/src/entities/Feedback.ts +++ b/src/entities/Feedback.ts @@ -27,4 +27,4 @@ export class Feedback { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/Order.ts b/src/entities/Order.ts index 7966b88..faa19db 100644 --- a/src/entities/Order.ts +++ b/src/entities/Order.ts @@ -1,4 +1,3 @@ - import { Entity, PrimaryGeneratedColumn, diff --git a/src/entities/OrderItem.ts b/src/entities/OrderItem.ts index 8de94dd..130b330 100644 --- a/src/entities/OrderItem.ts +++ b/src/entities/OrderItem.ts @@ -26,4 +26,4 @@ export class OrderItem { @IsNotEmpty() @IsNumber() quantity!: number; -} \ No newline at end of file +} diff --git a/src/entities/Product.ts b/src/entities/Product.ts index ce7f139..ae027ef 100644 --- a/src/entities/Product.ts +++ b/src/entities/Product.ts @@ -89,4 +89,4 @@ export class Product { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/User.ts b/src/entities/User.ts index 78a1fd1..787c5e0 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -28,6 +28,9 @@ export interface UserInterface { status?: 'active' | 'suspended'; userType: 'Admin' | 'Buyer' | 'Vendor'; role?: string; + twoFactorEnabled?: boolean; + twoFactorCode?: string; + twoFactorCodeExpiresAt?: Date; createdAt?: Date; updatedAt?: Date; } diff --git a/src/entities/VendorOrderItem.ts b/src/entities/VendorOrderItem.ts index f8613da..fc8b3dc 100644 --- a/src/entities/VendorOrderItem.ts +++ b/src/entities/VendorOrderItem.ts @@ -27,4 +27,4 @@ export class VendorOrderItem { @IsNotEmpty() @IsNumber() 'quantity'!: number; -} \ No newline at end of file +} diff --git a/src/entities/coupon.ts b/src/entities/coupon.ts index b4c9431..39631c3 100644 --- a/src/entities/coupon.ts +++ b/src/entities/coupon.ts @@ -65,4 +65,4 @@ export class Coupon { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/vendorOrders.ts b/src/entities/vendorOrders.ts index d2a784c..38269e6 100644 --- a/src/entities/vendorOrders.ts +++ b/src/entities/vendorOrders.ts @@ -46,4 +46,4 @@ export class VendorOrders { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/entities/wishList.ts b/src/entities/wishList.ts index a1f6a55..7f74023 100644 --- a/src/entities/wishList.ts +++ b/src/entities/wishList.ts @@ -32,4 +32,4 @@ export class wishList extends BaseEntity { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/helper/couponValidator.ts b/src/helper/couponValidator.ts index 9736aa8..a82865b 100644 --- a/src/helper/couponValidator.ts +++ b/src/helper/couponValidator.ts @@ -36,6 +36,9 @@ export const validateCouponUpdate = ( code: Joi.string().min(5).messages({ 'string.min': 'code must be at least 5 characters long.', }), + product: Joi.string().messages({ + 'any.required': 'product is required.', + }), discountRate: Joi.number().messages({ 'number.base': 'discountRate must be a number.', }), diff --git a/src/middlewares/isAllowed.ts b/src/middlewares/isAllowed.ts index 77c115b..afaa0a9 100644 --- a/src/middlewares/isAllowed.ts +++ b/src/middlewares/isAllowed.ts @@ -20,12 +20,6 @@ export interface UserInterface { updatedAt: Date; } -declare module 'express' { - interface Request { - user?: Partial; - } -} - export const checkUserStatus = async (req: Request, res: Response, next: NextFunction) => { try { if (!req.user) { diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index 3ab9f95..49a6c5e 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -16,7 +16,7 @@ import { listAllProducts, singleProduct, createOrder, - getOrders, + getOrders, getOrder, updateOrder, getOrdersHistory,Payment, getSingleVendorOrder, @@ -27,6 +27,8 @@ import { updateBuyerVendorOrder, } from '../controllers'; const router = Router(); + +router.get('/search', searchProduct); router.get('/all', listAllProducts); router.get('/recommended', authMiddleware as RequestHandler, hasRole('BUYER'), getRecommendedProducts); router.get('/collection', authMiddleware as RequestHandler, hasRole('VENDOR'), readProducts); @@ -41,6 +43,7 @@ router.put('/availability/:id', authMiddleware as RequestHandler, hasRole('VENDO router.post('/orders', authMiddleware as RequestHandler, hasRole('BUYER'), createOrder); router.get('/client/orders', authMiddleware as RequestHandler, hasRole('BUYER'), getOrders); +router.get('/client/orders/:orderId', authMiddleware as RequestHandler, hasRole('BUYER'), getOrder); router.put('/client/orders/:orderId', authMiddleware as RequestHandler, hasRole('BUYER'), updateOrder); router.get('/orders/history', authMiddleware as RequestHandler, hasRole('BUYER'), getOrdersHistory); @@ -53,6 +56,7 @@ router.put('/vendor/orders/:id', authMiddleware as RequestHandler, hasRole('VEND router.get('/admin/orders', authMiddleware as RequestHandler, hasRole('ADMIN'), getBuyerVendorOrders); router.get('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), getSingleBuyerVendorOrder); router.put('/admin/orders/:id', authMiddleware as RequestHandler, hasRole('ADMIN'), updateBuyerVendorOrder); -router.post('/payment/:id', authMiddleware as RequestHandler, hasRole('BUYER'), Payment) +router.post('/payment/:id', authMiddleware as RequestHandler, hasRole('BUYER'), Payment); + -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/UserRoutes.ts b/src/routes/UserRoutes.ts index 79b0551..ad25a36 100644 --- a/src/routes/UserRoutes.ts +++ b/src/routes/UserRoutes.ts @@ -1,4 +1,4 @@ -import { Router } from 'express'; +import { RequestHandler, Router } from 'express'; import { responseError } from '../utils/response.utils'; import { UserInterface } from '../entities/User'; import jwt from 'jsonwebtoken'; @@ -20,6 +20,7 @@ import { hasRole } from '../middlewares/roleCheck'; import { isTokenValide } from '../middlewares/isValid'; import passport from 'passport'; import '../utils/auth'; +import { authMiddleware } from '../middlewares/verifyToken'; const router = Router(); router.post('/register', userRegistration); @@ -34,7 +35,7 @@ router.post('/activate', isTokenValide, hasRole('ADMIN'), activateUser); router.post('/deactivate', isTokenValide, hasRole('ADMIN'), disactivateUser); router.post('/password/reset', userPasswordReset); router.post('/password/reset/link', sendPasswordResetLink); -router.put('/update', userProfileUpdate); +router.put('/update', authMiddleware as RequestHandler, userProfileUpdate); router.get('/google-auth', passport.authenticate('google', { scope: ['profile', 'email'] })); router.get( diff --git a/src/services/cartServices/clearCart.ts b/src/services/cartServices/clearCart.ts index 4806e01..1f24572 100644 --- a/src/services/cartServices/clearCart.ts +++ b/src/services/cartServices/clearCart.ts @@ -44,6 +44,8 @@ export const clearCartService = async (req: Request, res: Response) => { }); if (!cart) { + console.log(cart); + responseSuccess(res, 200, 'Cart is empty', { cart: [] }); return; } diff --git a/src/services/cartServices/removeProductInCart.ts b/src/services/cartServices/removeProductInCart.ts index 25bfe13..b8894d4 100644 --- a/src/services/cartServices/removeProductInCart.ts +++ b/src/services/cartServices/removeProductInCart.ts @@ -9,25 +9,20 @@ export const removeProductInCartService = async (req: Request, res: Response) => try { const cartItemRepository = getRepository(CartItem); const cartRepository = getRepository(Cart); + + if (req.user) { + const cartItem = await cartItemRepository.findOne({ + where: { + id: req.params.id, + }, + relations: ['cart', 'cart.user'], + }); - if (!req.params.id) { - responseError(res, 400, 'Cart item id is required'); - return; - } - - const cartItem = await cartItemRepository.findOne({ - where: { - id: req.params.id, - }, - relations: ['cart', 'cart.user'], - }); - - if (!cartItem) { - responseError(res, 404, 'Cart item not found'); - return; - } + if (!cartItem) { + responseError(res, 404, 'Cart item not found'); + return; + } - if (req.user) { if (cartItem?.cart.user.id !== req.user.id) { responseError(res, 401, 'You are not authorized to perform this action'); return; @@ -61,11 +56,6 @@ export const removeProductInCartService = async (req: Request, res: Response) => } if (!req.user) { - if (!req.params.id) { - responseError(res, 400, 'Cart item id is required'); - return; - } - const cartItem = await cartItemRepository.findOne({ where: { id: req.params.id, @@ -89,12 +79,12 @@ export const removeProductInCartService = async (req: Request, res: Response) => if (cart) { if (cart.items.length === 0) { + await cartRepository.remove(cart); responseSuccess(res, 200, 'cart removed successfully', { cart: [] }); return; } - cart.updateTotal(); await cartRepository.save(cart); diff --git a/src/services/couponServices/accessAllCoupon.ts b/src/services/couponServices/accessAllCoupon.ts index 9266a44..9f9aacf 100644 --- a/src/services/couponServices/accessAllCoupon.ts +++ b/src/services/couponServices/accessAllCoupon.ts @@ -13,7 +13,6 @@ export const accessAllCouponService = async (req: Request, res: Response) => { const user = await userRepository.findOne({ where: { id } }); if (!user) { - console.log('User not found with id:', id); return responseError(res, 404, 'User not found'); } @@ -25,13 +24,11 @@ export const accessAllCouponService = async (req: Request, res: Response) => { }); if (!coupons.length) { - console.log('No coupons found for user with id:', id); return responseError(res, 404, 'No coupons found'); } return responseSuccess(res, 200, 'Coupons retrieved successfully', coupons); } catch (error: any) { - console.log('Error retrieving all coupons:\n', error); return responseServerError(res, error); } }; diff --git a/src/services/couponServices/buyerApplyCoupon.ts b/src/services/couponServices/buyerApplyCoupon.ts index 85762f6..93fa208 100644 --- a/src/services/couponServices/buyerApplyCoupon.ts +++ b/src/services/couponServices/buyerApplyCoupon.ts @@ -3,6 +3,8 @@ import { getRepository } from 'typeorm'; import { Coupon } from '../../entities/coupon'; import { Cart } from '../../entities/Cart'; import { CartItem } from '../../entities/CartItem'; +import { sendNotification } from '../../utils/sendNotification'; +import { responseSuccess, responseError } from '../../utils/response.utils'; export const buyerApplyCouponService = async (req: Request, res: Response) => { try { @@ -13,7 +15,7 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { const couponRepository = getRepository(Coupon); const coupon = await couponRepository.findOne({ where: { code: couponCode }, - relations: ['product'], + relations: ['product', 'vendor'], }); if (!coupon) return res.status(404).json({ message: 'Invalid Coupon Code' }); @@ -32,7 +34,7 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { const cartRepository = getRepository(Cart); let cart = await cartRepository.findOne({ where: { user: { id: req.user?.id }, isCheckedOut: false }, - relations: ['items', 'items.product'], + relations: ['items', 'items.product', 'user'], }); if (!cart) return res.status(400).json({ message: "You don't have a product in cart" }); @@ -61,12 +63,11 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { await cartItemRepository.save(couponCartItem); } - cart = await cartRepository.findOne({ where: { id: cart.id }, relations: ['items', 'items.product'] }); - if (cart) { - cart.updateTotal(); - await cartRepository.save(cart); - } + cart = await cartRepository.findOne({ where: { id: cart.id }, relations: ['items', 'items.product', 'user'] }); + if (!cart) return; + cart.updateTotal(); + await cartRepository.save(cart); coupon.usageTimes += 1; if (req.user?.id) { @@ -75,6 +76,19 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { await couponRepository.save(coupon); + await sendNotification({ + content: `Coupon Code successfully activated discount on product: ${couponCartItem.product.name}`, + type: 'coupon', + user: cart.user + }) + + await sendNotification({ + content: `Buyer: "${cart?.user.firstName} ${cart?.user.lastName}" used coupon and got discount on product: "${couponCartItem.product.name}"`, + type:'coupon', + user: coupon.vendor, + link: `/coupons/vendor/${coupon.vendor.id}/checkout/${couponCode}` + }); + return res .status(200) .json({ @@ -82,6 +96,6 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { amountDiscounted: amountReducted, }); } catch (error) { - return res.status(500).json({ error: 'Internal server error' }); - } + return responseError(res, 500, (error as Error).message); + } }; diff --git a/src/services/couponServices/createCouponService.ts b/src/services/couponServices/createCouponService.ts index a824ddf..301e2ea 100644 --- a/src/services/couponServices/createCouponService.ts +++ b/src/services/couponServices/createCouponService.ts @@ -20,14 +20,12 @@ export const createCouponService = async (req: Request, res: Response) => { const userRepository = getRepository(User); const user = await userRepository.findOne({ where: { id: vendorId } }); if (!user) { - console.log('Error creating coupon: User not found', user); return responseError(res, 404, 'User not found'); } const productRepository = getRepository(Product); const product = await productRepository.findOne({ where: { id: productId } }); if (!product) { - console.log('Error creating coupon: Product not found', product); return responseError(res, 403, 'Product not found'); } @@ -49,7 +47,6 @@ export const createCouponService = async (req: Request, res: Response) => { await couponRepository.save(newCoupon); responseSuccess(res, 201, 'Coupon created successfully'); } catch (error: any) { - console.log('Error creating coupon:\n', error); return responseServerError(res, error); } }; diff --git a/src/services/couponServices/deleteCoupon.ts b/src/services/couponServices/deleteCoupon.ts index c984d9e..ac253e6 100644 --- a/src/services/couponServices/deleteCoupon.ts +++ b/src/services/couponServices/deleteCoupon.ts @@ -9,7 +9,6 @@ export const deleteCouponService = async (req: Request, res: Response) => { const coupon = await couponRepository.findOne({ where: { code: req.body.code } }); if (!coupon) { - console.log('Invalid coupon.'); return responseError(res, 404, 'Invalid coupon'); } @@ -17,7 +16,6 @@ export const deleteCouponService = async (req: Request, res: Response) => { return responseSuccess(res, 200, 'Coupon deleted successfully'); } catch (error: any) { - console.log('Error deleting coupon:\n', error); return responseServerError(res, error); } }; diff --git a/src/services/couponServices/readCoupon.ts b/src/services/couponServices/readCoupon.ts index 47e12ea..5af35df 100644 --- a/src/services/couponServices/readCoupon.ts +++ b/src/services/couponServices/readCoupon.ts @@ -17,7 +17,6 @@ export const readCouponService = async (req: Request, res: Response) => { return responseSuccess(res, 200, 'Coupon retrieved successfully', coupon); } catch (error: any) { - console.log('Error retrieving coupon:\n', error); return responseServerError(res, error); } }; diff --git a/src/services/couponServices/updateService.ts b/src/services/couponServices/updateService.ts index 26aeef6..d1d0eab 100644 --- a/src/services/couponServices/updateService.ts +++ b/src/services/couponServices/updateService.ts @@ -17,8 +17,8 @@ export const updateCouponService = async (req: Request, res: Response) => { const coupon = await couponRepository.findOne({ where: { code } }); if (coupon) { if (req.body.code !== undefined) { - const existtCoupon = await couponRepository.findOne({ where: { code: req.body.code } }); - if (existtCoupon) return responseError(res, 400, 'Coupon code already exists'); + const existCoupon = await couponRepository.findOne({ where: { code: req.body.code } }); + if (existCoupon) return responseError(res, 400, 'Coupon code already exists'); if (req.body.code === coupon.code) return responseError(res, 400, 'Coupon code already up to date'); coupon.code = req.body.code; } @@ -35,11 +35,12 @@ export const updateCouponService = async (req: Request, res: Response) => { coupon.discountType = req.body.discountType; } if (req.body.product !== undefined) { - const { id } = req.body.product; + const id = req.body.product; + const productRepository = getRepository(Product); const product = await productRepository.findOne({ where: { id } }); + if (!product) { - console.log('Error updating coupon: Product not found', product); return responseError(res, 404, 'Product not found'); } @@ -49,11 +50,9 @@ export const updateCouponService = async (req: Request, res: Response) => { await couponRepository.save(coupon); return responseSuccess(res, 200, 'Coupon updated successfully', coupon); } else { - console.log('Error updating coupon: Coupon not found', coupon); return responseError(res, 404, 'Coupon not found'); } } catch (error: any) { - console.log('Error while updating coupon:\n', error); return responseServerError(res, error); } -}; +}; \ No newline at end of file diff --git a/src/services/feedbackServices/adminDeleteFeedback.ts b/src/services/feedbackServices/adminDeleteFeedback.ts index 7bf6261..287b87c 100644 --- a/src/services/feedbackServices/adminDeleteFeedback.ts +++ b/src/services/feedbackServices/adminDeleteFeedback.ts @@ -18,7 +18,7 @@ export const adminDeleteFeedbackService = async (req: Request, res: Response) => await feedbackRepository.remove(feedback); - return responseSuccess(res, 200, 'Feedback successfully removed'); + return responseSuccess(res, 200, 'Feedback successfully removed'); } catch (error) { return responseError(res, 500, 'Server error'); } diff --git a/src/services/feedbackServices/createFeedback.ts b/src/services/feedbackServices/createFeedback.ts index fa731f3..8956bb7 100644 --- a/src/services/feedbackServices/createFeedback.ts +++ b/src/services/feedbackServices/createFeedback.ts @@ -5,6 +5,7 @@ import { Product } from '../../entities/Product'; import { User } from '../../entities/User'; import { responseError, responseSuccess } from '../../utils/response.utils'; import { Order } from '../../entities/Order'; +import { sendNotification } from '../../utils/sendNotification'; interface AuthRequest extends Request { user?: User; @@ -21,12 +22,12 @@ export const createFeedbackService = async (req: Request, res: Response) => { if (!orderId) { return responseError(res, 404, `Your feedback can't be recorded at this time Your order doesn't exist `); } - const product = await productRepository.findOne({ where: { id: productId } }); + const product = await productRepository.findOne({ where: { id: productId }, relations: ['vendor'] }); if (!product) { return responseError(res, 404, `Your feedback can't be recorded at this time product not found`); } - const order = await orderRepository.findBy({ id: orderId, orderStatus: 'completed', buyer: { id: req.user?.id }, orderItems: { product: { id: productId } } }) - if (!order.length) { + const order = await orderRepository.findOne({ where: {id: orderId, orderStatus: 'completed', buyer: { id: req.user?.id }, orderItems: { product: { id: productId } }}, relations: ['buyer'] }) + if (!order) { return responseError(res, 404, `Your feedback can't be recorded at this time Your order haven't been completed yet or doesn't contain this product`); } @@ -37,8 +38,15 @@ export const createFeedbackService = async (req: Request, res: Response) => { await feedbackRepository.save(feedback); + await sendNotification({ + content: `Buyer: "${order.buyer.firstName} ${order.buyer.lastName}" sent feedback on product: ${product.name}`, + type: "product", + user: product.vendor, + link: `/product/collection/${product.id}` + }) + return responseSuccess(res, 201, 'Feedback created successfully', feedback); } catch (error) { return responseError(res, 500, 'Server error'); } -}; +}; \ No newline at end of file diff --git a/src/services/notificationServices/deleteNotification.ts b/src/services/notificationServices/deleteNotification.ts index 1ef3c04..cc3c295 100644 --- a/src/services/notificationServices/deleteNotification.ts +++ b/src/services/notificationServices/deleteNotification.ts @@ -93,4 +93,4 @@ export const deleteAllNotificationService = async (req: Request, res: Response) } catch (error) { return responseError(res, 500, (error as Error).message); } -}; \ No newline at end of file +}; diff --git a/src/services/notificationServices/updateNotification.ts b/src/services/notificationServices/updateNotification.ts index c8295d9..4260789 100644 --- a/src/services/notificationServices/updateNotification.ts +++ b/src/services/notificationServices/updateNotification.ts @@ -82,20 +82,31 @@ export const updateAllNotificationsService = async (req: Request, res: Response) isRead: false } }, - relations: ['allNotifications'] + relations: { + allNotifications: true + } }); - + if (!notification || !notification.allNotifications.length) { responseSuccess(res, 200, "User doesn't have any unread notifications."); return; } - for (const notificationItem of notification.allNotifications) { + const notificationItems = await notificationItemRepo.find({ + where: { + notification: { + id: notification.id + } + } + }); + + for (const notificationItem of notificationItems) { notificationItem.isRead = true; await notificationItemRepo.save(notificationItem); } if (notification) { + notification.allNotifications = notificationItems; notification.updateUnread(); await notificationRepo.save(notification); } diff --git a/src/services/orderServices/updateOrderService.ts b/src/services/orderServices/updateOrderService.ts index 4ff7c3a..23b7662 100644 --- a/src/services/orderServices/updateOrderService.ts +++ b/src/services/orderServices/updateOrderService.ts @@ -32,9 +32,6 @@ export const updateOrderService = async (req: Request, res: Response) => { const transactionRepository: Repository = transactionalEntityManager.getRepository(Transaction); const buyerId = req.user?.id; - if (!buyerId) { - throw new Error('Unauthorized'); - } // Fetch order and related entities const order: Order | null = await orderRepository.findOne({ @@ -132,7 +129,6 @@ export const updateOrderService = async (req: Request, res: Response) => { return sendSuccessResponse(res, 200, 'Order updated successfully', orderResponse); }); } catch (error) { - console.error('Error updating order:', (error as Error).message); return sendErrorResponse(res, 500, (error as Error).message); } }; diff --git a/src/services/productServices/productStatus.ts b/src/services/productServices/productStatus.ts index 16509c3..708621b 100644 --- a/src/services/productServices/productStatus.ts +++ b/src/services/productServices/productStatus.ts @@ -7,12 +7,11 @@ import { responseSuccess, responseError, responseServerError } from '../../utils export const productStatusServices = async (req: Request, res: Response) => { try { const { isAvailable } = req.body; - const availability = isAvailable; const { id } = req.params; - if (availability === undefined) { + if (isAvailable === undefined) { console.log('Error: Please fill all the required fields'); - return responseError(res, 401, 'Please fill all t he required fields'); + return responseError(res, 400, 'Please fill all t he required fields'); } const userRepository = getRepository(User); @@ -45,15 +44,14 @@ export const productStatusServices = async (req: Request, res: Response) => { if (hasProduct.expirationDate && hasProduct.expirationDate < new Date()) { hasProduct.isAvailable = false; await productRepository.save(hasProduct); - return responseSuccess(res, 201, 'Product status is set to false because it is expired.'); + return responseSuccess(res, 200, 'Product status is set to false because it is expired.'); } else if (hasProduct.quantity < 1) { product.isAvailable = false; await productRepository.save(hasProduct); - return responseSuccess(res, 202, 'Product status is set to false because it is out of stock.'); + return responseSuccess(res, 200, 'Product status is set to false because it is out of stock.'); } if (hasProduct.isAvailable === isAvailable) { - console.log('Error: Product status is already updated'); responseError(res, 400, 'Product status is already up to date'); return; } @@ -63,7 +61,6 @@ export const productStatusServices = async (req: Request, res: Response) => { return responseSuccess(res, 200, 'Product status updated successfully'); } catch (error) { - console.log('Error: Product status is not update due to this error:\n', error); return responseServerError(res, 'Sorry, Something went wrong. Try again later.'); } }; diff --git a/src/services/productServices/searchProduct.ts b/src/services/productServices/searchProduct.ts index 9f33b5f..123672f 100644 --- a/src/services/productServices/searchProduct.ts +++ b/src/services/productServices/searchProduct.ts @@ -1,5 +1,5 @@ import { Request, Response } from 'express'; -import { getRepository, Like } from 'typeorm'; +import { getRepository } from 'typeorm'; import { Product } from '../../entities/Product'; interface SearchProductParams { @@ -10,33 +10,45 @@ interface SearchProductParams { limit?: number; } -export const searchProductService = async (params: SearchProductParams) => { - const { name, sortBy, sortOrder, page = 1, limit = 10 } = params; - - const productRepository = getRepository(Product); - let query = productRepository.createQueryBuilder('product'); - - if (name) { - query = query.where('product.name LIKE :name', { name: `%${name}%` }); - } - - if (sortBy && sortOrder) { - query = query.orderBy(`product.${sortBy}`, sortOrder as 'ASC' | 'DESC'); +export const searchProductService = async (req: Request, res: Response) => { + const { name, sortBy, sortOrder, page = 1, limit = 10 }: SearchProductParams = req.query as any; + try { + if (!name) { + console.log("no name"); + return res.status(400).json({ status: 'error', error: 'Please provide a search term' }); + } + + const productRepository = getRepository(Product); + let query = productRepository.createQueryBuilder('product'); + + query = query.where('LOWER(product.name) LIKE :name', { name: `%${name.toLowerCase()}%` }); + + if (sortBy && sortOrder) { + query = query.orderBy(`product.${sortBy}`, sortOrder as 'ASC' | 'DESC'); + } + + const skip = (page - 1) * limit; + + const [products, total] = await query.skip(skip).take(limit).getManyAndCount(); + + if (total === 0) { + return res.status(404).json({ status: 'error', error: 'No products found' }); + } + + const totalPages = Math.ceil(total / limit); + + return res.status(200).json({ + status: 'success', + data: products, + pagination: { + totalItems: total, + currentPage: page, + totalPages, + itemsPerPage: limit, + }, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ status: 'error', error: 'Something went wrong' }); } - - const skip = (page - 1) * limit; - - const [products, total] = await query.skip(skip).take(limit).getManyAndCount(); - - const totalPages = Math.ceil(total / limit); - - return { - data: products, - pagination: { - totalItems: total, - currentPage: page, - totalPages, - itemsPerPage: limit, - }, - }; -}; +}; \ No newline at end of file diff --git a/src/services/userServices/sendResetPasswordLinkService.ts b/src/services/userServices/sendResetPasswordLinkService.ts index f9b7dbf..1d34666 100644 --- a/src/services/userServices/sendResetPasswordLinkService.ts +++ b/src/services/userServices/sendResetPasswordLinkService.ts @@ -5,30 +5,30 @@ import { getRepository } from 'typeorm'; import { User } from '../../entities/User'; export const sendPasswordResetLinkService = async (req: Request, res: Response) => { - try { - const transporter = nodemailer.createTransport({ - host: process.env.HOST, - port: 587, - secure: false, // true for 465, false for other ports - auth: { - user: process.env.AUTH_EMAIL, - pass: process.env.AUTH_PASSWORD, - }, - }); - const email = req.query.email as string; + try { + const transporter = nodemailer.createTransport({ + host: process.env.HOST, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: process.env.AUTH_EMAIL, + pass: process.env.AUTH_PASSWORD, + }, + }); + const email = req.query.email as string; - if (!email) { - return responseError(res, 404, 'Missing required field'); - } - const userRepository = getRepository(User); - const existingUser = await userRepository.findOneBy({ email }); - if (!existingUser) { - return responseError(res, 404, 'User not found', existingUser); - } - const mailOptions: nodemailer.SendMailOptions = { - to: email, - subject: `Password reset link `, - html: ` + if (!email) { + return responseError(res, 400, 'Missing required field'); + } + const userRepository = getRepository(User); + const existingUser = await userRepository.findOneBy({ email }); + if (!existingUser) { + return responseError(res, 404, 'User not found', existingUser); + } + const mailOptions: nodemailer.SendMailOptions = { + to: email, + subject: `Password reset link `, + html: ` @@ -103,15 +103,15 @@ export const sendPasswordResetLinkService = async (req: Request, res: Response) `, - }; + }; - try { - const sendMail = await transporter.sendMail(mailOptions); - return responseSuccess(res, 200, 'Code sent on your email', sendMail); + try { + const sendMail = await transporter.sendMail(mailOptions); + return responseSuccess(res, 200, 'Code sent on your email', sendMail); + } catch (error) { + return responseError(res, 500, 'Error occurred while sending email'); + } } catch (error) { - return responseError(res, 500, 'Error occurred while sending email'); + return responseServerError(res, `Internal server error: `); } - } catch (error) { - return responseServerError(res, `Internal server error: `); - } }; diff --git a/src/services/userServices/userDisableTwoFactorAuth.ts b/src/services/userServices/userDisableTwoFactorAuth.ts index 63729fd..b4fc6e9 100644 --- a/src/services/userServices/userDisableTwoFactorAuth.ts +++ b/src/services/userServices/userDisableTwoFactorAuth.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import { User } from '../../entities/User'; import { getRepository } from 'typeorm'; +import { sendNotification } from '../../utils/sendNotification'; export const userDisableTwoFactorAuth = async (req: Request, res: Response) => { try { @@ -20,6 +21,11 @@ export const userDisableTwoFactorAuth = async (req: Request, res: Response) => { user.twoFactorEnabled = false; await userRepository.save(user); + await sendNotification({ + content: "You disabled Two factor authentication on you account", + type: 'user', + user: user + }) return res.status(200).json({ status: 'success', message: 'Two factor authentication disabled successfully' }); } catch (error) { if (error instanceof Error) { diff --git a/src/services/userServices/userEnableTwoFactorAuth.ts b/src/services/userServices/userEnableTwoFactorAuth.ts index 16b36be..c5d3cbf 100644 --- a/src/services/userServices/userEnableTwoFactorAuth.ts +++ b/src/services/userServices/userEnableTwoFactorAuth.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import { User } from '../../entities/User'; import { getRepository } from 'typeorm'; +import { sendNotification } from '../../utils/sendNotification'; export const userEnableTwoFactorAuth = async (req: Request, res: Response) => { try { @@ -20,6 +21,11 @@ export const userEnableTwoFactorAuth = async (req: Request, res: Response) => { user.twoFactorEnabled = true; await userRepository.save(user); + await sendNotification({ + content: "You enabled Two factor authentication on you account", + type: 'user', + user: user + }) return res.status(200).json({ status: 'success', message: 'Two factor authentication enabled successfully' }); } catch (error) { if (error instanceof Error) { diff --git a/src/services/userServices/userPasswordResetService.ts b/src/services/userServices/userPasswordResetService.ts index 8428f1a..93de01e 100644 --- a/src/services/userServices/userPasswordResetService.ts +++ b/src/services/userServices/userPasswordResetService.ts @@ -12,7 +12,7 @@ export const userPasswordResetService = async (req: Request, res: Response) => { const userId: any = userid; const userRepository = getRepository(User); if (!email || !userid) { - return responseError(res, 404, `Something went wrong while fetching your data`); + return responseError(res, 400, `Something went wrong while fetching your data`); } const existingUser = await userRepository.findOneBy({ email: mail, id: userId }); if (!existingUser) { @@ -20,19 +20,18 @@ export const userPasswordResetService = async (req: Request, res: Response) => { } if (!newPassword || !confirmPassword) { - return responseError(res, 200, 'Please provide all required fields'); + return responseError(res, 400, 'Please provide all required fields'); } if (newPassword !== confirmPassword) { - return responseError(res, 200, 'new password must match confirm password'); + return responseError(res, 400, 'new password must match confirm password'); } const saltRounds = 10; const hashedPassword = await bcrypt.hash(newPassword, saltRounds); existingUser.password = hashedPassword; const updadeUser = await userRepository.save(existingUser); - return responseSuccess(res, 201, 'Password updated successfully', updadeUser); + return responseSuccess(res, 200, 'Password updated successfully', updadeUser); } catch (error) { - console.log('error: reseting password in password reset service'); return responseServerError(res, 'Internal server error'); } }; diff --git a/src/services/userServices/userProfileUpdateServices.ts b/src/services/userServices/userProfileUpdateServices.ts index c140e38..82fb71d 100644 --- a/src/services/userServices/userProfileUpdateServices.ts +++ b/src/services/userServices/userProfileUpdateServices.ts @@ -4,44 +4,34 @@ import { User, UserInterface } from '../../entities/User'; import { getRepository } from 'typeorm'; import { userProfileUpdate } from '../../controllers/authController'; -declare module 'express' { - interface Request { - user?: Partial; - } -} export const userProfileUpdateServices = async (req: Request, res: Response) => { try { - if (!req.body) { - return responseError(res, 401, 'body required'); + + if (Object.keys(req.body).length === 0) { + return responseError(res, 400, 'body required'); } - const { firstName, lastName, gender, phoneNumber, photoUrl, email, id } = req.body; + const { firstName, lastName, gender, phoneNumber, photoUrl} = req.body; // Validate user input if ( - !firstName.trim() && - !lastName.trim() && - !gender.trim() && - !phoneNumber.trim() && - !photoUrl.trim() && - !email.trim() && - !id.trim() + !firstName || !lastName || !gender || + !phoneNumber || !photoUrl ) { return responseError(res, 400, 'Fill all the field'); } const userRepository = getRepository(User); const existingUser = await userRepository.findOne({ - where: { email: req.body.email }, + where: { + id: req.user?.id + }, }); if (!existingUser) { return responseError(res, 401, 'User not found'); } - if (existingUser.id !== id) { - return responseError(res, 403, 'You are not authorized to edit this profile.'); - } existingUser.firstName = firstName; existingUser.lastName = lastName; @@ -50,7 +40,7 @@ export const userProfileUpdateServices = async (req: Request, res: Response) => existingUser.photoUrl = photoUrl; await userRepository.save(existingUser); - return responseSuccess(res, 201, 'User Profile has successfully been updated'); + return responseSuccess(res, 200, 'User Profile has successfully been updated'); } catch (error) { responseError(res, 400, (error as Error).message); } diff --git a/src/services/userServices/userValidationService.ts b/src/services/userServices/userValidationService.ts index a28ff4e..be21726 100644 --- a/src/services/userServices/userValidationService.ts +++ b/src/services/userServices/userValidationService.ts @@ -1,25 +1,30 @@ import { Request, Response } from 'express'; import { User } from '../../entities/User'; import { getRepository } from 'typeorm'; +import { responseError } from '../../utils/response.utils'; export const userVerificationService = async (req: Request, res: Response) => { - const { id } = req.params; + try { + const { id } = req.params; - // Validate user input - if (!id) { - return res.status(400).json({ error: 'Missing user ID' }); - } + // Validate user input + if (!id) { + return res.status(400).json({ error: 'Missing user ID' }); + } - const userRepository = getRepository(User); - const user = await userRepository.findOneBy({ id }); + const userRepository = getRepository(User); + const user = await userRepository.findOneBy({ id }); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } - user.verified = true; + user.verified = true; - await userRepository.save(user); + await userRepository.save(user); - return res.status(200).send('

User verified successfully

'); + return res.status(200).send('

User verified successfully

'); + } catch (error) { + responseError(res, 400, (error as Error).message); + } }; diff --git a/src/services/wishListServices/addProduct.ts b/src/services/wishListServices/addProduct.ts index 79d0a38..4efb80d 100644 --- a/src/services/wishListServices/addProduct.ts +++ b/src/services/wishListServices/addProduct.ts @@ -54,4 +54,4 @@ export const addProductService = async (req: Request, res: Response) => { } catch (error) { return res.status(500).json({ error: 'Internal server error' }); } -}; +}; \ No newline at end of file diff --git a/src/services/wishListServices/clearAll.ts b/src/services/wishListServices/clearAll.ts index 7299454..81f3fc7 100644 --- a/src/services/wishListServices/clearAll.ts +++ b/src/services/wishListServices/clearAll.ts @@ -16,4 +16,4 @@ export const clearAllProductService = async (req: Request, res: Response) => { } catch (error) { return res.status(500).json({ error: 'Internal server error' }); } -}; +}; \ No newline at end of file diff --git a/src/services/wishListServices/getProducts.ts b/src/services/wishListServices/getProducts.ts index 98dc434..a419134 100644 --- a/src/services/wishListServices/getProducts.ts +++ b/src/services/wishListServices/getProducts.ts @@ -36,4 +36,4 @@ export const getProductsService = async (req: Request, res: Response) => { } catch (error) { return res.status(500).json({ error: 'Internal server error' }); } -}; +}; \ No newline at end of file diff --git a/src/services/wishListServices/removeProducts.ts b/src/services/wishListServices/removeProducts.ts index b42052f..f25a837 100644 --- a/src/services/wishListServices/removeProducts.ts +++ b/src/services/wishListServices/removeProducts.ts @@ -18,4 +18,4 @@ export const removeProductService = async (req: Request, res: Response) => { } catch (error) { return res.status(500).json({ error: 'Internal server error' }); } -}; +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 94cd76c..b56f8f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,8 +33,7 @@ "node", "jest", "express", - "node-nlp", - "joi" + "node-nlp" ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ @@ -106,4 +105,4 @@ }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] -} +} \ No newline at end of file