From 8acd8e41deaa9247669c03d1d9c4805f0ab129a0 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Sun, 1 Sep 2024 16:21:36 +0000 Subject: [PATCH] tests --- package.json | 6 ++--- .../v1/application/statistic.test.ts | 15 +++++++++-- .../api/__tests__/v1/network/network.test.ts | 8 +++--- .../v1/networkMembers/updateMember.test.ts | 6 ++--- src/pages/api/__tests__/v1/org/org.test.ts | 1 + src/pages/api/__tests__/v1/org/orgid.test.ts | 4 +-- src/pages/api/__tests__/v1/user/user.test.ts | 26 ++++++++++++++++--- src/pages/api/v1/user/_schema.ts | 2 +- src/pages/api/v1/user/index.ts | 2 -- src/server/api/routers/_schema.ts | 21 +++++++++++++++ src/server/api/routers/authRouter.ts | 21 +-------------- 11 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 src/server/api/routers/_schema.ts diff --git a/package.json b/package.json index 957db874..c6b0e8fd 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "format": "biome format src", "format:fix": "biome format src --write", "start": "node .next/standalone/server.js", - "test:dev": "jest --config jest.pages.config.ts && jest --config jest.api.config.ts", - "test": "jest --config jest.pages.config.ts && jest --config jest.api.config.ts --ci --coverage", + "test:dev": "jest --detectOpenHandles --verbose --config jest.pages.config.ts && jest --config jest.api.config.ts", + "test": "jest --config jest.pages.config.ts && jest --verbose --config jest.api.config.ts --ci --coverage", "studio": "prisma studio" }, "dependencies": { @@ -109,4 +109,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/application/statistic.test.ts b/src/pages/api/__tests__/v1/application/statistic.test.ts index b6d1b71f..e580c41b 100644 --- a/src/pages/api/__tests__/v1/application/statistic.test.ts +++ b/src/pages/api/__tests__/v1/application/statistic.test.ts @@ -4,12 +4,20 @@ import { NextApiRequest, NextApiResponse } from "next"; describe("/api/stats", () => { it("should allow only GET method", async () => { const methods = ["DELETE", "POST", "PUT", "PATCH", "OPTIONS", "HEAD"]; - const req = {} as NextApiRequest; + const req = { + method: "GET", + headers: { + "x-ztnet-auth": "validApiKey", + }, + query: {}, + body: {}, + } 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 + setHeader: jest.fn(), } as unknown as NextApiResponse; for (const method of methods) { @@ -29,7 +37,10 @@ describe("/api/stats", () => { const req = { method: "GET", headers: { "x-ztnet-auth": "invalidApiKey" }, + query: {}, + body: {}, } as unknown as NextApiRequest; + const res = { status: jest.fn().mockReturnThis(), end: jest.fn(), diff --git a/src/pages/api/__tests__/v1/network/network.test.ts b/src/pages/api/__tests__/v1/network/network.test.ts index e0cda60c..d08caae5 100644 --- a/src/pages/api/__tests__/v1/network/network.test.ts +++ b/src/pages/api/__tests__/v1/network/network.test.ts @@ -3,7 +3,7 @@ import { NextApiRequest, NextApiResponse } from "next"; describe("/api/createNetwork", () => { it("should respond 405 to unsupported methods", async () => { - const req = { method: "PUT" } as NextApiRequest; + const req = { method: "PUT", query: {} } as NextApiRequest; const res = { status: jest.fn().mockReturnThis(), end: jest.fn(), @@ -20,12 +20,13 @@ describe("/api/createNetwork", () => { const req = { method: "POST", headers: { "x-ztnet-auth": "invalidApiKey" }, + query: {}, } 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 + setHeader: jest.fn(), } as unknown as NextApiResponse; await apiNetworkHandler(req, res); @@ -37,12 +38,13 @@ describe("/api/createNetwork", () => { const req = { method: "GET", headers: { "x-ztnet-auth": "invalidApiKey" }, + query: {}, } 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 + setHeader: jest.fn(), } as unknown as NextApiResponse; await apiNetworkHandler(req, res); diff --git a/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts b/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts index 04d40008..3ce9b882 100644 --- a/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts +++ b/src/pages/api/__tests__/v1/networkMembers/updateMember.test.ts @@ -77,7 +77,7 @@ describe("Update Network Members", () => { method: "POST", headers: { "x-ztnet-auth": "validApiKey" }, query: { id: "networkId", memberId: "memberId" }, - body: { name: "New Name", authorized: "true" }, + body: { name: "New Name", authorized: true }, } as unknown as NextApiRequest; // Mock the database to return a network @@ -114,7 +114,7 @@ describe("Update Network Members", () => { method: "POST", headers: { "x-ztnet-auth": "validApiKey" }, query: { id: "networkId", memberId: "memberId" }, - body: { name: "New Name", authorized: "true" }, + body: { name: "New Name", authorized: true }, } as unknown as NextApiRequest; const res = createMockRes(); @@ -160,7 +160,7 @@ describe("Update Network Members", () => { method: "POST", headers: { "x-ztnet-auth": "invalidApiKey" }, query: { id: "networkId", memberId: "memberId" }, - body: { name: "New Name", authorized: "true" }, + body: { name: "New Name", authorized: true }, } as unknown as NextApiRequest; const res = createMockRes(); diff --git a/src/pages/api/__tests__/v1/org/org.test.ts b/src/pages/api/__tests__/v1/org/org.test.ts index b2efbb77..c0792a2b 100644 --- a/src/pages/api/__tests__/v1/org/org.test.ts +++ b/src/pages/api/__tests__/v1/org/org.test.ts @@ -68,6 +68,7 @@ describe("organization api validation", () => { .mockResolvedValue({ id: "newUserId", name: "Ztnet", email: "post@ztnet.network" }); mockRequest.headers["x-ztnet-auth"] = "not valid token"; + mockRequest.query = {}; await GET_userOrganization( mockRequest as NextApiRequest, diff --git a/src/pages/api/__tests__/v1/org/orgid.test.ts b/src/pages/api/__tests__/v1/org/orgid.test.ts index d2d21f5a..cfc3fce5 100644 --- a/src/pages/api/__tests__/v1/org/orgid.test.ts +++ b/src/pages/api/__tests__/v1/org/orgid.test.ts @@ -156,8 +156,8 @@ describe("organization api validation", () => { const validToken = encrypt(validTokenData, generateInstanceSecret(API_TOKEN_SECRET)); mockRequest.headers["x-ztnet-auth"] = validToken; - // add organizationId to the request - mockRequest.query = undefined; + // add empty query + mockRequest.query = {}; await apiNetworkHandler( mockRequest as NextApiRequest, mockResponse as NextApiResponse, diff --git a/src/pages/api/__tests__/v1/user/user.test.ts b/src/pages/api/__tests__/v1/user/user.test.ts index d1582ba5..d8e2e587 100644 --- a/src/pages/api/__tests__/v1/user/user.test.ts +++ b/src/pages/api/__tests__/v1/user/user.test.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from "next"; -import createUserHandler, { POST_createUser } from "~/pages/api/v1/user"; +import createUserHandler from "~/pages/api/v1/user"; import { prisma } from "~/server/db"; import { appRouter } from "~/server/api/root"; import { API_TOKEN_SECRET, encrypt, generateInstanceSecret } from "~/utils/encryption"; @@ -18,7 +18,12 @@ jest.mock("~/server/api/root", () => ({ })), }, })); - +jest.mock("~/utils/rateLimit", () => ({ + __esModule: true, + default: () => ({ + check: jest.fn().mockResolvedValue(true), + }), +})); jest.mock("~/server/api/trpc"); jest.mock("~/server/db", () => ({ @@ -126,9 +131,19 @@ describe("createUserHandler", () => { }), })); + mockRequest.method = "POST"; mockRequest.headers["x-ztnet-auth"] = "not defined"; + mockRequest.body = { + email: "ztnet@example.com", + password: "password123", + name: "Ztnet", + }; + + await createUserHandler( + mockRequest as NextApiRequest, + mockResponse as NextApiResponse, + ); - await POST_createUser(mockRequest as NextApiRequest, mockResponse as NextApiResponse); expect(mockResponse.status).toHaveBeenCalledWith(200); // Check if the response is as expected @@ -166,6 +181,7 @@ describe("createUserHandler", () => { method: "POST", headers: { "x-ztnet-auth": tokenWithIdHash }, body: { email: "test@example.com", password: "password123", name: "Test User" }, + query: {}, } as unknown as NextApiRequest; const res = { @@ -208,7 +224,9 @@ describe("createUserHandler", () => { it("should allow only POST method", async () => { const methods = ["GET", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]; - const req = {} as NextApiRequest; + const req = { + query: {}, + } as NextApiRequest; const res = createMockRes(); for (const method of methods) { diff --git a/src/pages/api/v1/user/_schema.ts b/src/pages/api/v1/user/_schema.ts index 89a6b2f5..a96bf335 100644 --- a/src/pages/api/v1/user/_schema.ts +++ b/src/pages/api/v1/user/_schema.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { passwordSchema } from "~/server/api/routers/authRouter"; +import { passwordSchema } from "~/server/api/routers/_schema"; // Input validation schema export const createUserSchema = z.object({ diff --git a/src/pages/api/v1/user/index.ts b/src/pages/api/v1/user/index.ts index 3f984215..945e835b 100644 --- a/src/pages/api/v1/user/index.ts +++ b/src/pages/api/v1/user/index.ts @@ -64,7 +64,6 @@ export const POST_createUser = async (req: NextApiRequest, res: NextApiResponse) // Input validation const validatedInput = createUserSchema.parse(req.body); - // get data from the post request const { email, password, name, expiresAt, generateApiToken } = validatedInput; @@ -81,7 +80,6 @@ export const POST_createUser = async (req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ message: "Invalid expiresAt date" }); } } - /** * * Create a transaction to make sure the user and API token are created together diff --git a/src/server/api/routers/_schema.ts b/src/server/api/routers/_schema.ts new file mode 100644 index 00000000..0596aae9 --- /dev/null +++ b/src/server/api/routers/_schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +// This regular expression (regex) is used to validate a password based on the following criteria: +// - The password must be at least 6 characters long. +// - The password must contain at least two of the following three character types: +// - Lowercase letters (a-z) +// - Uppercase letters (A-Z) +// - Digits (0-9) +export const mediumPassword = new RegExp( + "^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})", +); + +// create a zod password schema +export const passwordSchema = (errorMessage: string) => + z + .string() + .max(40, { message: "Password must not exceed 40 characters" }) + .refine((val) => mediumPassword.test(val), { + message: errorMessage, + }) + .optional(); diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index 240c6743..71aee569 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -24,16 +24,7 @@ import { validateOrganizationToken } from "../services/organizationAuthService"; import rateLimit from "~/utils/rateLimit"; import { ErrorCode } from "~/utils/errorCode"; import { MailTemplateKey } from "~/utils/enums"; - -// This regular expression (regex) is used to validate a password based on the following criteria: -// - The password must be at least 6 characters long. -// - The password must contain at least two of the following three character types: -// - Lowercase letters (a-z) -// - Uppercase letters (A-Z) -// - Digits (0-9) -const mediumPassword = new RegExp( - "^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})", -); +import { mediumPassword, passwordSchema } from "./_schema"; // allow 15 requests per 10 minutes const limiter = rateLimit({ @@ -44,16 +35,6 @@ const limiter = rateLimit({ const GENERAL_REQUEST_LIMIT = 60; const SHORT_REQUEST_LIMIT = 5; -// create a zod password schema -export const passwordSchema = (errorMessage: string) => - z - .string() - .max(40, { message: "Password must not exceed 40 characters" }) - .refine((val) => mediumPassword.test(val), { - message: errorMessage, - }) - .optional(); - export const authRouter = createTRPCRouter({ register: publicProcedure .input(