From 042bc686802dbe0b1e95699869b231320ffa3695 Mon Sep 17 00:00:00 2001 From: cephaschapa Date: Fri, 19 Jan 2024 10:45:41 +0200 Subject: [PATCH] feat(test): added userFile unit tests --- app/package-lock.json | 93 +++++++-- app/package.json | 4 + .../file/[file]/download-file/route.ts | 3 +- .../v0/user/{ => [user]}/file/[file]/route.ts | 6 +- .../api/v0/user/{ => [user]}/file/route.ts | 10 +- app/tests/api/userfile.test.ts | 194 ++++++++++++++++++ app/tests/helpers.ts | 58 ++++++ 7 files changed, 343 insertions(+), 25 deletions(-) rename app/src/app/api/v0/user/{ => [user]}/file/[file]/download-file/route.ts (94%) rename app/src/app/api/v0/user/{ => [user]}/file/[file]/route.ts (92%) rename app/src/app/api/v0/user/{ => [user]}/file/route.ts (95%) create mode 100644 app/tests/api/userfile.test.ts diff --git a/app/package-lock.json b/app/package-lock.json index bad227713..8d0c2965a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -16,6 +16,7 @@ "@storybook/react": "^7.4.5", "@storybook/testing-library": "^0.2.2", "@types/bcrypt": "^5.0.1", + "@types/form-data": "^2.5.0", "@types/http-errors": "^2.0.4", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.4", @@ -34,7 +35,10 @@ "eslint": "8.52.0", "eslint-config-next": "13.4.16", "eslint-plugin-storybook": "^0.6.13", + "fetch-blob": "^4.0.0", "fetch-mock": "^9.11.0", + "form-data": "^4.0.0", + "formdata-node": "^6.0.3", "framer-motion": "^10.16.2", "http-errors": "^2.0.0", "i18next": "^23.4.2", @@ -9916,6 +9920,15 @@ "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==" }, + "node_modules/@types/form-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.5.0.tgz", + "integrity": "sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==", + "deprecated": "This is a stub types definition. form-data provides its own type definitions, so you do not need this installed.", + "dependencies": { + "form-data": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", @@ -10073,6 +10086,19 @@ "form-data": "^3.0.0" } }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", @@ -11428,20 +11454,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/axios/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -15304,6 +15316,27 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-4.0.0.tgz", + "integrity": "sha512-nPmnhRmpNMjYWnp9EBMGs6z5lq9RXed5W1vuZcECrsDVQInM8AMQSooVb3X183Aole60adzjWbH9qlRFWzDDTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0" + }, + "engines": { + "node": ">=16.7" + } + }, "node_modules/fetch-mock": { "version": "9.11.0", "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz", @@ -15770,9 +15803,9 @@ "dev": true }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -15782,6 +15815,14 @@ "node": ">= 6" } }, + "node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "engines": { + "node": ">= 18" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -19000,6 +19041,24 @@ "node": ">= 0.10.5" } }, + "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", diff --git a/app/package.json b/app/package.json index a2693c680..a4f226a8e 100644 --- a/app/package.json +++ b/app/package.json @@ -36,6 +36,7 @@ "@storybook/react": "^7.4.5", "@storybook/testing-library": "^0.2.2", "@types/bcrypt": "^5.0.1", + "@types/form-data": "^2.5.0", "@types/http-errors": "^2.0.4", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.4", @@ -54,7 +55,10 @@ "eslint": "8.52.0", "eslint-config-next": "13.4.16", "eslint-plugin-storybook": "^0.6.13", + "fetch-blob": "^4.0.0", "fetch-mock": "^9.11.0", + "form-data": "^4.0.0", + "formdata-node": "^6.0.3", "framer-motion": "^10.16.2", "http-errors": "^2.0.0", "i18next": "^23.4.2", diff --git a/app/src/app/api/v0/user/file/[file]/download-file/route.ts b/app/src/app/api/v0/user/[user]/file/[file]/download-file/route.ts similarity index 94% rename from app/src/app/api/v0/user/file/[file]/download-file/route.ts rename to app/src/app/api/v0/user/[user]/file/[file]/download-file/route.ts index e92d77cf8..ad21f1946 100644 --- a/app/src/app/api/v0/user/file/[file]/download-file/route.ts +++ b/app/src/app/api/v0/user/[user]/file/[file]/download-file/route.ts @@ -9,6 +9,7 @@ export const GET = apiHandler( _req: Request, context: { session?: Session; params: Record }, ) => { + const userId = context.params.user; if (!context.session) { throw new createHttpError.Unauthorized("Unauthorized"); } @@ -16,7 +17,7 @@ export const GET = apiHandler( const user = await db.models.User.findOne({ attributes: ["userId"], where: { - userId: context.session.user.id, + userId: userId, }, }); if (!user) { diff --git a/app/src/app/api/v0/user/file/[file]/route.ts b/app/src/app/api/v0/user/[user]/file/[file]/route.ts similarity index 92% rename from app/src/app/api/v0/user/file/[file]/route.ts rename to app/src/app/api/v0/user/[user]/file/[file]/route.ts index d2654679c..cd016dd47 100644 --- a/app/src/app/api/v0/user/file/[file]/route.ts +++ b/app/src/app/api/v0/user/[user]/file/[file]/route.ts @@ -9,6 +9,7 @@ export const GET = apiHandler( _req: Request, context: { session?: Session; params: Record }, ) => { + const userId = context.params.user; if (!context.session) { throw new createHttpError.Unauthorized("Unauthorized"); } @@ -16,7 +17,7 @@ export const GET = apiHandler( const user = await db.models.User.findOne({ attributes: ["userId"], where: { - userId: context.session.user.id, + userId: userId, }, }); if (!user) { @@ -42,6 +43,7 @@ export const DELETE = apiHandler( _req: Request, context: { session?: Session; params: Record }, ) => { + const userId = context.params.user; if (!context.session) { throw new createHttpError.Unauthorized("Unauthorized"); } @@ -49,7 +51,7 @@ export const DELETE = apiHandler( const user = await db.models.User.findOne({ attributes: ["userId"], where: { - userId: context.session.user.id, + userId: userId, }, }); diff --git a/app/src/app/api/v0/user/file/route.ts b/app/src/app/api/v0/user/[user]/file/route.ts similarity index 95% rename from app/src/app/api/v0/user/file/route.ts rename to app/src/app/api/v0/user/[user]/file/route.ts index 31b496da3..8567f9f45 100644 --- a/app/src/app/api/v0/user/file/route.ts +++ b/app/src/app/api/v0/user/[user]/file/route.ts @@ -30,6 +30,7 @@ export const GET = apiHandler( _req: Request, context: { session?: Session; params: Record }, ) => { + const userId = context.params.user; if (!context.session) { throw new createHttpError.Unauthorized("Unauthorized"); } @@ -37,7 +38,7 @@ export const GET = apiHandler( const user = await db.models.User.findOne({ attributes: ["userId"], where: { - userId: context.session.user.id, + userId: userId, }, }); if (!user) { @@ -63,7 +64,8 @@ export const POST = apiHandler( req: NextRequest, context: { session?: Session; params: Record }, ) => { - const authorizedUser = context.session?.user; + const userId = context.params.user; + if (!context.session) { throw new createHttpError.Unauthorized("Unauthorized"); } @@ -71,7 +73,7 @@ export const POST = apiHandler( const user = await db.models.User.findOne({ attributes: ["userId"], where: { - userId: authorizedUser?.id, + userId: userId, }, }); @@ -103,8 +105,6 @@ export const POST = apiHandler( gpc_ref_no: formData.get("gpc_ref_no"), }; - console.log(fileType); - const body = createUserFileRequset.parse(fileData); const userFile = await db.models.UserFile.create({ diff --git a/app/tests/api/userfile.test.ts b/app/tests/api/userfile.test.ts new file mode 100644 index 000000000..2803ff674 --- /dev/null +++ b/app/tests/api/userfile.test.ts @@ -0,0 +1,194 @@ +import { + POST as createUserFile, + GET as findUserFiles, +} from "@/app/api/v0/user/[user]/file/route"; + +import { + DELETE as deleteUserfile, + GET as findUserFile, +} from "@/app/api/v0/user/[user]/file/[file]/route"; + +import { db } from "@/models"; +import assert from "node:assert"; +import { after, before, describe, it } from "node:test"; +import { + testfileBuffer, + filePath, + getFileDataFromStream, + mockRequest, + mockRequestFormData, + setupTests, + testUserID, +} from "../helpers"; +import { randomUUID } from "node:crypto"; +import fs from "fs"; + +enum STATUS { + INPROGRESS = "in progress", + PENDING = "pending", +} + +const fileData = { + id: randomUUID(), + userId: testUserID, + sector: "Energy Sector", + url: "http://www.acme.com", + status: STATUS.INPROGRESS, + data: testfileBuffer, + gpc_ref_no: "XXXTESTXXX", + file_reference: "XXXTESTXXX", +}; + +const invalidFileData = { + id: "1", + userId: "2", + sector: "333", + url: "invalid.com", + status: "7", + data: "", + gpc_ref_no: "43", + file_reference: "0", +}; + +describe("UserFile API", () => { + before(async () => { + setupTests(); + await db.initialize(); + await db.models.UserFile.destroy({ where: { userId: testUserID } }); + await db.models.User.upsert({ userId: testUserID, name: "TEST_USER" }); + }); + after(async () => { + if (db.sequelize) await db.sequelize.close(); + + // deletes the file once test are done + await fs.unlink(await filePath(), (err: any) => { + if (err) console.error(err); + }); + }); + + it("should create a user file", async () => { + // stream created file from path + const fileStream = await getFileDataFromStream(await filePath()); + const formData = new FormData(); + formData.append("id", randomUUID()); + formData.append("userId", fileData.userId); + formData.append("sector", fileData.sector); + formData.append("url", fileData.url); + formData.append("data", fileStream); + formData.append("status", fileData.status); + formData.append("file_reference", fileData.file_reference); + formData.append("gpc_ref_no", fileData.gpc_ref_no); + const req = mockRequestFormData(formData); + const res = await createUserFile(req, { params: { user: testUserID } }); + assert.equal(res.status, 200); + const { data } = await res.json(); + + assert.equal(data?.sector, fileData.sector); + assert.equal(data?.url, fileData.url); + assert.equal(data?.status, fileData.status); + assert.equal(data?.gpc_ref_no, fileData.gpc_ref_no); + assert.equal(fileData?.data.type, data?.data.type); + }); + + it("should not create a file if data is invalid", async () => { + const formData = new FormData(); + formData.append("id", invalidFileData.id); + formData.append("userId", invalidFileData.userId); + formData.append("sector", invalidFileData.sector); + formData.append("url", invalidFileData.url); + formData.append("status", invalidFileData.status); + formData.append("file_reference", invalidFileData.file_reference); + formData.append("gpc_ref_no", invalidFileData.gpc_ref_no); + const req = mockRequestFormData(formData); + const res = await createUserFile(req, { params: { user: testUserID } }); + const { data } = await res.json(); + + assert.equal(res.status, 400); + }); + + it("should find all user files", async () => { + const req = mockRequest(); + const res = await findUserFiles(req, { + params: { user: testUserID }, + }); + const { data } = await res.json(); + + const userFile = data[0]; + assert.equal(userFile?.sector, fileData.sector); + assert.equal(userFile?.url, fileData.url); + assert.equal(userFile?.status, fileData.status); + assert.equal(userFile?.gpc_ref_no, fileData.gpc_ref_no); + }); + + it("should find a user file", async () => { + const getFilesReq = mockRequest(); + const getFilesRes = await findUserFiles(getFilesReq, { + params: { user: testUserID }, + }); + const { data: userFilesData } = await getFilesRes.json(); + + const userFiles = userFilesData[0]; + const req = mockRequest(); + const res = await findUserFile(req, { + params: { user: testUserID, file: userFiles.id }, + }); + const { data: userFile } = await res.json(); + + assert.equal(userFile?.sector, fileData.sector); + assert.equal(userFile?.url, fileData.url); + assert.equal(userFile?.status, fileData.status); + assert.equal(userFile?.gpc_ref_no, fileData.gpc_ref_no); + }); + + it("should not find a user file", async () => { + const req = mockRequest(); + const res = await findUserFile(req, { + params: { user: testUserID, file: randomUUID() }, + }); + + assert.equal(res.status, 404); + }); + + it("should not find user files for non-existent user", async () => { + const req = mockRequest(); + const res = await findUserFiles(req, { + params: { user: randomUUID() }, + }); + + assert.equal(res.status, 404); + }); + + it("should delete user file", async () => { + const fileStream = await getFileDataFromStream(await filePath()); + const formData = new FormData(); + formData.append("id", randomUUID()); + formData.append("userId", fileData.userId); + formData.append("sector", fileData.sector); + formData.append("url", fileData.url); + formData.append("data", fileStream); + formData.append("status", fileData.status); + formData.append("file_reference", fileData.file_reference); + formData.append("gpc_ref_no", fileData.gpc_ref_no); + const req = mockRequestFormData(formData); + const res = await createUserFile(req, { params: { user: testUserID } }); + assert.equal(res.status, 200); + const { data } = await res.json(); + const deletRequest = mockRequest(); + const deleteResponse = await deleteUserfile(deletRequest, { + params: { user: testUserID, file: data.id }, + }); + + const { deleted } = await deleteResponse.json(); + assert.equal(deleted, true); + assert.equal(deleteResponse.status, 200); + }); + + it("should not delete a non-existent user file", async () => { + const deletRequest = mockRequest(); + const deleteResponse = await deleteUserfile(deletRequest, { + params: { user: testUserID, file: randomUUID() }, + }); + + assert.equal(deleteResponse.status, 404); + }); +}); diff --git a/app/tests/helpers.ts b/app/tests/helpers.ts index 997311a49..912e1356b 100644 --- a/app/tests/helpers.ts +++ b/app/tests/helpers.ts @@ -2,6 +2,12 @@ import { AppSession, Auth } from "@/lib/auth"; import env from "@next/env"; import { NextRequest } from "next/server"; import { mock } from "node:test"; +import stream from "stream"; +import { Blob } from "fetch-blob"; +import { promisify } from "node:util"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const mockUrl = "http://localhost:3000/api/v0"; @@ -17,6 +23,58 @@ export function mockRequest(body?: any) { return request; } +export function mockRequestFormData(formData: FormData) { + const request = new NextRequest(new URL(mockUrl)); + request.formData = mock.fn(() => Promise.resolve(formData)); + return request; +} + +const finished = promisify(stream.finished); + +export async function getFileDataFromStream(filePath: string): Promise { + const fileStream = fs.createReadStream(filePath); + const chunks: Buffer[] = []; + + fileStream.on("data", (chunk: Buffer) => { + chunks.push(chunk); + }); + + await finished(fileStream); + + const blob = new Blob(chunks, { type: "application/octet-stream" }); + return blob; +} + +const createTestCsvFile = async ( + fileName: string, + data: string, +): Promise => { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const filePath = path.join(__dirname, fileName); + + await fs.promises.writeFile(filePath, data, "utf8"); + return filePath; +}; +export const filePath = async () => { + const fakeCSVData = + "id,sector,gpc_ref_no,last_modified,created\n1,Energy Sector, XXXTESTXXX,12/12/2023\n2,Transport, XXXTESTXXX,12/12/2023"; + const filePath = await createTestCsvFile("test.csv", fakeCSVData); + return filePath; +}; + +export const testfileBuffer = { + type: "Buffer", + data: [ + 105, 100, 44, 115, 101, 99, 116, 111, 114, 44, 103, 112, 99, 95, 114, 101, + 102, 95, 110, 111, 44, 108, 97, 115, 116, 95, 109, 111, 100, 105, 102, 105, + 101, 100, 44, 99, 114, 101, 97, 116, 101, 100, 10, 49, 44, 69, 110, 101, + 114, 103, 121, 32, 83, 101, 99, 116, 111, 114, 44, 32, 88, 88, 88, 84, 69, + 83, 84, 88, 88, 88, 44, 49, 50, 47, 49, 50, 47, 50, 48, 50, 51, 10, 50, 44, + 84, 114, 97, 110, 115, 112, 111, 114, 116, 44, 32, 88, 88, 88, 84, 69, 83, + 84, 88, 88, 88, 44, 49, 50, 47, 49, 50, 47, 50, 48, 50, 51, + ], +}; + export const testUserID = "beb9634a-b68c-4c1b-a20b-2ab0ced5e3c2"; export function setupTests() {