diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3170cd8..77b8aec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,14 @@ env: GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}} GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}} + TEST_USER_EMAIL: ${{secrets.TEST_USER_EMAIL}} + TEST_USER_PASS: ${{secrets.TEST_USER_PASS}} + TEST_VENDOR_EMAIL: ${{secrets.TEST_VENDOR_EMAIL}} + TEST_VENDOR1_EMAIL: ${{secrets.TEST_VENDOR1_EMAIL}} + TEST_BUYER_EMAIL: ${{secrets.TEST_BUYER_EMAIL}} + TEST_SAMPLE_BUYER_EMAIL: ${{secrets.TEST_SAMPLE_BUYER_EMAIL}} + TEST_VENDOR2_EMAIL: ${{secrets.TEST_VENDOR2_EMAIL}} + STRIPE_SECRET_KEY: ${{secrets.STRIPE_SECRET_KEYT}} jobs: @@ -48,4 +56,4 @@ jobs: - name: Upload coverage report to Coveralls uses: coverallsapp/github-action@v2.2.3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1500c37..829a739 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,14 @@ package-lock.json coverage/ dist /src/logs -.DS_Store \ No newline at end of file +.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..4c75064 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "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", diff --git a/src/__test__/auth.test.ts b/src/__test__/auth.test.ts new file mode 100644 index 0000000..179a736 --- /dev/null +++ b/src/__test__/auth.test.ts @@ -0,0 +1,154 @@ +import request from 'supertest'; +import express, { Request, Response } from 'express'; +import { + userVerificationService, + userRegistrationService, + userLoginService, + userEnableTwoFactorAuth, + userDisableTwoFactorAuth, + userValidateOTP, + userResendOtpService, + logoutService, +} from '../services'; +import { userPasswordResetService } from '../services/userServices/userPasswordResetService'; +import { sendPasswordResetLinkService } from '../services/userServices/sendResetPasswordLinkService'; +import { activateUserService } from '../services/updateUserStatus/activateUserService'; +import { deactivateUserService } from '../services/updateUserStatus/deactivateUserService'; +import { userProfileUpdateServices } from '../services/userServices/userProfileUpdateServices'; +import { activateUser, disable2FA, disactivateUser, enable2FA, login, logout, resendOTP, sampleAPI, sendPasswordResetLink, userPasswordReset, userProfileUpdate, userRegistration, userVerification, verifyOTP } from '../controllers'; + +// Mock the services +jest.mock('../services', () => ({ + userVerificationService: jest.fn(), + userRegistrationService: jest.fn(), + userLoginService: jest.fn(), + userEnableTwoFactorAuth: jest.fn(), + userDisableTwoFactorAuth: jest.fn(), + userValidateOTP: jest.fn(), + userResendOtpService: jest.fn(), + logoutService: jest.fn(), +})); + +jest.mock('../services/userServices/userPasswordResetService', () => ({ + userPasswordResetService: jest.fn(), +})); + +jest.mock('../services/userServices/sendResetPasswordLinkService', () => ({ + sendPasswordResetLinkService: jest.fn(), +})); + +jest.mock('../services/updateUserStatus/activateUserService', () => ({ + activateUserService: jest.fn(), +})); + +jest.mock('../services/updateUserStatus/deactivateUserService', () => ({ + deactivateUserService: jest.fn(), +})); + +jest.mock('../services/userServices/userProfileUpdateServices', () => ({ + userProfileUpdateServices: jest.fn(), +})); + +const app = express(); +app.use(express.json()); + +app.post('/register', userRegistration); +app.post('/verify', userVerification); +app.post('/login', login); +app.post('/enable-2fa', enable2FA); +app.post('/disable-2fa', disable2FA); +app.post('/verify-otp', verifyOTP); +app.post('/resend-otp', resendOTP); +app.get('/sample', sampleAPI); +app.post('/reset-password', userPasswordReset); +app.post('/send-reset-link', sendPasswordResetLink); +app.post('/activate', activateUser); +app.post('/deactivate', disactivateUser); +app.post('/logout', logout); +app.put('/update-profile', userProfileUpdate); + +describe('User Controller', () => { + it('should call userRegistrationService on /register', async () => { + (userRegistrationService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(201).send()); + await request(app).post('/register').send({}); + expect(userRegistrationService).toHaveBeenCalled(); + }); + + it('should call userVerificationService on /verify', async () => { + (userVerificationService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/verify').send({}); + expect(userVerificationService).toHaveBeenCalled(); + }); + + it('should call userLoginService on /login', async () => { + (userLoginService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/login').send({}); + expect(userLoginService).toHaveBeenCalled(); + }); + + it('should call userEnableTwoFactorAuth on /enable-2fa', async () => { + (userEnableTwoFactorAuth as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/enable-2fa').send({}); + expect(userEnableTwoFactorAuth).toHaveBeenCalled(); + }); + + it('should call userDisableTwoFactorAuth on /disable-2fa', async () => { + (userDisableTwoFactorAuth as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/disable-2fa').send({}); + expect(userDisableTwoFactorAuth).toHaveBeenCalled(); + }); + + it('should call userValidateOTP on /verify-otp', async () => { + (userValidateOTP as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/verify-otp').send({}); + expect(userValidateOTP).toHaveBeenCalled(); + }); + + it('should call userResendOtpService on /resend-otp', async () => { + (userResendOtpService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/resend-otp').send({}); + expect(userResendOtpService).toHaveBeenCalled(); + }); + + it('should return 200 on /sample', async () => { + const response = await request(app).get('/sample'); + expect(response.status).toBe(200); + expect(response.body).toEqual({ message: 'Token is valid' }); + }); + + it('should call userPasswordResetService on /reset-password', async () => { + (userPasswordResetService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/reset-password').send({}); + expect(userPasswordResetService).toHaveBeenCalled(); + }); + + it('should call sendPasswordResetLinkService on /send-reset-link', async () => { + (sendPasswordResetLinkService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/send-reset-link').send({}); + expect(sendPasswordResetLinkService).toHaveBeenCalled(); + }); + + it('should call activateUserService on /activate', async () => { + (activateUserService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/activate').send({}); + expect(activateUserService).toHaveBeenCalled(); + }); + + it('should call deactivateUserService on /deactivate', async () => { + (deactivateUserService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/deactivate').send({}); + expect(deactivateUserService).toHaveBeenCalled(); + }); + + it('should call logoutService on /logout', async () => { + (logoutService as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).post('/logout').send({}); + expect(logoutService).toHaveBeenCalled(); + }); + + it('should call userProfileUpdateServices on /update-profile', async () => { + (userProfileUpdateServices as jest.Mock).mockImplementationOnce((req: Request, res: Response) => res.status(200).send()); + await request(app).put('/update-profile').send({}); + expect(userProfileUpdateServices).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/__test__/cart.test.ts b/src/__test__/cart.test.ts index ffe143f..e2d7c07 100644 --- a/src/__test__/cart.test.ts +++ b/src/__test__/cart.test.ts @@ -1,3 +1,4 @@ + import request from 'supertest'; import jwt from 'jsonwebtoken'; import { app, server } from '../index'; @@ -23,9 +24,30 @@ const cartItemId = uuid(); const sampleCartId = uuid(); const sampleCartItemId = uuid(); const samplecartItem3Id = uuid(); +const feedbackID = uuid(); +const feedbackID2 = uuid(); +const sampleAdminId = uuid(); + +let returnedCartId: 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', + lastName: 'user', + email: process.env.TEST_USER_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Vendor', + gender: 'Male', + phoneNumber: '10026380996347', + photoUrl: 'https://example.com/photo.jpg', + role: 'ADMIN', +}; + + const getAccessToken = (id: string, email: string) => { return jwt.sign( { @@ -206,7 +228,7 @@ describe('Cart| Order management for guest/buyer', () => { 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) @@ -276,26 +298,6 @@ describe('Cart| Order management for guest/buyer', () => { }); describe('Adding product to cart on guest/buyer', () => { - it('should get cart items of authenticated user', async () => { - const response = await request(app) - .get('/cart') - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - - expect(response.status).toBe(200); - expect(response.body.data.message).toBe('Cart retrieved successfully'); - expect(response.body.data.cart).toBeDefined; - }); - - it('should get cart items of authenticated user', async () => { - const response = await request(app) - .get('/cart') - .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.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`) @@ -313,15 +315,36 @@ describe('Cart| Order management for guest/buyer', () => { 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; }); - it('should get cart items of guest user', async () => { - const response = await request(app).get('/cart'); + it('should add second product to cart as guest', async () => { + const response = await request(app) + .post(`/cart`) + .set('Cookie', [`cartId=${returnedCartId}`]) + .send({ + productId: product1Id, + quantity: 3, + }); - expect(response.status).toBe(200); + 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`) + .set('Cookie', [`cartId=dfgdsf`]) + .send({ + productId: product1Id, + quantity: 3, + }); + + expect(response.status).toBe(400); + }); + it('should return 400 if you do not send proper request body', async () => { const response = await request(app).post(`/cart`); @@ -348,7 +371,7 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.message).toBe('Quantity must be greater than 0'); }); - it('should chnage quantity of product in cart if it is already there', async () => { + it('should change quantity of product in cart if it is already there', async () => { const response = await request(app) .post(`/cart`) .send({ productId: product1Id, quantity: 3 }) @@ -371,13 +394,33 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.cart).toBeDefined; }); - it('should get cart items of guest user', async () => { + it('should get Empty cart items of authenticated user', async () => { + const response = await request(app) + .get('/cart') + .set('Authorization', `Bearer ${getAccessToken(buyer2Id, sampleBuyer2.email)}`); + + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('Cart is empty'); + expect(response.body.data.cart).toBeDefined; + }); + + it('should get Empty cart items of guest user', async () => { const response = await request(app).get('/cart'); expect(response.status).toBe(200); expect(response.body.data.cart).toBeDefined; }); + it('should get cart items of guest user', async () => { + const response = await request(app) + .get('/cart') + .set('Cookie', [`cartId=${returnedCartId}`]); + + expect(response.status).toBe(200); + expect(response.body.data.message).toBe('Cart retrieved successfully'); + expect(response.body.data.cart).toBeDefined; + }); + it('should get cart items of guest user as empty with wrong cartId', async () => { const response = await request(app) .get('/cart') @@ -387,6 +430,15 @@ 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 Id syntax (IDs not in uuid form), when getting cart', async () => { + const response = await request(app) + .get(`/cart`) + .set('Cookie', [`cartId=dfgdsf`]); + + expect(response.status).toBe(400); + }); }); describe('Order management tests', () => { @@ -394,8 +446,9 @@ describe('Cart| Order management for guest/buyer', () => { let productId: any; let feedbackId: any; let feedback2Id: any; + describe('Create order', () => { - it('should return 400 when user ID is not provided', async () => { + it('should return 201 when user is found', async () => { const response = await request(app) .post('/product/orders') .send({ @@ -425,7 +478,6 @@ describe('Cart| Order management for guest/buyer', () => { .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); 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 () => { @@ -435,7 +487,7 @@ describe('Cart| Order management for guest/buyer', () => { 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`) @@ -460,11 +512,11 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.message).toBe('Transaction history retrieved successfully'); }); - it('should return 400 when user ID is not provided', async () => { + it('should return 400 when user is not AUTHORIZED', async () => { const response = await request(app) .get('/product/orders/history') - .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); - expect(response.status).toBe(200); + .set('Authorization', `Bearer ''`); + expect(response.status).toBe(403); }); }); @@ -475,6 +527,14 @@ describe('Cart| Order management for guest/buyer', () => { .send({ orderStatus: 'completed' }) .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); expect(response.status).toBe(200); + expect(response.body.message).toBe("Order updated successfully"); + }); + it('should update order status successfully', async () => { + const response = await request(app) + .put(`/product/client/orders/${orderId}`) + .send({ orderStatus: 'completed' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(401); }); }); describe('Add feedback to the product with order', () => { @@ -508,6 +568,13 @@ 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}`) @@ -515,10 +582,154 @@ describe('Cart| Order management for guest/buyer', () => { .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', () => { + it('should create new feedback for the ordered product', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .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; + }); + + it('should create another feedback for the ordered product', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ orderId, comment: 'Murigalike this product looks so fantastic' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(201); + feedback2Id = response.body.data.id; + }); + + it('should fail to create feedback with missing orderId', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ comment: 'Missing orderId' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(404); + }); + + it('should fail to create feedback with missing comment', async () => { + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ orderId }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(500); + }); + + it('should fail to create feedback with invalid productId', async () => { + const response = await request(app) + .post(`/feedback/invalidProductId/new`) + .send({ orderId, comment: 'Invalid productId' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(500); + }); + }); + + describe('Update feedback', () => { + it('should update existing feedback successfully', async () => { + const response = await request(app) + .put(`/feedback/update/${feedbackId}`) + .send({ orderId, comment: 'Well this product looks so lovely' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + + it('should fail to update feedback with invalid feedbackId', async () => { + const response = await request(app) + .put(`/feedback/update/invalidFeedbackId`) + .send({ orderId, comment: 'Invalid feedbackId' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(500); + }); + + it('should fail to update feedback without authorization', async () => { + const response = await request(app) + .put(`/feedback/update/${feedbackId}`) + .send({ orderId, comment: 'Unauthorized update' }); + expect(response.status).toBe(401); + }); + }); + + describe('Delete feedback', () => { + it('should remove recorded feedback', async () => { + const response = await request(app) + .delete(`/feedback/delete/${feedbackId}`) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + 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', () => { + it('should not allow creating feedback for a product not in the order', async () => { + const invalidOrderId = 999; // Assuming an invalid orderId + const response = await request(app) + .post(`/feedback/${productId}/new`) + .send({ orderId: invalidOrderId, comment: 'Invalid orderId' }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(500); + }); + + it('should fail to update feedback with a comment that is too long', async () => { + const longComment = 'a'.repeat(1001); // Assuming max length is 1000 + const response = await request(app) + .put(`/feedback/update/${feedback2Id}`) + .send({ orderId, comment: longComment }) + .set('Authorization', `Bearer ${getAccessToken(buyer1Id, sampleBuyer1.email)}`); + expect(response.status).toBe(200); + }); + }); }); + }); 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); + }); + it('should return 404 if product does not exist in cart', async () => { const response = await request(app) .delete(`/cart/${uuid()}`) @@ -652,4 +863,4 @@ describe('Cart| Order management for guest/buyer', () => { expect(response.body.data.cart).toBeDefined; }); }); -}); +}); \ No newline at end of file diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index 269e95e..192f32a 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -1,3 +1,4 @@ + import request from 'supertest'; import jwt from 'jsonwebtoken'; import { app, server } from '../index'; @@ -18,6 +19,7 @@ const buyer1Id = uuid(); const buyer2Id = uuid(); const product1Id = uuid(); const product2Id = uuid(); +const vendor2Id = uuid(); const couponCode = 'DISCOUNT20'; const couponCode1 = 'DISCOUNT10'; const couponCode2 = 'DISCOUNT99'; @@ -39,6 +41,20 @@ const getAccessToken = (id: string, email: string) => { ); }; +const sampleVendor2: UserInterface = { + id: vendor2Id, + firstName: 'Vendor', + lastName: 'User', + email: 'secondendor@example.com', + password: 'password123', + userType: 'Vendor', + gender: 'Male', + verified: true, + phoneNumber: '98000867890', + photoUrl: 'https://example.com/photo.jpg', + role: 'VENDOR', +}; + const sampleVendor1: UserInterface = { id: vendor1Id, firstName: 'Vendor', @@ -180,6 +196,7 @@ beforeAll(async () => { await userRepository?.save(sampleVendor1); await userRepository?.save(sampleBuyer1); await userRepository?.save(buyerNoCart); + await userRepository?.save(sampleVendor2); const productRepository = connection?.getRepository(Product); await productRepository?.save(sampleProduct1); @@ -241,6 +258,54 @@ describe('Coupon Management System', () => { expect(response.status).toBe(400); }, 10000); + + it('should return 403 if product not found', async () => { + const response = await request(app) + .post(`/coupons/vendor/${vendor1Id}/`) + .send({ + code: 'NEWCOUPON10', + discountRate: 10, + expirationDate: '2025-12-31', + maxUsageLimit: 50, + discountType: 'PERCENTAGE', + product: uuid(), + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(403); + }) + + it('should return 402 if coupon already exist', async () => { + const response = await request(app) + .post(`/coupons/vendor/${vendor1Id}/`) + .send({ + code: couponCode1, + discountRate: 10, + expirationDate: '2025-12-31', + maxUsageLimit: 50, + discountType: 'PERCENTAGE', + product: product1Id, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(402); + }) + + it('should return 500 if there is server error', async () => { + const response = await request(app) + .post(`/coupons/vendor/***** -- + ---/`) + .send({ + code: 'NEWCOUPON', + discountRate: 10, + expirationDate: '2025-12-31', + maxUsageLimit: 50, + discountType: 'PERCENTAGE', + product: product1Id, + }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(500); + }) }); describe('Get All Coupons', () => { @@ -264,6 +329,43 @@ describe('Coupon Management System', () => { }, 10000); }); + describe('Vendor access all Coupon', () => { + it('should return all coupons', async () => { + const response = await request(app) + .get(`/coupons/vendor/${vendor1Id}/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('success'); + }, 10000); + + it('should return 404 for invalid vendor id', async () => { + const invalidVendorId = uuid(); + const response = await request(app) + .get(`/coupons/vendor/${invalidVendorId}/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + 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) + .get(`/coupons/vendor/${vendor2Id}/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(vendor2Id, sampleVendor2.email)}`); + + expect(response.status).toBe(404); + }) + + it('should return 500 server error', async () => { + const response = await request(app) + .get(`/coupons/vendor/uihoji 090j hh =/access-coupons`) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(500); + }) + }); + describe('Read Coupon', () => { it('should read a single coupon by code', async () => { const response = await request(app) @@ -297,6 +399,15 @@ describe('Coupon Management System', () => { expect(response.body.status).toBe('success'); }, 10000); + it('should validate coupon update input', async () => { + const response = await request(app) + .put(`/coupons/vendor/${vendor1Id}/update-coupon/${couponCode1}`) + .send() + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(400); + }) + it('should return 404 for updating a non-existent coupon', async () => { const response = await request(app) .put(`/coupons/vendor/${vendor1Id}/update-coupon/${invalidCouponCode}`) @@ -306,7 +417,60 @@ describe('Coupon Management System', () => { .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); expect(response.status).toBe(404); - expect(response.body.message).toBe('Coupon not found'); + }, 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); + }, 10000); + + 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: uuid() }) + .set('Authorization', `Bearer ${getAccessToken(vendor1Id, sampleVendor1.email)}`); + + expect(response.status).toBe(200); + }, 10000); + + 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); }, 10000); }); diff --git a/src/__test__/getProduct.test.ts b/src/__test__/getProduct.test.ts index 96201dd..dc9ac8b 100644 --- a/src/__test__/getProduct.test.ts +++ b/src/__test__/getProduct.test.ts @@ -104,7 +104,7 @@ describe('Creating new product', () => { expect(response.status).toBe(201); expect(response.body.data.product).toBeDefined; - }, 20000); + }, 60000); }); describe('Get single product', () => { it('should get a single product', async () => { diff --git a/src/__test__/index.test.ts b/src/__test__/index.test.ts new file mode 100644 index 0000000..dfa39c4 --- /dev/null +++ b/src/__test__/index.test.ts @@ -0,0 +1,107 @@ +// index.test.ts + +import request from 'supertest'; +import { app, server } from '../index'; +import { dbConnection } from '../startups/dbConnection'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; + +beforeAll(async () => { + await dbConnection(); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + +describe('Express App', () => { + it('should have JSON parsing enabled', async () => { + const response = await request(app) + .get('/test') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(200); + }); + + it('Should respond to posting route', async () => { + const response = await request(app) + .post('/test/posting') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(200); + }); + + it('should respond to a valid route', async () => { + const response = await request(app) + .get('/test') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('Route works!'); + }); + + it('should not respond to invalid route', async () => { + const response = await request(app) + .get('/testing/mon') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(404); + }); + + it('should respond to an invalid route with an appropriate message', async () => { + const response = await request(app) + .get('/mon') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(404); + }); +}); + +describe('Application JSON', () =>{ +it('Should respond to json', async () =>{ +const data ={ + name: 'John', + age: 20, + gender:'male' +}; +const response = await request(app) +.post('/test/posting') +.set('Content-Type', 'application/json') +.send(data); + +expect(response.statusCode).toBe(200); +}); +}); + +describe('APIs protection', () => { + it('should respond with a 401 status for unauthorized request', async () => { + const response = await request(app) + .get('/test/secure') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(401); + }); + + it('should respond with a 500 status for server errors', async () => { + const response = await request(app) + .get('/test/error') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(500); + }); + + it('should respond with correct data', async () => { + const data = { + name: 'John', + age: 20, + gender: 'male' + }; + const response = await request(app) + .post('/test/posting') + .set('Content-Type', 'application/json') + .send(data); + + expect(response.status).toBe(200); + expect(response.body.data).toBeDefined; + }); +}); \ No newline at end of file diff --git a/src/__test__/index.utils.test.ts b/src/__test__/index.utils.test.ts new file mode 100644 index 0000000..aa8ba39 --- /dev/null +++ b/src/__test__/index.utils.test.ts @@ -0,0 +1,34 @@ +import { formatMoney, formatDate } from '../utils/index'; + +describe('Utility Functions', () => { + describe('formatMoney', () => { + it('should format a number as currency with default currency RWF', () => { + expect(formatMoney(1234.56)).toBeDefined(); + }); + + it('should format a number as currency with specified currency', () => { + expect(formatMoney(1234.56, 'USD')).toBe('$1,234.56'); + expect(formatMoney(1234.56, 'EUR')).toBe('€1,234.56'); + }); + + it('should format a number with no cents if amount is a whole number', () => { + expect(formatMoney(1234)).toBeDefined(); + }); + }); + + describe('formatDate', () => { + it('should format a date string into a more readable format', () => { + const date = new Date('2024-05-28'); + expect(formatDate(date)).toBe('May 28, 2024'); + }); + + it('should format another date correctly', () => { + const date = new Date('2020-01-01'); + expect(formatDate(date)).toBe('January 1, 2020'); + }); + + it('should handle invalid date strings gracefully', () => { + expect(formatDate(new Date('invalid-date'))).toBe('Invalid Date'); + }); + }); +}); diff --git a/src/__test__/isAllowed.test.ts b/src/__test__/isAllowed.test.ts index 471a950..d4636f1 100644 --- a/src/__test__/isAllowed.test.ts +++ b/src/__test__/isAllowed.test.ts @@ -6,6 +6,7 @@ import { User } from '../entities/User'; import { responseError } from '../utils/response.utils'; import { v4 as uuid } from 'uuid'; import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { server } from '..'; jest.mock('../utils/response.utils'); @@ -19,7 +20,7 @@ const suspendedUserId = uuid(); beforeAll(async () => { const connection = await dbConnection(); - const userRepository = connection?.getRepository(User); + const userRepository = await connection?.getRepository(User); const activeUser = new User(); activeUser.id = activeUserId; @@ -49,6 +50,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase(); + server.close(); }); describe('Middleware - checkUserStatus', () => { diff --git a/src/__test__/isValid.test.ts b/src/__test__/isValid.test.ts new file mode 100644 index 0000000..7eceb64 --- /dev/null +++ b/src/__test__/isValid.test.ts @@ -0,0 +1,44 @@ +import { isTokenValide } from '../middlewares/isValid'; +import { Request, Response, NextFunction } from 'express'; +import { getRepository } from 'typeorm'; +import { User } from '../entities/User'; + +jest.mock('typeorm', () => ({ + ...jest.requireActual('typeorm'), + getRepository: jest.fn().mockImplementation((entity: any) => { + if (entity === User) { + return { + findOne: jest.fn(), + }; + } + return jest.requireActual('typeorm').getRepository(entity); + }), +})); + +const mockRequest = (userPayload: any): Request => { + return { + cookies: { token: 'mockToken' }, + user: userPayload, + } as unknown as Request; +}; + +const mockResponse = () => { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +}; + +const mockNext = jest.fn(); + +describe('isTokenValide middleware', () => { + it('should return 401 if no user payload', async () => { + const req = mockRequest(null); + const res = mockResponse(); + + await isTokenValide(req as Request, res as Response, mockNext as NextFunction); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ Message: 'Sorry, You are not authorized' }); + }); +}); \ No newline at end of file diff --git a/src/__test__/logger.test.ts b/src/__test__/logger.test.ts new file mode 100644 index 0000000..71a766d --- /dev/null +++ b/src/__test__/logger.test.ts @@ -0,0 +1,80 @@ +import { dbConnection } from '../startups/dbConnection'; +import logger from '../utils/logger'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { server } from '../../src/index'; + +jest.mock('../utils/logger', () => ({ + __esModule: true, + default: { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + http: jest.fn(), + }, +})); + +beforeAll(async () => { + const connection = await dbConnection(); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + +describe('Logger', () => { + it('should create a logger with the correct configuration', () => { + expect(logger).toBeDefined(); + }); + + it('should log messages with the correct level and format', () => { + const testMessage = 'Test log message'; + const testLevel = 'info'; + + logger.log(testLevel, testMessage); + + expect(logger.log).toHaveBeenCalledWith(testLevel, testMessage); + }); + + it('should correctly handle info level logs', () => { + const testMessage = 'Test info message'; + + logger.info(testMessage); + + expect(logger.info).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle warn level logs', () => { + const testMessage = 'Test warn message'; + + logger.warn(testMessage); + + expect(logger.warn).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle error level logs', () => { + const testMessage = 'Test error message'; + + logger.error(testMessage); + + expect(logger.error).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle debug level logs', () => { + const testMessage = 'Test debug message'; + + logger.debug(testMessage); + + expect(logger.debug).toHaveBeenCalledWith(testMessage); + }); + + it('should correctly handle http level logs', () => { + const testMessage = 'Test http message'; + + logger.http(testMessage); + + expect(logger.http).toHaveBeenCalledWith(testMessage); + }); +}); \ No newline at end of file diff --git a/src/__test__/login.test.ts b/src/__test__/login.test.ts new file mode 100644 index 0000000..0246bd7 --- /dev/null +++ b/src/__test__/login.test.ts @@ -0,0 +1,83 @@ +import request from 'supertest'; +import { app, server } from '../index'; +import { createConnection, getRepository } from 'typeorm'; +import { User, UserInterface } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import jwt from 'jsonwebtoken'; +import { v4 as uuid } from 'uuid'; +import { dbConnection } from '../startups/dbConnection'; + + +const adminId = uuid(); +const adminId1 = uuid(); + +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; + + +if (!process.env.TEST_USER_EMAIL || !process.env.TEST_BUYER_EMAIL || !process.env.TEST_VENDOR1_EMAIL || !process.env.TEST_VENDOR_EMAIL || !process.env.TEST_USER_PASS) throw new Error('TEST_USER_PASS or TEST_USER_EMAIL not set in .env'); + +const sampleAdmin: UserInterface = { + id: adminId, + firstName: 'admin', + lastName: 'user', + email: process.env.TEST_USER_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'ADMIN', + }; + + const sampleAdmin1: UserInterface = { + id: adminId1, + firstName: 'admin', + lastName: 'user', + email: 'vendor@example.com', + password: process.env.TEST_USER_PASS, + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: false, + role: 'ADMIN', + }; + +beforeAll(async () => { + const connection = await dbConnection(); + + const userRepository = connection?.getRepository(User); + await userRepository?.save([sampleAdmin, sampleAdmin1]); + }); + +afterAll(async () => { + await cleanDatabase(); + + server.close(); +}); + +describe('POST /user/login', () => { + it('should not login a user with unverified email', async () => { + + const loginUser = { + email: 'vendor@example.com', + password: process.env.TEST_USER_LOGIN_PASS, + }; + + const loginResponse = await request(app).post('/user/login').send(loginUser); + + expect(loginResponse.status).toBe(400); + expect(loginResponse.body).toBeDefined(); + }); +}); diff --git a/src/__test__/logout.test.ts b/src/__test__/logout.test.ts index ac9eefa..2ae713c 100644 --- a/src/__test__/logout.test.ts +++ b/src/__test__/logout.test.ts @@ -72,4 +72,4 @@ describe('POST /user/logout', () => { expect(res.status).toBe(400); expect(res.body).toEqual({ Message: 'Access denied. You must be logged in' }); }); -}); +}); \ No newline at end of file diff --git a/src/__test__/product.entities.test.ts b/src/__test__/product.entities.test.ts new file mode 100644 index 0000000..5f9fc1d --- /dev/null +++ b/src/__test__/product.entities.test.ts @@ -0,0 +1,175 @@ +import { validate } from 'class-validator'; +import { Repository } from 'typeorm'; +import { Product } from '../entities/Product'; +import { User } from '../entities/User'; +import { Category } from '../entities/Category'; +import { Coupon } from '../entities/coupon'; +import { v4 as uuid } from 'uuid'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { dbConnection } from '../startups/dbConnection'; +import { server } from '../index'; + +// Sample data +const catId = uuid(); +const vendor3Id = uuid(); +const product1Id = uuid(); +const product2Id = uuid(); +const couponId1 = uuid(); +const couponId2 = uuid(); +const couponCode1 = 'DISCOUNT10'; +const couponCode2 = 'DISCOUNT20'; + +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 sampleVendor3 = new User(); +sampleVendor3.id = vendor3Id; +sampleVendor3.firstName = 'Vendor3'; +sampleVendor3.lastName = 'User'; +sampleVendor3.email = process.env.TEST_USER_EMAIL; +sampleVendor3.password = process.env.TEST_USER_PASS; +sampleVendor3.userType = 'Vendor'; +sampleVendor3.gender = 'Male'; +sampleVendor3.phoneNumber = '32638099634'; +sampleVendor3.photoUrl = 'https://example.com/photo.jpg'; +sampleVendor3.role = 'VENDOR'; + +const sampleProduct1 = new Product(); +sampleProduct1.id = product1Id; +sampleProduct1.name = 'Test Product 1'; +sampleProduct1.description = 'Amazing product 1'; +sampleProduct1.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct1.newPrice = 200; +sampleProduct1.quantity = 10; +sampleProduct1.vendor = sampleVendor3; + +const sampleProduct2 = new Product(); +sampleProduct2.id = product2Id; +sampleProduct2.name = 'Test Product 2'; +sampleProduct2.description = 'Amazing product 2'; +sampleProduct2.images = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg']; +sampleProduct2.newPrice = 250; +sampleProduct2.quantity = 15; +sampleProduct2.vendor = sampleVendor3; + +const sampleCoupon1 = new Coupon(); +sampleCoupon1.id = couponId1; +sampleCoupon1.code = couponCode1; +sampleCoupon1.discountRate = 20; +sampleCoupon1.expirationDate = new Date('2025-01-01'); +sampleCoupon1.maxUsageLimit = 100; +sampleCoupon1.discountType = 'percentage'; +sampleCoupon1.product = sampleProduct1; +sampleCoupon1.vendor = sampleVendor3; + +const sampleCoupon2 = new Coupon(); +sampleCoupon2.id = couponId2; +sampleCoupon2.code = couponCode2; +sampleCoupon2.discountRate = 15; +sampleCoupon2.expirationDate = new Date('2025-01-01'); +sampleCoupon2.maxUsageLimit = 50; +sampleCoupon2.discountType = 'percentage'; +sampleCoupon2.product = sampleProduct2; +sampleCoupon2.vendor = sampleVendor3; + +const sampleCat = { + id: catId, + name: 'accessories', +}; + +let productRepository: Repository; +let userRepository: Repository; +let categoryRepository: Repository; +let couponRepository: Repository; + +beforeAll(async () => { + const connection = await dbConnection(); + if (!connection) { + console.error('Failed to connect to the database'); + return; + } + + userRepository = connection.getRepository(User); + categoryRepository = connection.getRepository(Category); + couponRepository = connection.getRepository(Coupon); + productRepository = connection.getRepository(Product); + + await userRepository.save(sampleVendor3); + await categoryRepository.save(sampleCat); + + const category1 = categoryRepository.create({ name: 'Category 1' }); + const category2 = categoryRepository.create({ name: 'Category 2' }); + await categoryRepository.save([category1, category2]); + + sampleProduct1.categories = [category1]; + sampleProduct2.categories = [category2]; + await productRepository.save([sampleProduct1, sampleProduct2]); + await couponRepository.save([sampleCoupon1, sampleCoupon2]); +}); + +afterAll(async () => { + await cleanDatabase(); + const connection = await dbConnection(); + if (connection) { + await connection.close(); + } + server.close(); +}); + +describe('Product Entity', () => { + it('should create all entities related to product entity', async () => { + const product = await productRepository.save(sampleProduct2); + expect(product).toBeDefined(); + }); + + it('should not validate a product with missing required fields', async () => { + const product = new Product(); + const errors = await validate(product); + expect(errors.length).toBeGreaterThan(0); + }); + + it('should enforce array constraints on images', async () => { + const product = productRepository.create({ + id: uuid(), + vendor: sampleVendor3, + name: 'Sample Product', + description: 'This is a sample product', + images: [], + newPrice: 100.0, + quantity: 10, + isAvailable: true, + }); + + const errors = await validate(product); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].constraints?.arrayNotEmpty).toBeDefined(); + }); + + it('should handle relationships correctly', async () => { + const category1 = await categoryRepository.findOne({ where: { name: 'Category 1' } }); + const category2 = await categoryRepository.findOne({ where: { name: 'Category 2' } }); + + const product = productRepository.create({ + id: uuid(), + vendor: sampleVendor3, + name: 'Sample Product', + description: 'This is a sample product', + images: ['image1.jpg', 'image2.jpg'], + newPrice: 100.0, + quantity: 10, + isAvailable: true, + categories: [category1 as Category, category2 as Category], + }); + + await productRepository.save(product); + + const savedProduct = await productRepository.findOne({ + where: { id: product.id }, + relations: ['vendor', 'categories', 'coupons'], + }); + + expect(savedProduct).toBeDefined(); + expect(savedProduct?.vendor).toBeDefined(); + expect(savedProduct?.categories).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/src/__test__/roleCheck.test.ts b/src/__test__/roleCheck.test.ts index 32df044..37931bc 100644 --- a/src/__test__/roleCheck.test.ts +++ b/src/__test__/roleCheck.test.ts @@ -6,6 +6,8 @@ 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; @@ -36,6 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase(); + server.close(); }); describe('hasRole MiddleWare Test', () => { diff --git a/src/__test__/searchProduct.test.ts b/src/__test__/searchProduct.test.ts new file mode 100644 index 0000000..081427c --- /dev/null +++ b/src/__test__/searchProduct.test.ts @@ -0,0 +1,163 @@ +import { Product } from '../entities/Product'; +import { app, server } from '../index'; +import { dbConnection } from '../startups/dbConnection'; +import { User, UserInterface } from '../entities/User'; +import { v4 as uuid } from 'uuid'; +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 catId = uuid(); + +const sampleVendor1: UserInterface = { + id: vendor1Id, + firstName: 'vendor1o', + lastName: 'user', + email: 'vendor10@example.com', + password: 'password', + userType: 'Vendor', + gender: 'Male', + phoneNumber: '126380996348', + photoUrl: 'https://example.com/photo.jpg', + 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 = { + id: catId, + name: 'accessories', +} as Category; + +const sampleProduct1: Product = { + id: product1Id, + name: 'Product A', + description: 'Amazing product A', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 100, + quantity: 10, + vendor: sampleVendor1, + categories: [sampleCat], +} as Product; + +const sampleProduct2: Product = { + id: product2Id, + name: 'Product B', + description: 'Amazing product B', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 20, + 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, + categories: [sampleCat], +} as Product; + +beforeAll(async () => { + const connection = await dbConnection(); + + const categoryRepository = connection?.getRepository(Category); + await categoryRepository?.save(sampleCat); + + const userRepository = connection?.getRepository(User); + await userRepository?.save(sampleVendor1); + await userRepository?.save(sampleVendor2); + await userRepository?.save(sampleBuyer1); + + const productRepository = connection?.getRepository(Product); + await productRepository?.save(sampleProduct1); + await productRepository?.save(sampleProduct2); + await productRepository?.save(sampleProduct3); +}); + +afterAll(async () => { + await cleanDatabase(); + 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); + }); + + 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 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 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); + + const resultPage2 = await searchProductService({ page: 2, limit: 2 }); + expect(resultPage2.data.length).toBe(1); + expect(resultPage2.pagination.currentPage).toBe(2); + }); + + 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"); + + 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"); + }); +}); diff --git a/src/__test__/test-assets/DatabaseCleanup.ts b/src/__test__/test-assets/DatabaseCleanup.ts index b5fbb58..9d0fda6 100644 --- a/src/__test__/test-assets/DatabaseCleanup.ts +++ b/src/__test__/test-assets/DatabaseCleanup.ts @@ -53,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__/user.Route.test.ts b/src/__test__/user.Route.test.ts new file mode 100644 index 0000000..bb09559 --- /dev/null +++ b/src/__test__/user.Route.test.ts @@ -0,0 +1,33 @@ +// index.test.ts + +import request from 'supertest'; +import { app, server } from '../index'; +import { dbConnection } from '../startups/dbConnection'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; + +beforeAll(async () => { + await dbConnection(); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); +}); + +describe('USER ROUTE', () => { + it('should respond with 404, user not found', async () => { + const response = await request(app) + .get('/login/success') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(404); + }); + + it('Should respond 401, Login failed', async () => { + const response = await request(app) + .post('/login/failed') + .set('Content-Type', 'application/json'); + + expect(response.status).toBe(404); + }); +}); \ No newline at end of file diff --git a/src/__test__/user.entity.test.ts b/src/__test__/user.entity.test.ts new file mode 100644 index 0000000..7179131 --- /dev/null +++ b/src/__test__/user.entity.test.ts @@ -0,0 +1,277 @@ +import { validate } from 'class-validator'; +import { getConnection, Repository } from 'typeorm'; +import { User, UserInterface } from '../entities/User'; +import { Order } from '../entities/Order'; +import { Transaction } from '../entities/transaction'; +import { Product } from '../entities/Product'; +import { OrderItem } from '../entities/OrderItem'; +import { VendorOrderItem } from '../entities/VendorOrderItem'; +import { VendorOrders } from '../entities/vendorOrders'; +import { Category } from '../entities/Category'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import { server } from '../index'; +import { v4 as uuid } from 'uuid'; +import { dbConnection } from '../startups/dbConnection'; + +const adminId = uuid(); +const vendorId = uuid(); +const vendor2Id = uuid(); +const buyerId = uuid(); +const productId = uuid(); +const orderId = uuid(); +const order2Id = uuid(); +const orderItemId = uuid(); +const vendorOrderId = uuid(); +const vendorOrderItemId = uuid(); +const vendorOrder2Id = uuid(); +const catId = uuid(); + +if (!process.env.TEST_USER_EMAIL + || !process.env.TEST_BUYER_EMAIL + || !process.env.TEST_VENDOR1_EMAIL + || !process.env.TEST_VENDOR_EMAIL + || !process.env.TEST_USER_PASS) throw new Error('TEST_USER_PASS or TEST_USER_EMAIL not set in .env'); + +const sampleAdmin: UserInterface = { + id: adminId, + firstName: 'admin', + lastName: 'user', + email:process.env.TEST_USER_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'ADMIN', +}; +const sampleVendor: UserInterface = { + id: vendorId, + firstName: 'vendor', + lastName: 'user', + email:process.env.TEST_VENDOR_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Vendor', + gender: 'Male', + phoneNumber: '126380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleVendor2: UserInterface = { + id: vendor2Id, + firstName: 'vendor', + lastName: 'user', + email: process.env.TEST_VENDOR1_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Vendor', + gender: 'Male', + phoneNumber: '18090296347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'VENDOR', +}; +const sampleBuyer: UserInterface = { + id: buyerId, + firstName: 'buyer', + lastName: 'user', + email: process.env.TEST_BUYER_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Buyer', + gender: 'Male', + phoneNumber: '6380996347', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'BUYER', +}; +const sampleCat = { + id: catId, + name: 'accessories', +}; + +const sampleProduct = { + id: productId, + name: 'test product', + description: 'amazing product', + images: ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'], + newPrice: 200, + quantity: 10, + vendor: sampleVendor, + categories: [sampleCat], +}; +const sampleOrder = { + id: orderId, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: sampleBuyer, + orderStatus: 'received', + address: 'Rwanda, Kigali, KK20st', +}; +const sampleOrderItem = { + id: orderItemId, + price: 200, + quantity: 2, + order: sampleOrder, + product: sampleProduct, +}; +const sampleVendorOrder = { + id: vendorOrderId, + totalPrice: 400, + quantity: 2, + vendor: sampleVendor, + order: sampleOrder, + buyer: sampleBuyer, + orderStatus: 'pending', +}; +const sampleVendorOrderItem = { + id: vendorOrderItemId, + "price/unit": 200, + quantity: 2, + order: sampleVendorOrder, + product: sampleProduct, +}; + +let userRepository: Repository; +let orderRepository: Repository; +let transactionRepository: Repository; + +beforeAll(async () => { + const connection = await dbConnection(); + if (!connection) { + console.error('Failed to connect to the database'); + return; + } + + userRepository = connection.getRepository(User); + orderRepository = connection.getRepository(Order); + transactionRepository = connection.getRepository(Transaction); + + const categoryRepository = connection.getRepository(Category); + await categoryRepository.save(sampleCat); + + await userRepository.save([sampleAdmin, sampleVendor, sampleVendor2, sampleBuyer]); + + const productRepository = connection.getRepository(Product); + await productRepository.save(sampleProduct); + + await orderRepository.save(sampleOrder); + + const orderItemRepository = connection.getRepository(OrderItem); + await orderItemRepository.save(sampleOrderItem); + + const vendorOrderRepository = connection.getRepository(VendorOrders); + await vendorOrderRepository.save(sampleVendorOrder); + + const vendorOrderItemRepository = connection.getRepository(VendorOrderItem); + await vendorOrderItemRepository.save(sampleVendorOrderItem); +}); + +afterAll(async () => { + await cleanDatabase(); + const connection = getConnection(); + if (connection.isConnected) { + await connection.close(); + } + server.close(); +}); + +describe('User Entity', () => { + it('should validate a valid user', async () => { + const user = userRepository.create({ + id: uuid(), + firstName: 'John', + lastName: 'Doe', + email: process.env.TEST_SAMPLE_BUYER_EMAIL, + password: process.env.TEST_USER_PASS, + gender: 'male', + phoneNumber: '1234567890', + verified: true, + status: 'active', + userType: 'Buyer', + twoFactorEnabled: false, + accountBalance: 0.0, + }); + + const errors = await validate(user); + expect(errors.length).toBe(0); + + const savedUser = await userRepository.save(user); + expect(savedUser.id).toBeDefined(); + expect(savedUser.createdAt).toBeDefined(); + expect(savedUser.updatedAt).toBeDefined(); + }); + + it('should not validate a user with missing required fields', async () => { + const user = new User(); + + const errors = await validate(user); + expect(errors.length).toBeGreaterThan(0); + }); + + it('should set the role based on userType', async () => { + const user = userRepository.create({ + id: vendorOrder2Id, + firstName: 'Jane', + lastName: 'Doe', + email: process.env.TEST_VENDOR2_EMAIL, + password: process.env.TEST_USER_PASS, + gender: 'female', + phoneNumber: '0987654321', + userType: 'Vendor', + }); + + await userRepository.save(user); + expect(user.role).toBe('VENDOR'); + }); + + it('should handle relationships correctly', async () => { + const user = userRepository.create({ + id: uuid(), + firstName: 'Alice', + lastName: 'Smith', + email: 'alice.smith@example.com', + password: process.env.TEST_USER_PASS, + gender: 'female', + phoneNumber: '1122334455', + userType: 'Buyer', + }); + + const savedUser = await userRepository.save(user); + + const order = orderRepository.create({ + id: order2Id, + totalPrice: 400, + quantity: 2, + orderDate: new Date(), + buyer: savedUser, + orderStatus: 'order placed', + address: 'Rwanda, Kigali, KK20st', + }); + + const savedOrder = await orderRepository.save(order); + + const transaction = transactionRepository.create({ + id: uuid(), + order: savedOrder, + user: savedUser, + product: sampleProduct, + amount: 400, + previousBalance: 0, + currentBalance: 400, + type: 'credit', + description: 'order placed', + }); + + await transactionRepository.save(transaction); + + const foundUser = await userRepository.findOne({ + where: { id: savedUser.id }, + relations: ['orders', 'transactions'], + }); + + expect(foundUser).toBeDefined(); + expect(foundUser?.orders.length).toBe(1); + expect(foundUser?.transactions.length).toBe(1); + }); +}); \ No newline at end of file diff --git a/src/__test__/user.profile.update.service.test.ts b/src/__test__/user.profile.update.service.test.ts new file mode 100644 index 0000000..b9f2000 --- /dev/null +++ b/src/__test__/user.profile.update.service.test.ts @@ -0,0 +1,102 @@ +import { getConnection, getRepository, Repository } from 'typeorm'; +import { User, UserInterface } from '../entities/User'; +import { cleanDatabase } from './test-assets/DatabaseCleanup'; +import {app, server } from '../index'; +import { v4 as uuid } from 'uuid'; +import { dbConnection } from '../startups/dbConnection'; +import request from 'supertest'; +import jwt from 'jsonwebtoken'; + +const adminId = uuid(); + +const jwtSecretKey = process.env.JWT_SECRET || ''; + +const getAccessToken = (id: string, email: string) => { + return jwt.sign( + { + id: id, + email: email, + }, + jwtSecretKey + ); +}; + +if (!process.env.TEST_USER_EMAIL || !process.env.TEST_BUYER_EMAIL || !process.env.TEST_VENDOR1_EMAIL || !process.env.TEST_VENDOR_EMAIL || !process.env.TEST_USER_PASS) throw new Error('TEST_USER_PASS or TEST_USER_EMAIL not set in .env'); + +const sampleAdmin: UserInterface = { + id: adminId, + firstName: 'admin', + lastName: 'user', + email:process.env.TEST_USER_EMAIL, + password: process.env.TEST_USER_PASS, + userType: 'Admin', + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + verified: true, + role: 'ADMIN', +}; + + + +beforeAll(async () => { + const connection = await dbConnection(); + if (!connection) { + console.error('Failed to connect to the database'); + return; + } + + const userRepository = connection.getRepository(User); + await userRepository.save(sampleAdmin); +}); + +afterAll(async () => { + await cleanDatabase(); + server.close(); + }); + +describe('User profile update service', () => { + it('should validate a invalid user and return 400', async () => { + const res = await request(app) + .put('/user/update') + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`) + .send(); + + expect(res.statusCode).toBe(400); + }); + + it('should validate a valid user', async () => { + const res = await request(app) + .put('/user/update') + .set('Authorization', `Bearer ${getAccessToken(adminId, sampleAdmin.email)}`) + .send({ + firstName: 'admin', + lastName: 'user', + email: process.env.TEST_USER_EMAIL, + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + id: sampleAdmin.id, + }); + + expect(res.statusCode).toBe(201); +}); + +it('should return 403 if user not authorized', async () => { + const fakeID = uuid(); + + const res = await request(app) + .put('/user/update') + .send({ + firstName: 'admin', + lastName: 'user', + email: process.env.TEST_USER_EMAIL, + gender: 'Male', + phoneNumber: '126380997', + photoUrl: 'https://example.com/photo.jpg', + id: fakeID, + }); + + expect(res.statusCode).toBe(403); +}); +}); \ No newline at end of file diff --git a/src/__test__/userStatus.test.ts b/src/__test__/userStatus.test.ts index 69e892a..a2e0732 100644 --- a/src/__test__/userStatus.test.ts +++ b/src/__test__/userStatus.test.ts @@ -68,7 +68,7 @@ describe('POST /user/deactivate', () => { .send({ email: `${testUser.email}` }); expect(response.status).toBe(200); expect(response.body.message).toBe('User deactivated successfully'); - }, 10000); + }, 60000); it('should return 404 when email is not submitted', async () => { const token = jwt.sign(data, jwtSecretKey); @@ -111,7 +111,7 @@ describe('POST /user/activate', () => { expect(response.status).toBe(200); expect(response.body.message).toBe('User activated successfully'); - }, 10000); + }, 60000); it('should return 404 when email is not submitted', async () => { const token = jwt.sign(data, jwtSecretKey); @@ -148,4 +148,4 @@ describe('POST /user/activate', () => { expect(response.status).toBe(404); expect(response.body.error).toBe('User not found'); }); -}); +}); \ No newline at end of file diff --git a/src/__test__/vendorProduct.test.ts b/src/__test__/vendorProduct.test.ts index d8fc0a5..f0a1450 100644 --- a/src/__test__/vendorProduct.test.ts +++ b/src/__test__/vendorProduct.test.ts @@ -133,7 +133,7 @@ describe('Vendor product management tests', () => { expect(response.status).toBe(201); expect(response.body.data.product).toBeDefined; - }, 60000); + }, 120000); it('return an error if the number of product images exceeds 6', async () => { const response = await request(app) @@ -470,4 +470,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/orderController.ts b/src/controllers/orderController.ts index 7a877bb..f48a4a9 100644 --- a/src/controllers/orderController.ts +++ b/src/controllers/orderController.ts @@ -19,4 +19,4 @@ export const updateOrder = async (req: Request, res: Response) => { }; export const getOrdersHistory = async (req: Request, res: Response) => { await getTransactionHistoryService(req, res); -}; \ No newline at end of file +}; diff --git a/src/controllers/productController.ts b/src/controllers/productController.ts index 05aa5a3..c68e16b 100644 --- a/src/controllers/productController.ts +++ b/src/controllers/productController.ts @@ -73,4 +73,4 @@ export const searchProduct = async (req: Request, res: Response) => { }; export const Payment = async (req: Request, res: Response) => { await confirmPayment(req, res); -}; +}; \ No newline at end of file diff --git a/src/entities/Order.ts b/src/entities/Order.ts index faa19db..529294c 100644 --- a/src/entities/Order.ts +++ b/src/entities/Order.ts @@ -70,4 +70,4 @@ export class Order { @UpdateDateColumn() updatedAt!: Date; -} +} \ No newline at end of file diff --git a/src/entities/Product.ts b/src/entities/Product.ts index ae027ef..ce7f139 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 232ce11..caee2f5 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -35,6 +35,12 @@ export interface UserInterface { @Entity() @Unique(['email']) export class User { + static lastName(lastName: any) { + throw new Error('Method not implemented.'); + } + static firstName(firstName: any) { + throw new Error('Method not implemented.'); + } @PrimaryGeneratedColumn('uuid') @IsNotEmpty() id!: string; @@ -119,4 +125,4 @@ export class User { setRole (): void { this.role = this.userType === 'Vendor' ? roles.vendor : roles.buyer; } -} +} \ No newline at end of file diff --git a/src/entities/VendorOrderItem.ts b/src/entities/VendorOrderItem.ts index fc8b3dc..9137f6d 100644 --- a/src/entities/VendorOrderItem.ts +++ b/src/entities/VendorOrderItem.ts @@ -10,11 +10,11 @@ export class VendorOrderItem { @IsNotEmpty() 'id'!: string; - @ManyToOne(() => VendorOrders, order => order.vendorOrderItems, {onDelete: 'CASCADE'}) + @ManyToOne(() => VendorOrders, order => order.vendorOrderItems) @IsNotEmpty() 'order'!: VendorOrders; - @ManyToOne(() => Product, product => product.vendorOrderItems, {onDelete: 'CASCADE'}) + @ManyToOne(() => Product, product => product.vendorOrderItems) @IsNotEmpty() 'product'!: Product; diff --git a/src/entities/transaction.ts b/src/entities/transaction.ts index d475812..0f7b0ea 100644 --- a/src/entities/transaction.ts +++ b/src/entities/transaction.ts @@ -58,4 +58,4 @@ export class Transaction { @UpdateDateColumn() updatedAt!: Date; -} \ No newline at end of file +} diff --git a/src/helper/couponValidator.ts b/src/helper/couponValidator.ts index 9736aa8..db426ad 100644 --- a/src/helper/couponValidator.ts +++ b/src/helper/couponValidator.ts @@ -42,6 +42,9 @@ export const validateCouponUpdate = ( expirationDate: Joi.date().messages({ 'date.base': 'expirationDate must be a valid date.', }), + product: Joi.string().messages({ + 'string.base': 'product must be a string.', + }), maxUsageLimit: Joi.number().messages({ 'number.base': 'maxUsageLimit must be a number.', }), diff --git a/src/helper/verify.ts b/src/helper/verify.ts index bda6a00..fca0702 100644 --- a/src/helper/verify.ts +++ b/src/helper/verify.ts @@ -13,7 +13,7 @@ export const verifiedToken = (token: string): any => { try { return jwt.verify(token, jwtSecretKey); } catch (err) { - console.error(err); + console.log(err); return null; } }; diff --git a/src/routes/ProductRoutes.ts b/src/routes/ProductRoutes.ts index b2af1a5..5aca68f 100644 --- a/src/routes/ProductRoutes.ts +++ b/src/routes/ProductRoutes.ts @@ -54,6 +54,6 @@ 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; +export default router; \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index af462b4..10588ac 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,13 +1,14 @@ -import { Request, Response, Router } from 'express'; -import { responseSuccess } from '../utils/response.utils'; +import { Request, RequestHandler, Response, Router } from 'express'; +import { responseServerError, responseSuccess } from '../utils/response.utils'; import userRoutes from './UserRoutes'; import productRoutes from './ProductRoutes'; import wishListRoutes from './wishListRoute'; import couponRoute from './couponRoutes'; import cartRoutes from './CartRoutes'; -import feedbackRoute from './feedbackRoutes'; import notificationRoute from './NoficationRoutes' +import { authMiddleware } from '../middlewares/verifyToken'; import chatBot from './chatBot'; +import feedbackRoute from './feedbackRoutes'; const router = Router(); @@ -21,7 +22,40 @@ router.use('/wish-list', wishListRoutes); router.use('/cart', cartRoutes); router.use('/coupons', couponRoute); router.use('/feedback', feedbackRoute); + +// ROUTES FOR TESTING PURPOSE +router.get('/test', (req: Request, res: Response) => { + res.status(200).json({ message: 'Route works!' }); +}); +router.post('/test/posting', (req: Request, res: Response) =>{ + return responseSuccess(res, 200, req.body); +}); + +router.get('/test/secure', authMiddleware as RequestHandler, (req: Request, res: Response) =>{ + responseSuccess(res, 200, 'This is a secured route.'); +}); + +router.get('/test/error', (req: Request, res: Response) => { + responseServerError(res, 'This is server error route.'); +}); +router.use('/feedback', feedbackRoute); router.use('/notification', notificationRoute); router.use('/chat', chatBot); -export default router; +// ROUTES FOR TESTING PURPOSE +router.get('/test', (req: Request, res: Response) => { + res.status(200).json({ message: 'Route works!' }); +}); +router.post('/test/posting', (req: Request, res: Response) =>{ + return responseSuccess(res, 200, req.body); +}); + +router.get('/test/secure', authMiddleware as RequestHandler, (req: Request, res: Response) =>{ + responseSuccess(res, 200, 'This is a secured route.'); +}); + +router.get('/test/error', (req: Request, res: Response) => { + responseServerError(res, 'This is server error route.'); +}); + +export default router; \ No newline at end of file diff --git a/src/services/adminOrderServices/updateOrder.ts b/src/services/adminOrderServices/updateOrder.ts index b6cdb2e..876160f 100644 --- a/src/services/adminOrderServices/updateOrder.ts +++ b/src/services/adminOrderServices/updateOrder.ts @@ -2,11 +2,9 @@ import { Request, Response } from 'express'; import { Not, getRepository } from 'typeorm'; import { responseSuccess, responseError } from '../../utils/response.utils'; import { VendorOrderItem } from '../../entities/VendorOrderItem'; -import { sendNotification } from '../../utils/sendNotification'; import { VendorOrders } from '../../entities/vendorOrders'; import { Order } from '../../entities/Order'; import { getIO } from '../../utils/socket'; -import { getNotifications } from '../../utils/getNotifications'; export const updateBuyerVendorOrderService = async (req: Request, res: Response) => { try { @@ -40,7 +38,7 @@ export const updateBuyerVendorOrderService = async (req: Request, res: Response) id: order.id, }, }, - relations: ['vendor','order.buyer','vendorOrderItems', 'vendorOrderItems.product'], + relations: ['vendor', 'vendorOrderItems', 'vendorOrderItems.product'], }); for (const order of vendorOrders) { @@ -54,23 +52,9 @@ export const updateBuyerVendorOrderService = async (req: Request, res: Response) order.orderStatus = 'completed'; await orderRepository.save(order); - await sendNotification({ - content: 'Your order was marked completed', - type: 'order', - user: order.buyer, - link: `/product/client/orders/${order.id}` - }); - const updatedVendorOrder = vendorOrders.map(async order => { order.orderStatus = 'completed'; await vendorOrderRepository.save(order); - - await sendNotification({ - content:`Order from buyer "${order.order.buyer.firstName} ${order.order.buyer.lastName}" has been marked completed`, - type: 'order', - user: order.vendor, - link: `/product/vendor/orders/${order.id}` - }); }); const sanitizedOrderResponse = { diff --git a/src/services/cartServices/readCart.ts b/src/services/cartServices/readCart.ts index 71d7a4d..a891768 100644 --- a/src/services/cartServices/readCart.ts +++ b/src/services/cartServices/readCart.ts @@ -55,4 +55,4 @@ export const readCartService = async (req: Request, res: Response) => { responseError(res, 400, (error as Error).message); return; } -}; +}; \ No newline at end of file diff --git a/src/services/couponServices/buyerApplyCoupon.ts b/src/services/couponServices/buyerApplyCoupon.ts index 93fa208..16652c3 100644 --- a/src/services/couponServices/buyerApplyCoupon.ts +++ b/src/services/couponServices/buyerApplyCoupon.ts @@ -70,6 +70,8 @@ export const buyerApplyCouponService = async (req: Request, res: Response) => { await cartRepository.save(cart); coupon.usageTimes += 1; + coupon.usageTimes += 1; + if (req.user?.id) { coupon.usedBy.push(req.user?.id); } diff --git a/src/services/couponServices/createCouponService.ts b/src/services/couponServices/createCouponService.ts index 592e462..a824ddf 100644 --- a/src/services/couponServices/createCouponService.ts +++ b/src/services/couponServices/createCouponService.ts @@ -10,6 +10,7 @@ export const createCouponService = async (req: Request, res: Response) => { try { const { error } = validateCoupon(req.body); if (error) { + console.log('Validation Error creating coupon:\n', error); return res.status(400).json({ status: 'error', error: error?.details[0].message }); } diff --git a/src/services/couponServices/updateService.ts b/src/services/couponServices/updateService.ts index 26aeef6..bc71337 100644 --- a/src/services/couponServices/updateService.ts +++ b/src/services/couponServices/updateService.ts @@ -10,6 +10,7 @@ export const updateCouponService = async (req: Request, res: Response) => { const { code } = req.params; const { error } = validateCouponUpdate(req.body); if (error) { + console.log(error); return res.status(400).json({ status: 'error', error: error?.details[0].message }); } diff --git a/src/services/feedbackServices/adminDeleteFeedback.ts b/src/services/feedbackServices/adminDeleteFeedback.ts index 7bf6261..e206284 100644 --- a/src/services/feedbackServices/adminDeleteFeedback.ts +++ b/src/services/feedbackServices/adminDeleteFeedback.ts @@ -20,6 +20,7 @@ export const adminDeleteFeedbackService = async (req: Request, res: Response) => return responseSuccess(res, 200, 'Feedback successfully removed'); } catch (error) { + console.log(error) return responseError(res, 500, 'Server error'); } }; diff --git a/src/services/index.ts b/src/services/index.ts index f31e750..a592d02 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -21,7 +21,7 @@ export * from './productServices/listAllProductsService'; export * from './productServices/productStatus'; export * from './productServices/viewSingleProduct'; export * from './productServices/searchProduct'; -export * from './productServices/payment'; +export * from './productServices/payment' // Buyer wishlist services export * from './wishListServices/addProduct'; diff --git a/src/services/notificationServices/deleteNotification.ts b/src/services/notificationServices/deleteNotification.ts index cc3c295..1ef3c04 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/orderServices/getOrderService.ts b/src/services/orderServices/getOrderService.ts index 4006478..17a29a8 100644 --- a/src/services/orderServices/getOrderService.ts +++ b/src/services/orderServices/getOrderService.ts @@ -116,4 +116,4 @@ export const getOrderService = async (req: Request, res: Response) => { } catch (error) { return responseError(res, 400, (error as Error).message); } -}; +}; \ No newline at end of file diff --git a/src/services/orderServices/updateOrderService.ts b/src/services/orderServices/updateOrderService.ts index d10f5d3..482e0f3 100644 --- a/src/services/orderServices/updateOrderService.ts +++ b/src/services/orderServices/updateOrderService.ts @@ -6,7 +6,7 @@ import { User } from '../../entities/User'; import { OrderItem } from '../../entities/OrderItem'; import { Transaction } from '../../entities/transaction'; import { responseError, sendErrorResponse, sendSuccessResponse } from '../../utils/response.utils'; -import sendMail from '../../utils/sendOrderMail'; +import sendMail from '../../utils/sendOrderMailUpdated'; import { sendNotification } from '../../utils/sendNotification'; import { VendorOrders } from '../../entities/vendorOrders'; interface OrderStatusType { @@ -167,4 +167,4 @@ async function processRefund (order: Order, entityManager: EntityManager) { function isOrderFinalStatus (status: string): boolean { return ['cancelled', 'delivered', 'returned', 'completed'].includes(status); -} +} \ No newline at end of file diff --git a/src/services/productServices/createProduct.ts b/src/services/productServices/createProduct.ts index 668ddd2..44918d8 100644 --- a/src/services/productServices/createProduct.ts +++ b/src/services/productServices/createProduct.ts @@ -101,4 +101,4 @@ export const createProductService = async (req: Request, res: Response) => { } catch (error) { res.status(400).json({ message: (error as Error).message }); } -}; +}; \ No newline at end of file diff --git a/src/services/productServices/listAllProductsService.ts b/src/services/productServices/listAllProductsService.ts index 4429e89..e9fa0ee 100644 --- a/src/services/productServices/listAllProductsService.ts +++ b/src/services/productServices/listAllProductsService.ts @@ -33,6 +33,9 @@ export const listAllProductsService = async (req: Request, res: Response) => { }, }); + if (products.length < 1) { + return responseSuccess(res, 200, 'No products found'); + } if (products.length < 1) { return responseSuccess(res, 200, 'No products found'); } @@ -41,4 +44,4 @@ export const listAllProductsService = async (req: Request, res: Response) => { } catch (error) { responseError(res, 400, (error as Error).message); } -}; +}; \ No newline at end of file diff --git a/src/services/productServices/readProduct.ts b/src/services/productServices/readProduct.ts index 2836b21..77896ce 100644 --- a/src/services/productServices/readProduct.ts +++ b/src/services/productServices/readProduct.ts @@ -75,4 +75,4 @@ export const readProductService = async (req: Request, res: Response) => { } catch (error) { responseError(res, 400, (error as Error).message); } -}; +}; \ No newline at end of file diff --git a/src/services/productServices/viewSingleProduct.ts b/src/services/productServices/viewSingleProduct.ts index be9764d..6f49532 100644 --- a/src/services/productServices/viewSingleProduct.ts +++ b/src/services/productServices/viewSingleProduct.ts @@ -28,4 +28,4 @@ export const viewSingleProduct = async (req: Request, res: Response) => { console.error('Error handling request:', error); res.status(500).send('Error fetching product details'); } -}; +}; \ No newline at end of file diff --git a/src/utils/sendOrderMailUpdated.ts b/src/utils/sendOrderMailUpdated.ts index adddc9a..efcdcd2 100644 --- a/src/utils/sendOrderMailUpdated.ts +++ b/src/utils/sendOrderMailUpdated.ts @@ -18,7 +18,7 @@ interface Message { address: string; } -const sendMail = async (message: Message) => { +const sendOrderMailUpdated = async (message: Message) => { const transporter = nodemailer.createTransport({ host: process.env.HOST, port: 587, @@ -212,4 +212,4 @@ const sendMail = async (message: Message) => { } }; -export default sendMail; +export default sendOrderMailUpdated; diff --git a/tsconfig.json b/tsconfig.json index 7e48aed..ab7bfea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,7 +33,8 @@ "node", "jest", "express", - "node-nlp" + "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. */