test: add unit tests for Analytics, Search, and Notifications modules
Add 15 test files with 45 test cases covering all untested handlers: - Analytics: track-event, generate-report, update-market-index, get-heatmap, get-price-trend, get-market-report, get-district-stats - Search: reindex-all, sync-listing, search-properties, geo-search, listing-approved event handler - Notifications: send-notification, agent-verified listener, user-registered listener Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { GenerateReportHandler } from '../commands/generate-report/generate-report.handler';
|
||||
import { GenerateReportCommand } from '../commands/generate-report/generate-report.command';
|
||||
import { type IMarketIndexRepository, type MarketReportResult } from '../../domain/repositories/market-index.repository';
|
||||
|
||||
describe('GenerateReportHandler', () => {
|
||||
let handler: GenerateReportHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new GenerateReportHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('generates a market report for a city and period', async () => {
|
||||
const reportData: MarketReportResult[] = [
|
||||
{
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType: 'APARTMENT',
|
||||
period: '2026-Q1',
|
||||
medianPrice: '5000000000',
|
||||
avgPriceM2: 80000000,
|
||||
totalListings: 150,
|
||||
daysOnMarket: 45,
|
||||
inventoryLevel: 3.2,
|
||||
absorptionRate: 0.65,
|
||||
yoyChange: 0.08,
|
||||
},
|
||||
];
|
||||
mockRepo.getMarketReport.mockResolvedValue(reportData);
|
||||
|
||||
const command = new GenerateReportCommand('Hồ Chí Minh', '2026-Q1', 'APARTMENT');
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.city).toBe('Hồ Chí Minh');
|
||||
expect(result.period).toBe('2026-Q1');
|
||||
expect(result.data).toEqual(reportData);
|
||||
expect(result.generatedAt).toBeDefined();
|
||||
expect(mockRepo.getMarketReport).toHaveBeenCalledWith('Hồ Chí Minh', '2026-Q1', 'APARTMENT');
|
||||
});
|
||||
|
||||
it('generates report without propertyType filter', async () => {
|
||||
mockRepo.getMarketReport.mockResolvedValue([]);
|
||||
|
||||
const command = new GenerateReportCommand('Hà Nội', '2026-Q1');
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.city).toBe('Hà Nội');
|
||||
expect(result.data).toEqual([]);
|
||||
expect(mockRepo.getMarketReport).toHaveBeenCalledWith('Hà Nội', '2026-Q1', undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { GetDistrictStatsHandler } from '../queries/get-district-stats/get-district-stats.handler';
|
||||
import { GetDistrictStatsQuery } from '../queries/get-district-stats/get-district-stats.query';
|
||||
import { type IMarketIndexRepository, type DistrictStatsResult } from '../../domain/repositories/market-index.repository';
|
||||
|
||||
describe('GetDistrictStatsHandler', () => {
|
||||
let handler: GetDistrictStatsHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new GetDistrictStatsHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('returns district statistics', async () => {
|
||||
const stats: DistrictStatsResult[] = [
|
||||
{
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType: 'APARTMENT',
|
||||
medianPrice: '6000000000',
|
||||
avgPriceM2: 120000000,
|
||||
totalListings: 200,
|
||||
daysOnMarket: 30,
|
||||
inventoryLevel: 2.5,
|
||||
absorptionRate: 0.8,
|
||||
yoyChange: 0.12,
|
||||
},
|
||||
{
|
||||
district: 'Quận 7',
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType: 'APARTMENT',
|
||||
medianPrice: '4000000000',
|
||||
avgPriceM2: 80000000,
|
||||
totalListings: 350,
|
||||
daysOnMarket: 40,
|
||||
inventoryLevel: 3.0,
|
||||
absorptionRate: 0.65,
|
||||
yoyChange: 0.05,
|
||||
},
|
||||
];
|
||||
mockRepo.getDistrictStats.mockResolvedValue(stats);
|
||||
|
||||
const query = new GetDistrictStatsQuery('Hồ Chí Minh', '2026-Q1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.city).toBe('Hồ Chí Minh');
|
||||
expect(result.period).toBe('2026-Q1');
|
||||
expect(result.districts).toEqual(stats);
|
||||
expect(mockRepo.getDistrictStats).toHaveBeenCalledWith('Hồ Chí Minh', '2026-Q1');
|
||||
});
|
||||
|
||||
it('returns empty array when no stats available', async () => {
|
||||
mockRepo.getDistrictStats.mockResolvedValue([]);
|
||||
|
||||
const query = new GetDistrictStatsQuery('Cần Thơ', '2026-Q1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.districts).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { GetHeatmapHandler } from '../queries/get-heatmap/get-heatmap.handler';
|
||||
import { GetHeatmapQuery } from '../queries/get-heatmap/get-heatmap.query';
|
||||
import { type IMarketIndexRepository, type HeatmapDataPoint } from '../../domain/repositories/market-index.repository';
|
||||
|
||||
describe('GetHeatmapHandler', () => {
|
||||
let handler: GetHeatmapHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new GetHeatmapHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('returns heatmap data for a city and period', async () => {
|
||||
const dataPoints: HeatmapDataPoint[] = [
|
||||
{ district: 'Quận 1', city: 'Hồ Chí Minh', avgPriceM2: 120000000, totalListings: 200, medianPrice: '6000000000' },
|
||||
{ district: 'Quận 7', city: 'Hồ Chí Minh', avgPriceM2: 80000000, totalListings: 350, medianPrice: '4000000000' },
|
||||
];
|
||||
mockRepo.getHeatmap.mockResolvedValue(dataPoints);
|
||||
|
||||
const query = new GetHeatmapQuery('Hồ Chí Minh', '2026-Q1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.city).toBe('Hồ Chí Minh');
|
||||
expect(result.period).toBe('2026-Q1');
|
||||
expect(result.dataPoints).toEqual(dataPoints);
|
||||
expect(mockRepo.getHeatmap).toHaveBeenCalledWith('Hồ Chí Minh', '2026-Q1');
|
||||
});
|
||||
|
||||
it('returns empty dataPoints when no data available', async () => {
|
||||
mockRepo.getHeatmap.mockResolvedValue([]);
|
||||
|
||||
const query = new GetHeatmapQuery('Đà Nẵng', '2025-Q4');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.dataPoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { GetMarketReportHandler } from '../queries/get-market-report/get-market-report.handler';
|
||||
import { GetMarketReportQuery } from '../queries/get-market-report/get-market-report.query';
|
||||
import { type IMarketIndexRepository, type MarketReportResult } from '../../domain/repositories/market-index.repository';
|
||||
|
||||
describe('GetMarketReportHandler', () => {
|
||||
let handler: GetMarketReportHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new GetMarketReportHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('returns market report with district data', async () => {
|
||||
const districts: MarketReportResult[] = [
|
||||
{
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType: 'APARTMENT',
|
||||
period: '2026-Q1',
|
||||
medianPrice: '5000000000',
|
||||
avgPriceM2: 80000000,
|
||||
totalListings: 150,
|
||||
daysOnMarket: 45,
|
||||
inventoryLevel: 3.2,
|
||||
absorptionRate: 0.65,
|
||||
yoyChange: 0.08,
|
||||
},
|
||||
];
|
||||
mockRepo.getMarketReport.mockResolvedValue(districts);
|
||||
|
||||
const query = new GetMarketReportQuery('Hồ Chí Minh', '2026-Q1', 'APARTMENT');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.city).toBe('Hồ Chí Minh');
|
||||
expect(result.period).toBe('2026-Q1');
|
||||
expect(result.districts).toEqual(districts);
|
||||
expect(mockRepo.getMarketReport).toHaveBeenCalledWith('Hồ Chí Minh', '2026-Q1', 'APARTMENT');
|
||||
});
|
||||
|
||||
it('returns report without propertyType filter', async () => {
|
||||
mockRepo.getMarketReport.mockResolvedValue([]);
|
||||
|
||||
const query = new GetMarketReportQuery('Hà Nội', '2026-Q1');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.districts).toEqual([]);
|
||||
expect(mockRepo.getMarketReport).toHaveBeenCalledWith('Hà Nội', '2026-Q1', undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { GetPriceTrendHandler } from '../queries/get-price-trend/get-price-trend.handler';
|
||||
import { GetPriceTrendQuery } from '../queries/get-price-trend/get-price-trend.query';
|
||||
import { type IMarketIndexRepository, type PriceTrendPoint } from '../../domain/repositories/market-index.repository';
|
||||
|
||||
describe('GetPriceTrendHandler', () => {
|
||||
let handler: GetPriceTrendHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new GetPriceTrendHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('returns price trend data for a district', async () => {
|
||||
const trendData: PriceTrendPoint[] = [
|
||||
{ period: '2025-Q4', medianPrice: '4500000000', avgPriceM2: 75000000, totalListings: 120 },
|
||||
{ period: '2026-Q1', medianPrice: '5000000000', avgPriceM2: 80000000, totalListings: 150 },
|
||||
];
|
||||
mockRepo.getPriceTrend.mockResolvedValue(trendData);
|
||||
|
||||
const query = new GetPriceTrendQuery('Quận 1', 'Hồ Chí Minh', 'APARTMENT', ['2025-Q4', '2026-Q1']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.district).toBe('Quận 1');
|
||||
expect(result.city).toBe('Hồ Chí Minh');
|
||||
expect(result.propertyType).toBe('APARTMENT');
|
||||
expect(result.trend).toEqual(trendData);
|
||||
expect(mockRepo.getPriceTrend).toHaveBeenCalledWith('Quận 1', 'Hồ Chí Minh', 'APARTMENT', ['2025-Q4', '2026-Q1']);
|
||||
});
|
||||
|
||||
it('returns empty trend when no data available', async () => {
|
||||
mockRepo.getPriceTrend.mockResolvedValue([]);
|
||||
|
||||
const query = new GetPriceTrendQuery('Quận 9', 'Hồ Chí Minh', 'HOUSE', ['2026-Q1']);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result.trend).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TrackEventHandler } from '../commands/track-event/track-event.handler';
|
||||
import { TrackEventCommand } from '../commands/track-event/track-event.command';
|
||||
|
||||
describe('TrackEventHandler', () => {
|
||||
let handler: TrackEventHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new TrackEventHandler();
|
||||
});
|
||||
|
||||
it('tracks an event and returns result', async () => {
|
||||
const command = new TrackEventCommand(
|
||||
'page_view',
|
||||
'listing-123',
|
||||
'LISTING',
|
||||
{ source: 'search' },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.tracked).toBe(true);
|
||||
expect(result.eventType).toBe('page_view');
|
||||
});
|
||||
|
||||
it('tracks event without userId', async () => {
|
||||
const command = new TrackEventCommand(
|
||||
'search',
|
||||
'query-abc',
|
||||
'SEARCH',
|
||||
{ keyword: 'Quận 7' },
|
||||
);
|
||||
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.tracked).toBe(true);
|
||||
expect(result.eventType).toBe('search');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { UpdateMarketIndexHandler } from '../commands/update-market-index/update-market-index.handler';
|
||||
import { UpdateMarketIndexCommand } from '../commands/update-market-index/update-market-index.command';
|
||||
import { type IMarketIndexRepository } from '../../domain/repositories/market-index.repository';
|
||||
import { MarketIndexEntity } from '../../domain/entities/market-index.entity';
|
||||
|
||||
function createExistingEntity(): MarketIndexEntity {
|
||||
return new MarketIndexEntity('idx-1', {
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
propertyType: 'APARTMENT',
|
||||
period: '2026-Q1',
|
||||
medianPrice: BigInt(4000000000),
|
||||
avgPriceM2: 70000000,
|
||||
totalListings: 100,
|
||||
daysOnMarket: 40,
|
||||
inventoryLevel: 3.0,
|
||||
absorptionRate: 0.6,
|
||||
yoyChange: 0.05,
|
||||
});
|
||||
}
|
||||
|
||||
describe('UpdateMarketIndexHandler', () => {
|
||||
let handler: UpdateMarketIndexHandler;
|
||||
let mockRepo: { [K in keyof IMarketIndexRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepo = {
|
||||
findById: vi.fn(),
|
||||
findByKey: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getMarketReport: vi.fn(),
|
||||
getHeatmap: vi.fn(),
|
||||
getPriceTrend: vi.fn(),
|
||||
getDistrictStats: vi.fn(),
|
||||
};
|
||||
handler = new UpdateMarketIndexHandler(mockRepo as any);
|
||||
});
|
||||
|
||||
it('creates a new market index when none exists', async () => {
|
||||
mockRepo.findByKey.mockResolvedValue(null);
|
||||
mockRepo.save.mockResolvedValue(undefined);
|
||||
|
||||
const command = new UpdateMarketIndexCommand(
|
||||
'Quận 7', 'Hồ Chí Minh', 'APARTMENT', '2026-Q1',
|
||||
BigInt(5000000000), 80000000, 150, 45, 3.2, 0.65, 0.08,
|
||||
);
|
||||
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.created).toBe(true);
|
||||
expect(result.id).toBeDefined();
|
||||
expect(mockRepo.save).toHaveBeenCalledTimes(1);
|
||||
expect(mockRepo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates an existing market index', async () => {
|
||||
const existing = createExistingEntity();
|
||||
mockRepo.findByKey.mockResolvedValue(existing);
|
||||
mockRepo.update.mockResolvedValue(undefined);
|
||||
|
||||
const command = new UpdateMarketIndexCommand(
|
||||
'Quận 1', 'Hồ Chí Minh', 'APARTMENT', '2026-Q1',
|
||||
BigInt(5500000000), 85000000, 160, 42, 3.5, 0.7, 0.1,
|
||||
);
|
||||
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.created).toBe(false);
|
||||
expect(result.id).toBe('idx-1');
|
||||
expect(mockRepo.update).toHaveBeenCalledTimes(1);
|
||||
expect(mockRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes correct key parameters to findByKey', async () => {
|
||||
mockRepo.findByKey.mockResolvedValue(null);
|
||||
mockRepo.save.mockResolvedValue(undefined);
|
||||
|
||||
const command = new UpdateMarketIndexCommand(
|
||||
'Quận 3', 'Hồ Chí Minh', 'HOUSE', '2026-Q2',
|
||||
BigInt(3000000000), 60000000, 80, 50, 2.5,
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockRepo.findByKey).toHaveBeenCalledWith('Quận 3', 'Hồ Chí Minh', 'HOUSE', '2026-Q2');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user