test: add unit tests for Analytics, Search, and Notifications modules
Add 15 test files with 45 test cases covering all untested handlers: - Analytics: track-event, generate-report, update-market-index, get-heatmap, get-price-trend, get-market-report, get-district-stats - Search: reindex-all, sync-listing, search-properties, geo-search, listing-approved event handler - Notifications: send-notification, agent-verified listener, user-registered listener Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { AgentVerifiedListener } from '../listeners/agent-verified.listener';
|
||||
import { AgentVerifiedEvent } from '@modules/auth/domain/events/agent-verified.event';
|
||||
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
|
||||
|
||||
describe('AgentVerifiedListener', () => {
|
||||
let listener: AgentVerifiedListener;
|
||||
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
|
||||
let mockPrisma: { agent: { findUnique: ReturnType<typeof vi.fn> } };
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommandBus = { execute: vi.fn().mockResolvedValue(undefined) };
|
||||
mockPrisma = {
|
||||
agent: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
};
|
||||
mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
listener = new AgentVerifiedListener(
|
||||
mockCommandBus as any,
|
||||
mockPrisma as any,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends email notification when agent has email', async () => {
|
||||
mockPrisma.agent.findUnique.mockResolvedValue({
|
||||
id: 'agent-1',
|
||||
user: { id: 'user-1', email: 'agent@test.com' },
|
||||
});
|
||||
|
||||
const event = new AgentVerifiedEvent('agent-1', 'user-1');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledWith(
|
||||
expect.any(SendNotificationCommand),
|
||||
);
|
||||
|
||||
const cmd = mockCommandBus.execute.mock.calls[0]![0] as SendNotificationCommand;
|
||||
expect(cmd.userId).toBe('user-1');
|
||||
expect(cmd.channel).toBe('EMAIL');
|
||||
expect(cmd.templateKey).toBe('agent.verified');
|
||||
expect(cmd.recipientAddress).toBe('agent@test.com');
|
||||
});
|
||||
|
||||
it('skips notification when agent not found', async () => {
|
||||
mockPrisma.agent.findUnique.mockResolvedValue(null);
|
||||
|
||||
const event = new AgentVerifiedEvent('agent-1', 'user-1');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips notification when agent has no email', async () => {
|
||||
mockPrisma.agent.findUnique.mockResolvedValue({
|
||||
id: 'agent-1',
|
||||
user: { id: 'user-1', email: null },
|
||||
});
|
||||
|
||||
const event = new AgentVerifiedEvent('agent-1', 'user-1');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { SendNotificationHandler } from '../commands/send-notification/send-notification.handler';
|
||||
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
|
||||
|
||||
describe('SendNotificationHandler', () => {
|
||||
let handler: SendNotificationHandler;
|
||||
let mockNotificationRepo: {
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
updateStatus: ReturnType<typeof vi.fn>;
|
||||
findByUserId: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockPreferenceRepo: {
|
||||
findByUserId: ReturnType<typeof vi.fn>;
|
||||
isEnabled: ReturnType<typeof vi.fn>;
|
||||
upsert: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let mockEmailService: { send: ReturnType<typeof vi.fn> };
|
||||
let mockFcmService: { send: ReturnType<typeof vi.fn> };
|
||||
let mockTemplateService: { render: ReturnType<typeof vi.fn> };
|
||||
let mockEventBus: { publish: ReturnType<typeof vi.fn> };
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
const notificationEntity = {
|
||||
id: 'notif-1',
|
||||
userId: 'user-1',
|
||||
channel: 'EMAIL' as const,
|
||||
templateKey: 'user.registered',
|
||||
subject: 'Chào mừng!',
|
||||
body: '<p>Chào mừng!</p>',
|
||||
metadata: null,
|
||||
status: 'PENDING' as const,
|
||||
errorDetail: null,
|
||||
sentAt: null,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockNotificationRepo = {
|
||||
create: vi.fn().mockResolvedValue(notificationEntity),
|
||||
updateStatus: vi.fn().mockResolvedValue(undefined),
|
||||
findByUserId: vi.fn(),
|
||||
};
|
||||
mockPreferenceRepo = {
|
||||
findByUserId: vi.fn(),
|
||||
isEnabled: vi.fn().mockResolvedValue(true),
|
||||
upsert: vi.fn(),
|
||||
};
|
||||
mockEmailService = { send: vi.fn().mockResolvedValue({ messageId: 'msg-1' }) };
|
||||
mockFcmService = { send: vi.fn().mockResolvedValue('fcm-msg-1') };
|
||||
mockTemplateService = {
|
||||
render: vi.fn().mockReturnValue({ subject: 'Chào mừng!', body: '<p>Chào mừng!</p>' }),
|
||||
};
|
||||
mockEventBus = { publish: vi.fn() };
|
||||
mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
handler = new SendNotificationHandler(
|
||||
mockNotificationRepo as any,
|
||||
mockPreferenceRepo as any,
|
||||
mockEmailService as any,
|
||||
mockFcmService as any,
|
||||
mockTemplateService as any,
|
||||
mockEventBus as any,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends email notification successfully', async () => {
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'EMAIL', 'user.registered', { phone: '0901234567', role: 'BUYER' }, 'test@test.com',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockPreferenceRepo.isEnabled).toHaveBeenCalledWith('user-1', 'EMAIL', 'user.registered');
|
||||
expect(mockTemplateService.render).toHaveBeenCalledWith('user.registered', { phone: '0901234567', role: 'BUYER' });
|
||||
expect(mockNotificationRepo.create).toHaveBeenCalled();
|
||||
expect(mockEmailService.send).toHaveBeenCalledWith({
|
||||
to: 'test@test.com',
|
||||
subject: 'Chào mừng!',
|
||||
html: '<p>Chào mừng!</p>',
|
||||
});
|
||||
expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'SENT');
|
||||
expect(mockEventBus.publish).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends push notification successfully', async () => {
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'PUSH', 'user.registered', { phone: '0901234567', role: 'BUYER' }, 'fcm-token-abc',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockFcmService.send).toHaveBeenCalledWith({
|
||||
token: 'fcm-token-abc',
|
||||
title: 'Chào mừng!',
|
||||
body: 'Chào mừng!', // HTML stripped
|
||||
});
|
||||
expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'SENT');
|
||||
});
|
||||
|
||||
it('skips notification when user preference is disabled', async () => {
|
||||
mockPreferenceRepo.isEnabled.mockResolvedValue(false);
|
||||
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'EMAIL', 'user.registered', {}, 'test@test.com',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockNotificationRepo.create).not.toHaveBeenCalled();
|
||||
expect(mockEmailService.send).not.toHaveBeenCalled();
|
||||
expect(mockLogger.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs pending status for unimplemented channels (SMS)', async () => {
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'SMS', 'user.registered', {}, '+84901234567',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalled();
|
||||
expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'PENDING');
|
||||
expect(mockEmailService.send).not.toHaveBeenCalled();
|
||||
expect(mockFcmService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs pending status for unimplemented channels (ZALO_OA)', async () => {
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'ZALO_OA', 'user.registered', {}, 'zalo-id',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalled();
|
||||
expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'PENDING');
|
||||
});
|
||||
|
||||
it('marks notification as FAILED when email send throws', async () => {
|
||||
mockEmailService.send.mockRejectedValue(new Error('SMTP connection refused'));
|
||||
|
||||
const command = new SendNotificationCommand(
|
||||
'user-1', 'EMAIL', 'user.registered', {}, 'test@test.com',
|
||||
);
|
||||
|
||||
await handler.execute(command);
|
||||
|
||||
expect(mockNotificationRepo.updateStatus).toHaveBeenCalledWith('notif-1', 'FAILED', 'SMTP connection refused');
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
expect(mockEventBus.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { UserRegisteredListener } from '../listeners/user-registered.listener';
|
||||
import { UserRegisteredEvent } from '@modules/auth/domain/events/user-registered.event';
|
||||
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
|
||||
|
||||
describe('UserRegisteredListener', () => {
|
||||
let listener: UserRegisteredListener;
|
||||
let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
|
||||
let mockPrisma: { user: { findUnique: ReturnType<typeof vi.fn> } };
|
||||
let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockCommandBus = { execute: vi.fn().mockResolvedValue(undefined) };
|
||||
mockPrisma = {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
};
|
||||
mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
listener = new UserRegisteredListener(
|
||||
mockCommandBus as any,
|
||||
mockPrisma as any,
|
||||
mockLogger as any,
|
||||
);
|
||||
});
|
||||
|
||||
it('sends welcome email when user has email', async () => {
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
email: 'user@test.com',
|
||||
phone: '0901234567',
|
||||
});
|
||||
|
||||
const event = new UserRegisteredEvent('user-1', '0901234567', 'BUYER');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledWith(
|
||||
expect.any(SendNotificationCommand),
|
||||
);
|
||||
|
||||
const cmd = mockCommandBus.execute.mock.calls[0]![0] as SendNotificationCommand;
|
||||
expect(cmd.userId).toBe('user-1');
|
||||
expect(cmd.channel).toBe('EMAIL');
|
||||
expect(cmd.templateKey).toBe('user.registered');
|
||||
expect(cmd.templateData).toEqual({ phone: '0901234567', role: 'BUYER' });
|
||||
expect(cmd.recipientAddress).toBe('user@test.com');
|
||||
});
|
||||
|
||||
it('skips notification when user not found', async () => {
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
const event = new UserRegisteredEvent('user-1', '0901234567', 'BUYER');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips notification when user has no email', async () => {
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
email: null,
|
||||
phone: '0901234567',
|
||||
});
|
||||
|
||||
const event = new UserRegisteredEvent('user-1', '0901234567', 'SELLER');
|
||||
await listener.handle(event);
|
||||
|
||||
expect(mockCommandBus.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user