diff --git a/package-lock.json b/package-lock.json index aa3d203..51e5731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/node-fetch": "^2.6.11", "@types/passport-google-oauth20": "^2.0.14", "axios": "^1.6.8", "bcrypt": "^5.1.1", @@ -26,7 +27,6 @@ "jsonwebtoken": "^9.0.2", "mailgun-js": "^0.22.0", "morgan": "^1.10.0", - "nock": "^13.5.4", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -65,6 +65,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.2.5", "ts-jest": "^29.1.4", "ts-node-dev": "^2.0.0", @@ -1418,6 +1419,25 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1879,6 +1899,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.15", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", @@ -3441,6 +3470,35 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", + "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4648,6 +4706,28 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4828,6 +4908,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", @@ -6257,6 +6348,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -7233,23 +7334,47 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" } }, "node_modules/node-int64": { @@ -8206,6 +8331,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true + }, "node_modules/promisify-call": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", @@ -10134,6 +10265,14 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 27a4093..1acc5e7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "homepage": "https://github.com/atlp-rwanda/dynamites-ecomm-be#readme", "dependencies": { + "@types/node-fetch": "^2.6.11", "@types/passport-google-oauth20": "^2.0.14", "axios": "^1.6.8", "bcrypt": "^5.1.1", @@ -42,6 +43,7 @@ "jsonwebtoken": "^9.0.2", "mailgun-js": "^0.22.0", "morgan": "^1.10.0", + "node-fetch": "^3.3.2", "nodemailer": "^6.9.13", "nodemon": "^3.1.0", "otplib": "^12.0.1", @@ -103,6 +105,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "prettier": "^3.2.5", "ts-jest": "^29.1.2", "ts-node-dev": "^2.0.0", diff --git a/src/__test__/Momo.test.ts b/src/__test__/Momo.test.ts new file mode 100644 index 0000000..c4460bd --- /dev/null +++ b/src/__test__/Momo.test.ts @@ -0,0 +1,456 @@ +import request from 'supertest'; +import express from 'express'; +import bodyParser from 'body-parser'; +import app from '../app'; +import { + MomohandlePayment, + checkPaymentStatus, + purchaseAccessToken, + requestToPay, + requestToPayStatus, + validateMomo, +} from '../controller/buyerController'; +import { getBuyerToken } from './testSetup'; +import dbConnection from '../database'; +import { Order } from '../database/models/orderEntity'; +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +let token: string; +let order: Order; +app.post('/buyer/momoPay', (req, res) => { + MomohandlePayment(req, res); +}); + +app.post('/buyer/getPaymentStatus/:id', (req, res) => { + checkPaymentStatus(req, res); +}); +beforeAll(async () => { + await dbConnection.initialize(); + await dbConnection.synchronize(true); // This will drop all tables + token = await getBuyerToken(); + // Create a mock order in the database + const orderRepository = dbConnection.getRepository(Order); + order = orderRepository.create({ + totalAmount: 100, + status: 'Pending', + trackingNumber: '123456', + paid: false, + }); + await orderRepository.save(order); +}); + +// Type definitions +type Idata = { + access_token: string; + token_type: string; + expires_in: string; +}; + +type IStatus = { + amount: string; + currency: string; + externalId: string; + payer: object; + payerMessage: string; + payeeNote: string; + status: string; + reason?: object; +}; + +type Ivalidate = { + result: boolean; +}; + +// Mock data +const mockToken: Idata = { + access_token: 'mockToken', + token_type: 'Bearer', + expires_in: '3600', +}; + +const mockStatus: IStatus = { + amount: '100', + currency: 'USD', + externalId: 'mockExternalId', + payer: {}, + payerMessage: 'Payment for order', + payeeNote: 'Thank you for your purchase', + status: 'Success', + reason: {}, +}; + +const mockInvalidation: Ivalidate = { + result: true, +}; + +afterEach(() => { + jest.clearAllMocks(); +}); + +const tokenUrl = process.env.TokenUrl as string; +const subscriptionKey = process.env.subscriptionKey as string; +const requesttoPayUrl = process.env.RequestToPayUrl as string; +const targetEnv = process.env.TargetEnv as string; + +// Mocking fetch for purchaseAccessToken +global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + access_token: 'testToken', + token_type: 'Bearer', + expires_in: '3600', + }), +}) as jest.Mock; + +// Test suite for Momo functions +describe('Test Momo Functions', () => { + describe('purchaseAccessToken', () => { + const mockAccessToken = 'testToken'; + const tokenUrl = 'https://example.com/token'; + const subscriptionKey = 'test_subscription_key'; + const XRefId = 'test_xref_id'; + const targetEnv = 'test_env'; + + beforeEach(() => { + // Clear all instances and calls to constructor and all methods: + (fetch as jest.Mock).mockClear(); + }); + + it('successfully fetches an access token', async () => { + // Define the mock response for fetch + (fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: async () => ({ + access_token: mockAccessToken, + token_type: 'Bearer', + expires_in: '3600', + }), + }); + + const accessToken = 'testToken'; + (purchaseAccessToken as jest.Mock).mockResolvedValue(mockAccessToken); + + // Check the result + expect(accessToken).toBe(mockAccessToken); + + // Ensure fetch was called with the correct URL and headers + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(tokenUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + 'Content-Type': 'application/json', + }, + }); + }); + + it('handles fetch failure', async () => { + // Define the mock response for fetch to simulate a failed request + (fetch as jest.Mock).mockResolvedValue({ + ok: false, + statusText: 'Internal Server Error', + }); + + // Call the function + const accessToken = await purchaseAccessToken(); + + // Check the result + expect(accessToken).toBeNull(); + + // Ensure fetch was called with the correct URL and headers + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(tokenUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + 'Content-Type': 'application/json', + }, + }); + }); + + it('handles fetch throwing an error', async () => { + // Define the mock to throw an error + const mockError = new Error('Failed to fetch'); + (fetch as jest.Mock).mockRejectedValue(mockError); + + // Call the function and handle the error + try { + await purchaseAccessToken(); + } catch (error) { + expect(error).toBe(mockError); + } + + // Ensure fetch was called with the correct URL and headers + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(tokenUrl, { + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Reference-Id': XRefId, + 'Content-Type': 'application/json', + }, + }); + }); + }); + + describe('requestToPay', () => { + it('successfully sends payment request', async () => { + const response = await requestToPay( + mockToken.access_token, + 'mockXrefid', + mockStatus.externalId, + mockStatus.currency, + mockStatus.amount, + 'mockNumber', + mockStatus.payerMessage, + mockStatus.payeeNote + ); + + // Expecting the entire Response object including ok and json method + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + requesttoPayUrl, + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${mockToken.access_token}`, + 'X-Target-Environment': targetEnv, + 'X-Reference-Id': 'mockXrefid', + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + amount: mockStatus.amount, + currency: mockStatus.currency, + externalId: mockStatus.externalId, + payer: { + partyIdType: 'MSISDN', + partyId: 'mockNumber', + }, + payerMessage: mockStatus.payerMessage, + payeeNote: mockStatus.payeeNote, + }), + }) + ); + }); + }); + + describe('requestToPayStatus', () => { + it('successfully fetches the payment status', async () => { + const mockId = 'mockId'; + const mockToken = 'mockToken'; + const mockResponse = { + amount: '100', + currency: 'USD', + externalId: 'mockExternalId', + payer: {}, + payerMessage: 'Payment for order', + payeeNote: 'Thank you for your purchase', + status: 'Success', + reason: {}, + }; + + // Mock the fetch response + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse), + }) as jest.Mock; + + const response = await requestToPayStatus(mockId, mockToken); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + `${requesttoPayUrl}/${mockId}`, + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Ocp-Apim-Subscription-Key': subscriptionKey, + Authorization: `Bearer ${mockToken}`, + 'X-Target-Environment': targetEnv, + }), + }) + ); + }); + }); + + describe('validateMomo', () => { + it('successfully validates the Momo account', async () => { + const mockToken = 'mockToken'; + const mockMomoAccount = 'mockMomoAccount'; + const mockResponse = { result: true }; + + // Mock the fetch response + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse), + }) as jest.Mock; + + const response = await validateMomo(mockToken, mockMomoAccount); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + `https://sandbox.momodeveloper.mtn.com/collection/v1_0/accountholder/msisdn/${mockMomoAccount}/active`, + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + Authorization: `Bearer ${mockToken}`, + 'Ocp-Apim-Subscription-Key': subscriptionKey, + 'X-Target-Environment': targetEnv, + }), + }) + ); + + expect(response).toBe(true); + }); + }); + + const mockOrder = { + id: 1, + totalAmount: 100, + paid: false, + }; + + const mockOrderPaid = { + id: 1, + totalAmount: 100, + paid: true, + }; + const mockRequestId = 'mockRequestId'; + const mockResponse = { ok: true }; + + describe('MomohandlePayment', () => { + it('successfully handles payment', async () => { + (validateMomo as jest.Mock) = jest.fn().mockResolvedValue(true); + (requestToPay as jest.Mock) = jest.fn().mockResolvedValue(mockResponse); + (requestToPayStatus as jest.Mock) = jest + .fn() + .mockResolvedValue(mockStatus); + const res = await request(app) + .post('/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ order, momoNumber: '1234567890' }); + + expect(res.status).toBe(202); + expect(res.body).toEqual({ + message: 'Transaction Accepted', + requestId: expect.any(String), + }); + + expect(purchaseAccessToken).toHaveBeenCalledTimes(1); + expect(validateMomo).toHaveBeenCalledWith(mockToken, '1234567890'); + expect(requestToPay).toHaveBeenCalledWith( + mockToken, + expect.any(String), + expect.any(String), + 'EUR', + mockOrder.totalAmount.toString(), + '1234567890', + `paid by 1234567890`, + `paid to 1234567890` + ); + }); + + it('returns 400 if Momo number is invalid', async () => { + (validateMomo as jest.Mock).mockResolvedValueOnce(false); + + const res = await request(app) + .post('/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ orderId: mockOrder.id, momoNumber: 'invalidNumber' }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ message: 'Your Momo Number does not Exist' }); + }); + + it('returns 404 if order not found', async () => { + const res = await request(app) + .post('/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ order, momoNumber: '1234567890' }); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ success: false, message: 'Order not found' }); + }); + + it('returns 400 if order is already paid', async () => { + const res = await request(app) + .post('/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ order, momoNumber: '1234567890' }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + success: false, + message: 'Order has already been paid', + }); + }); + + it('returns 400 if transaction fails', async () => { + (requestToPay as jest.Mock).mockResolvedValueOnce({ ok: false }); + + const res = await request(app) + .post('/buyer/momoPay') + .set('Authorization', `Bearer ${token}`) + .send({ order, momoNumber: '1234567890' }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ message: 'Transaction Fail' }); + }); + }); + + describe('checkPaymentStatus', () => { + it('successfully checks payment status', async () => { + const mockStatus = { status: 'SUCCESSFUL', reason: {} }; + (requestToPayStatus as jest.Mock).mockResolvedValue(mockStatus); + + const res = await request(app) + .post(`/buyer/getPaymentStatus/${mockOrder.id}`) + .set('Authorization', `Bearer ${token}`) + .send({ requestId: mockRequestId }); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + success: true, + message: 'Transaction Done Successfully', + }); + + expect(requestToPayStatus).toHaveBeenCalledWith(mockRequestId, mockToken); + expect(requestToPayStatus).toHaveBeenCalledWith( + expect.objectContaining({ paid: true }) + ); + }); + + it('returns 404 if order not found', async () => { + const res = await request(app) + .post(`/buyer/getPaymentStatus/999`) + .set('Authorization', `Bearer ${token}`) + .send({ requestId: mockRequestId }); + + expect(res.status).toBe(404); + expect(res.body).toEqual({ success: false, message: 'Order not found' }); + }); + + it('returns 400 if transaction fails', async () => { + const mockStatus = { + status: 'FAILED', + reason: { message: 'Insufficient funds' }, + }; + (requestToPayStatus as jest.Mock).mockResolvedValue(mockStatus); + + const res = await request(app) + .post(`/buyer/getPaymentStatus/${mockOrder.id}`) + .set('Authorization', `Bearer ${token}`) + .send({ requestId: mockRequestId }); + + expect(res.status).toBe(400); + expect(res.body).toEqual({ + success: false, + message: 'Transaction failed', + reason: mockStatus.reason, + }); + }); + }); +}); diff --git a/src/__test__/buyerWishlist.test.ts b/src/__test__/buyerWishlist.test.ts index 608ed7a..f541597 100644 --- a/src/__test__/buyerWishlist.test.ts +++ b/src/__test__/buyerWishlist.test.ts @@ -69,6 +69,27 @@ describe('POST /api/v1/buyer/addItemToWishList', () => { expect(res.statusCode).toEqual(201); expect(res.body.message).toContain('Wishlist successfully created'); }); + + it('should not allow adding an item already in the wishlist', async () => { + await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + time: '2024-05-21T12:00:00Z', + }); + + const res = await request(app) + .post('/api/v1/buyer/addItemToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: productId, + time: '2024-05-21T12:00:00Z', + }); + + expect(res.statusCode).toEqual(409); + expect(res.body.message).toContain('Product is already in the wishlist'); + }); }); describe('DELETE /api/v1/buyer/removeToWishList', () => { @@ -96,15 +117,55 @@ describe('GET /api/v1/buyer/getWishList', () => { expect(res.statusCode).toEqual(200); expect(res.body.message).toContain('Data retrieved successfully'); }); +}); +describe('GET /api/v1/buyer/getOneWishList', () => { + it('should get all wishlists', async () => { + const res = await request(app) + .get('/api/v1/buyer/getOneWishList') + .set('Authorization', `Bearer ${buyerToken}`); - describe('GET /api/v1/buyer/getOneWishList', () => { - it('should get all wishlists', async () => { - const res = await request(app) - .get('/api/v1/buyer/getOneWishList') - .set('Authorization', `Bearer ${buyerToken}`); + expect(res.statusCode).toEqual(200); + expect(res.body.message).toContain('Data retrieved successfully'); + }); +}); + +describe('RemoveProductFromWishList', () => { + it('should return an error when the wishlist or product is not found', async () => { + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: 9999, + }); + + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); + }); +}); + +describe('GET /api/v1/buyer/getOneWishList', () => { + const invalidToken = 'invalid Token'; + it('should return an error when the token is invalid', async () => { + const res = await request(app) + .get('/api/v1/buyer/getOneWishList') + .set('Authorization', `Bearer ${invalidToken}`); + + expect(res.statusCode).toEqual(401); + expect(res.body.message).toContain('Unauthorized: Invalid token'); + }); +}); + +describe('Removing a product from the wishlist', () => { + it('should respond with 404 if the product is not in the wishlist', async () => { + const NonExistingProductId = 'nonexistentProductId'; + const res = await request(app) + .delete('/api/v1/buyer/removeToWishList') + .set('Authorization', `Bearer ${buyerToken}`) + .send({ + productId: NonExistingProductId, + }); - expect(res.statusCode).toEqual(200); - expect(res.body.message).toContain('Data retrieved successfully'); - }); + expect(res.statusCode).toEqual(404); + expect(res.body.message).toContain('Product not found in wishlist'); }); }); diff --git a/src/__test__/coupon.test.ts b/src/__test__/coupon.test.ts index b6d8a61..f1dd8ca 100644 --- a/src/__test__/coupon.test.ts +++ b/src/__test__/coupon.test.ts @@ -2,331 +2,340 @@ import request from 'supertest'; import app from '../app'; import { getVendorToken, afterAllHook, beforeAllHook } from './testSetup'; - beforeAll(beforeAllHook); afterAll(afterAllHook); - describe('Coupon Controller Tests', () => { - let token: string; - let couponId: number; - let productId: number; - - beforeAll(async () => { - token = await getVendorToken(); - }); - - it('should create a new coupon with valid data', async () => { - const categoryData = { - name: 'Category', - description: 'category description', - }; - - const categoryResponse = await request(app) - .post('/api/v1/category') - .set('Authorization', `Bearer ${token}`) - .send(categoryData); - - const categoryId = categoryResponse.body.data.id; - const productData = { - name: 'New Product', - image: 'new_product.jpg', - gallery: [], - shortDesc: 'This is a new product', - longDesc: 'Detailed description of the new product', - categoryId: categoryId, - quantity: 10, - regularPrice: 5, - salesPrice: 4, - tags: ['tag1', 'tag2'], - type: 'Simple', - isAvailable: true, - }; - - const productResponse = await request(app) - .post('/api/v1/product') - .set('Authorization', `Bearer ${token}`) - .send(productData); - - expect(productResponse.statusCode).toEqual(201); - expect(productResponse.body.message).toEqual('Product successfully created'); - expect(productResponse.body.data).toBeDefined(); - productId = productResponse.body.data.id; - - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(201); - expect(response.body.message).toEqual('Coupon created successfully'); - expect(response.body.data).toBeDefined(); - couponId = response.body.data.id; - }); - - it('should return validation errors for invalid coupon data', async () => { - const invalidCouponData = { - description: '', - percentage: 120, - expirationDate: '2022-12-31', - applicableProducts: [], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(invalidCouponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.errors).toBeDefined(); - }); - - it ('should return a 404 for a non-existent product', async () => { - const nonExistentProductId = 999; - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [nonExistentProductId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.error).toEqual(`Product with id ${nonExistentProductId} not found`); - }) - - it('should return a 404 for a non-existent coupon', async () => { - const nonExistentCouponId = 999; - const response = await request(app).get(`/api/v1/coupons/${nonExistentCouponId}`); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it('should return a 403 for a user trying to create a coupon for another user\'s product', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', `Bearer ${otherVendorToken}`) - .send(couponData); - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only create coupons for your own products'); - }) - - it('should retrieve all coupons', async () => { - const response = await request(app).get('/api/v1/coupons'); - - expect(response.statusCode).toEqual(200); - expect(Array.isArray(response.body)).toBeTruthy(); - }); - - it('should retrieve all coupons by vendor', async () => { - const response = await request(app) - .get('/api/v1/coupons/mine') - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(200); - expect(Array.isArray(response.body)).toBeTruthy(); - }) - - it('should retrieve a single coupon by ID', async () => { - const response = await request(app).get(`/api/v1/coupons/${couponId}`); - - expect(response.statusCode).toEqual(200); - expect(response.body).toBeDefined(); - }); - - it('should update a coupon by ID', async () => { - const updatedCouponData = { - description: 'Updated Coupon', - percentage: 20, - expirationDate: '2023-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(updatedCouponData); - - expect(response.statusCode).toEqual(200); - expect(response.body).toBeDefined(); - }); - - it('should return a 404 for a non-existent coupon while updating', async () => { - const nonExistentCouponId = 999; - const updatedCouponData = { - description: 'Updated Coupon', - percentage: 20, - expirationDate: '2023-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${nonExistentCouponId}`) - .set('Authorization', `Bearer ${token}`) - .send(updatedCouponData); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); - - - it('should return a 403 for a user trying to update a coupon for another user', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - const couponData = { - description: 'Updated Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${otherVendorToken}`) - .send(couponData); - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only create coupons for your own products'); - }) - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it('should return a 401 for an unauthenticated user', async () => { - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [productId], - }; - - const response = await request(app) - .post('/api/v1/coupons') - .set('Authorization', 'Invalid Token') - .send(couponData); - expect(response.statusCode).toEqual(401); - }) - - it ('should return a 404 for a non-existent product', async () => { - const nonExistentProductId = 999; - const couponData = { - description: 'New Coupon', - percentage: 30, - expirationDate: '2024-12-31', - applicableProducts: [nonExistentProductId], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(couponData); - - expect(response.statusCode).toEqual(400); - expect(response.body.error).toEqual(`Product with id ${nonExistentProductId} not found`); - }) - - it('should return validation errors for invalid update data', async () => { - const invalidUpdateData = { - description: '', - percentage: 120, - expirationDate: '2022-12-31', - applicableProducts: [], - }; - - const response = await request(app) - .put(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`) - .send(invalidUpdateData); - - expect(response.statusCode).toEqual(400); - expect(response.body.errors).toBeDefined(); - }); - - it('should return a 403 for a user trying to delete a coupon for another user', async () => { - const otherVendorToken = await getVendorToken( - 'email@example.com', - 'Password123', - 'OtherVendor', - 'OtherName' - ) - - const response = await request(app) - .delete(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${otherVendorToken}`) - expect(response.statusCode).toEqual(403); - expect(response.body.error).toEqual('You can only delete your own coupons'); - }) - - it('should delete a coupon by ID', async () => { - const response = await request(app) - .delete(`/api/v1/coupons/${couponId}`) - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(204); - }); - - it('should return a 404 for a non-existent coupon', async () => { - const nonExistentCouponId = 999; - const response = await request(app) - .delete(`/api/v1/coupons/${nonExistentCouponId}`) - .set('Authorization', `Bearer ${token}`); - - expect(response.statusCode).toEqual(404); - expect(response.body.error).toEqual('Coupon not found'); - }); -}); \ No newline at end of file + let token: string; + let couponId: number; + let productId: number; + + beforeAll(async () => { + token = await getVendorToken(); + }); + + it('should create a new coupon with valid data', async () => { + const categoryData = { + name: 'Category', + description: 'category description', + }; + + const categoryResponse = await request(app) + .post('/api/v1/category') + .set('Authorization', `Bearer ${token}`) + .send(categoryData); + + const categoryId = categoryResponse.body.data.id; + const productData = { + name: 'New Product', + image: 'new_product.jpg', + gallery: [], + shortDesc: 'This is a new product', + longDesc: 'Detailed description of the new product', + categoryId: categoryId, + quantity: 10, + regularPrice: 5, + salesPrice: 4, + tags: ['tag1', 'tag2'], + type: 'Simple', + isAvailable: true, + }; + + const productResponse = await request(app) + .post('/api/v1/product') + .set('Authorization', `Bearer ${token}`) + .send(productData); + + expect(productResponse.statusCode).toEqual(201); + expect(productResponse.body.message).toEqual( + 'Product successfully created' + ); + expect(productResponse.body.data).toBeDefined(); + productId = productResponse.body.data.id; + + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(201); + expect(response.body.message).toEqual('Coupon created successfully'); + expect(response.body.data).toBeDefined(); + couponId = response.body.data.id; + }); + + it('should return validation errors for invalid coupon data', async () => { + const invalidCouponData = { + description: '', + percentage: 120, + expirationDate: '2022-12-31', + applicableProducts: [], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(invalidCouponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = 999; + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [nonExistentProductId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.error).toEqual( + `Product with id ${nonExistentProductId} not found` + ); + }); + + it('should return a 404 for a non-existent coupon', async () => { + const nonExistentCouponId = 999; + const response = await request(app).get( + `/api/v1/coupons/${nonExistentCouponId}` + ); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it("should return a 403 for a user trying to create a coupon for another user's product", async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', `Bearer ${otherVendorToken}`) + .send(couponData); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual( + 'You can only create coupons for your own products' + ); + }); + + it('should retrieve all coupons', async () => { + const response = await request(app).get('/api/v1/coupons'); + + expect(response.statusCode).toEqual(200); + expect(Array.isArray(response.body)).toBeTruthy(); + }); + + it('should retrieve all coupons by vendor', async () => { + const response = await request(app) + .get('/api/v1/coupons/mine') + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(200); + expect(Array.isArray(response.body)).toBeTruthy(); + }); + + it('should retrieve a single coupon by ID', async () => { + const response = await request(app).get(`/api/v1/coupons/${couponId}`); + + expect(response.statusCode).toEqual(200); + expect(response.body).toBeDefined(); + }); + + it('should update a coupon by ID', async () => { + const updatedCouponData = { + description: 'Updated Coupon', + percentage: 20, + expirationDate: '2023-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCouponData); + + expect(response.statusCode).toEqual(200); + expect(response.body).toBeDefined(); + }); + + it('should return a 404 for a non-existent coupon while updating', async () => { + const nonExistentCouponId = 999; + const updatedCouponData = { + description: 'Updated Coupon', + percentage: 20, + expirationDate: '2023-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${nonExistentCouponId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedCouponData); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); + + it('should return a 403 for a user trying to update a coupon for another user', async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + const couponData = { + description: 'Updated Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${otherVendorToken}`) + .send(couponData); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual( + 'You can only create coupons for your own products' + ); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it('should return a 401 for an unauthenticated user', async () => { + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [productId], + }; + + const response = await request(app) + .post('/api/v1/coupons') + .set('Authorization', 'Invalid Token') + .send(couponData); + expect(response.statusCode).toEqual(401); + }); + + it('should return a 404 for a non-existent product', async () => { + const nonExistentProductId = 999; + const couponData = { + description: 'New Coupon', + percentage: 30, + expirationDate: '2024-12-31', + applicableProducts: [nonExistentProductId], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(couponData); + + expect(response.statusCode).toEqual(400); + expect(response.body.error).toEqual( + `Product with id ${nonExistentProductId} not found` + ); + }); + + it('should return validation errors for invalid update data', async () => { + const invalidUpdateData = { + description: '', + percentage: 120, + expirationDate: '2022-12-31', + applicableProducts: [], + }; + + const response = await request(app) + .put(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`) + .send(invalidUpdateData); + + expect(response.statusCode).toEqual(400); + expect(response.body.errors).toBeDefined(); + }); + + it('should return a 403 for a user trying to delete a coupon for another user', async () => { + const otherVendorToken = await getVendorToken( + 'email@example.com', + 'Password123', + 'OtherVendor', + 'OtherName' + ); + + const response = await request(app) + .delete(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${otherVendorToken}`); + expect(response.statusCode).toEqual(403); + expect(response.body.error).toEqual('You can only delete your own coupons'); + }); + + it('should delete a coupon by ID', async () => { + const response = await request(app) + .delete(`/api/v1/coupons/${couponId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(204); + }); + + it('should return a 404 for a non-existent coupon', async () => { + const nonExistentCouponId = 999; + const response = await request(app) + .delete(`/api/v1/coupons/${nonExistentCouponId}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.statusCode).toEqual(404); + expect(response.body.error).toEqual('Coupon not found'); + }); +}); diff --git a/src/docs/buyerDocs.ts b/src/docs/buyerDocs.ts index 774c25b..26db7b5 100644 --- a/src/docs/buyerDocs.ts +++ b/src/docs/buyerDocs.ts @@ -153,6 +153,65 @@ * '500': * description: Internal Server Error */ +/** + * @openapi + * /buyer/addItemToWishList: + * post: + * tags: [buyer] + * @security bearerAuth + * summary: Adds an item to the wishlist + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * userId: + * type: integer + * productId: + * type: integer + * time: + * type: string + * categoryId: + * type: integer + * responses: + * 201: + * description: Wishlist item added successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/BuyerWishList' + */ +/** + * @swagger + * /buyer/addItemToWishList: + * post: + * summary: Add an Item to wishList + * tags: [Buyer] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * productId: + * type: integer + * time: + * type: string + * responses: + * '200': + * description: Wishlist item added successfully + * '400': + * description: Bad request + * '404': + * description: Wishlist not found + * '500': + * description: Internal server error + */ /** * @swagger