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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export default tseslint.config(
|
||||
'**/*.cjs',
|
||||
'**/*.mjs',
|
||||
'!eslint.config.mjs',
|
||||
'**/next-env.d.ts',
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user