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,140 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CreateListingDto } from '../dto/create-listing.dto';
|
||||
|
||||
describe('CreateListingDto', () => {
|
||||
it('should pass validation with all required fields', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5500000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ 3PN view sông Sài Gòn',
|
||||
description: 'Căn hộ cao cấp 3 phòng ngủ, nội thất đầy đủ',
|
||||
address: '208 Nguyễn Hữu Cảnh',
|
||||
ward: 'Phường 22',
|
||||
district: 'Bình Thạnh',
|
||||
city: 'Hồ Chí Minh',
|
||||
latitude: 10.7942,
|
||||
longitude: 106.7219,
|
||||
areaM2: 85.5,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail validation when title is too short', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'AB',
|
||||
description: 'Căn hộ cao cấp 3 phòng ngủ, nội thất đầy đủ',
|
||||
address: '123 ABC',
|
||||
ward: 'Ward',
|
||||
district: 'District',
|
||||
city: 'City',
|
||||
latitude: 10.77,
|
||||
longitude: 106.70,
|
||||
areaM2: 80,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const titleError = errors.find((e) => e.property === 'title');
|
||||
expect(titleError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation with invalid transactionType', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'INVALID',
|
||||
priceVND: '5000000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
description: 'Mô tả đầy đủ chi tiết',
|
||||
address: '123 ABC',
|
||||
ward: 'Ward',
|
||||
district: 'District',
|
||||
city: 'City',
|
||||
latitude: 10.77,
|
||||
longitude: 106.70,
|
||||
areaM2: 80,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const typeError = errors.find((e) => e.property === 'transactionType');
|
||||
expect(typeError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when latitude is out of range', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
description: 'Mô tả đầy đủ chi tiết',
|
||||
address: '123 ABC',
|
||||
ward: 'Ward',
|
||||
district: 'District',
|
||||
city: 'City',
|
||||
latitude: 999,
|
||||
longitude: 106.70,
|
||||
areaM2: 80,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const latError = errors.find((e) => e.property === 'latitude');
|
||||
expect(latError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass validation with optional fields', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5500000000',
|
||||
propertyType: 'TOWNHOUSE',
|
||||
title: 'Nhà phố đẹp Quận 3',
|
||||
description: 'Nhà phố 3 tầng mặt tiền rộng',
|
||||
address: '456 Lê Lợi',
|
||||
ward: 'Phường 1',
|
||||
district: 'Quận 3',
|
||||
city: 'Hồ Chí Minh',
|
||||
latitude: 10.78,
|
||||
longitude: 106.69,
|
||||
areaM2: 120,
|
||||
bedrooms: 3,
|
||||
bathrooms: 2,
|
||||
floors: 3,
|
||||
direction: 'EAST',
|
||||
yearBuilt: 2020,
|
||||
legalStatus: 'Sổ hồng',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should fail validation when areaM2 is less than 1', async () => {
|
||||
const dto = plainToInstance(CreateListingDto, {
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
description: 'Mô tả đầy đủ chi tiết',
|
||||
address: '123 ABC',
|
||||
ward: 'Ward',
|
||||
district: 'District',
|
||||
city: 'City',
|
||||
latitude: 10.77,
|
||||
longitude: 106.70,
|
||||
areaM2: 0,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const areaError = errors.find((e) => e.property === 'areaM2');
|
||||
expect(areaError).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ListingsController } from '../controllers/listings.controller';
|
||||
|
||||
describe('ListingsController', () => {
|
||||
let controller: ListingsController;
|
||||
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
|
||||
let mockQueryBus: { execute: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommandBus = { execute: vi.fn() };
|
||||
mockQueryBus = { execute: vi.fn() };
|
||||
controller = new ListingsController(mockCommandBus as any, mockQueryBus as any);
|
||||
});
|
||||
|
||||
describe('createListing', () => {
|
||||
it('should execute CreateListingCommand via command bus', async () => {
|
||||
const mockResult = {
|
||||
listingId: 'listing-1',
|
||||
propertyId: 'prop-1',
|
||||
status: 'DRAFT',
|
||||
duplicateWarnings: [],
|
||||
};
|
||||
mockCommandBus.execute.mockResolvedValue(mockResult);
|
||||
|
||||
const dto = {
|
||||
transactionType: 'SALE' as const,
|
||||
priceVND: 5_000_000_000n,
|
||||
propertyType: 'APARTMENT' as const,
|
||||
title: 'Căn hộ đẹp Quận 1',
|
||||
description: 'Mô tả chi tiết căn hộ',
|
||||
address: '123 Nguyễn Huệ',
|
||||
ward: 'Bến Nghé',
|
||||
district: 'Quận 1',
|
||||
city: 'Hồ Chí Minh',
|
||||
latitude: 10.7769,
|
||||
longitude: 106.7009,
|
||||
areaM2: 80,
|
||||
};
|
||||
const user = { sub: 'seller-1', email: 'test@example.com', role: 'SELLER' };
|
||||
|
||||
const result = await controller.createListing(dto as any, user as any);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getListing', () => {
|
||||
it('should execute GetListingQuery via query bus', async () => {
|
||||
const mockDetail = {
|
||||
id: 'listing-1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
};
|
||||
mockQueryBus.execute.mockResolvedValue(mockDetail);
|
||||
|
||||
const result = await controller.getListing('listing-1');
|
||||
|
||||
expect(result).toEqual(mockDetail);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchListings', () => {
|
||||
it('should execute SearchListingsQuery via query bus', async () => {
|
||||
const mockResults = {
|
||||
data: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
totalPages: 0,
|
||||
};
|
||||
mockQueryBus.execute.mockResolvedValue(mockResults);
|
||||
|
||||
const dto = { status: 'ACTIVE' as const, page: 1, limit: 20 };
|
||||
const result = await controller.searchListings(dto as any);
|
||||
|
||||
expect(result).toEqual(mockResults);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('should execute UpdateListingStatusCommand via command bus', async () => {
|
||||
mockCommandBus.execute.mockResolvedValue({ status: 'ACTIVE' });
|
||||
|
||||
const dto = { status: 'ACTIVE' as const };
|
||||
const user = { sub: 'admin-1', email: 'admin@example.com', role: 'ADMIN' };
|
||||
|
||||
const result = await controller.updateStatus('listing-1', dto as any, user as any);
|
||||
|
||||
expect(result).toEqual({ status: 'ACTIVE' });
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingModeration', () => {
|
||||
it('should execute GetPendingModerationQuery with defaults', async () => {
|
||||
const mockResults = { data: [], total: 0, page: 1, limit: 20, totalPages: 0 };
|
||||
mockQueryBus.execute.mockResolvedValue(mockResults);
|
||||
|
||||
const result = await controller.getPendingModeration();
|
||||
|
||||
expect(result).toEqual(mockResults);
|
||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should execute GetPendingModerationQuery with custom page/limit', async () => {
|
||||
const mockResults = { data: [], total: 0, page: 2, limit: 10, totalPages: 0 };
|
||||
mockQueryBus.execute.mockResolvedValue(mockResults);
|
||||
|
||||
const result = await controller.getPendingModeration(2, 10);
|
||||
|
||||
expect(result).toEqual(mockResults);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moderateListing', () => {
|
||||
it('should execute ModerateListingCommand via command bus', async () => {
|
||||
mockCommandBus.execute.mockResolvedValue({ status: 'ACTIVE' });
|
||||
|
||||
const dto = { action: 'approve' as const, moderationScore: 90, notes: 'Hợp lệ' };
|
||||
const user = { sub: 'admin-1', email: 'admin@example.com', role: 'ADMIN' };
|
||||
|
||||
const result = await controller.moderateListing('listing-1', dto as any, user as any);
|
||||
|
||||
expect(result).toEqual({ status: 'ACTIVE' });
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ModerateListingDto } from '../dto/moderate-listing.dto';
|
||||
|
||||
describe('ModerateListingDto', () => {
|
||||
it('should pass validation with approve action', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'approve',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.action).toBe('approve');
|
||||
});
|
||||
|
||||
it('should pass validation with reject action and notes', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'reject',
|
||||
notes: 'Ảnh không rõ ràng',
|
||||
moderationScore: 30,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.action).toBe('reject');
|
||||
expect(dto.notes).toBe('Ảnh không rõ ràng');
|
||||
expect(dto.moderationScore).toBe(30);
|
||||
});
|
||||
|
||||
it('should fail validation with invalid action', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'suspend',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const actionError = errors.find((e) => e.property === 'action');
|
||||
expect(actionError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when moderationScore exceeds 100', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'approve',
|
||||
moderationScore: 150,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const scoreError = errors.find((e) => e.property === 'moderationScore');
|
||||
expect(scoreError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when moderationScore is negative', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'approve',
|
||||
moderationScore: -10,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const scoreError = errors.find((e) => e.property === 'moderationScore');
|
||||
expect(scoreError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass validation with optional fields omitted', async () => {
|
||||
const dto = plainToInstance(ModerateListingDto, {
|
||||
action: 'reject',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.moderationScore).toBeUndefined();
|
||||
expect(dto.notes).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SearchListingsDto } from '../dto/search-listings.dto';
|
||||
|
||||
describe('SearchListingsDto', () => {
|
||||
it('should pass validation with no filters (all optional)', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should pass validation with valid status and filters', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
propertyType: 'APARTMENT',
|
||||
city: 'Hồ Chí Minh',
|
||||
district: 'Quận 1',
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.status).toBe('ACTIVE');
|
||||
expect(dto.transactionType).toBe('SALE');
|
||||
});
|
||||
|
||||
it('should fail validation with invalid status enum', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {
|
||||
status: 'INVALID_STATUS',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const statusError = errors.find((e) => e.property === 'status');
|
||||
expect(statusError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when limit exceeds 100', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {
|
||||
limit: 200,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const limitError = errors.find((e) => e.property === 'limit');
|
||||
expect(limitError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when page is less than 1', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {
|
||||
page: 0,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const pageError = errors.find((e) => e.property === 'page');
|
||||
expect(pageError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should pass validation with area and bedroom filters', async () => {
|
||||
const dto = plainToInstance(SearchListingsDto, {
|
||||
minArea: 50,
|
||||
maxArea: 200,
|
||||
bedrooms: 2,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.minArea).toBe(50);
|
||||
expect(dto.maxArea).toBe(200);
|
||||
expect(dto.bedrooms).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UpdateListingStatusDto } from '../dto/update-listing-status.dto';
|
||||
|
||||
describe('UpdateListingStatusDto', () => {
|
||||
it('should pass validation with valid status', async () => {
|
||||
const dto = plainToInstance(UpdateListingStatusDto, {
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.status).toBe('ACTIVE');
|
||||
});
|
||||
|
||||
it('should pass validation with status and moderation notes', async () => {
|
||||
const dto = plainToInstance(UpdateListingStatusDto, {
|
||||
status: 'REJECTED',
|
||||
moderationNotes: 'Đã xác minh thông tin pháp lý',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.status).toBe('REJECTED');
|
||||
expect(dto.moderationNotes).toBe('Đã xác minh thông tin pháp lý');
|
||||
});
|
||||
|
||||
it('should fail validation with invalid status', async () => {
|
||||
const dto = plainToInstance(UpdateListingStatusDto, {
|
||||
status: 'INVALID_STATUS',
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const statusError = errors.find((e) => e.property === 'status');
|
||||
expect(statusError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fail validation when status is missing', async () => {
|
||||
const dto = plainToInstance(UpdateListingStatusDto, {});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const statusError = errors.find((e) => e.property === 'status');
|
||||
expect(statusError).toBeDefined();
|
||||
});
|
||||
|
||||
it('should accept all valid ListingStatus enum values', async () => {
|
||||
const validStatuses = ['DRAFT', 'PENDING_REVIEW', 'ACTIVE', 'RESERVED', 'SOLD', 'RENTED', 'EXPIRED', 'REJECTED'];
|
||||
|
||||
for (const status of validStatuses) {
|
||||
const dto = plainToInstance(UpdateListingStatusDto, { status });
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user