# Test Templates for Untested Files ## Repository Test Template ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { PrismaInquiryRepository } from '../prisma-inquiry.repository'; import { InquiryEntity } from '../../domain/entities/inquiry.entity'; describe('PrismaInquiryRepository', () => { let repository: PrismaInquiryRepository; let mockPrisma: any; beforeEach(() => { mockPrisma = { inquiry: { findUnique: vi.fn(), findMany: vi.fn(), create: vi.fn(), update: vi.fn(), count: vi.fn(), }, }; repository = new PrismaInquiryRepository(mockPrisma); }); describe('findById', () => { it('should return inquiry entity when found', async () => { const inquiryData = { id: 'inquiry-1', listingId: 'listing-1', userId: 'user-1', message: 'Test message', phone: '0901234567', isRead: false, createdAt: new Date('2026-01-01'), }; mockPrisma.inquiry.findUnique.mockResolvedValue(inquiryData); const result = await repository.findById('inquiry-1'); expect(mockPrisma.inquiry.findUnique).toHaveBeenCalledWith({ where: { id: 'inquiry-1' }, }); expect(result).toBeInstanceOf(InquiryEntity); }); it('should return null when not found', async () => { mockPrisma.inquiry.findUnique.mockResolvedValue(null); const result = await repository.findById('non-existent'); expect(result).toBeNull(); }); }); describe('save', () => { it('should create inquiry with correct data', async () => { const entity = new InquiryEntity('inquiry-1', { listingId: 'listing-1', userId: 'user-1', message: 'Test', phone: '0901234567', isRead: false, }, new Date()); await repository.save(entity); expect(mockPrisma.inquiry.create).toHaveBeenCalledWith({ data: { id: 'inquiry-1', listingId: 'listing-1', userId: 'user-1', message: 'Test', phone: '0901234567', isRead: false, }, }); }); }); describe('findByListing', () => { it('should return paginated results with correct limit clamping', async () => { mockPrisma.inquiry.findMany.mockResolvedValue([]); mockPrisma.inquiry.count.mockResolvedValue(0); await repository.findByListing('listing-1', 1, 150); // limit > 100 const calls = mockPrisma.inquiry.findMany.mock.calls[0][0]; expect(calls.take).toBe(100); // Clamped to 100 expect(calls.skip).toBe(0); // (1 - 1) * 100 }); it('should calculate pagination correctly', async () => { const mockData = Array(20).fill({ id: 'id', listingId: 'listing-1', userId: 'user-1', message: 'msg', phone: 'phone', isRead: false, createdAt: new Date(), listing: { property: { title: 'Property' } }, user: { id: 'user-1', fullName: 'Name', phone: 'phone' }, }); mockPrisma.inquiry.findMany.mockResolvedValue(mockData); mockPrisma.inquiry.count.mockResolvedValue(150); const result = await repository.findByListing('listing-1', 2, 20); expect(result.page).toBe(2); expect(result.limit).toBe(20); expect(result.total).toBe(150); expect(result.totalPages).toBe(8); // ceil(150/20) const calls = mockPrisma.inquiry.findMany.mock.calls[0][0]; expect(calls.skip).toBe(20); // (2 - 1) * 20 }); }); }); ``` --- ## Value Object Test Template ```typescript import { describe, it, expect } from 'vitest'; import { LeadScore } from '../lead-score.vo'; describe('LeadScore ValueObject', () => { describe('create', () => { it('should create valid score', () => { const result = LeadScore.create(50); expect(result.isOk()).toBe(true); expect(result.unwrap().value).toBe(50); }); it('should accept boundary values', () => { expect(LeadScore.create(0).isOk()).toBe(true); expect(LeadScore.create(100).isOk()).toBe(true); }); it('should reject negative values', () => { const result = LeadScore.create(-1); expect(result.isErr()).toBe(true); expect(result.unwrapErr()).toBe('Điểm lead phải từ 0 đến 100'); }); it('should reject values over 100', () => { const result = LeadScore.create(101); expect(result.isErr()).toBe(true); expect(result.unwrapErr()).toBe('Điểm lead phải từ 0 đến 100'); }); it('should reject non-integer values', () => { const result = LeadScore.create(50.5); expect(result.isErr()).toBe(true); }); it('should have correct getter', () => { const score = LeadScore.create(75).unwrap(); expect(score.value).toBe(75); }); }); }); ``` --- ## DTO Test Template ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { validate } from 'class-validator'; import { CreateLeadDto } from '../create-lead.dto'; describe('CreateLeadDto', () => { let dto: CreateLeadDto; beforeEach(() => { dto = new CreateLeadDto(); }); describe('validation', () => { it('should pass with all required fields', async () => { dto.name = 'Nguyễn Văn A'; dto.phone = '0901234567'; dto.source = 'website'; const errors = await validate(dto); expect(errors).toHaveLength(0); }); it('should fail when name is missing', async () => { dto.phone = '0901234567'; dto.source = 'website'; const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors[0]?.property).toBe('name'); }); it('should fail when phone is missing', async () => { dto.name = 'Nguyễn Văn A'; dto.source = 'website'; const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors[0]?.property).toBe('phone'); }); it('should pass with optional email field', async () => { dto.name = 'Nguyễn Văn A'; dto.phone = '0901234567'; dto.source = 'website'; // email not set const errors = await validate(dto); expect(errors).toHaveLength(0); }); it('should fail with invalid email format', async () => { dto.name = 'Nguyễn Văn A'; dto.phone = '0901234567'; dto.source = 'website'; dto.email = 'invalid-email'; const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors.some(e => e.property === 'email')).toBe(true); }); it('should fail when score is outside range', async () => { dto.name = 'Nguyễn Văn A'; dto.phone = '0901234567'; dto.source = 'website'; dto.score = 150; const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); expect(errors.some(e => e.property === 'score')).toBe(true); }); it('should pass with score in valid range', async () => { dto.name = 'Nguyễn Văn A'; dto.phone = '0901234567'; dto.source = 'website'; dto.score = 75; const errors = await validate(dto); expect(errors).toHaveLength(0); }); }); }); ``` --- ## Controller Test Template ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { InquiriesController } from '../inquiries.controller'; import { CreateInquiryCommand } from '../../application/commands/create-inquiry/create-inquiry.command'; import { GetInquiriesByListingQuery } from '../../application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query'; describe('InquiriesController', () => { let controller: InquiriesController; let mockCommandBus: any; let mockQueryBus: any; let mockUser: any; beforeEach(() => { mockCommandBus = { execute: vi.fn() }; mockQueryBus = { execute: vi.fn() }; mockUser = { sub: 'user-1' }; controller = new InquiriesController(mockCommandBus, mockQueryBus); }); describe('createInquiry', () => { it('should dispatch CreateInquiryCommand with correct parameters', async () => { const dto = { listingId: 'listing-1', message: 'Test message', phone: '0901234567', }; const expected = { id: 'inquiry-1', listingId: 'listing-1', createdAt: new Date(), }; mockCommandBus.execute.mockResolvedValue(expected); const result = await controller.createInquiry(dto as any, mockUser); expect(mockCommandBus.execute).toHaveBeenCalledWith( expect.any(CreateInquiryCommand), ); const command = mockCommandBus.execute.mock.calls[0][0]; expect(command.userId).toBe('user-1'); expect(command.listingId).toBe('listing-1'); expect(command.message).toBe('Test message'); expect(command.phone).toBe('0901234567'); expect(result).toEqual(expected); }); it('should convert undefined phone to null', async () => { const dto = { listingId: 'listing-1', message: 'Test message', // phone undefined }; mockCommandBus.execute.mockResolvedValue({ id: 'inquiry-1' }); await controller.createInquiry(dto as any, mockUser); const command = mockCommandBus.execute.mock.calls[0][0]; expect(command.phone).toBeNull(); }); }); describe('getByListing', () => { it('should dispatch GetInquiriesByListingQuery with defaults', async () => { const dto = { page: undefined, limit: undefined }; const expected = { data: [], total: 0, page: 1, limit: 20 }; mockQueryBus.execute.mockResolvedValue(expected); const result = await controller.getByListing('listing-1', dto as any); expect(mockQueryBus.execute).toHaveBeenCalledWith( expect.any(GetInquiriesByListingQuery), ); const query = mockQueryBus.execute.mock.calls[0][0]; expect(query.listingId).toBe('listing-1'); expect(query.page).toBe(1); expect(query.limit).toBe(20); expect(result).toEqual(expected); }); it('should use provided pagination values', async () => { const dto = { page: 3, limit: 50 }; mockQueryBus.execute.mockResolvedValue({ data: [] }); await controller.getByListing('listing-1', dto as any); const query = mockQueryBus.execute.mock.calls[0][0]; expect(query.page).toBe(3); expect(query.limit).toBe(50); }); }); describe('markAsRead', () => { it('should dispatch command and return success', async () => { mockCommandBus.execute.mockResolvedValue(undefined); const result = await controller.markAsRead('inquiry-1', mockUser); expect(mockCommandBus.execute).toHaveBeenCalledWith( expect.any(MarkInquiryReadCommand), ); const command = mockCommandBus.execute.mock.calls[0][0]; expect(command.inquiryId).toBe('inquiry-1'); expect(command.userId).toBe('user-1'); expect(result).toEqual({ success: true }); }); }); }); ``` --- ## Pagination Test Helper ```typescript import { describe, it, expect } from 'vitest'; describe('Pagination calculations', () => { it('should calculate skip correctly', () => { const testCases = [ { page: 1, take: 20, expectedSkip: 0 }, { page: 2, take: 20, expectedSkip: 20 }, { page: 5, take: 50, expectedSkip: 200 }, ]; testCases.forEach(({ page, take, expectedSkip }) => { const skip = (page - 1) * take; expect(skip).toBe(expectedSkip); }); }); it('should calculate totalPages correctly', () => { const testCases = [ { total: 0, take: 20, expectedPages: 0 }, { total: 20, take: 20, expectedPages: 1 }, { total: 21, take: 20, expectedPages: 2 }, { total: 150, take: 20, expectedPages: 8 }, ]; testCases.forEach(({ total, take, expectedPages }) => { const totalPages = Math.ceil(total / take); expect(totalPages).toBe(expectedPages); }); }); it('should clamp limit to 100', () => { const testCases = [ { limit: 10, expectedTake: 10 }, { limit: 100, expectedTake: 100 }, { limit: 150, expectedTake: 100 }, { limit: 1000, expectedTake: 100 }, ]; testCases.forEach(({ limit, expectedTake }) => { const take = Math.min(limit, 100); expect(take).toBe(expectedTake); }); }); }); ``` --- ## Aggregation Test Helper ```typescript import { describe, it, expect } from 'vitest'; describe('Aggregation calculations', () => { it('should calculate conversion rate correctly', () => { const testCases = [ { totalLeads: 10, convertedCount: 5, expected: 50.00 }, { totalLeads: 100, convertedCount: 33, expected: 33.00 }, { totalLeads: 0, convertedCount: 0, expected: 0 }, { totalLeads: 3, convertedCount: 1, expected: 33.33 }, ]; testCases.forEach(({ totalLeads, convertedCount, expected }) => { const rate = totalLeads > 0 ? Math.round((convertedCount / totalLeads) * 10000) / 100 : 0; expect(rate).toBe(expected); }); }); it('should calculate average score correctly', () => { const testCases = [ { scores: [100], expected: 100.0 }, { scores: [75, 85], expected: 80.0 }, { scores: [60, 70, 80], expected: 70.0 }, { scores: [], expected: null }, ]; testCases.forEach(({ scores, expected }) => { const scoreCount = scores.length; const scoreSum = scores.reduce((a, b) => a + b, 0); const avg = scoreCount > 0 ? Math.round((scoreSum / scoreCount) * 10) / 10 : null; expect(avg).toBe(expected); }); }); it('should calculate average rating correctly', () => { const testCases = [ { ratings: [5, 5, 5], expected: 5.0 }, { ratings: [1, 2, 3, 4, 5], expected: 3.0 }, { ratings: [4, 4, 5], expected: 4.3 }, { ratings: [], expected: 0 }, ]; testCases.forEach(({ ratings, expected }) => { const total = ratings.length; const sum = ratings.reduce((a, b) => a + b, 0); const avg = total > 0 ? Math.round((sum / total) * 10) / 10 : 0; expect(avg).toBe(expected); }); }); it('should build rating distribution correctly', () => { const ratings = [1, 3, 3, 4, 5, 5, 5]; const distribution: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; for (const rating of ratings) { distribution[rating] = (distribution[rating] ?? 0) + 1; } expect(distribution).toEqual({ 1: 1, 2: 0, 3: 2, 4: 1, 5: 3, }); }); }); ``` --- ## DTO Pagination Helper Test ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { ListLeadsDto } from '../list-leads.dto'; describe('ListLeadsDto - Pagination', () => { describe('type transformation', () => { it('should transform string page to number', async () => { const plain = { page: '2', limit: '50', status: 'NEW' }; const dto = plainToClass(ListLeadsDto, plain); const errors = await validate(dto); expect(errors).toHaveLength(0); expect(dto.page).toBe(2); expect(typeof dto.page).toBe('number'); }); }); describe('validation', () => { it('should fail when page is 0', async () => { const dto = plainToClass(ListLeadsDto, { page: 0 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); }); it('should fail when limit exceeds 100', async () => { const dto = plainToClass(ListLeadsDto, { limit: 150 }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); }); it('should pass with valid status enum', async () => { const dto = plainToClass(ListLeadsDto, { status: 'NEW' }); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it('should fail with invalid status', async () => { const dto = plainToClass(ListLeadsDto, { status: 'INVALID_STATUS' }); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); }); }); }); ```