test(api): add unit tests for admin, leads, and reviews modules
Add missing test coverage: - reject-listing handler spec - user-deactivated listener spec - lead-score value object spec - rating value object spec Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { ListingEntity } from '@modules/listings/domain/entities/listing.entity';
|
||||
import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
|
||||
import { Price } from '@modules/listings/domain/value-objects/price.vo';
|
||||
import { RejectListingCommand } from '../commands/reject-listing/reject-listing.command';
|
||||
import { RejectListingHandler } from '../commands/reject-listing/reject-listing.handler';
|
||||
|
||||
function createPendingListing(id = 'listing-1'): ListingEntity {
|
||||
const price = Price.create(1_000_000_000n).unwrap();
|
||||
const listing = ListingEntity.createNew(
|
||||
id, 'prop-1', 'seller-1', 'SALE', price, 100, 'agent-1',
|
||||
);
|
||||
listing.submitForReview();
|
||||
listing.clearDomainEvents();
|
||||
return listing;
|
||||
}
|
||||
|
||||
describe('RejectListingHandler', () => {
|
||||
let handler: RejectListingHandler;
|
||||
let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockListingRepo = {
|
||||
findById: vi.fn(),
|
||||
findByIdWithProperty: vi.fn(),
|
||||
save: vi.fn(),
|
||||
update: vi.fn(),
|
||||
search: vi.fn(),
|
||||
findByStatus: vi.fn(),
|
||||
findBySellerId: vi.fn(),
|
||||
};
|
||||
|
||||
mockEventBus = { publish: vi.fn() };
|
||||
|
||||
handler = new RejectListingHandler(
|
||||
mockListingRepo as any,
|
||||
mockEventBus as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a pending listing successfully', async () => {
|
||||
const listing = createPendingListing();
|
||||
mockListingRepo.findById.mockResolvedValue(listing);
|
||||
mockListingRepo.update.mockResolvedValue(undefined);
|
||||
|
||||
const command = new RejectListingCommand('listing-1', 'admin-1', 'Vi phạm chính sách');
|
||||
const result = await handler.execute(command);
|
||||
|
||||
expect(result.status).toBe('REJECTED');
|
||||
expect(result.listingId).toBe('listing-1');
|
||||
expect(result.message).toBe('Listing đã bị từ chối');
|
||||
expect(mockListingRepo.update).toHaveBeenCalledTimes(1);
|
||||
expect(mockEventBus.publish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws NotFoundException when listing does not exist', async () => {
|
||||
mockListingRepo.findById.mockResolvedValue(null);
|
||||
|
||||
const command = new RejectListingCommand('nonexistent', 'admin-1', 'Lý do');
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow('Listing không tồn tại');
|
||||
});
|
||||
|
||||
it('throws ValidationException when listing is not pending review', async () => {
|
||||
const price = Price.create(500_000_000n).unwrap();
|
||||
const listing = ListingEntity.createNew(
|
||||
'listing-1', 'prop-1', 'seller-1', 'SALE', price, 80,
|
||||
);
|
||||
listing.clearDomainEvents();
|
||||
mockListingRepo.findById.mockResolvedValue(listing);
|
||||
|
||||
const command = new RejectListingCommand('listing-1', 'admin-1', 'Lý do');
|
||||
|
||||
await expect(handler.execute(command)).rejects.toThrow(/trạng thái/);
|
||||
});
|
||||
|
||||
it('passes rejection reason to listing.reject()', async () => {
|
||||
const listing = createPendingListing();
|
||||
const rejectSpy = vi.spyOn(listing, 'reject');
|
||||
mockListingRepo.findById.mockResolvedValue(listing);
|
||||
mockListingRepo.update.mockResolvedValue(undefined);
|
||||
|
||||
const command = new RejectListingCommand('listing-1', 'admin-1', 'Nội dung không phù hợp');
|
||||
await handler.execute(command);
|
||||
|
||||
expect(rejectSpy).toHaveBeenCalledWith('Nội dung không phù hợp');
|
||||
});
|
||||
|
||||
it('publishes ListingRejectedEvent with correct data', async () => {
|
||||
const listing = createPendingListing('listing-99');
|
||||
mockListingRepo.findById.mockResolvedValue(listing);
|
||||
mockListingRepo.update.mockResolvedValue(undefined);
|
||||
|
||||
const command = new RejectListingCommand('listing-99', 'admin-42', 'Spam');
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
aggregateId: 'listing-99',
|
||||
adminId: 'admin-42',
|
||||
reason: 'Spam',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { UserDeactivatedListener } from '../listeners/user-deactivated.listener';
|
||||
|
||||
describe('UserDeactivatedListener', () => {
|
||||
let listener: UserDeactivatedListener;
|
||||
let mockPrisma: {
|
||||
listing: { updateMany: ReturnType<typeof vi.fn> };
|
||||
};
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockPrisma = {
|
||||
listing: { updateMany: vi.fn().mockResolvedValue({ count: 5 }) },
|
||||
};
|
||||
mockLogger = { log: vi.fn(), warn: vi.fn() };
|
||||
|
||||
listener = new UserDeactivatedListener(
|
||||
mockPrisma as any,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('expires ACTIVE and PENDING_REVIEW listings for deactivated user', async () => {
|
||||
await listener.handle({
|
||||
aggregateId: 'user-1',
|
||||
eventName: 'user.deactivated',
|
||||
occurredAt: new Date(),
|
||||
} as any);
|
||||
|
||||
expect(mockPrisma.listing.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sellerId: 'user-1',
|
||||
status: { in: ['ACTIVE', 'PENDING_REVIEW'] },
|
||||
},
|
||||
data: { status: 'EXPIRED' },
|
||||
});
|
||||
});
|
||||
|
||||
it('logs the deactivation handling', async () => {
|
||||
await listener.handle({
|
||||
aggregateId: 'user-42',
|
||||
eventName: 'user.deactivated',
|
||||
occurredAt: new Date(),
|
||||
} as any);
|
||||
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('user-42'),
|
||||
'UserDeactivatedListener',
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the number of expired listings', async () => {
|
||||
mockPrisma.listing.updateMany.mockResolvedValue({ count: 3 });
|
||||
|
||||
await listener.handle({
|
||||
aggregateId: 'user-1',
|
||||
eventName: 'user.deactivated',
|
||||
occurredAt: new Date(),
|
||||
} as any);
|
||||
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('3'),
|
||||
'UserDeactivatedListener',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles user with no listings gracefully', async () => {
|
||||
mockPrisma.listing.updateMany.mockResolvedValue({ count: 0 });
|
||||
|
||||
await listener.handle({
|
||||
aggregateId: 'user-no-listings',
|
||||
eventName: 'user.deactivated',
|
||||
occurredAt: new Date(),
|
||||
} as any);
|
||||
|
||||
expect(mockPrisma.listing.updateMany).toHaveBeenCalled();
|
||||
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('0'),
|
||||
'UserDeactivatedListener',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { LeadScore } from '../value-objects/lead-score.vo';
|
||||
|
||||
describe('LeadScore Value Object', () => {
|
||||
it('should create a valid lead score', () => {
|
||||
const result = LeadScore.create(75);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(75);
|
||||
});
|
||||
|
||||
it('should accept minimum score of 0', () => {
|
||||
const result = LeadScore.create(0);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(0);
|
||||
});
|
||||
|
||||
it('should accept maximum score of 100', () => {
|
||||
const result = LeadScore.create(100);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(100);
|
||||
});
|
||||
|
||||
it('should reject negative score', () => {
|
||||
const result = LeadScore.create(-1);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Điểm lead phải từ 0 đến 100');
|
||||
});
|
||||
|
||||
it('should reject score above 100', () => {
|
||||
const result = LeadScore.create(101);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Điểm lead phải từ 0 đến 100');
|
||||
});
|
||||
|
||||
it('should accept decimal scores', () => {
|
||||
const result = LeadScore.create(50.5);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(50.5);
|
||||
});
|
||||
|
||||
it('should accept boundary score of 1', () => {
|
||||
const result = LeadScore.create(1);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(1);
|
||||
});
|
||||
|
||||
it('should accept boundary score of 99', () => {
|
||||
const result = LeadScore.create(99);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(99);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Rating } from '../value-objects/rating.vo';
|
||||
|
||||
describe('Rating Value Object', () => {
|
||||
it('should create a valid rating of 1', () => {
|
||||
const result = Rating.create(1);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(1);
|
||||
});
|
||||
|
||||
it('should create a valid rating of 5', () => {
|
||||
const result = Rating.create(5);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(5);
|
||||
});
|
||||
|
||||
it('should create a valid rating of 3', () => {
|
||||
const result = Rating.create(3);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(3);
|
||||
});
|
||||
|
||||
it('should reject rating of 0', () => {
|
||||
const result = Rating.create(0);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Đánh giá phải từ 1 đến 5 sao');
|
||||
});
|
||||
|
||||
it('should reject rating above 5', () => {
|
||||
const result = Rating.create(6);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Đánh giá phải từ 1 đến 5 sao');
|
||||
});
|
||||
|
||||
it('should reject negative rating', () => {
|
||||
const result = Rating.create(-1);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Đánh giá phải từ 1 đến 5 sao');
|
||||
});
|
||||
|
||||
it('should reject non-integer rating', () => {
|
||||
const result = Rating.create(3.5);
|
||||
expect(result.isErr).toBe(true);
|
||||
expect(result.unwrapErr()).toBe('Đánh giá phải từ 1 đến 5 sao');
|
||||
});
|
||||
|
||||
it('should create all valid integer ratings (1-5)', () => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const result = Rating.create(i);
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(result.unwrap().value).toBe(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user