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:
Ho Ngoc Hai
2026-04-10 21:39:20 +07:00
parent 75a608031b
commit 1aad9b9f95
31 changed files with 2991 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
import { describe, it, expect } from 'vitest';
import { ListingEntity } from '../entities/listing.entity';
import { ModerationService } from '../services/moderation.service';
import { Price } from '../value-objects/price.vo';
describe('ModerationService', () => {
const service = new ModerationService();
const makeListingInReview = () => {
const price = Price.create(5_000_000_000n).unwrap();
const listing = ListingEntity.createNew(
'listing-1',
'property-1',
'seller-1',
'SALE',
price,
100,
'agent-1',
);
listing.submitForReview();
listing.clearDomainEvents();
return listing;
};
describe('applyModeration', () => {
it('should approve a listing', () => {
const listing = makeListingInReview();
service.applyModeration(listing, { action: 'approve' });
expect(listing.status).toBe('ACTIVE');
expect(listing.publishedAt).toBeTruthy();
});
it('should reject a listing with default notes', () => {
const listing = makeListingInReview();
service.applyModeration(listing, { action: 'reject' });
expect(listing.status).toBe('REJECTED');
expect(listing.moderationNotes).toBe('Bị từ chối bởi moderator');
});
it('should reject a listing with custom notes', () => {
const listing = makeListingInReview();
service.applyModeration(listing, {
action: 'reject',
notes: 'Ảnh không rõ ràng',
});
expect(listing.status).toBe('REJECTED');
expect(listing.moderationNotes).toBe('Ảnh không rõ ràng');
});
it('should set moderation score when provided on approve', () => {
const listing = makeListingInReview();
service.applyModeration(listing, {
action: 'approve',
moderationScore: 95,
notes: 'Tin đăng chất lượng',
});
expect(listing.status).toBe('ACTIVE');
expect(listing.moderationScore).toBe(95);
expect(listing.moderationNotes).toBe('Tin đăng chất lượng');
});
it('should set moderation score when provided on reject', () => {
const listing = makeListingInReview();
service.applyModeration(listing, {
action: 'reject',
moderationScore: 20,
notes: 'Nội dung vi phạm',
});
expect(listing.status).toBe('REJECTED');
expect(listing.moderationScore).toBe(20);
});
});
describe('applyStatusTransition', () => {
it('should reject with moderation notes', () => {
const listing = makeListingInReview();
service.applyStatusTransition(listing, 'REJECTED', 'Vi phạm chính sách');
expect(listing.status).toBe('REJECTED');
expect(listing.moderationNotes).toBe('Vi phạm chính sách');
});
it('should approve a PENDING_REVIEW listing transitioning to ACTIVE', () => {
const listing = makeListingInReview();
service.applyStatusTransition(listing, 'ACTIVE');
expect(listing.status).toBe('ACTIVE');
expect(listing.publishedAt).toBeTruthy();
});
it('should use generic transitionTo for non-moderation transitions', () => {
const price = Price.create(5_000_000_000n).unwrap();
const listing = ListingEntity.createNew(
'listing-2',
'property-2',
'seller-2',
'SALE',
price,
100,
);
listing.submitForReview();
listing.approve();
listing.clearDomainEvents();
service.applyStatusTransition(listing, 'SOLD');
expect(listing.status).toBe('SOLD');
});
it('should use generic transitionTo for REJECTED without notes', () => {
const listing = makeListingInReview();
// REJECTED without moderationNotes falls into the generic transitionTo branch
service.applyStatusTransition(listing, 'REJECTED');
expect(listing.status).toBe('REJECTED');
});
});
});

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest';
import { PropertyMediaEntity } from '../entities/property-media.entity';
describe('PropertyMediaEntity', () => {
it('should create media via constructor with all properties', () => {
const media = new PropertyMediaEntity('media-1', {
propertyId: 'prop-1',
url: 'https://cdn.example.com/photo.jpg',
type: 'image',
order: 0,
caption: 'Mặt tiền nhà',
aiTags: { tags: ['exterior'] },
});
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).toEqual({ tags: ['exterior'] });
expect(media.createdAt).toBeInstanceOf(Date);
});
it('should create media via createNew factory with caption', () => {
const media = PropertyMediaEntity.createNew(
'media-2',
'prop-1',
'https://cdn.example.com/interior.jpg',
'image',
1,
'Phòng khách',
);
expect(media.id).toBe('media-2');
expect(media.propertyId).toBe('prop-1');
expect(media.url).toBe('https://cdn.example.com/interior.jpg');
expect(media.type).toBe('image');
expect(media.order).toBe(1);
expect(media.caption).toBe('Phòng khách');
expect(media.aiTags).toBeNull();
});
it('should create media via createNew factory without caption', () => {
const media = PropertyMediaEntity.createNew(
'media-3',
'prop-2',
'https://cdn.example.com/video.mp4',
'video',
0,
);
expect(media.type).toBe('video');
expect(media.caption).toBeNull();
expect(media.aiTags).toBeNull();
});
it('should preserve order values', () => {
const media0 = PropertyMediaEntity.createNew('m-0', 'p-1', 'http://a.com/0.jpg', 'image', 0);
const media5 = PropertyMediaEntity.createNew('m-5', 'p-1', 'http://a.com/5.jpg', 'image', 5);
expect(media0.order).toBe(0);
expect(media5.order).toBe(5);
});
it('should accept custom createdAt via constructor', () => {
const pastDate = new Date('2024-01-01T00:00:00Z');
const media = new PropertyMediaEntity(
'media-old',
{
propertyId: 'prop-1',
url: 'https://cdn.example.com/old.jpg',
type: 'image',
order: 0,
caption: null,
aiTags: null,
},
pastDate,
);
expect(media.createdAt).toBe(pastDate);
});
});