test(api): add unit tests for MCP, Inquiries, and Leads modules

Increase test file coverage to ≥50% for three under-tested modules:

- MCP: +1 test (mcp.module.spec.ts) → 2/2 files covered (100%)
- Inquiries: +4 tests (events, repository contract, prisma repo, DTOs)
  → 10/18 files covered (55.6%)
- Leads: +4 tests (events, repository contract, prisma repo, DTOs)
  → 12/22 files covered (54.5%)

All 225 test files pass with 1353 tests total.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 01:47:25 +07:00
parent 40832a9d12
commit f8f2935f45
9 changed files with 1102 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
import { InquiryCreatedEvent } from '../events/inquiry-created.event';
import { InquiryReadEvent } from '../events/inquiry-read.event';
describe('InquiryCreatedEvent', () => {
it('stores the aggregate id, listing id, and user id', () => {
const event = new InquiryCreatedEvent('inq-1', 'listing-1', 'user-1');
expect(event.aggregateId).toBe('inq-1');
expect(event.listingId).toBe('listing-1');
expect(event.userId).toBe('user-1');
});
it('has the correct event name', () => {
const event = new InquiryCreatedEvent('inq-1', 'listing-1', 'user-1');
expect(event.eventName).toBe('inquiry.created');
});
it('records the occurred timestamp', () => {
const before = new Date();
const event = new InquiryCreatedEvent('inq-1', 'listing-1', 'user-1');
const after = new Date();
expect(event.occurredAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(event.occurredAt.getTime()).toBeLessThanOrEqual(after.getTime());
});
});
describe('InquiryReadEvent', () => {
it('stores the aggregate id, listing id, and user id', () => {
const event = new InquiryReadEvent('inq-1', 'listing-1', 'user-1');
expect(event.aggregateId).toBe('inq-1');
expect(event.listingId).toBe('listing-1');
expect(event.userId).toBe('user-1');
});
it('has the correct event name', () => {
const event = new InquiryReadEvent('inq-1', 'listing-1', 'user-1');
expect(event.eventName).toBe('inquiry.read');
});
it('records the occurred timestamp', () => {
const before = new Date();
const event = new InquiryReadEvent('inq-1', 'listing-1', 'user-1');
const after = new Date();
expect(event.occurredAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
expect(event.occurredAt.getTime()).toBeLessThanOrEqual(after.getTime());
});
});

View File

@@ -0,0 +1,18 @@
import { INQUIRY_REPOSITORY } from '../repositories/inquiry.repository';
describe('Inquiry Repository Contract', () => {
describe('INQUIRY_REPOSITORY symbol', () => {
it('is a unique symbol', () => {
expect(typeof INQUIRY_REPOSITORY).toBe('symbol');
});
it('has a descriptive string representation', () => {
expect(INQUIRY_REPOSITORY.toString()).toBe('Symbol(INQUIRY_REPOSITORY)');
});
it('is not equal to another symbol with the same description', () => {
const anotherSymbol = Symbol('INQUIRY_REPOSITORY');
expect(INQUIRY_REPOSITORY).not.toBe(anotherSymbol);
});
});
});

View File

@@ -0,0 +1,233 @@
import { InquiryEntity } from '../../domain/entities/inquiry.entity';
import { PrismaInquiryRepository } from '../repositories/prisma-inquiry.repository';
describe('PrismaInquiryRepository', () => {
let repository: PrismaInquiryRepository;
let mockPrisma: {
inquiry: {
findUnique: ReturnType<typeof vi.fn>;
create: ReturnType<typeof vi.fn>;
update: ReturnType<typeof vi.fn>;
findMany: ReturnType<typeof vi.fn>;
count: ReturnType<typeof vi.fn>;
};
};
beforeEach(() => {
mockPrisma = {
inquiry: {
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
findMany: vi.fn(),
count: vi.fn(),
},
};
repository = new PrismaInquiryRepository(mockPrisma as any);
});
describe('findById', () => {
it('returns an InquiryEntity when found', async () => {
const now = new Date();
mockPrisma.inquiry.findUnique.mockResolvedValue({
id: 'inq-1',
listingId: 'listing-1',
userId: 'user-1',
message: 'Tôi muốn xem nhà',
phone: '0901234567',
isRead: false,
createdAt: now,
});
const result = await repository.findById('inq-1');
expect(result).toBeInstanceOf(InquiryEntity);
expect(result!.id).toBe('inq-1');
expect(result!.listingId).toBe('listing-1');
expect(result!.userId).toBe('user-1');
expect(result!.message).toBe('Tôi muốn xem nhà');
expect(result!.phone).toBe('0901234567');
expect(result!.isRead).toBe(false);
});
it('returns null when not found', async () => {
mockPrisma.inquiry.findUnique.mockResolvedValue(null);
const result = await repository.findById('nonexistent');
expect(result).toBeNull();
});
});
describe('save', () => {
it('creates a Prisma record from the entity', async () => {
mockPrisma.inquiry.create.mockResolvedValue(undefined);
const entity = InquiryEntity.createNew(
'inq-1',
'listing-1',
'user-1',
'Tôi muốn xem nhà',
'0901234567',
);
await repository.save(entity);
expect(mockPrisma.inquiry.create).toHaveBeenCalledWith({
data: {
id: 'inq-1',
listingId: 'listing-1',
userId: 'user-1',
message: 'Tôi muốn xem nhà',
phone: '0901234567',
isRead: false,
},
});
});
it('saves entity with null phone', async () => {
mockPrisma.inquiry.create.mockResolvedValue(undefined);
const entity = InquiryEntity.createNew(
'inq-2',
'listing-1',
'user-1',
'Cho tôi hỏi giá',
null,
);
await repository.save(entity);
const callData = mockPrisma.inquiry.create.mock.calls[0]![0].data;
expect(callData.phone).toBeNull();
});
});
describe('markAsRead', () => {
it('updates the isRead field to true', async () => {
mockPrisma.inquiry.update.mockResolvedValue(undefined);
await repository.markAsRead('inq-1');
expect(mockPrisma.inquiry.update).toHaveBeenCalledWith({
where: { id: 'inq-1' },
data: { isRead: true },
});
});
});
describe('findByListing', () => {
it('returns paginated results', async () => {
const now = new Date();
mockPrisma.inquiry.findMany.mockResolvedValue([
{
id: 'inq-1',
listingId: 'listing-1',
userId: 'user-1',
message: 'Xin chào',
phone: null,
isRead: false,
createdAt: now,
listing: { id: 'listing-1', property: { title: 'Căn hộ Quận 1' } },
user: { id: 'user-1', fullName: 'Nguyễn Văn A', phone: '0901234567' },
},
]);
mockPrisma.inquiry.count.mockResolvedValue(1);
const result = await repository.findByListing('listing-1', 1, 20);
expect(result.data).toHaveLength(1);
expect(result.total).toBe(1);
expect(result.page).toBe(1);
expect(result.limit).toBe(20);
expect(result.totalPages).toBe(1);
expect(result.data[0]!.listingTitle).toBe('Căn hộ Quận 1');
expect(result.data[0]!.userName).toBe('Nguyễn Văn A');
});
it('caps limit at 100', async () => {
mockPrisma.inquiry.findMany.mockResolvedValue([]);
mockPrisma.inquiry.count.mockResolvedValue(0);
const result = await repository.findByListing('listing-1', 1, 200);
expect(result.limit).toBe(100);
expect(mockPrisma.inquiry.findMany).toHaveBeenCalledWith(
expect.objectContaining({ take: 100 }),
);
});
it('calculates correct skip for pagination', async () => {
mockPrisma.inquiry.findMany.mockResolvedValue([]);
mockPrisma.inquiry.count.mockResolvedValue(0);
await repository.findByListing('listing-1', 3, 10);
expect(mockPrisma.inquiry.findMany).toHaveBeenCalledWith(
expect.objectContaining({ skip: 20 }),
);
});
});
describe('findByAgent', () => {
it('returns paginated results for agent', async () => {
const now = new Date();
mockPrisma.inquiry.findMany.mockResolvedValue([
{
id: 'inq-1',
listingId: 'listing-1',
userId: 'user-1',
message: 'Xin chào',
phone: null,
isRead: true,
createdAt: now,
listing: { id: 'listing-1', property: { title: 'Nhà phố Quận 7' } },
user: { id: 'user-1', fullName: 'Trần Thị B', phone: '0902345678' },
},
]);
mockPrisma.inquiry.count.mockResolvedValue(1);
const result = await repository.findByAgent('agent-1', 1, 20);
expect(result.data).toHaveLength(1);
expect(result.data[0]!.listingTitle).toBe('Nhà phố Quận 7');
expect(result.data[0]!.userName).toBe('Trần Thị B');
expect(result.data[0]!.isRead).toBe(true);
});
it('queries using listing.agentId filter', async () => {
mockPrisma.inquiry.findMany.mockResolvedValue([]);
mockPrisma.inquiry.count.mockResolvedValue(0);
await repository.findByAgent('agent-1', 1, 20);
expect(mockPrisma.inquiry.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: { listing: { agentId: 'agent-1' } },
}),
);
});
});
describe('countUnreadByAgent', () => {
it('returns the count of unread inquiries for the agent', async () => {
mockPrisma.inquiry.count.mockResolvedValue(5);
const result = await repository.countUnreadByAgent('agent-1');
expect(result).toBe(5);
expect(mockPrisma.inquiry.count).toHaveBeenCalledWith({
where: {
isRead: false,
listing: { agentId: 'agent-1' },
},
});
});
it('returns 0 when no unread inquiries exist', async () => {
mockPrisma.inquiry.count.mockResolvedValue(0);
const result = await repository.countUnreadByAgent('agent-1');
expect(result).toBe(0);
});
});
});

