test(api): add domain layer unit tests across all modules

Cover admin events, notifications, reviews, search VOs, listings (property,
media, events, price/geo/address VOs), auth events, payment events,
subscription events, and analytics events. Raises domain test coverage
from ~24% to ~75%.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 00:36:39 +07:00
parent 801e29e65c
commit 62f4f001b6
14 changed files with 973 additions and 1 deletions

View File

@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import { ListingCreatedEvent } from '../events/listing-created.event';
import { ListingApprovedEvent } from '../events/listing-approved.event';
import { ListingSoldEvent } from '../events/listing-sold.event';
describe('Listings Domain Events', () => {
describe('ListingCreatedEvent', () => {
it('creates event with correct properties', () => {
const event = new ListingCreatedEvent('listing-1', 'prop-1', 'seller-1', 'SALE');
expect(event.eventName).toBe('listing.created');
expect(event.aggregateId).toBe('listing-1');
expect(event.propertyId).toBe('prop-1');
expect(event.sellerId).toBe('seller-1');
expect(event.transactionType).toBe('SALE');
expect(event.occurredAt).toBeInstanceOf(Date);
});
it('creates event for RENT transaction', () => {
const event = new ListingCreatedEvent('listing-2', 'prop-2', 'seller-2', 'RENT');
expect(event.transactionType).toBe('RENT');
});
});
describe('ListingApprovedEvent', () => {
it('creates event with correct properties', () => {
const event = new ListingApprovedEvent('listing-1', 'prop-1');
expect(event.eventName).toBe('listing.approved');
expect(event.aggregateId).toBe('listing-1');
expect(event.propertyId).toBe('prop-1');
expect(event.occurredAt).toBeInstanceOf(Date);
});
});
describe('ListingSoldEvent', () => {
it('creates event with SOLD status', () => {
const event = new ListingSoldEvent('listing-1', 'prop-1', 'SOLD');
expect(event.eventName).toBe('listing.sold');
expect(event.aggregateId).toBe('listing-1');
expect(event.propertyId).toBe('prop-1');
expect(event.finalStatus).toBe('SOLD');
expect(event.occurredAt).toBeInstanceOf(Date);
});
it('creates event with RENTED status', () => {
const event = new ListingSoldEvent('listing-2', 'prop-2', 'RENTED');
expect(event.finalStatus).toBe('RENTED');
});
});
});

View File

