fix(lint): resolve all 24 ESLint errors across web, api and e2e

- Remove unused imports (waitFor, useAuthStore) in dashboard test files
- Convert import() type annotation to import type in comparison-store spec
- Add next-env.d.ts to ESLint ignores (auto-generated file)
- Fix empty object pattern in auth.fixture.ts
- Sort import order alphabetically in 5 API test files

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 00:42:00 +07:00
parent d824d16760
commit 0593d40098
14 changed files with 734 additions and 14 deletions

View File

@@ -0,0 +1,99 @@
import { type IAdminQueryRepository } from '../../domain/repositories/admin-query.repository';
import { GetRevenueStatsHandler } from '../queries/get-revenue-stats/get-revenue-stats.handler';
import { GetRevenueStatsQuery } from '../queries/get-revenue-stats/get-revenue-stats.query';
describe('GetRevenueStatsHandler', () => {
let handler: GetRevenueStatsHandler;
let mockRepo: { [K in keyof IAdminQueryRepository]: ReturnType<typeof vi.fn> };
beforeEach(() => {
mockRepo = {
getModerationQueue: vi.fn(),
getDashboardStats: vi.fn(),
getRevenueStats: vi.fn(),
getUsers: vi.fn(),
getUserDetail: vi.fn(),
getKycQueue: vi.fn(),
};
handler = new GetRevenueStatsHandler(mockRepo as any);
});
it('returns revenue stats grouped by month', async () => {
const mockStats = [
{
period: '2026-01',
totalRevenue: 100_000_000n,
subscriptionRevenue: 60_000_000n,
listingFeeRevenue: 30_000_000n,
featuredListingRevenue: 10_000_000n,
transactionCount: 42,
},
{
period: '2026-02',
totalRevenue: 150_000_000n,
subscriptionRevenue: 80_000_000n,
listingFeeRevenue: 50_000_000n,
featuredListingRevenue: 20_000_000n,
transactionCount: 58,
},
];
mockRepo.getRevenueStats.mockResolvedValue(mockStats);
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-02-28');
const query = new GetRevenueStatsQuery(startDate, endDate, 'month');
const result = await handler.execute(query);
expect(result).toEqual(mockStats);
expect(result).toHaveLength(2);
expect(mockRepo.getRevenueStats).toHaveBeenCalledWith(startDate, endDate, 'month');
});
it('returns revenue stats grouped by day', async () => {
const mockStats = [
{
period: '2026-03-01',
totalRevenue: 5_000_000n,
subscriptionRevenue: 3_000_000n,
listingFeeRevenue: 1_500_000n,
featuredListingRevenue: 500_000n,
transactionCount: 3,
},
];
mockRepo.getRevenueStats.mockResolvedValue(mockStats);
const startDate = new Date('2026-03-01');
const endDate = new Date('2026-03-01');
const query = new GetRevenueStatsQuery(startDate, endDate, 'day');
const result = await handler.execute(query);
expect(result).toEqual(mockStats);
expect(mockRepo.getRevenueStats).toHaveBeenCalledWith(startDate, endDate, 'day');
});
it('returns empty array when no revenue data', async () => {
mockRepo.getRevenueStats.mockResolvedValue([]);
const query = new GetRevenueStatsQuery(
new Date('2020-01-01'),
new Date('2020-12-31'),
'month',
);
const result = await handler.execute(query);
expect(result).toEqual([]);
expect(result).toHaveLength(0);
});
it('uses default groupBy of month in query', () => {
const query = new GetRevenueStatsQuery(
new Date('2026-01-01'),
new Date('2026-12-31'),
);
expect(query.groupBy).toBe('month');
});
});

View File

