test(api): add unit tests for analytics, metrics, notifications, payments, and search modules

New test coverage for infrastructure and presentation layers across
multiple modules including Momo/ZaloPay payment services, Typesense
search repository, listing indexer, and notification handlers.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 23:07:14 +07:00
parent 7fb25eb2b1
commit c9782fd48d
18 changed files with 2097 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
import { MarketIndexEntity } from '../entities/market-index.entity';
import { MarketIndexUpdatedEvent } from '../events/market-index-updated.event';
describe('MarketIndexEntity', () => {
it('createNew creates entity with all properties and adds MarketIndexUpdatedEvent', () => {
const entity = MarketIndexEntity.createNew(
'idx-1',
'Quận 1',
'Hồ Chí Minh',
'APARTMENT',
'2026-Q1',
BigInt(5000000000),
80000000,
150,
45,
3,
0.65,
0.08,
);
expect(entity.id).toBe('idx-1');
expect(entity.district).toBe('Quận 1');
expect(entity.city).toBe('Hồ Chí Minh');
expect(entity.propertyType).toBe('APARTMENT');
expect(entity.period).toBe('2026-Q1');
expect(entity.medianPrice).toBe(BigInt(5000000000));
expect(entity.avgPriceM2).toBe(80000000);
expect(entity.totalListings).toBe(150);
expect(entity.daysOnMarket).toBe(45);
expect(entity.inventoryLevel).toBe(3);
expect(entity.absorptionRate).toBe(0.65);
expect(entity.yoyChange).toBe(0.08);
const events = entity.domainEvents;
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(MarketIndexUpdatedEvent);
const event = events[0] as MarketIndexUpdatedEvent;
expect(event.aggregateId).toBe('idx-1');
expect(event.district).toBe('Quận 1');
expect(event.city).toBe('Hồ Chí Minh');
expect(event.period).toBe('2026-Q1');
});
it('createNew handles optional absorptionRate and yoyChange (null when not provided)', () => {
const entity = MarketIndexEntity.createNew(
'idx-2',
'Quận 2',
'Hồ Chí Minh',
'VILLA',
'2026-Q1',
BigInt(8000000000),
120000000,
50,
60,
2,
);
expect(entity.absorptionRate).toBeNull();
expect(entity.yoyChange).toBeNull();
});
it('updateMetrics updates all metric fields and adds MarketIndexUpdatedEvent', () => {
const entity = MarketIndexEntity.createNew(
'idx-3',
'Quận 3',
'Hồ Chí Minh',
'APARTMENT',
'2026-Q1',
BigInt(5000000000),
80000000,
150,
45,
3,
0.65,
0.08,
);
entity.clearDomainEvents();
entity.updateMetrics(
BigInt(6000000000),
90000000,
200,
40,
4,
0.70,
0.10,
);
expect(entity.medianPrice).toBe(BigInt(6000000000));
expect(entity.avgPriceM2).toBe(90000000);
expect(entity.totalListings).toBe(200);
expect(entity.daysOnMarket).toBe(40);
expect(entity.inventoryLevel).toBe(4);
expect(entity.absorptionRate).toBe(0.70);
expect(entity.yoyChange).toBe(0.10);
const events = entity.domainEvents;
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(MarketIndexUpdatedEvent);
});
it('updateMetrics preserves existing absorptionRate when not provided', () => {
const entity = MarketIndexEntity.createNew(
'idx-4',
'Quận 4',
'Hồ Chí Minh',
'APARTMENT',
'2026-Q1',
BigInt(5000000000),
80000000,
150,
45,
3,
0.65,
0.08,
);
entity.updateMetrics(
BigInt(5500000000),
85000000,
160,
42,
3,
);
expect(entity.absorptionRate).toBe(0.65);
expect(entity.yoyChange).toBe(0.08);
});
it('constructor sets all properties from props', () => {
const createdAt = new Date('2026-01-01');
const updatedAt = new Date('2026-03-01');
const entity = new MarketIndexEntity(
'idx-5',
{
district: 'Bình Thạnh',
city: 'Hồ Chí Minh',
propertyType: 'TOWNHOUSE',
period: '2026-Q1',
medianPrice: BigInt(3000000000),
avgPriceM2: 60000000,
totalListings: 80,
daysOnMarket: 30,
inventoryLevel: 2,
absorptionRate: 0.50,
yoyChange: 0.05,
},
createdAt,
updatedAt,
);
expect(entity.id).toBe('idx-5');
expect(entity.district).toBe('Bình Thạnh');
expect(entity.city).toBe('Hồ Chí Minh');
expect(entity.propertyType).toBe('TOWNHOUSE');
expect(entity.period).toBe('2026-Q1');
expect(entity.medianPrice).toBe(BigInt(3000000000));
expect(entity.avgPriceM2).toBe(60000000);
expect(entity.totalListings).toBe(80);
expect(entity.daysOnMarket).toBe(30);
expect(entity.inventoryLevel).toBe(2);
expect(entity.absorptionRate).toBe(0.50);
expect(entity.yoyChange).toBe(0.05);
expect(entity.createdAt).toBe(createdAt);
expect(entity.domainEvents).toHaveLength(0);
});
});

