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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user