From dc34c041d7e51a0718d95a887dfe283fcb3238d9 Mon Sep 17 00:00:00 2001 From: martinvibes Date: Tue, 1 Jul 2025 16:11:41 +0100 Subject: [PATCH] refactor DTOs to domain modules with validation tests --- jest.config.ts | 42 +++++----- package.json | 2 + src/dtos/emailVerification.dto.ts | 15 ---- src/dtos/verifyEmailDTO.ts | 6 -- .../dto/emailVerification.dto.test.ts | 56 +++++++++++++ .../dto/resendVerificationDTO.test.ts | 27 +++++++ .../auth/__tests__/dto/verifyEmailDTO.test.ts | 28 +++++++ src/modules/auth/dto/emailVerification.dto.ts | 46 +++++++++++ .../auth/dto}/resendVerificationDTO.ts | 0 src/modules/auth/dto/verifyEmailDTO.ts | 7 ++ tests/__mocks__/prisma.ts | 6 ++ tests/dto-validation.test.ts | 58 +++++++++++++ tests/email.test.ts | 81 +++++++++---------- 13 files changed, 292 insertions(+), 82 deletions(-) delete mode 100644 src/dtos/emailVerification.dto.ts delete mode 100644 src/dtos/verifyEmailDTO.ts create mode 100644 src/modules/auth/__tests__/dto/emailVerification.dto.test.ts create mode 100644 src/modules/auth/__tests__/dto/resendVerificationDTO.test.ts create mode 100644 src/modules/auth/__tests__/dto/verifyEmailDTO.test.ts create mode 100644 src/modules/auth/dto/emailVerification.dto.ts rename src/{dtos => modules/auth/dto}/resendVerificationDTO.ts (100%) create mode 100644 src/modules/auth/dto/verifyEmailDTO.ts create mode 100644 tests/__mocks__/prisma.ts create mode 100644 tests/dto-validation.test.ts diff --git a/jest.config.ts b/jest.config.ts index bc91cea..1c16350 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,33 +1,37 @@ -const { config } = require('dotenv'); -config({ path: '.env.test' }); +import { config } from "dotenv"; + +config({ path: ".env.test" }); /** @type {import('ts-jest').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/tests'], +export default { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/tests", "/src"], transform: { - '^.+\\.tsx?$': [ - 'ts-jest', + "^.+\\.tsx?$": [ + "ts-jest", { - tsconfig: './tsconfig.json', + tsconfig: "./tsconfig.json", }, ], }, testMatch: [ - '**/tests/**/*.test.ts', - '**/__tests__/**/*.test.ts', - '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + "**/tests/**/*.test.ts", + "**/__tests__/**/*.test.ts", + "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", + "**/src/**/__tests__/**/*.test.ts", + "**/src/**/*.test.ts", ], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], moduleNameMapper: { - '^@/(.*)$': '/src/$1', + "^@/(.*)$": "/src/$1", + "^@prisma/client$": "/tests/__mocks__/prisma.ts", }, collectCoverageFrom: [ - 'src/**/*.{js,ts}', - '!**/node_modules/**', - '!**/tests/**', + "src/**/*.{js,ts}", + "!**/node_modules/**", + "!**/tests/**", ], - coverageDirectory: 'coverage', + coverageDirectory: "coverage", testTimeout: 30000, -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index e4a9dd5..0848f23 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "type": "commonjs", "main": "dist/index.js", "scripts": { + "test:dto": "jest --runInBand src/modules/auth/__tests__/dto --detectOpenHandles", + "test:dto:independent": "jest --runInBand src/modules/auth/__tests__/dto --detectOpenHandles", "dev": "ts-node-dev src/index.ts", "build": "tsc", "start": "node dist/index.js", diff --git a/src/dtos/emailVerification.dto.ts b/src/dtos/emailVerification.dto.ts deleted file mode 100644 index 2c7fba7..0000000 --- a/src/dtos/emailVerification.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface RegisterDTO { - name: string; - email: string; - password: string; - wallet: string; - } - - export interface VerifyEmailDTO { - token: string; - } - - export interface ResendVerificationDTO { - email: string; - } - \ No newline at end of file diff --git a/src/dtos/verifyEmailDTO.ts b/src/dtos/verifyEmailDTO.ts deleted file mode 100644 index 6c61e64..0000000 --- a/src/dtos/verifyEmailDTO.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsString } from "class-validator"; - -export class VerifyEmailDTO { - @IsString() - token: string; -} diff --git a/src/modules/auth/__tests__/dto/emailVerification.dto.test.ts b/src/modules/auth/__tests__/dto/emailVerification.dto.test.ts new file mode 100644 index 0000000..07c297d --- /dev/null +++ b/src/modules/auth/__tests__/dto/emailVerification.dto.test.ts @@ -0,0 +1,56 @@ +import { RegisterDTO } from "../../dto/emailVerification.dto"; + +describe("RegisterDTO - DTO validation", () => { + it("should validate correct data", async () => { + const dto = new RegisterDTO({ + name: "John Doe", + email: "john.doe@example.com", + password: "SecurePassword123", + wallet: "0x1234567890abcdef", + }); + const errors = await RegisterDTO.validate(dto); + expect(errors).toHaveLength(0); + }); + + it("should validate required fields", async () => { + const dto = new RegisterDTO({ + email: "john.doe@example.com", + password: "SecurePassword123", + }); + const errors = await RegisterDTO.validate(dto); + expect(errors).toHaveLength(2); + expect(errors[0].property).toBe("name"); + expect(errors[1].property).toBe("wallet"); + }); + + it("should validate email format", async () => { + const dto = new RegisterDTO({ + name: "John Doe", + email: "invalid-email", + password: "SecurePassword123", + wallet: "0x1234567890abcdef", + }); + const errors = await RegisterDTO.validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("email"); + }); + + it("should validate password length", async () => { + const dto = new RegisterDTO({ + name: "John Doe", + email: "john.doe@example.com", + password: "short", + wallet: "0x1234567890abcdef", + }); + const errors = await RegisterDTO.validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("password"); + }); + + it("should fail with invalid email", async () => { + const dto = new RegisterDTO(); + dto.email = "invalid-email"; + const errors = await RegisterDTO.validate(dto); + expect(errors.length).toBeGreaterThan(0); + }); +}); diff --git a/src/modules/auth/__tests__/dto/resendVerificationDTO.test.ts b/src/modules/auth/__tests__/dto/resendVerificationDTO.test.ts new file mode 100644 index 0000000..900f2c1 --- /dev/null +++ b/src/modules/auth/__tests__/dto/resendVerificationDTO.test.ts @@ -0,0 +1,27 @@ +import { validate } from "class-validator"; +import { ResendVerificationDTO } from "../../dto/resendVerificationDTO"; + +describe("ResendVerificationDTO - DTO validation", () => { + it("should validate correct email", async () => { + const dto = new ResendVerificationDTO(); + dto.email = "test@example.com"; + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it("should fail with invalid email", async () => { + const dto = new ResendVerificationDTO(); + dto.email = "invalid-email"; + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("email"); + }); + + it("should fail with empty email", async () => { + const dto = new ResendVerificationDTO(); + dto.email = ""; + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("email"); + }); +}); diff --git a/src/modules/auth/__tests__/dto/verifyEmailDTO.test.ts b/src/modules/auth/__tests__/dto/verifyEmailDTO.test.ts new file mode 100644 index 0000000..dac0098 --- /dev/null +++ b/src/modules/auth/__tests__/dto/verifyEmailDTO.test.ts @@ -0,0 +1,28 @@ +import { validate } from "class-validator"; +import { VerifyEmailDTO } from "../../dto/verifyEmailDTO"; + +describe("VerifyEmailDTO - DTO validation", () => { + it("should validate correct token", async () => { + const dto = new VerifyEmailDTO(); + dto.token = "valid-token-123"; + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it("should fail with empty token", async () => { + const dto = new VerifyEmailDTO(); + dto.token = ""; + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("token"); + }); + + it("should fail with invalid type", async () => { + const dto = new VerifyEmailDTO(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dto.token = 123 as any; // Invalid type + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("token"); + }); +}); diff --git a/src/modules/auth/dto/emailVerification.dto.ts b/src/modules/auth/dto/emailVerification.dto.ts new file mode 100644 index 0000000..66ab2cf --- /dev/null +++ b/src/modules/auth/dto/emailVerification.dto.ts @@ -0,0 +1,46 @@ +import { + IsString, + IsEmail, + IsNotEmpty, + MinLength, + validate, +} from "class-validator"; + +export class RegisterDTO { + @IsString() + @IsNotEmpty() + name: string; + + @IsEmail() + @IsNotEmpty() + email: string; + + @IsString() + @IsNotEmpty() + @MinLength(8) + password: string; + + @IsString() + @IsNotEmpty() + wallet: string; + + constructor(partial: Partial = {}) { + Object.assign(this, partial); + this.name = this.name || ""; + this.email = this.email || ""; + this.password = this.password || ""; + this.wallet = this.wallet || ""; + } + + static validate(dto: RegisterDTO) { + return validate(dto); + } +} + +export interface VerifyEmailDTO { + token: string; +} + +export interface ResendVerificationDTO { + email: string; +} diff --git a/src/dtos/resendVerificationDTO.ts b/src/modules/auth/dto/resendVerificationDTO.ts similarity index 100% rename from src/dtos/resendVerificationDTO.ts rename to src/modules/auth/dto/resendVerificationDTO.ts diff --git a/src/modules/auth/dto/verifyEmailDTO.ts b/src/modules/auth/dto/verifyEmailDTO.ts new file mode 100644 index 0000000..3d81e80 --- /dev/null +++ b/src/modules/auth/dto/verifyEmailDTO.ts @@ -0,0 +1,7 @@ +import { IsString, IsNotEmpty } from "class-validator"; + +export class VerifyEmailDTO { + @IsString() + @IsNotEmpty() + token: string; +} diff --git a/tests/__mocks__/prisma.ts b/tests/__mocks__/prisma.ts new file mode 100644 index 0000000..c4c5c93 --- /dev/null +++ b/tests/__mocks__/prisma.ts @@ -0,0 +1,6 @@ +export const PrismaClient = class { + constructor() {} +}; + +export const prismaClientSingleton = () => new PrismaClient(); +export const prisma = prismaClientSingleton(); diff --git a/tests/dto-validation.test.ts b/tests/dto-validation.test.ts new file mode 100644 index 0000000..2b104ec --- /dev/null +++ b/tests/dto-validation.test.ts @@ -0,0 +1,58 @@ +import { validate } from "class-validator"; +import { RegisterDTO } from "../src/modules/auth/dto/emailVerification.dto"; + +describe("DTO Validation", () => { + describe("RegisterDTO", () => { + it("should validate correct data", async () => { + const dto: RegisterDTO = { + name: "John Doe", + email: "john.doe@example.com", + password: "SecurePassword123", + wallet: "0x1234567890abcdef", + }; + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it("should validate required fields", async () => { + const dto = new RegisterDTO({ + email: "john.doe@example.com", + password: "SecurePassword123", + }); + const errors = await validate(dto); + expect(errors).toHaveLength(2); + expect(errors[0].property).toBe("name"); + expect(errors[1].property).toBe("wallet"); + expect(errors[0].constraints?.required).toBe("Name is required"); + expect(errors[1].constraints?.required).toBe("Wallet is required"); + }); + + it("should validate email format", async () => { + const dto = new RegisterDTO({ + name: "John Doe", + email: "invalid-email", + password: "SecurePassword123", + wallet: "0x1234567890abcdef", + }); + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("email"); + expect(errors[0].constraints?.email).toBeDefined(); + }); + + it("should validate password length", async () => { + const dto = new RegisterDTO({ + name: "John Doe", + email: "john.doe@example.com", + password: "short", // Too short + wallet: "0x1234567890abcdef", + }); + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].property).toBe("password"); + expect(errors[0].constraints?.minlength).toBe( + "Password must be at least 8 characters long" + ); + }); + }); +}); diff --git a/tests/email.test.ts b/tests/email.test.ts index 4171eb5..eaba1a3 100644 --- a/tests/email.test.ts +++ b/tests/email.test.ts @@ -1,16 +1,17 @@ -import request from 'supertest'; -import app from '../src/index'; -import { AppDataSource } from '../src/config/ormconfig'; +import request from "supertest"; +import app from "../src/index"; +import { AppDataSource } from "../src/config/ormconfig"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars let userId: string; let verificationToken: string; const testUser = { - name: 'Alice', - lastName: 'Doe', - email: 'alice@example.com', - password: 'securepassword', - wallet: '0x987654321abcdef' + name: "Alice", + lastName: "Doe", + email: "alice@example.com", + password: "securepassword", + wallet: "0x987654321abcdef", }; beforeAll(async () => { @@ -21,78 +22,74 @@ afterAll(async () => { await AppDataSource.destroy(); }); -describe('Email Verification API', () => { - it('should register a new user and send a verification email', async () => { - const res = await request(app) - .post('/auth/register') - .send(testUser); +describe("Email Verification API", () => { + it("should register a new user and send a verification email", async () => { + const res = await request(app).post("/auth/register").send(testUser); expect(res.status).toBe(201); - expect(res.body).toHaveProperty('id'); - expect(res.body).toHaveProperty('verificationToken'); + expect(res.body).toHaveProperty("id"); + expect(res.body).toHaveProperty("verificationToken"); expect(res.body.email).toBe(testUser.email); userId = res.body.id; verificationToken = res.body.verificationToken; }); - it('should verify user email with a valid token', async () => { - const res = await request(app) - .get(`/auth/verify-email?token=${verificationToken}`); + it("should verify user email with a valid token", async () => { + const res = await request(app).get( + `/auth/verify-email?token=${verificationToken}` + ); expect(res.status).toBe(200); - expect(res.body.message).toBe('Email verified successfully'); + expect(res.body.message).toBe("Email verified successfully"); }); - it('should not verify email with an invalid token', async () => { - const res = await request(app) - .get('/auth/verify-email?token=invalidtoken'); + it("should not verify email with an invalid token", async () => { + const res = await request(app).get("/auth/verify-email?token=invalidtoken"); expect(res.status).toBe(400); - expect(res.body.error).toBe('Invalid or expired verification token'); + expect(res.body.error).toBe("Invalid or expired verification token"); }); - it('should not allow unverified users to access protected routes', async () => { + it("should not allow unverified users to access protected routes", async () => { const res = await request(app) - .get('/protected-route') - .set('Authorization', `Bearer unverifiedUserToken`); + .get("/protected-route") + .set("Authorization", `Bearer unverifiedUserToken`); expect(res.status).toBe(403); - expect(res.body.error).toBe('Email not verified'); + expect(res.body.error).toBe("Email not verified"); }); - it('should allow verified users to access protected routes', async () => { - const loginRes = await request(app) - .post('/auth/login') - .send({ - email: testUser.email, - password: testUser.password - }); + it("should allow verified users to access protected routes", async () => { + const loginRes = await request(app).post("/auth/login").send({ + email: testUser.email, + password: testUser.password, + }); const token = loginRes.body.token; const res = await request(app) - .get('/protected-route') - .set('Authorization', `Bearer ${token}`); + .get("/protected-route") + .set("Authorization", `Bearer ${token}`); expect(res.status).toBe(200); }); - it('should resend verification email if requested', async () => { + it("should resend verification email if requested", async () => { const res = await request(app) - .post('/auth/resend-verification') + .post("/auth/resend-verification") .send({ email: testUser.email }); expect(res.status).toBe(200); - expect(res.body.message).toBe('Verification email resent successfully'); + expect(res.body.message).toBe("Verification email resent successfully"); }); - it('should return error for resending verification to a verified email', async () => { + it("should return error for resending verification to a verified email", async () => { const res = await request(app) - .post('/auth/resend-verification') + .post("/auth/resend-verification") .send({ email: testUser.email }); expect(res.status).toBe(400); - expect(res.body.error).toBe('Email already verified'); + expect(res.body.error).toBe("Email already verified"); }); });