Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
30b19b2
feat: add request validation middleware with per-endpoint rate limiti…
Jul 11, 2026
311e197
feat: add comprehensive input sanitization middleware for XSS protect…
Jul 11, 2026
8c404e7
feat: add OpenAPI/Swagger configuration with comprehensive API docume…
Jul 11, 2026
ac0e52b
feat: add Swagger JSDoc annotations and rate limiting to auth routes
Jul 11, 2026
edb0dfe
feat: add Swagger JSDoc annotations and URL sanitization to user routes
Jul 11, 2026
3e44ad9
feat: add Swagger JSDoc annotations to invoice routes with full endpo…
Jul 11, 2026
5454cba
feat: add Swagger JSDoc annotations to escrow routes with full endpoi…
Jul 11, 2026
f2940a8
feat: integrate rate limiting, input sanitization, and Swagger UI int…
Jul 11, 2026
37c02f3
feat: add Vitest configuration for test suite
Jul 11, 2026
1595ae5
feat: add test fixtures and factories for users, invoices, escrows, a…
Jul 11, 2026
a1dc550
test: add unit tests for config loading and default values
Jul 11, 2026
5c50eb4
test: add unit tests for all Zod validation schemas
Jul 11, 2026
1c1483a
test: add unit tests for pagination utility functions
Jul 11, 2026
dfca2a9
test: add unit tests for custom error classes
Jul 11, 2026
e48778d
test: add unit tests for input sanitization and URL validation
Jul 11, 2026
241e979
test: add integration tests for auth flows including register, login,…
Jul 11, 2026
e5d302d
test: add integration tests for invoice CRUD operations
Jul 11, 2026
247a58d
test: add integration tests for escrow lifecycle operations
Jul 11, 2026
c952bce
chore: add sanitize-html, swagger-jsdoc, swagger-ui-express, and type…
Jul 11, 2026
1e1223c
feat: add test setup with Prisma and Stellar service mocks
Jul 11, 2026
aff0a51
Merge branch 'main' into feat/security-middleware-api-docs-tests
TheCodingChef-eth Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,438 changes: 600 additions & 838 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@
"morgan": "~1.10.1",
"multer": "~2.0.1",
"pg": "~8.20.0",
"sharp": "~0.33.5",
"sanitize-html": "~2.17.6",
"stellar-sdk": "~13.3.0",
"swagger-jsdoc": "~6.3.0",
"swagger-ui-express": "~5.0.1",
"zod": "~4.4.3"
},
"devDependencies": {
Expand All @@ -51,6 +53,9 @@
"@types/morgan": "~1.9.9",
"@types/multer": "~1.4.12",
"@types/node": "~25.8.0",
"@types/sanitize-html": "~2.16.1",
"@types/swagger-jsdoc": "~6.0.4",
"@types/swagger-ui-express": "~4.1.8",
"eslint": "~9.18.0",
"prisma": "~7.8.0",
"tsx": "~4.22.0",
Expand Down
87 changes: 87 additions & 0 deletions src/__tests__/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export const mockUser = {
id: "clx1234567890",
fullname: "John Doe",
email: "john@example.com",
password: "$2a$12$hashedpassword",
walletAddress: "GCKFBEIYVYQV6FYNUWZV6MO7VVI7RMUO6ESLO45S73JQ27LHJKR2",
profileImage: null,
country: "US",
role: "FREELANCER" as const,
isVerified: true,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
};

export const mockClient = {
...mockUser,
id: "clx1234567891",
fullname: "Jane Smith",
email: "jane@example.com",
role: "CLIENT" as const,
};

export const mockAdmin = {
...mockUser,
id: "clx1234567892",
fullname: "Admin User",
email: "admin@example.com",
role: "ADMIN" as const,
};

export const mockInvoice = {
id: "clx_inv_001",
creatorId: "clx1234567890",
recipientId: "clx1234567891",
title: "Web Development Services",
description: "Frontend development for e-commerce site",
amount: 1500,
currency: "USDC",
status: "DRAFT" as const,
dueDate: new Date("2024-02-01"),
paidAt: null,
txHash: null,
contractId: null,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
creator: { id: "clx1234567890", fullname: "John Doe", email: "john@example.com" },
recipient: { id: "clx1234567891", fullname: "Jane Smith", email: "jane@example.com" },
escrow: null,
};

export const mockEscrow = {
id: "clx_esc_001",
invoiceId: "clx_inv_001",
clientId: "clx1234567891",
freelancerId: "clx1234567890",
contractId: "contract_mock_123",
txHash: null,
amount: 1500,
currency: "USDC",
status: "PENDING" as const,
fundedAt: null,
releasedAt: null,
refundedAt: null,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
invoice: { id: "clx_inv_001", title: "Web Development Services", amount: 1500, status: "IN_ESCROW" as const },
client: { id: "clx1234567891", fullname: "Jane Smith", email: "jane@example.com" },
freelancer: { id: "clx1234567890", fullname: "John Doe", email: "john@example.com" },
payments: [],
};

export const mockPayment = {
id: "clx_pay_001",
userId: "clx1234567891",
escrowId: "clx_esc_001",
amount: 1500,
currency: "USDC",
txHash: "tx_mock_fund",
status: "SUCCESS" as const,
description: "Funded escrow for invoice",
createdAt: new Date("2024-01-01"),
};

export const mockTokens = {
accessToken: "eyJhbGciOiJIUzI1NiJ9.mock.access.token",
refreshToken: "eyJhbGciOiJIUzI1NiJ9.mock.refresh.token",
};
182 changes: 182 additions & 0 deletions src/__tests__/integration/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Request, Response } from "express";
import bcrypt from "bcryptjs";
import { prisma } from "../../config/prisma.js";
import * as authController from "../../controllers/auth.controller.js";
import { mockUser } from "../fixtures/index.js";