@@ -0,0 +1,234 @@
import { describe, it, expect } from 'vitest';
import { PropertyEntity } from '../entities/property.entity';
import { PropertyMediaEntity } from '../entities/property-media.entity';
import { Address } from '../value-objects/address.vo';
import { GeoPoint } from '../value-objects/geo-point.vo';
import { Price } from '../value-objects/price.vo';
describe('PropertyEntity', () => {
const makeAddress = () =>
Address.create('123 Nguyễn Huệ', 'Bến Nghé', 'Quận 1', 'Hồ Chí Minh').unwrap();
const makeGeoPoint = () => GeoPoint.create(10.7769, 106.7009).unwrap();
it('creates property with all required fields', () => {
const property = PropertyEntity.createNew('prop-1', {
propertyType: 'TOWNHOUSE',
title: 'Nhà phố đẹp Quận 1',
description: 'Nhà phố 3 tầng, mặt tiền rộng',
address: makeAddress(),
location: makeGeoPoint(),
areaM2: 120,
usableAreaM2: 100,
bedrooms: 3,
bathrooms: 2,
floors: 3,
floor: null,
totalFloors: null,
direction: 'EAST',
yearBuilt: 2020,
legalStatus: 'Sổ hồng',
amenities: { parking: true },
nearbyPOIs: [],
metroDistanceM: 500,
projectName: null,
});
expect(property.id).toBe('prop-1');
expect(property.propertyType).toBe('TOWNHOUSE');
expect(property.title).toBe('Nhà phố đẹp Quận 1');
expect(property.areaM2).toBe(120);
expect(property.usableAreaM2).toBe(100);
expect(property.bedrooms).toBe(3);
expect(property.bathrooms).toBe(2);
expect(property.floors).toBe(3);
expect(property.direction).toBe('EAST');
expect(property.yearBuilt).toBe(2020);
expect(property.legalStatus).toBe('Sổ hồng');
expect(property.metroDistanceM).toBe(500);
expect(property.projectName).toBeNull();
});
it('creates property with nullable fields as null', () => {
const property = PropertyEntity.createNew('prop-2', {
propertyType: 'APARTMENT',
title: 'Căn hộ Thủ Đức',
description: 'Căn hộ 2PN',
address: makeAddress(),
location: makeGeoPoint(),
areaM2: 65,
usableAreaM2: null,
bedrooms: null,
bathrooms: null,
floors: null,
floor: 10,
totalFloors: 25,
direction: null,
yearBuilt: null,
legalStatus: null,
amenities: null,
nearbyPOIs: null,
metroDistanceM: null,
projectName: 'Vinhomes Grand Park',
});
expect(property.usableAreaM2).toBeNull();
expect(property.bedrooms).toBeNull();
expect(property.direction).toBeNull();
expect(property.floor).toBe(10);
expect(property.totalFloors).toBe(25);
expect(property.projectName).toBe('Vinhomes Grand Park');
});
});
describe('PropertyMediaEntity', () => {
it('creates media with all fields', () => {
const media = PropertyMediaEntity.createNew(
'media-1',
'prop-1',
'https://cdn.example.com/photo.jpg',
'image',
0,
'Mặt tiền nhà',
);
expect(media.id).toBe('media-1');
expect(media.propertyId).toBe('prop-1');
expect(media.url).toBe('https://cdn.example.com/photo.jpg');
expect(media.type).toBe('image');
expect(media.order).toBe(0);
expect(media.caption).toBe('Mặt tiền nhà');
expect(media.aiTags).toBeNull();
});
it('creates media without caption', () => {
const media = PropertyMediaEntity.createNew(
'media-2',
'prop-1',
'https://cdn.example.com/video.mp4',
'video',
1,
);
expect(media.type).toBe('video');
expect(media.caption).toBeNull();
});
});
describe('Listings Value Objects', () => {
describe('Price', () => {
it('creates valid price', () => {
const result = Price.create(5_000_000_000n);
expect(result.isOk).toBe(true);
expect(result.unwrap().amountVND).toBe(5_000_000_000n);
});
it('rejects zero price', () => {
const result = Price.create(0n);
expect(result.isErr).toBe(true);
});
it('rejects negative price', () => {
const result = Price.create(-1_000_000n);
expect(result.isErr).toBe(true);
});
it('calculates price per m2', () => {
const price = Price.create(5_000_000_000n).unwrap();
const perM2 = price.calculatePerM2(100);
expect(perM2).toBe(50_000_000);
});
it('returns 0 for zero area', () => {
const price = Price.create(5_000_000_000n).unwrap();
expect(price.calculatePerM2(0)).toBe(0);
});
it('returns 0 for negative area', () => {
const price = Price.create(5_000_000_000n).unwrap();
expect(price.calculatePerM2(-10)).toBe(0);
});
});
describe('GeoPoint', () => {
it('creates valid geo point', () => {
const result = GeoPoint.create(10.7769, 106.7009);
expect(result.isOk).toBe(true);
const point = result.unwrap();
expect(point.latitude).toBe(10.7769);
expect(point.longitude).toBe(106.7009);
});
it('rejects invalid latitude above 90', () => {
const result = GeoPoint.create(91, 106);
expect(result.isErr).toBe(true);
});
it('rejects invalid latitude below -90', () => {
const result = GeoPoint.create(-91, 106);
expect(result.isErr).toBe(true);
});
it('rejects invalid longitude above 180', () => {
const result = GeoPoint.create(10, 181);
expect(result.isErr).toBe(true);
});
it('rejects invalid longitude below -180', () => {
const result = GeoPoint.create(10, -181);
expect(result.isErr).toBe(true);
});
it('generates WKT format', () => {
const point = GeoPoint.create(10.7769, 106.7009).unwrap();
expect(point.toWKT()).toBe('POINT(106.7009 10.7769)');
});
it('accepts boundary values', () => {
expect(GeoPoint.create(90, 180).isOk).toBe(true);
expect(GeoPoint.create(-90, -180).isOk).toBe(true);
});
});
describe('Address', () => {
it('creates valid address', () => {
const result = Address.create('123 Nguyễn Huệ', 'Bến Nghé', 'Quận 1', 'Hồ Chí Minh');
expect(result.isOk).toBe(true);
const addr = result.unwrap();
expect(addr.address).toBe('123 Nguyễn Huệ');
expect(addr.ward).toBe('Bến Nghé');
expect(addr.district).toBe('Quận 1');
expect(addr.city).toBe('Hồ Chí Minh');
});
it('generates full address string', () => {
const addr = Address.create('123 Nguyễn Huệ', 'Bến Nghé', 'Quận 1', 'Hồ Chí Minh').unwrap();
expect(addr.fullAddress).toBe('123 Nguyễn Huệ, Bến Nghé, Quận 1, Hồ Chí Minh');
});
it('trims whitespace', () => {
const addr = Address.create(' 123 Lê Lợi ', ' Bến Thành ', ' Quận 1 ', ' HCM ').unwrap();
expect(addr.address).toBe('123 Lê Lợi');
expect(addr.ward).toBe('Bến Thành');
});
it('rejects empty address', () => {
expect(Address.create('', 'Bến Nghé', 'Quận 1', 'HCM').isErr).toBe(true);
});
it('rejects empty ward', () => {
expect(Address.create('123', '', 'Quận 1', 'HCM').isErr).toBe(true);
});
it('rejects empty district', () => {
expect(Address.create('123', 'Bến Nghé', '', 'HCM').isErr).toBe(true);
});
it('rejects empty city', () => {
expect(Address.create('123', 'Bến Nghé', 'Quận 1', '').isErr).toBe(true);
});
it('rejects whitespace-only values', () => {
expect(Address.create(' ', 'Bến Nghé', 'Quận 1', 'HCM').isErr).toBe(true);
});
});
});