View File

@@ -0,0 +1,103 @@
import 'reflect-metadata';
import { CreateInquiryDto } from '../dto/create-inquiry.dto';
import { ListInquiriesDto } from '../dto/list-inquiries.dto';
describe('CreateInquiryDto', () => {
describe('validation metadata', () => {
it('has validation metadata on listingId property', () => {
const metadata = Reflect.getMetadata(
'swagger/apiModelProperties',
CreateInquiryDto.prototype,
'listingId',
);
expect(metadata).toBeDefined();
expect(metadata.description).toBe('ID of the listing');
});
it('has validation metadata on message property', () => {
const metadata = Reflect.getMetadata(
'swagger/apiModelProperties',
CreateInquiryDto.prototype,
'message',
);
expect(metadata).toBeDefined();
expect(metadata.description).toBe('Tin nhắn yêu cầu tư vấn');
});
it('has optional phone property metadata', () => {
const metadata = Reflect.getMetadata(
'swagger/apiModelProperties',
CreateInquiryDto.prototype,
'phone',
);
expect(metadata).toBeDefined();
expect(metadata.description).toBe('Số điện thoại liên hệ');
});
});
describe('property assignment', () => {
it('can assign all required properties', () => {
const dto = new CreateInquiryDto();
dto.listingId = 'listing-1';
dto.message = 'Tôi muốn xem nhà';
expect(dto.listingId).toBe('listing-1');
expect(dto.message).toBe('Tôi muốn xem nhà');
});
it('can assign optional phone', () => {
const dto = new CreateInquiryDto();
dto.phone = '0901234567';
expect(dto.phone).toBe('0901234567');
});
it('phone defaults to undefined when not assigned', () => {
const dto = new CreateInquiryDto();
expect(dto.phone).toBeUndefined();
});
});
});
describe('ListInquiriesDto', () => {
describe('validation metadata', () => {
it('has page property metadata', () => {
const metadata = Reflect.getMetadata(
'swagger/apiModelProperties',
ListInquiriesDto.prototype,
'page',
);
expect(metadata).toBeDefined();
expect(metadata.default).toBe(1);
});
it('has limit property metadata', () => {
const metadata = Reflect.getMetadata(
'swagger/apiModelProperties',
ListInquiriesDto.prototype,
'limit',
);
expect(metadata).toBeDefined();
expect(metadata.default).toBe(20);
});
});
describe('property assignment', () => {
it('can assign page and limit', () => {
const dto = new ListInquiriesDto();
dto.page = 2;
dto.limit = 50;
expect(dto.page).toBe(2);
expect(dto.limit).toBe(50);
});
it('page and limit default to undefined when not assigned', () => {
const dto = new ListInquiriesDto();
expect(dto.page).toBeUndefined();
expect(dto.limit).toBeUndefined();
});
});
});