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:
@@ -0,0 +1,331 @@
|
||||
import { LeadEntity } from '../../domain/entities/lead.entity';
|
||||
import { LeadScore } from '../../domain/value-objects/lead-score.vo';
|
||||
import { PrismaLeadRepository } from '../repositories/prisma-lead.repository';
|
||||
|
||||
describe('PrismaLeadRepository', () => {
|
||||
let repository: PrismaLeadRepository;
|
||||
let mockPrisma: {
|
||||
lead: {
|
||||
findUnique: ReturnType<typeof vi.fn>;
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
lead: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
};
|
||||
repository = new PrismaLeadRepository(mockPrisma as any);
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('returns a LeadEntity when found', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.lead.findUnique.mockResolvedValue({
|
||||
id: 'lead-1',
|
||||
agentId: 'agent-1',
|
||||
name: 'Nguyễn Văn A',
|
||||
phone: '0901234567',
|
||||
email: 'a@example.com',
|
||||
source: 'website',
|
||||
score: 80,
|
||||
notes: { interest: 'căn hộ' },
|
||||
status: 'NEW',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const result = await repository.findById('lead-1');
|
||||
|
||||
expect(result).toBeInstanceOf(LeadEntity);
|
||||
expect(result!.id).toBe('lead-1');
|
||||
expect(result!.agentId).toBe('agent-1');
|
||||
expect(result!.name).toBe('Nguyễn Văn A');
|
||||
expect(result!.phone).toBe('0901234567');
|
||||
expect(result!.email).toBe('a@example.com');
|
||||
expect(result!.source).toBe('website');
|
||||
expect(result!.score!.value).toBe(80);
|
||||
expect(result!.status).toBe('NEW');
|
||||
});
|
||||
|
||||
it('returns a LeadEntity with null score', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.lead.findUnique.mockResolvedValue({
|
||||
id: 'lead-2',
|
||||
agentId: 'agent-1',
|
||||
name: 'Trần Thị B',
|
||||
phone: '0902345678',
|
||||
email: null,
|
||||
source: 'referral',
|
||||
score: null,
|
||||
notes: null,
|
||||
status: 'CONTACTED',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const result = await repository.findById('lead-2');
|
||||
|
||||
expect(result!.score).toBeNull();
|
||||
expect(result!.email).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when not found', async () => {
|
||||
mockPrisma.lead.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await repository.findById('nonexistent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
it('creates a Prisma record from the entity', async () => {
|
||||
mockPrisma.lead.create.mockResolvedValue(undefined);
|
||||
const score = LeadScore.create(80).unwrap();
|
||||
const entity = LeadEntity.createNew(
|
||||
'lead-1',
|
||||
'agent-1',
|
||||
'Nguyễn Văn A',
|
||||
'0901234567',
|
||||
'a@example.com',
|
||||
'website',
|
||||
score,
|
||||
{ interest: 'căn hộ' },
|
||||
);
|
||||
|
||||
await repository.save(entity);
|
||||
|
||||
expect(mockPrisma.lead.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
id: 'lead-1',
|
||||
agentId: 'agent-1',
|
||||
name: 'Nguyễn Văn A',
|
||||
phone: '0901234567',
|
||||
email: 'a@example.com',
|
||||
source: 'website',
|
||||
score: 80,
|
||||
notes: { interest: 'căn hộ' },
|
||||
status: 'NEW',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('saves entity with null score and email', async () => {
|
||||
mockPrisma.lead.create.mockResolvedValue(undefined);
|
||||
const entity = LeadEntity.createNew(
|
||||
'lead-2',
|
||||
'agent-1',
|
||||
'Trần Thị B',
|
||||
'0902345678',
|
||||
null,
|
||||
'referral',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
await repository.save(entity);
|
||||
|
||||
const callData = mockPrisma.lead.create.mock.calls[0]![0].data;
|
||||
expect(callData.score).toBeNull();
|
||||
expect(callData.email).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('updates the entity status and score', async () => {
|
||||
mockPrisma.lead.update.mockResolvedValue(undefined);
|
||||
const entity = LeadEntity.createNew(
|
||||
'lead-1',
|
||||
'agent-1',
|
||||
'Test',
|
||||
'0900000000',
|
||||
null,
|
||||
'website',
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
await repository.update(entity);
|
||||
|
||||
expect(mockPrisma.lead.update).toHaveBeenCalledWith({
|
||||
where: { id: 'lead-1' },
|
||||
data: {
|
||||
status: 'NEW',
|
||||
score: null,
|
||||
notes: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('deletes the lead by id', async () => {
|
||||
mockPrisma.lead.delete.mockResolvedValue(undefined);
|
||||
|
||||
await repository.delete('lead-1');
|
||||
|
||||
expect(mockPrisma.lead.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'lead-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByAgent', () => {
|
||||
it('returns paginated results', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.lead.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'lead-1',
|
||||
agentId: 'agent-1',
|
||||
name: 'Nguyễn Văn A',
|
||||
phone: '0901234567',
|
||||
email: 'a@example.com',
|
||||
source: 'website',
|
||||
score: 80,
|
||||
notes: null,
|
||||
status: 'NEW',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
]);
|
||||
mockPrisma.lead.count.mockResolvedValue(1);
|
||||
|
||||
const result = await repository.findByAgent('agent-1', null, 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]!.name).toBe('Nguyễn Văn A');
|
||||
});
|
||||
|
||||
it('filters by status when provided', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
mockPrisma.lead.count.mockResolvedValue(0);
|
||||
|
||||
await repository.findByAgent('agent-1', 'NEW', 1, 20);
|
||||
|
||||
expect(mockPrisma.lead.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { agentId: 'agent-1', status: 'NEW' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not include status filter when null', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
mockPrisma.lead.count.mockResolvedValue(0);
|
||||
|
||||
await repository.findByAgent('agent-1', null, 1, 20);
|
||||
|
||||
expect(mockPrisma.lead.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { agentId: 'agent-1' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('caps limit at 100', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
mockPrisma.lead.count.mockResolvedValue(0);
|
||||
|
||||
const result = await repository.findByAgent('agent-1', null, 1, 200);
|
||||
|
||||
expect(result.limit).toBe(100);
|
||||
expect(mockPrisma.lead.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ take: 100 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates correct skip for pagination', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
mockPrisma.lead.count.mockResolvedValue(0);
|
||||
|
||||
await repository.findByAgent('agent-1', null, 3, 10);
|
||||
|
||||
expect(mockPrisma.lead.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ skip: 20 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('calculates totalPages correctly', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
mockPrisma.lead.count.mockResolvedValue(45);
|
||||
|
||||
const result = await repository.findByAgent('agent-1', null, 1, 20);
|
||||
|
||||
expect(result.totalPages).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatsByAgent', () => {
|
||||
it('returns stats with conversion rate and avg score', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([
|
||||
{ status: 'NEW', score: 60 },
|
||||
{ status: 'CONTACTED', score: 80 },
|
||||
{ status: 'CONVERTED', score: 90 },
|
||||
{ status: 'LOST', score: 30 },
|
||||
]);
|
||||
|
||||
const result = await repository.getStatsByAgent('agent-1');
|
||||
|
||||
expect(result.totalLeads).toBe(4);
|
||||
expect(result.byStatus).toEqual({
|
||||
NEW: 1,
|
||||
CONTACTED: 1,
|
||||
CONVERTED: 1,
|
||||
LOST: 1,
|
||||
});
|
||||
expect(result.conversionRate).toBe(25);
|
||||
expect(result.avgScore).toBe(65);
|
||||
});
|
||||
|
||||
it('returns zero conversion rate for empty leads', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await repository.getStatsByAgent('agent-1');
|
||||
|
||||
expect(result.totalLeads).toBe(0);
|
||||
expect(result.byStatus).toEqual({});
|
||||
expect(result.conversionRate).toBe(0);
|
||||
expect(result.avgScore).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null avgScore when no leads have scores', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([
|
||||
{ status: 'NEW', score: null },
|
||||
{ status: 'CONTACTED', score: null },
|
||||
]);
|
||||
|
||||
const result = await repository.getStatsByAgent('agent-1');
|
||||
|
||||
expect(result.totalLeads).toBe(2);
|
||||
expect(result.avgScore).toBeNull();
|
||||
});
|
||||
|
||||
it('calculates avgScore only from scored leads', async () => {
|
||||
mockPrisma.lead.findMany.mockResolvedValue([
|
||||
{ status: 'NEW', score: 60 },
|
||||
{ status: 'CONTACTED', score: null },
|
||||
{ status: 'QUALIFIED', score: 80 },
|
||||
]);
|
||||
|
||||
const result = await repository.getStatsByAgent('agent-1');
|
||||
|
||||
expect(result.avgScore).toBe(70);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user