vi.mock("jsonwebtoken", () => ({
default: {
sign: vi.fn().mockReturnValue("mock-jwt-token"),
verify: vi.fn().mockReturnValue({ userId: "clx1234567890", email: "john@example.com", role: "FREELANCER" }),
TokenExpiredError: class TokenExpiredError extends Error {},
JsonWebTokenError: class JsonWebTokenError extends Error {},
},
}));

function mockReq(body: Record<string, unknown> = {}, cookies: Record<string, string> = {}): Request {
return {
body,
cookies,
headers: {},
params: {},
query: {},
ip: "127.0.0.1",
socket: { remoteAddress: "127.0.0.1" },
} as unknown as Request;
}

function mockRes(): Response {
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
cookie: vi.fn().mockReturnThis(),
clearCookie: vi.fn().mockReturnThis(),
} as unknown as Response;
return res;
}

describe("Auth Controller", () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe("register", () => {
it("should register a new user", async () => {
const hashedPassword = await bcrypt.hash("password123", 12);
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
vi.mocked(prisma.user.create).mockResolvedValue({
...mockUser,
password: hashedPassword,
});

const req = mockReq({
fullname: "John Doe",
email: "john@example.com",
password: "password123",
role: "FREELANCER",
});
const res = mockRes();

await authController.register(req, res);

expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
data: expect.objectContaining({
user: expect.objectContaining({ email: "john@example.com" }),
}),
})
);
});

it("should throw ConflictError for existing email", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as never);

const req = mockReq({
fullname: "John Doe",
email: "john@example.com",
password: "password123",
role: "FREELANCER",
});
const res = mockRes();

await expect(authController.register(req, res)).rejects.toThrow("Email already registered");
});
});

describe("login", () => {
it("should login with valid credentials", async () => {
const hashedPassword = await bcrypt.hash("password123", 12);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
...mockUser,
password: hashedPassword,
});

const req = mockReq({ email: "john@example.com", password: "password123" });
const res = mockRes();

await authController.login(req, res);

expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
data: expect.objectContaining({
accessToken: expect.any(String),
}),
})
);
});

it("should throw UnauthorizedError for invalid email", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);

const req = mockReq({ email: "nonexistent@example.com", password: "password123" });
const res = mockRes();

await expect(authController.login(req, res)).rejects.toThrow("Invalid email or password");
});

it("should throw UnauthorizedError for wrong password", async () => {
const hashedPassword = await bcrypt.hash("wrongpassword", 12);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
...mockUser,
password: hashedPassword,
});

const req = mockReq({ email: "john@example.com", password: "password123" });
const res = mockRes();

await expect(authController.login(req, res)).rejects.toThrow("Invalid email or password");
});
});

describe("logout", () => {
it("should clear refresh token cookie", () => {
const req = mockReq();
const res = mockRes();

authController.logout(req, res);

expect(res.clearCookie).toHaveBeenCalledWith("refreshToken", expect.any(Object));
expect(res.json).toHaveBeenCalledWith({
success: true,
data: { message: "Logged out successfully" },
});
});
});

describe("getMe", () => {
it("should return current user profile", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as never);

const req = mockReq();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(req as any).user = { userId: "clx1234567890" };
const res = mockRes();

await authController.getMe(req, res);

expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
data: expect.objectContaining({
user: expect.objectContaining({ email: "john@example.com" }),
}),
})
);
});

it("should throw NotFoundError for non-existent user", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue(null);

const req = mockReq();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(req as any).user = { userId: "nonexistent" };
const res = mockRes();

await expect(authController.getMe(req, res)).rejects.toThrow("User not found");
});
});
});
Loading
Loading