diff --git a/jest.api.config.ts b/jest.api.config.ts index 76a61b3a..03869a8e 100644 --- a/jest.api.config.ts +++ b/jest.api.config.ts @@ -2,41 +2,44 @@ import type { JestConfigWithTsJest } from "ts-jest"; import nextJest from "next/jest.js"; const nextConfig = { - dir: "./", // Path to your Next.js app + dir: "./", // Path to your Next.js app }; const baseConfig: JestConfigWithTsJest = { - clearMocks: true, - coverageProvider: "v8", - preset: "ts-jest/presets/js-with-ts", - transform: { - "^.+\\.mjs$": "ts-jest", - }, + clearMocks: true, + coverageProvider: "v8", + preset: "ts-jest/presets/js-with-ts", + transform: { + "^.+\\.mjs$": "ts-jest", + }, }; const filesConfig = { - setupFiles: ["dotenv/config"], - setupFilesAfterEnv: ["/jest.setup.ts"], + setupFiles: ["dotenv/config"], + setupFilesAfterEnv: ["/jest.setup.ts"], }; const moduleConfig = { - moduleNameMapper: { - "^~/(.*)$": "/src/$1", - }, + moduleNameMapper: { + "^~/(.*)$": "/src/$1", + }, }; const testConfig = { - testMatch: ["**/server/api/__tests__/**/*.test.ts"], + testMatch: [ + "**/server/api/__tests__/**/*.test.ts", + "**/pages/api/__tests__/**/*.test.ts", + ], }; const jestConfig: JestConfigWithTsJest = { - ...baseConfig, - ...filesConfig, - ...moduleConfig, - ...testConfig, + ...baseConfig, + ...filesConfig, + ...moduleConfig, + ...testConfig, }; const createJestConfig = nextJest(nextConfig); -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument +// biome-ignore lint/suspicious/noExplicitAny: export default createJestConfig(jestConfig as any); diff --git a/package.json b/package.json index d8cbd21c..edeb05ef 100644 --- a/package.json +++ b/package.json @@ -105,4 +105,4 @@ "prisma": { "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" } -} \ No newline at end of file +} diff --git a/src/pages/api/__tests__/v1/network/network.test.ts b/src/pages/api/__tests__/v1/network/network.test.ts new file mode 100644 index 00000000..e0cda60c --- /dev/null +++ b/src/pages/api/__tests__/v1/network/network.test.ts @@ -0,0 +1,52 @@ +import apiNetworkHandler from "~/pages/api/v1/network"; +import { NextApiRequest, NextApiResponse } from "next"; + +describe("/api/createNetwork", () => { + it("should respond 405 to unsupported methods", async () => { + const req = { method: "PUT" } as NextApiRequest; + const res = { + status: jest.fn().mockReturnThis(), + end: jest.fn(), + json: jest.fn().mockReturnThis(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(405); + }); + + it("should respond 401 when invalid API key for POST", async () => { + const req = { + method: "POST", + headers: { "x-ztnet-auth": "invalidApiKey" }, + } as unknown as NextApiRequest; + const res = { + status: jest.fn().mockReturnThis(), + end: jest.fn(), + json: jest.fn().mockReturnThis(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); + + it("should respond 401 when invalid API key for GET", async () => { + const req = { + method: "GET", + headers: { "x-ztnet-auth": "invalidApiKey" }, + } as unknown as NextApiRequest; + const res = { + status: jest.fn().mockReturnThis(), + end: jest.fn(), + json: jest.fn().mockReturnThis(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + }); +}); diff --git a/src/pages/api/__tests__/v1/network/networkById.test.ts b/src/pages/api/__tests__/v1/network/networkById.test.ts new file mode 100644 index 00000000..2e248b29 --- /dev/null +++ b/src/pages/api/__tests__/v1/network/networkById.test.ts @@ -0,0 +1,193 @@ +import { TRPCError } from "@trpc/server"; +import { getHTTPStatusCodeFromError } from "@trpc/server/http"; +import { NextApiRequest, NextApiResponse } from "next"; +import apiNetworkByIdHandler from "~/pages/api/v1/network/[id]"; +import { prisma } from "~/server/db"; +import * as encryptionModule from "~/utils/encryption"; +import * as ztController from "~/utils/ztApi"; +// import rateLimit from "~/utils/rateLimit"; + +// Mock the ztController module +jest.mock("~/utils/encryption", () => { + const originalModule = jest.requireActual("~/utils/encryption"); + return { + ...originalModule, + decryptAndVerifyToken: jest.fn(), + }; +}); + +// Mock the ztController module +jest.mock("~/utils/ztApi", () => { + const originalModule = jest.requireActual("~/utils/ztApi"); + return { + ...originalModule, + local_network_detail: jest.fn(), + }; +}); + +// Mock the rateLimit module +jest.mock("~/utils/rateLimit", () => ({ + __esModule: true, // Ensure correct handling of ES module + default: jest.fn(() => ({ + check: jest.fn().mockResolvedValue(null), // Mock implementation of the check method + })), +})); + +// Add these tests to your describe block +it("should respond 200 when network is found", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "userId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "test_nw_id", + nwname: "credent_second", + authorId: 1, + }); + + // Mock the ztController to return a network detail + (ztController.local_network_detail as jest.Mock).mockResolvedValue({ + network: { id: "networkId", name: "networkName" }, + }); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ id: "networkId", name: "networkName" }); +}); + +it("should respond 401 when network is not found", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "userId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue(null); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: "Network not found or access denied." }); +}); + +it("should respond with an error when ztController throws an error", async () => { + // Mock the decryption to return a valid user ID + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockResolvedValue({ + userId: "ztnetUserId", + }); + + // Mock the database to return a network + prisma.network.findUnique = jest.fn().mockResolvedValue({ + nwid: "networkId", + name: "networkName", + authorId: 1, + }); + + // Mock the ztController to throw an error + const error = new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Internal server error", + }); + + (ztController.local_network_detail as jest.Mock).mockRejectedValue(error); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "validApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + const httpCode = getHTTPStatusCodeFromError(error); + expect(res.status).toHaveBeenCalledWith(httpCode); + expect(res.json).toHaveBeenCalledWith({ error: error.message }); +}); + +it("should respond 401 when decryptAndVerifyToken fails", async () => { + // Mock the decryption to fail + (encryptionModule.decryptAndVerifyToken as jest.Mock).mockRejectedValue( + new Error("Invalid token"), + ); + + const req = { + method: "GET", + headers: { "x-ztnet-auth": "invalidApiKey" }, + query: { id: "networkId" }, + } as unknown as NextApiRequest; + + const res = { + status: jest.fn().mockReturnThis(), + json: jest.fn(), + end: jest.fn(), + setHeader: jest.fn(), // Mock `setHeader` if rate limiter uses it + } as unknown as NextApiResponse; + + await apiNetworkByIdHandler(req, res); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ error: expect.any(String) }), + ); +}); + +// it("should respond with 429 when rate limit is exceeded", async () => { +// // Apply the mock directly to the rateLimit().check for this specific test +// rateLimit().check = jest.fn().mockImplementation(() => { +// throw new Error(); +// }); + +// const req = { +// method: "GET", +// headers: { "x-ztnet-auth": "validApiKey" }, +// query: { id: "networkId" }, +// } as unknown as NextApiRequest; + +// const res = { +// status: jest.fn().mockReturnThis(), +// json: jest.fn(), +// end: jest.fn(), +// setHeader: jest.fn(), +// } as unknown as NextApiResponse; + +// await apiNetworkByIdHandler(req, res); + +// expect(res.status).toHaveBeenCalledWith(429); +// expect(res.json).toHaveBeenCalledWith({ error: "Rate limit exceeded" }); +// }); diff --git a/src/pages/api/v1/network/[id]/index.ts b/src/pages/api/v1/network/[id]/index.ts index b0e7c4ad..8a969699 100644 --- a/src/pages/api/v1/network/[id]/index.ts +++ b/src/pages/api/v1/network/[id]/index.ts @@ -14,7 +14,7 @@ const limiter = rateLimit({ const REQUEST_PR_MINUTE = 50; -export default async function createNetworkHandler( +export default async function apiNetworkByIdHandler( req: NextApiRequest, res: NextApiResponse, ) { diff --git a/src/pages/api/v1/network/[id]/member/index.ts b/src/pages/api/v1/network/[id]/member/index.ts index 14d56c36..a1fd2947 100644 --- a/src/pages/api/v1/network/[id]/member/index.ts +++ b/src/pages/api/v1/network/[id]/member/index.ts @@ -14,7 +14,7 @@ const limiter = rateLimit({ const REQUEST_PR_MINUTE = 50; -export default async function createNetworkHandler( +export default async function apiNetworkMembersHandler( req: NextApiRequest, res: NextApiResponse, ) { diff --git a/src/pages/api/v1/network/index.ts b/src/pages/api/v1/network/index.ts index 8078a142..b6edddd7 100644 --- a/src/pages/api/v1/network/index.ts +++ b/src/pages/api/v1/network/index.ts @@ -15,7 +15,7 @@ const limiter = rateLimit({ const REQUEST_PR_MINUTE = 50; -export default async function createNetworkHandler( +export default async function apiNetworkHandler( req: NextApiRequest, res: NextApiResponse, ) {