Move 36 root-level audit/analysis documents and 7 web app audit documents into docs/audits/ directory to declutter the project root. Remove stale EXPLORATION_SUMMARY.txt. Co-Authored-By: Paperclip <noreply@paperclip.ing>
16 KiB
16 KiB
Test Templates for Untested Files
Repository Test Template
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
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
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
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
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
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<number, number> = { 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
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);
});
});
});