test: increase test coverage for listings, auth, and search modules
Add 33 new test files to reach coverage targets: - Listings: 13 → 28 test files (50%+) - Auth: 21 → 36 test files (50%+) - Search: 10 → 13 test files (59%+) New tests cover domain entities, value objects, services, guards, decorators, DTOs, repositories, controllers, and event handlers. Total: 204 test files, 1178 tests passing. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { findByIdWithProperty, searchListings, findBySellerIdQuery } from '../repositories/listing-read.queries';
|
||||
|
||||
describe('listing-read.queries', () => {
|
||||
let mockPrisma: {
|
||||
listing: {
|
||||
findUnique: ReturnType<typeof vi.fn>;
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
listing: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('findByIdWithProperty', () => {
|
||||
it('should return null when listing not found', async () => {
|
||||
mockPrisma.listing.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await findByIdWithProperty(mockPrisma as any, 'non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return mapped ListingDetailData when listing is found', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.listing.findUnique.mockResolvedValue({
|
||||
id: 'listing-1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: 5_000_000_000n,
|
||||
pricePerM2: 62_500_000,
|
||||
rentPriceMonthly: null,
|
||||
commissionPct: 2.0,
|
||||
viewCount: 10,
|
||||
saveCount: 2,
|
||||
inquiryCount: 1,
|
||||
publishedAt: now,
|
||||
createdAt: now,
|
||||
property: {
|
||||
id: 'prop-1',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp',
|
||||
description: 'Mô tả',
|
||||
address: '123 Nguyễn Huệ',
|
||||
ward: 'Bến Nghé',
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 80,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floors: null,
|
||||
direction: 'EAST',
|
||||
yearBuilt: 2022,
|
||||
legalStatus: 'Sổ hồng',
|
||||
amenities: null,
|
||||
projectName: 'Vinhomes',
|
||||
media: [
|
||||
{ id: 'm-1', url: 'https://cdn.example.com/1.jpg', type: 'image', order: 0, caption: null },
|
||||
],
|
||||
},
|
||||
seller: { id: 'seller-1', fullName: 'Nguyễn Văn A', phone: '0901234567' },
|
||||
agent: { id: 'agent-1', userId: 'user-a', agency: 'Đất Xanh' },
|
||||
});
|
||||
|
||||
const result = await findByIdWithProperty(mockPrisma as any, 'listing-1');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.id).toBe('listing-1');
|
||||
expect(result!.status).toBe('ACTIVE');
|
||||
expect(result!.priceVND).toBe('5000000000');
|
||||
expect(result!.property.title).toBe('Căn hộ đẹp');
|
||||
expect(result!.property.media).toHaveLength(1);
|
||||
expect(result!.seller.fullName).toBe('Nguyễn Văn A');
|
||||
expect(result!.agent!.agency).toBe('Đất Xanh');
|
||||
expect(result!.publishedAt).toBe(now.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchListings', () => {
|
||||
it('should return paginated results with empty data', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([]);
|
||||
mockPrisma.listing.count.mockResolvedValue(0);
|
||||
|
||||
const result = await searchListings(mockPrisma as any, { page: 1, limit: 20 });
|
||||
|
||||
expect(result.data).toEqual([]);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.limit).toBe(20);
|
||||
expect(result.totalPages).toBe(0);
|
||||
});
|
||||
|
||||
it('should map listing data correctly', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.listing.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'listing-1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: 5_000_000_000n,
|
||||
pricePerM2: 62_500_000,
|
||||
viewCount: 5,
|
||||
publishedAt: now,
|
||||
property: {
|
||||
id: 'prop-1',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp',
|
||||
address: '123 Nguyễn Huệ',
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 80,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
media: [{ url: 'https://cdn.example.com/thumb.jpg' }],
|
||||
},
|
||||
seller: { id: 'seller-1', fullName: 'Nguyễn Văn A' },
|
||||
},
|
||||
]);
|
||||
mockPrisma.listing.count.mockResolvedValue(1);
|
||||
|
||||
const result = await searchListings(mockPrisma as any, { status: 'ACTIVE', page: 1, limit: 20 });
|
||||
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0]!.id).toBe('listing-1');
|
||||
expect(result.data[0]!.priceVND).toBe('5000000000');
|
||||
expect(result.data[0]!.property.thumbnail).toBe('https://cdn.example.com/thumb.jpg');
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.totalPages).toBe(1);
|
||||
});
|
||||
|
||||
it('should cap limit at 100', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([]);
|
||||
mockPrisma.listing.count.mockResolvedValue(0);
|
||||
|
||||
const result = await searchListings(mockPrisma as any, { limit: 500 });
|
||||
|
||||
expect(result.limit).toBe(100);
|
||||
});
|
||||
|
||||
it('should default page to 1 and limit to 20', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([]);
|
||||
mockPrisma.listing.count.mockResolvedValue(0);
|
||||
|
||||
const result = await searchListings(mockPrisma as any, {});
|
||||
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.limit).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findBySellerIdQuery', () => {
|
||||
it('should return paginated seller listings', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([]);
|
||||
mockPrisma.listing.count.mockResolvedValue(0);
|
||||
|
||||
const result = await findBySellerIdQuery(mockPrisma as any, 'seller-1', 1, 10);
|
||||
|
||||
expect(result.data).toEqual([]);
|
||||
expect(result.total).toBe(0);
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.limit).toBe(10);
|
||||
});
|
||||
|
||||
it('should map seller listing data correctly', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'listing-1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: 3_000_000_000n,
|
||||
property: {
|
||||
id: 'prop-1',
|
||||
title: 'Nhà phố đẹp',
|
||||
district: 'Quận 3',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 120,
|
||||
media: [{ url: 'https://cdn.example.com/thumb.jpg' }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockPrisma.listing.count.mockResolvedValue(1);
|
||||
|
||||
const result = await findBySellerIdQuery(mockPrisma as any, 'seller-1', 1, 10);
|
||||
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0]!.priceVND).toBe('3000000000');
|
||||
expect(result.data[0]!.property.thumbnail).toBe('https://cdn.example.com/thumb.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { PrismaDuplicateDetector } from '../services/prisma-duplicate-detector';
|
||||
|
||||
describe('PrismaDuplicateDetector', () => {
|
||||
let detector: PrismaDuplicateDetector;
|
||||
let mockPrisma: { $queryRaw: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
$queryRaw: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
detector = new PrismaDuplicateDetector(mockPrisma as any);
|
||||
});
|
||||
|
||||
it('should return empty array when no nearby properties found', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
const result = await detector.findDuplicates({
|
||||
excludePropertyId: 'prop-new',
|
||||
latitude: 10.7769,
|
||||
longitude: 106.7009,
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
propertyType: 'APARTMENT',
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockPrisma.$queryRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return duplicates when nearby properties with similar titles exist', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([
|
||||
{
|
||||
listing_id: 'listing-dup',
|
||||
property_id: 'prop-dup',
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
address: '123 Nguyễn Huệ',
|
||||
district: 'Quận 1',
|
||||
property_type: 'APARTMENT',
|
||||
distance_meters: 50,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await detector.findDuplicates({
|
||||
excludePropertyId: 'prop-new',
|
||||
latitude: 10.7769,
|
||||
longitude: 106.7009,
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
propertyType: 'APARTMENT',
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]!.listingId).toBe('listing-dup');
|
||||
expect(result[0]!.propertyId).toBe('prop-dup');
|
||||
expect(result[0]!.distanceMeters).toBe(50);
|
||||
expect(result[0]!.titleSimilarity).toBe(1); // exact match
|
||||
});
|
||||
|
||||
it('should filter out low-similarity results', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([
|
||||
{
|
||||
listing_id: 'listing-diff',
|
||||
property_id: 'prop-diff',
|
||||
title: 'Biệt thự sang trọng Thảo Điền',
|
||||
address: '456 Quốc Hương',
|
||||
district: 'Quận 2',
|
||||
property_type: 'APARTMENT',
|
||||
distance_meters: 80,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await detector.findDuplicates({
|
||||
excludePropertyId: 'prop-new',
|
||||
latitude: 10.7769,
|
||||
longitude: 106.7009,
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
propertyType: 'APARTMENT',
|
||||
});
|
||||
|
||||
// Titles are very different, so similarity should be below threshold
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should use custom radius and similarity threshold', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
await detector.findDuplicates({
|
||||
excludePropertyId: 'prop-new',
|
||||
latitude: 10.7769,
|
||||
longitude: 106.7009,
|
||||
title: 'Test listing',
|
||||
propertyType: 'TOWNHOUSE',
|
||||
radiusMeters: 200,
|
||||
minTitleSimilarity: 0.9,
|
||||
});
|
||||
|
||||
expect(mockPrisma.$queryRaw).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ListingEntity } from '../../domain/entities/listing.entity';
|
||||
import { Price } from '../../domain/value-objects/price.vo';
|
||||
import { PrismaListingRepository } from '../repositories/prisma-listing.repository';
|
||||
|
||||
describe('PrismaListingRepository', () => {
|
||||
let repository: PrismaListingRepository;
|
||||
let mockPrisma: {
|
||||
listing: {
|
||||
findUnique: ReturnType<typeof vi.fn>;
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
listing: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
};
|
||||
repository = new PrismaListingRepository(mockPrisma as any);
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null when listing not found', async () => {
|
||||
mockPrisma.listing.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await repository.findById('non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockPrisma.listing.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'non-existent' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a ListingEntity when listing is found', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.listing.findUnique.mockResolvedValue({
|
||||
id: 'listing-1',
|
||||
propertyId: 'prop-1',
|
||||
agentId: 'agent-1',
|
||||
sellerId: 'seller-1',
|
||||
transactionType: 'SALE',
|
||||
status: 'ACTIVE',
|
||||
priceVND: 5_000_000_000n,
|
||||
pricePerM2: 62_500_000,
|
||||
rentPriceMonthly: null,
|
||||
commissionPct: 2.0,
|
||||
aiPriceEstimate: null,
|
||||
aiConfidence: null,
|
||||
moderationScore: null,
|
||||
moderationNotes: null,
|
||||
viewCount: 10,
|
||||
saveCount: 2,
|
||||
inquiryCount: 1,
|
||||
featuredUntil: null,
|
||||
expiresAt: null,
|
||||
publishedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const result = await repository.findById('listing-1');
|
||||
|
||||
expect(result).toBeInstanceOf(ListingEntity);
|
||||
expect(result!.id).toBe('listing-1');
|
||||
expect(result!.propertyId).toBe('prop-1');
|
||||
expect(result!.status).toBe('ACTIVE');
|
||||
expect(result!.viewCount).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
it('should call prisma.listing.create with correct data', async () => {
|
||||
const price = Price.create(5_000_000_000n).unwrap();
|
||||
const listing = ListingEntity.createNew(
|
||||
'listing-new',
|
||||
'prop-new',
|
||||
'seller-1',
|
||||
'SALE',
|
||||
price,
|
||||
100,
|
||||
'agent-1',
|
||||
);
|
||||
|
||||
await repository.save(listing);
|
||||
|
||||
expect(mockPrisma.listing.create).toHaveBeenCalledTimes(1);
|
||||
const createCall = mockPrisma.listing.create.mock.calls[0]![0];
|
||||
expect(createCall.data.id).toBe('listing-new');
|
||||
expect(createCall.data.propertyId).toBe('prop-new');
|
||||
expect(createCall.data.sellerId).toBe('seller-1');
|
||||
expect(createCall.data.status).toBe('DRAFT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should call prisma.listing.update with correct data', async () => {
|
||||
const price = Price.create(5_000_000_000n).unwrap();
|
||||
const listing = ListingEntity.createNew(
|
||||
'listing-upd',
|
||||
'prop-upd',
|
||||
'seller-1',
|
||||
'SALE',
|
||||
price,
|
||||
80,
|
||||
);
|
||||
|
||||
await repository.update(listing);
|
||||
|
||||
expect(mockPrisma.listing.update).toHaveBeenCalledTimes(1);
|
||||
const updateCall = mockPrisma.listing.update.mock.calls[0]![0];
|
||||
expect(updateCall.where.id).toBe('listing-upd');
|
||||
expect(updateCall.data.status).toBe('DRAFT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByStatus', () => {
|
||||
it('should delegate to search with status filter', async () => {
|
||||
mockPrisma.listing.findMany.mockResolvedValue([]);
|
||||
mockPrisma.listing.count.mockResolvedValue(0);
|
||||
|
||||
const result = await repository.findByStatus('PENDING_REVIEW', 1, 20);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { PrismaPriceValidator } from '../services/prisma-price-validator';
|
||||
|
||||
describe('PrismaPriceValidator', () => {
|
||||
let validator: PrismaPriceValidator;
|
||||
let mockPrisma: { $queryRaw: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
$queryRaw: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
mockLogger = {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
validator = new PrismaPriceValidator(mockPrisma as any, mockLogger as any);
|
||||
});
|
||||
|
||||
it('should return valid for normal price within default range', async () => {
|
||||
// No market data — falls back to defaults
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
const result = await validator.validate({
|
||||
priceVND: 5_000_000_000n,
|
||||
areaM2: 80,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 1',
|
||||
});
|
||||
|
||||
// 5B / 80 = 62.5M per m2; APARTMENT default range: 15M-200M
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuspicious).toBe(false);
|
||||
expect(result.pricePerM2).toBe(62_500_000);
|
||||
});
|
||||
|
||||
it('should return invalid for zero area', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
const result = await validator.validate({
|
||||
priceVND: 5_000_000_000n,
|
||||
areaM2: 0,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 1',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.isSuspicious).toBe(true);
|
||||
expect(result.reason).toContain('không hợp lệ');
|
||||
});
|
||||
|
||||
it('should flag suspiciously low price', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
// 100M / 100 = 1M per m2 — way below APARTMENT min of 15M * 0.5 = 7.5M
|
||||
const result = await validator.validate({
|
||||
priceVND: 100_000_000n,
|
||||
areaM2: 100,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 7',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuspicious).toBe(true);
|
||||
expect(result.reason).toContain('thấp hơn');
|
||||
});
|
||||
|
||||
it('should flag suspiciously high price', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([]);
|
||||
|
||||
// 100B / 100 = 1B per m2 — above APARTMENT max of 200M * 1.5 = 300M
|
||||
const result = await validator.validate({
|
||||
priceVND: 100_000_000_000n,
|
||||
areaM2: 100,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 1',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuspicious).toBe(true);
|
||||
expect(result.reason).toContain('cao hơn');
|
||||
});
|
||||
|
||||
it('should use market data when available', async () => {
|
||||
mockPrisma.$queryRaw.mockResolvedValue([
|
||||
{ min_price: 50_000_000, max_price: 100_000_000 },
|
||||
]);
|
||||
|
||||
const result = await validator.validate({
|
||||
priceVND: 6_000_000_000n,
|
||||
areaM2: 80,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 1',
|
||||
});
|
||||
|
||||
// 6B / 80 = 75M per m2; market range: 50M-100M — within range
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.isSuspicious).toBe(false);
|
||||
expect(result.minPricePerM2).toBe(50_000_000);
|
||||
expect(result.maxPricePerM2).toBe(100_000_000);
|
||||
});
|
||||
|
||||
it('should fall back to defaults when market query fails', async () => {
|
||||
mockPrisma.$queryRaw.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const result = await validator.validate({
|
||||
priceVND: 5_000_000_000n,
|
||||
areaM2: 80,
|
||||
propertyType: 'APARTMENT',
|
||||
district: 'Quận 1',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(mockLogger.warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { PropertyMediaEntity } from '../../domain/entities/property-media.entity';
|
||||
import { PropertyEntity } from '../../domain/entities/property.entity';
|
||||
import { PrismaPropertyRepository } from '../repositories/prisma-property.repository';
|
||||
|
||||
describe('PrismaPropertyRepository', () => {
|
||||
let repository: PrismaPropertyRepository;
|
||||
let mockPrisma: {
|
||||
property: {
|
||||
findUnique: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
propertyMedia: {
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
$executeRaw: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
property: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
propertyMedia: {
|
||||
create: vi.fn().mockResolvedValue(undefined),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
$executeRaw: vi.fn().mockResolvedValue(1),
|
||||
};
|
||||
repository = new PrismaPropertyRepository(mockPrisma as any);
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should return null when property not found', async () => {
|
||||
mockPrisma.property.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await repository.findById('non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockPrisma.property.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: 'non-existent' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a PropertyEntity when property is found', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.property.findUnique.mockResolvedValue({
|
||||
id: 'prop-1',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp',
|
||||
description: 'Mô tả chi tiết',
|
||||
address: '123 Nguyễn Huệ',
|
||||
ward: 'Bến Nghé',
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
location: null, // PostGIS geometry placeholder
|
||||
areaM2: 80,
|
||||
usableAreaM2: 70,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floors: null,
|
||||
floor: 10,
|
||||
totalFloors: 25,
|
||||
direction: 'EAST',
|
||||
yearBuilt: 2022,
|
||||
legalStatus: 'Sổ hồng',
|
||||
amenities: null,
|
||||
nearbyPOIs: null,
|
||||
metroDistanceM: 300,
|
||||
projectName: 'Vinhomes',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const result = await repository.findById('prop-1');
|
||||
|
||||
expect(result).toBeInstanceOf(PropertyEntity);
|
||||
expect(result!.id).toBe('prop-1');
|
||||
expect(result!.title).toBe('Căn hộ đẹp');
|
||||
expect(result!.propertyType).toBe('APARTMENT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMedia', () => {
|
||||
it('should call prisma.propertyMedia.create with correct data', async () => {
|
||||
const media = PropertyMediaEntity.createNew(
|
||||
'media-1',
|
||||
'prop-1',
|
||||
'https://cdn.example.com/photo.jpg',
|
||||
'image',
|
||||
0,
|
||||
'Mặt tiền',
|
||||
);
|
||||
|
||||
await repository.addMedia(media);
|
||||
|
||||
expect(mockPrisma.propertyMedia.create).toHaveBeenCalledTimes(1);
|
||||
const createCall = mockPrisma.propertyMedia.create.mock.calls[0]![0];
|
||||
expect(createCall.data.id).toBe('media-1');
|
||||
expect(createCall.data.propertyId).toBe('prop-1');
|
||||
expect(createCall.data.url).toBe('https://cdn.example.com/photo.jpg');
|
||||
expect(createCall.data.type).toBe('image');
|
||||
expect(createCall.data.order).toBe(0);
|
||||
expect(createCall.data.caption).toBe('Mặt tiền');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findMediaByPropertyId', () => {
|
||||
it('should return mapped PropertyMediaEntity array', async () => {
|
||||
const now = new Date();
|
||||
mockPrisma.propertyMedia.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'media-1',
|
||||
propertyId: 'prop-1',
|
||||
url: 'https://cdn.example.com/1.jpg',
|
||||
type: 'image',
|
||||
order: 0,
|
||||
caption: null,
|
||||
aiTags: null,
|
||||
createdAt: now,
|
||||
},
|
||||
{
|
||||
id: 'media-2',
|
||||
propertyId: 'prop-1',
|
||||
url: 'https://cdn.example.com/2.jpg',
|
||||
type: 'image',
|
||||
order: 1,
|
||||
caption: 'Phòng ngủ',
|
||||
aiTags: null,
|
||||
createdAt: now,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await repository.findMediaByPropertyId('prop-1');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBeInstanceOf(PropertyMediaEntity);
|
||||
expect(result[0]!.id).toBe('media-1');
|
||||
expect(result[1]!.caption).toBe('Phòng ngủ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMedia', () => {
|
||||
it('should call prisma.propertyMedia.delete', async () => {
|
||||
await repository.deleteMedia('media-1');
|
||||
|
||||
expect(mockPrisma.propertyMedia.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'media-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('countMediaByPropertyId', () => {
|
||||
it('should return count from prisma', async () => {
|
||||
mockPrisma.propertyMedia.count.mockResolvedValue(5);
|
||||
|
||||
const result = await repository.countMediaByPropertyId('prop-1');
|
||||
|
||||
expect(result).toBe(5);
|
||||
expect(mockPrisma.propertyMedia.count).toHaveBeenCalledWith({
|
||||
where: { propertyId: 'prop-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user