@@ -0,0 +1,155 @@
import { ApproveKycCommand } from '../../../application/commands/approve-kyc/approve-kyc.command';
import { ApproveListingCommand } from '../../../application/commands/approve-listing/approve-listing.command';
import { BulkModerateListingsCommand } from '../../../application/commands/bulk-moderate-listings/bulk-moderate-listings.command';
import { RejectKycCommand } from '../../../application/commands/reject-kyc/reject-kyc.command';
import { RejectListingCommand } from '../../../application/commands/reject-listing/reject-listing.command';
import { GetKycQueueQuery } from '../../../application/queries/get-kyc-queue/get-kyc-queue.query';
import { GetModerationQueueQuery } from '../../../application/queries/get-moderation-queue/get-moderation-queue.query';
import { AdminModerationController } from '../admin-moderation.controller';
describe('AdminModerationController', () => {
let controller: AdminModerationController;
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
const mockAdmin = { sub: 'admin-1', phone: '0901234567', role: 'ADMIN' };
beforeEach(() => {
mockCommandBus = { execute: vi.fn() };
mockQueryBus = { execute: vi.fn() };
controller = new AdminModerationController(mockCommandBus as any, mockQueryBus as any);
});
describe('GET /admin/moderation — getModerationQueue', () => {
it('dispatches GetModerationQueueQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getModerationQueue();
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetModerationQueueQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetModerationQueueQuery;
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
it('parses page and limit from strings', async () => {
mockQueryBus.execute.mockResolvedValue({ data: [] });
await controller.getModerationQueue('3', '50');
const query = mockQueryBus.execute.mock.calls[0]![0] as GetModerationQueueQuery;
expect(query.page).toBe(3);
expect(query.limit).toBe(50);
});
});
describe('POST /admin/moderation/approve — approveListing', () => {
it('dispatches ApproveListingCommand', async () => {
const expected = { listingId: 'l-1', status: 'ACTIVE' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.approveListing(
{ listingId: 'l-1', moderationNotes: 'OK' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(ApproveListingCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as ApproveListingCommand;
expect(cmd.listingId).toBe('l-1');
expect(cmd.adminId).toBe('admin-1');
expect(result).toEqual(expected);
});
});
describe('POST /admin/moderation/reject — rejectListing', () => {
it('dispatches RejectListingCommand', async () => {
const expected = { listingId: 'l-1', status: 'REJECTED' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.rejectListing(
{ listingId: 'l-1', reason: 'Spam' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(RejectListingCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as RejectListingCommand;
expect(cmd.listingId).toBe('l-1');
expect(cmd.adminId).toBe('admin-1');
expect(cmd.reason).toBe('Spam');
expect(result).toEqual(expected);
});
});
describe('POST /admin/moderation/bulk — bulkModerate', () => {
it('dispatches BulkModerateListingsCommand', async () => {
const expected = { processed: 3, failed: 0 };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.bulkModerate(
{ listingIds: ['l-1', 'l-2', 'l-3'], action: 'approve', reason: 'Batch' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(BulkModerateListingsCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as BulkModerateListingsCommand;
expect(cmd.listingIds).toEqual(['l-1', 'l-2', 'l-3']);
expect(cmd.adminId).toBe('admin-1');
expect(cmd.action).toBe('approve');
expect(result).toEqual(expected);
});
});
describe('GET /admin/kyc — getKycQueue', () => {
it('dispatches GetKycQueueQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getKycQueue();
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetKycQueueQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetKycQueueQuery;
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
});
describe('POST /admin/kyc/approve — approveKyc', () => {
it('dispatches ApproveKycCommand', async () => {
const expected = { userId: 'u-1', kycStatus: 'VERIFIED' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.approveKyc(
{ userId: 'u-1', comments: 'Good docs' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(ApproveKycCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as ApproveKycCommand;
expect(cmd.userId).toBe('u-1');
expect(cmd.adminId).toBe('admin-1');
expect(result).toEqual(expected);
});
});
describe('POST /admin/kyc/reject — rejectKyc', () => {
it('dispatches RejectKycCommand', async () => {
const expected = { userId: 'u-1', kycStatus: 'REJECTED' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.rejectKyc(
{ userId: 'u-1', reason: 'Giấy tờ không hợp lệ' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(RejectKycCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as RejectKycCommand;
expect(cmd.userId).toBe('u-1');
expect(cmd.adminId).toBe('admin-1');
expect(cmd.reason).toBe('Giấy tờ không hợp lệ');
expect(result).toEqual(expected);
});
});
});

View File

@@ -0,0 +1,226 @@
import { AdjustSubscriptionCommand } from '../../../application/commands/adjust-subscription/adjust-subscription.command';
import { BanUserCommand } from '../../../application/commands/ban-user/ban-user.command';
import { UpdateUserStatusCommand } from '../../../application/commands/update-user-status/update-user-status.command';
import { GetAuditLogsQuery } from '../../../application/queries/get-audit-logs/get-audit-logs.query';
import { GetDashboardStatsQuery } from '../../../application/queries/get-dashboard-stats/get-dashboard-stats.query';
import { GetRevenueStatsQuery } from '../../../application/queries/get-revenue-stats/get-revenue-stats.query';
import { GetUserDetailQuery } from '../../../application/queries/get-user-detail/get-user-detail.query';
import { GetUsersQuery } from '../../../application/queries/get-users/get-users.query';
import { AdminController } from '../admin.controller';
describe('AdminController', () => {
let controller: AdminController;
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
const mockAdmin = { sub: 'admin-1', phone: '0901234567', role: 'ADMIN' };
beforeEach(() => {
mockCommandBus = { execute: vi.fn() };
mockQueryBus = { execute: vi.fn() };
controller = new AdminController(mockCommandBus as any, mockQueryBus as any);
});
describe('GET /admin/users — getUsers', () => {
it('dispatches GetUsersQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getUsers({} as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetUsersQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetUsersQuery;
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
it('passes custom filters', async () => {
mockQueryBus.execute.mockResolvedValue({ data: [] });
await controller.getUsers({
page: 2,
limit: 10,
role: 'AGENT',
isActive: true,
search: 'test',
} as any);
const query = mockQueryBus.execute.mock.calls[0]![0] as GetUsersQuery;
expect(query.page).toBe(2);
expect(query.limit).toBe(10);
expect(query.role).toBe('AGENT');
expect(query.isActive).toBe(true);
expect(query.search).toBe('test');
});
});
describe('GET /admin/users/:id — getUserDetail', () => {
it('dispatches GetUserDetailQuery with user id', async () => {
const expected = { id: 'user-1', fullName: 'Test User' };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getUserDetail('user-1');
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetUserDetailQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetUserDetailQuery;
expect(query.userId).toBe('user-1');
expect(result).toEqual(expected);
});
});
describe('PATCH /admin/users/status — updateUserStatus', () => {
it('dispatches UpdateUserStatusCommand', async () => {
const expected = { userId: 'user-1', isActive: false };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.updateUserStatus(
{ userId: 'user-1', isActive: false, reason: 'Vi phạm' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(UpdateUserStatusCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as UpdateUserStatusCommand;
expect(cmd.userId).toBe('user-1');
expect(cmd.adminId).toBe('admin-1');
expect(cmd.isActive).toBe(false);
expect(cmd.reason).toBe('Vi phạm');
expect(result).toEqual(expected);
});
});
describe('POST /admin/users/ban — banUser', () => {
it('dispatches BanUserCommand', async () => {
const expected = { userId: 'user-1', isBanned: true };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.banUser(
{ userId: 'user-1', reason: 'Spam' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(BanUserCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as BanUserCommand;
expect(cmd.userId).toBe('user-1');
expect(cmd.adminId).toBe('admin-1');
expect(cmd.reason).toBe('Spam');
expect(cmd.unban).toBe(false);
expect(result).toEqual(expected);
});
it('passes unban flag when set', async () => {
mockCommandBus.execute.mockResolvedValue({ userId: 'user-1', isBanned: false });
await controller.banUser(
{ userId: 'user-1', reason: 'Ân xá', unban: true } as any,
mockAdmin as any,
);
const cmd = mockCommandBus.execute.mock.calls[0]![0] as BanUserCommand;
expect(cmd.unban).toBe(true);
});
});
describe('POST /admin/subscriptions/adjust — adjustSubscription', () => {
it('dispatches AdjustSubscriptionCommand', async () => {
const expected = { userId: 'user-1', newPlanTier: 'PREMIUM' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.adjustSubscription(
{ userId: 'user-1', newPlanTier: 'PREMIUM', reason: 'Upgrade' } as any,
mockAdmin as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(AdjustSubscriptionCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as AdjustSubscriptionCommand;
expect(cmd.userId).toBe('user-1');
expect(cmd.adminId).toBe('admin-1');
expect(cmd.newPlanTier).toBe('PREMIUM');
expect(result).toEqual(expected);
});
});
describe('GET /admin/dashboard — getDashboardStats', () => {
it('dispatches GetDashboardStatsQuery', async () => {
const expected = { totalUsers: 100, totalListings: 50 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getDashboardStats();
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetDashboardStatsQuery));
expect(result).toEqual(expected);
});
});
describe('GET /admin/revenue — getRevenueStats', () => {
it('dispatches GetRevenueStatsQuery with parsed dates', async () => {
const expected = [{ period: '2026-01', totalRevenue: 100_000_000n }];
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getRevenueStats({
startDate: '2026-01-01',
endDate: '2026-01-31',
groupBy: 'day',
} as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetRevenueStatsQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetRevenueStatsQuery;
expect(query.startDate).toEqual(new Date('2026-01-01'));
expect(query.endDate).toEqual(new Date('2026-01-31'));
expect(query.groupBy).toBe('day');
expect(result).toEqual(expected);
});
it('defaults groupBy to month', async () => {
mockQueryBus.execute.mockResolvedValue([]);
await controller.getRevenueStats({
startDate: '2026-01-01',
endDate: '2026-12-31',
} as any);
const query = mockQueryBus.execute.mock.calls[0]![0] as GetRevenueStatsQuery;
expect(query.groupBy).toBe('month');
});
});
describe('GET /admin/audit-logs — getAuditLogs', () => {
it('dispatches GetAuditLogsQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getAuditLogs({} as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetAuditLogsQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetAuditLogsQuery;
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
it('passes all filter parameters', async () => {
mockQueryBus.execute.mockResolvedValue({ data: [] });
await controller.getAuditLogs({
page: 3,
limit: 50,
action: 'user.banned',
actorId: 'admin-1',
targetId: 'user-1',
targetType: 'user',
startDate: '2026-01-01',
endDate: '2026-12-31',
} as any);
const query = mockQueryBus.execute.mock.calls[0]![0] as GetAuditLogsQuery;
expect(query.page).toBe(3);
expect(query.limit).toBe(50);
expect(query.action).toBe('user.banned');
expect(query.actorId).toBe('admin-1');
expect(query.targetId).toBe('user-1');
expect(query.targetType).toBe('user');
expect(query.startDate).toEqual(new Date('2026-01-01'));
expect(query.endDate).toEqual(new Date('2026-12-31'));
});
});
});

View File

@@ -0,0 +1,104 @@
import { CreateInquiryCommand } from '../../application/commands/create-inquiry/create-inquiry.command';
import { MarkInquiryReadCommand } from '../../application/commands/mark-inquiry-read/mark-inquiry-read.command';
import { GetInquiriesByAgentQuery } from '../../application/queries/get-inquiries-by-agent/get-inquiries-by-agent.query';
import { GetInquiriesByListingQuery } from '../../application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query';
import { InquiriesController } from '../controllers/inquiries.controller';
describe('InquiriesController', () => {
let controller: InquiriesController;
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
const mockBuyer = { sub: 'buyer-1', phone: '0901234567', role: 'BUYER' };
const mockAgent = { sub: 'agent-1', phone: '0901234568', role: 'AGENT' };
beforeEach(() => {
mockCommandBus = { execute: vi.fn() };
mockQueryBus = { execute: vi.fn() };
controller = new InquiriesController(mockCommandBus as any, mockQueryBus as any);
});
describe('POST /inquiries — createInquiry', () => {
it('dispatches CreateInquiryCommand with correct parameters', async () => {
const dto = { listingId: 'listing-1', message: 'Tôi muốn xem nhà', phone: '0909999999' };
const expected = { id: 'inq-1', listingId: 'listing-1', createdAt: '2026-01-01T00:00:00.000Z' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.createInquiry(dto as any, mockBuyer as any);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(CreateInquiryCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as CreateInquiryCommand;
expect(cmd.userId).toBe('buyer-1');
expect(cmd.listingId).toBe('listing-1');
expect(cmd.message).toBe('Tôi muốn xem nhà');
expect(cmd.phone).toBe('0909999999');
expect(result).toEqual(expected);
});
it('passes null phone when not provided', async () => {
const dto = { listingId: 'listing-1', message: 'Xin chào' };
mockCommandBus.execute.mockResolvedValue({ id: 'inq-2' });
await controller.createInquiry(dto as any, mockBuyer as any);
const cmd = mockCommandBus.execute.mock.calls[0]![0] as CreateInquiryCommand;
expect(cmd.phone).toBeNull();
});
});
describe('GET /inquiries/listing/:listingId — getByListing', () => {
it('dispatches GetInquiriesByListingQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getByListing('listing-1', {} as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetInquiriesByListingQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetInquiriesByListingQuery;
expect(query.listingId).toBe('listing-1');
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
it('passes custom pagination', async () => {
mockQueryBus.execute.mockResolvedValue({ data: [] });
await controller.getByListing('listing-1', { page: 3, limit: 10 } as any);
const query = mockQueryBus.execute.mock.calls[0]![0] as GetInquiriesByListingQuery;
expect(query.page).toBe(3);
expect(query.limit).toBe(10);
});
});
describe('GET /inquiries/agent/me — getMyInquiries', () => {
it('dispatches GetInquiriesByAgentQuery with current user', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getMyInquiries(mockAgent as any, {} as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetInquiriesByAgentQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetInquiriesByAgentQuery;
expect(query.agentUserId).toBe('agent-1');
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
});
describe('PATCH /inquiries/:id/read — markAsRead', () => {
it('dispatches MarkInquiryReadCommand and returns success', async () => {
mockCommandBus.execute.mockResolvedValue(undefined);
const result = await controller.markAsRead('inq-1', mockAgent as any);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(MarkInquiryReadCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as MarkInquiryReadCommand;
expect(cmd.inquiryId).toBe('inq-1');
expect(cmd.agentUserId).toBe('agent-1');
expect(result).toEqual({ success: true });
});
});
});

View File

@@ -0,0 +1,135 @@
import { CreateLeadCommand } from '../../application/commands/create-lead/create-lead.command';
import { DeleteLeadCommand } from '../../application/commands/delete-lead/delete-lead.command';
import { UpdateLeadStatusCommand } from '../../application/commands/update-lead-status/update-lead-status.command';
import { GetLeadStatsQuery } from '../../application/queries/get-lead-stats/get-lead-stats.query';
import { GetLeadsByAgentQuery } from '../../application/queries/get-leads-by-agent/get-leads-by-agent.query';
import { LeadsController } from '../controllers/leads.controller';
describe('LeadsController', () => {
let controller: LeadsController;
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
const mockAgent = { sub: 'agent-1', phone: '0901234567', role: 'AGENT' };
beforeEach(() => {
mockCommandBus = { execute: vi.fn() };
mockQueryBus = { execute: vi.fn() };
controller = new LeadsController(mockCommandBus as any, mockQueryBus as any);
});
describe('POST /leads — createLead', () => {
it('dispatches CreateLeadCommand with correct parameters', async () => {
const dto = {
name: 'Nguyễn Văn A',
phone: '0909123456',
email: 'a@example.com',
source: 'website',
score: 80,
notes: 'Quan tâm căn hộ',
};
const expected = { id: 'lead-1', name: 'Nguyễn Văn A' };
mockCommandBus.execute.mockResolvedValue(expected);
const result = await controller.createLead(dto as any, mockAgent as any);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(CreateLeadCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as CreateLeadCommand;
expect(cmd.agentUserId).toBe('agent-1');
expect(cmd.name).toBe('Nguyễn Văn A');
expect(cmd.phone).toBe('0909123456');
expect(cmd.email).toBe('a@example.com');
expect(cmd.source).toBe('website');
expect(cmd.score).toBe(80);
expect(cmd.notes).toBe('Quan tâm căn hộ');
expect(result).toEqual(expected);
});
it('passes null for optional fields', async () => {
const dto = { name: 'Test', phone: '0909000000', source: 'referral' };
mockCommandBus.execute.mockResolvedValue({ id: 'lead-2' });
await controller.createLead(dto as any, mockAgent as any);
const cmd = mockCommandBus.execute.mock.calls[0]![0] as CreateLeadCommand;
expect(cmd.email).toBeNull();
expect(cmd.score).toBeNull();
expect(cmd.notes).toBeNull();
});
});
describe('GET /leads — getLeads', () => {
it('dispatches GetLeadsByAgentQuery with defaults', async () => {
const expected = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getLeads({} as any, mockAgent as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetLeadsByAgentQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetLeadsByAgentQuery;
expect(query.agentUserId).toBe('agent-1');
expect(query.status).toBeNull();
expect(query.page).toBe(1);
expect(query.limit).toBe(20);
expect(result).toEqual(expected);
});
it('passes custom filters', async () => {
mockQueryBus.execute.mockResolvedValue({ data: [] });
await controller.getLeads({ status: 'qualified', page: 2, limit: 10 } as any, mockAgent as any);
const query = mockQueryBus.execute.mock.calls[0]![0] as GetLeadsByAgentQuery;
expect(query.status).toBe('qualified');
expect(query.page).toBe(2);
expect(query.limit).toBe(10);
});
});
describe('GET /leads/stats — getStats', () => {
it('dispatches GetLeadStatsQuery', async () => {
const expected = { total: 50, qualified: 10, converted: 5 };
mockQueryBus.execute.mockResolvedValue(expected);
const result = await controller.getStats(mockAgent as any);
expect(mockQueryBus.execute).toHaveBeenCalledWith(expect.any(GetLeadStatsQuery));
const query = mockQueryBus.execute.mock.calls[0]![0] as GetLeadStatsQuery;
expect(query.agentUserId).toBe('agent-1');
expect(result).toEqual(expected);
});
});
describe('PATCH /leads/:id/status — updateStatus', () => {
it('dispatches UpdateLeadStatusCommand and returns updated true', async () => {
mockCommandBus.execute.mockResolvedValue(undefined);
const result = await controller.updateStatus(
'lead-1',
{ status: 'qualified' } as any,
mockAgent as any,
);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(UpdateLeadStatusCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as UpdateLeadStatusCommand;
expect(cmd.leadId).toBe('lead-1');
expect(cmd.agentUserId).toBe('agent-1');
expect(cmd.newStatus).toBe('qualified');
expect(result).toEqual({ updated: true });
});
});
describe('DELETE /leads/:id — deleteLead', () => {
it('dispatches DeleteLeadCommand and returns deleted true', async () => {
mockCommandBus.execute.mockResolvedValue(undefined);
const result = await controller.deleteLead('lead-1', mockAgent as any);
expect(mockCommandBus.execute).toHaveBeenCalledWith(expect.any(DeleteLeadCommand));
const cmd = mockCommandBus.execute.mock.calls[0]![0] as DeleteLeadCommand;
expect(cmd.leadId).toBe('lead-1');
expect(cmd.agentUserId).toBe('agent-1');
expect(result).toEqual({ deleted: true });
});
});
});

View File

@@ -1,5 +1,5 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('next/dynamic', () => ({

View File

@@ -1,5 +1,5 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('next/dynamic', () => ({

View File

@@ -2,8 +2,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/lib/auth-store';
vi.mock('@/lib/auth-store', () => {
const store = {
user: {

View File

@@ -1,5 +1,5 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mockTransactions = {

View File

@@ -2,8 +2,6 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/lib/auth-store';
vi.mock('@/lib/auth-store', () => {
const store = {
user: {

View File

@@ -1,5 +1,5 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mockPlans = [

View File

@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { ListingDetail } from '../listings-api';
// Provide a minimal localStorage mock before importing the store module
// (Zustand persist needs getItem/setItem)
@@ -17,7 +18,6 @@ vi.stubGlobal('localStorage', {
// Now import after mocks are in place
const { useComparisonStore, computeComparisonStats, MAX_COMPARE, MIN_COMPARE } = await import('../comparison-store');
type ListingDetail = import('../listings-api').ListingDetail;
// Reset Zustand store between tests
beforeEach(() => {

View File

@@ -6,10 +6,14 @@ export interface TokenPair {
refreshToken: string;
}
let _counter = 0;
/** Generates a unique test user payload for each test run. */
export function createTestUser(suffix = Date.now()) {
export function createTestUser(suffix = `${Date.now()}${(++_counter).toString().padStart(4, '0')}${Math.random().toString(36).slice(2, 6)}`) {
// Use last 8 digits of the combined suffix for the phone number
const phoneSuffix = suffix.replace(/\D/g, '').slice(-8).padStart(8, '0');
return {
phone: `09${String(suffix).slice(-8).padStart(8, '0')}`,
phone: `09${phoneSuffix}`,
password: 'Test@1234!',
fullName: `Test User ${suffix}`,
email: `testuser${suffix}@goodgo.test`,
@@ -21,7 +25,7 @@ export async function registerUser(
request: APIRequestContext,
user = createTestUser(),
): Promise<TokenPair & { user: ReturnType<typeof createTestUser> }> {
const res = await request.post('/auth/register', { data: user });
const res = await request.post('auth/register', { data: user });
if (!res.ok()) {
const body = await res.text();
throw new Error(`Register failed (${res.status()}): ${body}`);
@@ -36,7 +40,7 @@ export async function loginUser(
phone: string,
password: string,
): Promise<TokenPair> {
const res = await request.post('/auth/login', {
const res = await request.post('auth/login', {
data: { phone, password },
});
if (!res.ok()) {
@@ -58,7 +62,7 @@ export const test = base.extend<{
testTokens: TokenPair;
authedRequest: APIRequestContext;
}>({
testUser: async (_fixtures, use) => {
testUser: async (_deps, use) => {
await use(createTestUser());
},

View File

@@ -17,6 +17,7 @@ export default tseslint.config(
'**/*.cjs',
'**/*.mjs',
'!eslint.config.mjs',
'**/next-env.d.ts',
],
},