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,100 @@
|
||||
import { GeoSearchHandler } from '../queries/geo-search/geo-search.handler';
|
||||
import { GeoSearchQuery } from '../queries/geo-search/geo-search.query';
|
||||
import { type ISearchRepository, type SearchResult } from '../../domain/repositories/search.repository';
|
||||
|
||||
function createMockSearchResult(overrides?: Partial<SearchResult>): SearchResult {
|
||||
return {
|
||||
hits: [],
|
||||
totalFound: 0,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
totalPages: 0,
|
||||
searchTimeMs: 5,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('GeoSearchHandler', () => {
|
||||
let handler: GeoSearchHandler;
|
||||
let mockSearchRepo: { [K in keyof ISearchRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearchRepo = {
|
||||
indexDocument: vi.fn(),
|
||||
indexDocuments: vi.fn(),
|
||||
removeDocument: vi.fn(),
|
||||
search: vi.fn(),
|
||||
ensureCollection: vi.fn(),
|
||||
dropCollection: vi.fn(),
|
||||
};
|
||||
handler = new GeoSearchHandler(mockSearchRepo as any);
|
||||
});
|
||||
|
||||
it('performs geo search with basic parameters', async () => {
|
||||
const expected = createMockSearchResult({ totalFound: 10 });
|
||||
mockSearchRepo.search.mockResolvedValue(expected);
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 5);
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
expect(mockSearchRepo.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: '*',
|
||||
geoPoint: { lat: 10.7769, lng: 106.7009 },
|
||||
geoRadiusKm: 5,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('caps radius at 100km', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 200);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.geoRadiusKm).toBe(100);
|
||||
});
|
||||
|
||||
it('applies property and transaction type filters', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 10, 'APARTMENT', 'RENT');
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('propertyType:=APARTMENT');
|
||||
expect(searchCall.filterBy).toContain('transactionType:=RENT');
|
||||
});
|
||||
|
||||
it('applies price range filter', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 5, undefined, undefined, 1000000000, 5000000000);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('priceVND:[1000000000..5000000000]');
|
||||
});
|
||||
|
||||
it('applies only priceMin filter', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 5, undefined, undefined, 2000000000);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('priceVND:>=2000000000');
|
||||
});
|
||||
|
||||
it('applies only priceMax filter', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new GeoSearchQuery(10.7769, 106.7009, 5, undefined, undefined, undefined, 3000000000);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('priceVND:<=3000000000');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ReindexAllHandler } from '../commands/reindex-all/reindex-all.handler';
|
||||
|
||||
describe('ReindexAllHandler', () => {
|
||||
let handler: ReindexAllHandler;
|
||||
let mockIndexer: { reindexAll: ReturnType<typeof vi.fn>; indexListing: ReturnType<typeof vi.fn>; removeListing: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockIndexer = {
|
||||
reindexAll: vi.fn(),
|
||||
indexListing: vi.fn(),
|
||||
removeListing: vi.fn(),
|
||||
};
|
||||
handler = new ReindexAllHandler(mockIndexer as any);
|
||||
});
|
||||
|
||||
it('delegates to indexer.reindexAll and returns result', async () => {
|
||||
mockIndexer.reindexAll.mockResolvedValue({ indexed: 500, total: 500 });
|
||||
|
||||
const result = await handler.execute();
|
||||
|
||||
expect(result.indexed).toBe(500);
|
||||
expect(result.total).toBe(500);
|
||||
expect(mockIndexer.reindexAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns zero counts when no listings exist', async () => {
|
||||
mockIndexer.reindexAll.mockResolvedValue({ indexed: 0, total: 0 });
|
||||
|
||||
const result = await handler.execute();
|
||||
|
||||
expect(result.indexed).toBe(0);
|
||||
expect(result.total).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { SearchPropertiesHandler } from '../queries/search-properties/search-properties.handler';
|
||||
import { SearchPropertiesQuery } from '../queries/search-properties/search-properties.query';
|
||||
import { type ISearchRepository, type SearchResult } from '../../domain/repositories/search.repository';
|
||||
|
||||
function createMockSearchResult(overrides?: Partial<SearchResult>): SearchResult {
|
||||
return {
|
||||
hits: [],
|
||||
totalFound: 0,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
totalPages: 0,
|
||||
searchTimeMs: 5,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('SearchPropertiesHandler', () => {
|
||||
let handler: SearchPropertiesHandler;
|
||||
let mockSearchRepo: { [K in keyof ISearchRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockSearchRepo = {
|
||||
indexDocument: vi.fn(),
|
||||
indexDocuments: vi.fn(),
|
||||
removeDocument: vi.fn(),
|
||||
search: vi.fn(),
|
||||
ensureCollection: vi.fn(),
|
||||
dropCollection: vi.fn(),
|
||||
};
|
||||
handler = new SearchPropertiesHandler(mockSearchRepo as any);
|
||||
});
|
||||
|
||||
it('searches with basic query', async () => {
|
||||
const expected = createMockSearchResult({ totalFound: 5 });
|
||||
mockSearchRepo.search.mockResolvedValue(expected);
|
||||
|
||||
const query = new SearchPropertiesQuery('Quận 7 căn hộ');
|
||||
const result = await handler.execute(query);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
expect(mockSearchRepo.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: 'Quận 7 căn hộ',
|
||||
filterBy: 'status:=ACTIVE',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('applies all filters correctly', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new SearchPropertiesQuery(
|
||||
'nhà phố', 'HOUSE', 'SALE', 2000000000, 5000000000,
|
||||
50, 200, 3, 'Quận 7', 'Hồ Chí Minh', 'price_asc', 1, 20,
|
||||
);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('status:=ACTIVE');
|
||||
expect(searchCall.filterBy).toContain('propertyType:=HOUSE');
|
||||
expect(searchCall.filterBy).toContain('transactionType:=SALE');
|
||||
expect(searchCall.filterBy).toContain('priceVND:[2000000000..5000000000]');
|
||||
expect(searchCall.filterBy).toContain('areaM2:[50..200]');
|
||||
expect(searchCall.filterBy).toContain('bedrooms:>=3');
|
||||
expect(searchCall.filterBy).toContain('district:=Quận 7');
|
||||
expect(searchCall.filterBy).toContain('city:=Hồ Chí Minh');
|
||||
});
|
||||
|
||||
it('applies only priceMin when priceMax is undefined', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new SearchPropertiesQuery(undefined, undefined, undefined, 1000000000);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('priceVND:>=1000000000');
|
||||
});
|
||||
|
||||
it('applies only priceMax when priceMin is undefined', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new SearchPropertiesQuery(undefined, undefined, undefined, undefined, 5000000000);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('priceVND:<=5000000000');
|
||||
});
|
||||
|
||||
it('applies only areaMin when areaMax is undefined', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new SearchPropertiesQuery(undefined, undefined, undefined, undefined, undefined, 50);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('areaM2:>=50');
|
||||
});
|
||||
|
||||
it('applies only areaMax when areaMin is undefined', async () => {
|
||||
mockSearchRepo.search.mockResolvedValue(createMockSearchResult());
|
||||
|
||||
const query = new SearchPropertiesQuery(undefined, undefined, undefined, undefined, undefined, undefined, 200);
|
||||
await handler.execute(query);
|
||||
|
||||
const searchCall = mockSearchRepo.search.mock.calls[0]![0];
|
||||
expect(searchCall.filterBy).toContain('areaM2:<=200');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { SyncListingHandler } from '../commands/sync-listing/sync-listing.handler';
|
||||
import { SyncListingCommand } from '../commands/sync-listing/sync-listing.command';
|
||||
|
||||
describe('SyncListingHandler', () => {
|
||||
let handler: SyncListingHandler;
|
||||
let mockIndexer: { reindexAll: ReturnType<typeof vi.fn>; indexListing: ReturnType<typeof vi.fn>; removeListing: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockIndexer = {
|
||||
reindexAll: vi.fn(),
|
||||
indexListing: vi.fn(),
|
||||
removeListing: vi.fn(),
|
||||
};
|
||||
handler = new SyncListingHandler(mockIndexer as any);
|
||||
});
|
||||
|
||||
it('indexes a listing by id', async () => {
|
||||
mockIndexer.indexListing.mockResolvedValue(undefined);
|
||||
|
||||
const command = new SyncListingCommand('listing-123');
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockIndexer.indexListing).toHaveBeenCalledWith('listing-123');
|
||||
expect(mockIndexer.indexListing).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ListingApprovedEventHandler } from '../event-handlers/listing-approved.handler';
|
||||
|
||||
describe('ListingApprovedEventHandler', () => {
|
||||
let handler: ListingApprovedEventHandler;
|
||||
let mockIndexer: { indexListing: ReturnType<typeof vi.fn>; removeListing: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockIndexer = {
|
||||
indexListing: vi.fn(),
|
||||
removeListing: vi.fn(),
|
||||
};
|
||||
mockLogger = {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
handler = new ListingApprovedEventHandler(mockIndexer as any, mockLogger as any);
|
||||
});
|
||||
|
||||
it('indexes listing on listing.approved event', async () => {
|
||||
mockIndexer.indexListing.mockResolvedValue(undefined);
|
||||
|
||||
await handler.handle({ listingId: 'listing-1' });
|
||||
|
||||
expect(mockIndexer.indexListing).toHaveBeenCalledWith('listing-1');
|
||||
expect(mockLogger.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('indexes listing on listing.updated event', async () => {
|
||||
mockIndexer.indexListing.mockResolvedValue(undefined);
|
||||
|
||||
await handler.handleUpdate({ listingId: 'listing-2' });
|
||||
|
||||
expect(mockIndexer.indexListing).toHaveBeenCalledWith('listing-2');
|
||||
});
|
||||
|
||||
it('removes listing on listing.deactivated event', async () => {
|
||||
mockIndexer.removeListing.mockResolvedValue(undefined);
|
||||
|
||||
await handler.handleDeactivation({ listingId: 'listing-3' });
|
||||
|
||||
expect(mockIndexer.removeListing).toHaveBeenCalledWith('listing-3');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user