View File

@@ -0,0 +1,70 @@
import { ValuationEntity } from '../entities/valuation.entity';
describe('ValuationEntity', () => {
const baseProps = {
propertyId: 'prop-1',
estimatedPrice: BigInt(5000000000),
confidence: 0.85,
pricePerM2: 50000000,
comparables: [{ id: 'comp-1', price: 4800000000 }],
features: { area: 100, bedrooms: 3 },
modelVersion: 'v1',
};
it('createNew creates entity with all properties', () => {
const entity = ValuationEntity.createNew(
'val-1',
baseProps.propertyId,
baseProps.estimatedPrice,
baseProps.confidence,
baseProps.pricePerM2,
baseProps.comparables,
baseProps.features,
baseProps.modelVersion,
);
expect(entity.id).toBe('val-1');
expect(entity.propertyId).toBe('prop-1');
expect(entity.estimatedPrice).toBe(BigInt(5000000000));
expect(entity.confidence).toBe(0.85);
expect(entity.pricePerM2).toBe(50000000);
expect(entity.comparables).toEqual(baseProps.comparables);
expect(entity.features).toEqual(baseProps.features);
expect(entity.modelVersion).toBe('v1');
});
it('constructor sets all properties correctly', () => {
const createdAt = new Date('2026-01-15');
const entity = new ValuationEntity('val-2', baseProps, createdAt);
expect(entity.id).toBe('val-2');
expect(entity.propertyId).toBe('prop-1');
expect(entity.estimatedPrice).toBe(BigInt(5000000000));
expect(entity.confidence).toBe(0.85);
expect(entity.pricePerM2).toBe(50000000);
expect(entity.comparables).toEqual(baseProps.comparables);
expect(entity.features).toEqual(baseProps.features);
expect(entity.modelVersion).toBe('v1');
expect(entity.createdAt).toBe(createdAt);
});
it('all getters return expected values', () => {
const entity = new ValuationEntity('val-3', {
propertyId: 'prop-99',
estimatedPrice: BigInt(9999000000),
confidence: 0.92,
pricePerM2: 99000000,
comparables: [],
features: { location: 'central' },
modelVersion: 'v2',
});
expect(entity.propertyId).toBe('prop-99');
expect(entity.estimatedPrice).toBe(BigInt(9999000000));
expect(entity.confidence).toBe(0.92);
expect(entity.pricePerM2).toBe(99000000);
expect(entity.comparables).toEqual([]);
expect(entity.features).toEqual({ location: 'central' });
expect(entity.modelVersion).toBe('v2');
});
});