more backend unit test coverage
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// Mock dependencies
|
||||
jest.mock('../../../../../services/email/core/EmailClient', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
sendEmail: jest.fn().mockResolvedValue({ success: true, messageId: 'msg-123' }),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../services/email/core/TemplateManager', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
renderTemplate: jest.fn().mockResolvedValue('<html>Test</html>'),
|
||||
}));
|
||||
});
|
||||
|
||||
const AuthEmailService = require('../../../../../services/email/domain/AuthEmailService');
|
||||
|
||||
describe('AuthEmailService', () => {
|
||||
let service;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||
service = new AuthEmailService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should initialize only once', async () => {
|
||||
await service.initialize();
|
||||
await service.initialize();
|
||||
|
||||
expect(service.emailClient.initialize).toHaveBeenCalledTimes(1);
|
||||
expect(service.templateManager.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendVerificationEmail', () => {
|
||||
it('should send verification email with correct variables', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
const token = 'verify-token';
|
||||
|
||||
const result = await service.sendVerificationEmail(user, token);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'emailVerificationToUser',
|
||||
expect.objectContaining({
|
||||
recipientName: 'John',
|
||||
verificationUrl: 'http://localhost:3000/verify-email?token=verify-token',
|
||||
})
|
||||
);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'john@example.com',
|
||||
'Verify Your Email - Village Share',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default name when firstName is missing', async () => {
|
||||
const user = { email: 'john@example.com' };
|
||||
const token = 'verify-token';
|
||||
|
||||
await service.sendVerificationEmail(user, token);
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'emailVerificationToUser',
|
||||
expect.objectContaining({ recipientName: 'there' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordResetEmail', () => {
|
||||
it('should send password reset email with reset URL', async () => {
|
||||
const user = { firstName: 'Jane', email: 'jane@example.com' };
|
||||
const token = 'reset-token';
|
||||
|
||||
const result = await service.sendPasswordResetEmail(user, token);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'passwordResetToUser',
|
||||
expect.objectContaining({
|
||||
recipientName: 'Jane',
|
||||
resetUrl: 'http://localhost:3000/reset-password?token=reset-token',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordChangedEmail', () => {
|
||||
it('should send password changed confirmation', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
const result = await service.sendPasswordChangedEmail(user);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'passwordChangedToUser',
|
||||
expect.objectContaining({
|
||||
recipientName: 'John',
|
||||
email: 'john@example.com',
|
||||
timestamp: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPersonalInfoChangedEmail', () => {
|
||||
it('should send personal info changed notification', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
const result = await service.sendPersonalInfoChangedEmail(user);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'personalInfoChangedToUser',
|
||||
expect.objectContaining({ recipientName: 'John' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTwoFactorOtpEmail', () => {
|
||||
it('should send OTP code email', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
const otpCode = '123456';
|
||||
|
||||
const result = await service.sendTwoFactorOtpEmail(user, otpCode);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'twoFactorOtpToUser',
|
||||
expect.objectContaining({
|
||||
recipientName: 'John',
|
||||
otpCode: '123456',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTwoFactorEnabledEmail', () => {
|
||||
it('should send 2FA enabled confirmation', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
const result = await service.sendTwoFactorEnabledEmail(user);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'twoFactorEnabledToUser',
|
||||
expect.objectContaining({ recipientName: 'John' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendTwoFactorDisabledEmail', () => {
|
||||
it('should send 2FA disabled notification', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
const result = await service.sendTwoFactorDisabledEmail(user);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'twoFactorDisabledToUser',
|
||||
expect.objectContaining({ recipientName: 'John' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendRecoveryCodeUsedEmail', () => {
|
||||
it('should send recovery code used notification with green color for many codes', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
const result = await service.sendRecoveryCodeUsedEmail(user, 8);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'recoveryCodeUsedToUser',
|
||||
expect.objectContaining({
|
||||
remainingCodes: 8,
|
||||
remainingCodesColor: '#28a745',
|
||||
lowCodesWarning: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use orange color for medium remaining codes', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
await service.sendRecoveryCodeUsedEmail(user, 4);
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'recoveryCodeUsedToUser',
|
||||
expect.objectContaining({
|
||||
remainingCodesColor: '#fd7e14',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use red color and warning for low remaining codes', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
|
||||
await service.sendRecoveryCodeUsedEmail(user, 1);
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'recoveryCodeUsedToUser',
|
||||
expect.objectContaining({
|
||||
remainingCodesColor: '#dc3545',
|
||||
lowCodesWarning: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
// Mock dependencies
|
||||
jest.mock('../../../../../services/email/core/EmailClient', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
sendEmail: jest.fn().mockResolvedValue({ success: true, messageId: 'msg-123' }),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../services/email/core/TemplateManager', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
renderTemplate: jest.fn().mockResolvedValue('<html>Test</html>'),
|
||||
}));
|
||||
});
|
||||
|
||||
const FeedbackEmailService = require('../../../../../services/email/domain/FeedbackEmailService');
|
||||
|
||||
describe('FeedbackEmailService', () => {
|
||||
let service;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env = { ...originalEnv, FEEDBACK_EMAIL: 'feedback@example.com' };
|
||||
service = new FeedbackEmailService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should initialize only once', async () => {
|
||||
await service.initialize();
|
||||
await service.initialize();
|
||||
|
||||
expect(service.emailClient.initialize).toHaveBeenCalledTimes(1);
|
||||
expect(service.templateManager.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendFeedbackConfirmation', () => {
|
||||
it('should send feedback confirmation to user', async () => {
|
||||
const user = { firstName: 'John', email: 'john@example.com' };
|
||||
const feedback = {
|
||||
feedbackText: 'Great app!',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const result = await service.sendFeedbackConfirmation(user, feedback);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'feedbackConfirmationToUser',
|
||||
expect.objectContaining({
|
||||
userName: 'John',
|
||||
userEmail: 'john@example.com',
|
||||
feedbackText: 'Great app!',
|
||||
})
|
||||
);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'john@example.com',
|
||||
'Thank You for Your Feedback - Village Share',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default name when firstName is missing', async () => {
|
||||
const user = { email: 'john@example.com' };
|
||||
const feedback = {
|
||||
feedbackText: 'Great app!',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
await service.sendFeedbackConfirmation(user, feedback);
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'feedbackConfirmationToUser',
|
||||
expect.objectContaining({ userName: 'there' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendFeedbackNotificationToAdmin', () => {
|
||||
it('should send feedback notification to admin', async () => {
|
||||
const user = {
|
||||
id: 'user-123',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john@example.com',
|
||||
};
|
||||
const feedback = {
|
||||
id: 'feedback-123',
|
||||
feedbackText: 'Great app!',
|
||||
url: 'https://example.com/page',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const result = await service.sendFeedbackNotificationToAdmin(user, feedback);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'feedbackNotificationToAdmin',
|
||||
expect.objectContaining({
|
||||
userName: 'John Doe',
|
||||
userEmail: 'john@example.com',
|
||||
userId: 'user-123',
|
||||
feedbackText: 'Great app!',
|
||||
feedbackId: 'feedback-123',
|
||||
url: 'https://example.com/page',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
})
|
||||
);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'feedback@example.com',
|
||||
'New Feedback from John Doe',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return error when no admin email configured', async () => {
|
||||
delete process.env.FEEDBACK_EMAIL;
|
||||
delete process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||
|
||||
const user = { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' };
|
||||
const feedback = { id: 'feedback-123', feedbackText: 'Test', createdAt: new Date() };
|
||||
|
||||
const result = await service.sendFeedbackNotificationToAdmin(user, feedback);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('No admin email configured');
|
||||
});
|
||||
|
||||
it('should use CUSTOMER_SUPPORT_EMAIL when FEEDBACK_EMAIL not set', async () => {
|
||||
delete process.env.FEEDBACK_EMAIL;
|
||||
process.env.CUSTOMER_SUPPORT_EMAIL = 'support@example.com';
|
||||
|
||||
const user = { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' };
|
||||
const feedback = { id: 'feedback-123', feedbackText: 'Test', createdAt: new Date() };
|
||||
|
||||
await service.sendFeedbackNotificationToAdmin(user, feedback);
|
||||
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'support@example.com',
|
||||
expect.any(String),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default values for optional fields', async () => {
|
||||
const user = { id: 'user-123', firstName: 'John', lastName: 'Doe', email: 'john@example.com' };
|
||||
const feedback = { id: 'feedback-123', feedbackText: 'Test', createdAt: new Date() };
|
||||
|
||||
await service.sendFeedbackNotificationToAdmin(user, feedback);
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'feedbackNotificationToAdmin',
|
||||
expect.objectContaining({
|
||||
url: 'Not provided',
|
||||
userAgent: 'Not provided',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
// Mock dependencies
|
||||
jest.mock('../../../../../services/email/core/EmailClient', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
sendEmail: jest.fn().mockResolvedValue({ success: true, messageId: 'msg-123' }),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../services/email/core/TemplateManager', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
renderTemplate: jest.fn().mockResolvedValue('<html>Test</html>'),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../utils/logger', () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}));
|
||||
|
||||
const ForumEmailService = require('../../../../../services/email/domain/ForumEmailService');
|
||||
|
||||
describe('ForumEmailService', () => {
|
||||
let service;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||
service = new ForumEmailService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should initialize only once', async () => {
|
||||
await service.initialize();
|
||||
await service.initialize();
|
||||
|
||||
expect(service.emailClient.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumCommentNotification', () => {
|
||||
it('should send comment notification to post author', async () => {
|
||||
const postAuthor = { firstName: 'John', email: 'john@example.com' };
|
||||
const commenter = { firstName: 'Jane', lastName: 'Doe' };
|
||||
const post = { id: 123, title: 'Test Post' };
|
||||
const comment = { content: 'Great post!', createdAt: new Date() };
|
||||
|
||||
const result = await service.sendForumCommentNotification(postAuthor, commenter, post, comment);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'forumCommentToPostAuthor',
|
||||
expect.objectContaining({
|
||||
postAuthorName: 'John',
|
||||
commenterName: 'Jane Doe',
|
||||
postTitle: 'Test Post',
|
||||
commentContent: 'Great post!',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
service.templateManager.renderTemplate.mockRejectedValue(new Error('Template error'));
|
||||
|
||||
const result = await service.sendForumCommentNotification(
|
||||
{ email: 'test@example.com' },
|
||||
{ firstName: 'Jane', lastName: 'Doe' },
|
||||
{ id: 1, title: 'Test' },
|
||||
{ content: 'Test', createdAt: new Date() }
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Template error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumReplyNotification', () => {
|
||||
it('should send reply notification to comment author', async () => {
|
||||
const commentAuthor = { firstName: 'John', email: 'john@example.com' };
|
||||
const replier = { firstName: 'Jane', lastName: 'Doe' };
|
||||
const post = { id: 123, title: 'Test Post' };
|
||||
const reply = { content: 'Good point!', createdAt: new Date() };
|
||||
const parentComment = { content: 'Original comment' };
|
||||
|
||||
const result = await service.sendForumReplyNotification(
|
||||
commentAuthor, replier, post, reply, parentComment
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'forumReplyToCommentAuthor',
|
||||
expect.objectContaining({
|
||||
commentAuthorName: 'John',
|
||||
replierName: 'Jane Doe',
|
||||
parentCommentContent: 'Original comment',
|
||||
replyContent: 'Good point!',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumAnswerAcceptedNotification', () => {
|
||||
it('should send answer accepted notification', async () => {
|
||||
const commentAuthor = { firstName: 'John', email: 'john@example.com' };
|
||||
const postAuthor = { firstName: 'Jane', lastName: 'Doe' };
|
||||
const post = { id: 123, title: 'Test Question' };
|
||||
const comment = { content: 'The answer is...' };
|
||||
|
||||
const result = await service.sendForumAnswerAcceptedNotification(
|
||||
commentAuthor, postAuthor, post, comment
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'john@example.com',
|
||||
'Your comment was marked as the accepted answer!',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumThreadActivityNotification', () => {
|
||||
it('should send thread activity notification', async () => {
|
||||
const participant = { firstName: 'John', email: 'john@example.com' };
|
||||
const commenter = { firstName: 'Jane', lastName: 'Doe' };
|
||||
const post = { id: 123, title: 'Test Post' };
|
||||
const comment = { content: 'New comment', createdAt: new Date() };
|
||||
|
||||
const result = await service.sendForumThreadActivityNotification(
|
||||
participant, commenter, post, comment
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumPostClosedNotification', () => {
|
||||
it('should send post closed notification', async () => {
|
||||
const recipient = { firstName: 'John', email: 'john@example.com' };
|
||||
const closer = { firstName: 'Admin', lastName: 'User' };
|
||||
const post = { id: 123, title: 'Test Post' };
|
||||
const closedAt = new Date();
|
||||
|
||||
const result = await service.sendForumPostClosedNotification(
|
||||
recipient, closer, post, closedAt
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'john@example.com',
|
||||
'Discussion closed: Test Post',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumPostDeletionNotification', () => {
|
||||
it('should send post deletion notification', async () => {
|
||||
const postAuthor = { firstName: 'John', email: 'john@example.com' };
|
||||
const admin = { firstName: 'Admin', lastName: 'User' };
|
||||
const post = { title: 'Deleted Post' };
|
||||
const deletionReason = 'Violated community guidelines';
|
||||
|
||||
const result = await service.sendForumPostDeletionNotification(
|
||||
postAuthor, admin, post, deletionReason
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'forumPostDeletionToAuthor',
|
||||
expect.objectContaining({
|
||||
deletionReason: 'Violated community guidelines',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendForumCommentDeletionNotification', () => {
|
||||
it('should send comment deletion notification', async () => {
|
||||
const commentAuthor = { firstName: 'John', email: 'john@example.com' };
|
||||
const admin = { firstName: 'Admin', lastName: 'User' };
|
||||
const post = { id: 123, title: 'Test Post' };
|
||||
const deletionReason = 'Violated community guidelines';
|
||||
|
||||
const result = await service.sendForumCommentDeletionNotification(
|
||||
commentAuthor, admin, post, deletionReason
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendItemRequestNotification', () => {
|
||||
it('should send item request notification to nearby users', async () => {
|
||||
const recipient = { firstName: 'John', email: 'john@example.com' };
|
||||
const requester = { firstName: 'Jane', lastName: 'Doe' };
|
||||
const post = { id: 123, title: 'Looking for a Drill', content: 'Need a power drill for the weekend' };
|
||||
const distance = '2.5';
|
||||
|
||||
const result = await service.sendItemRequestNotification(
|
||||
recipient, requester, post, distance
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'forumItemRequestNotification',
|
||||
expect.objectContaining({
|
||||
itemRequested: 'Looking for a Drill',
|
||||
distance: '2.5',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
// Mock dependencies
|
||||
jest.mock('../../../../../services/email/core/EmailClient', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
sendEmail: jest.fn().mockResolvedValue({ success: true, messageId: 'msg-123' }),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../services/email/core/TemplateManager', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
initialize: jest.fn().mockResolvedValue(),
|
||||
renderTemplate: jest.fn().mockResolvedValue('<html>Test</html>'),
|
||||
}));
|
||||
});
|
||||
|
||||
jest.mock('../../../../../utils/logger', () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}));
|
||||
|
||||
const PaymentEmailService = require('../../../../../services/email/domain/PaymentEmailService');
|
||||
|
||||
describe('PaymentEmailService', () => {
|
||||
let service;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
FRONTEND_URL: 'http://localhost:3000',
|
||||
ADMIN_EMAIL: 'admin@example.com',
|
||||
};
|
||||
service = new PaymentEmailService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should initialize only once', async () => {
|
||||
await service.initialize();
|
||||
await service.initialize();
|
||||
|
||||
expect(service.emailClient.initialize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPaymentDeclinedNotification', () => {
|
||||
it('should send payment declined notification to renter', async () => {
|
||||
const result = await service.sendPaymentDeclinedNotification('renter@example.com', {
|
||||
renterFirstName: 'John',
|
||||
itemName: 'Test Item',
|
||||
declineReason: 'Card declined',
|
||||
updatePaymentUrl: 'http://localhost:3000/update-payment',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'paymentDeclinedToRenter',
|
||||
expect.objectContaining({
|
||||
renterFirstName: 'John',
|
||||
itemName: 'Test Item',
|
||||
declineReason: 'Card declined',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default values for missing params', async () => {
|
||||
await service.sendPaymentDeclinedNotification('renter@example.com', {});
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'paymentDeclinedToRenter',
|
||||
expect.objectContaining({
|
||||
renterFirstName: 'there',
|
||||
itemName: 'the item',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
service.templateManager.renderTemplate.mockRejectedValue(new Error('Template error'));
|
||||
|
||||
const result = await service.sendPaymentDeclinedNotification('test@example.com', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Template error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPaymentMethodUpdatedNotification', () => {
|
||||
it('should send payment method updated notification to owner', async () => {
|
||||
const result = await service.sendPaymentMethodUpdatedNotification('owner@example.com', {
|
||||
ownerFirstName: 'Jane',
|
||||
itemName: 'Test Item',
|
||||
approvalUrl: 'http://localhost:3000/approve',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'owner@example.com',
|
||||
'Payment Method Updated - Test Item',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPayoutFailedNotification', () => {
|
||||
it('should send payout failed notification to owner', async () => {
|
||||
const result = await service.sendPayoutFailedNotification('owner@example.com', {
|
||||
ownerName: 'John',
|
||||
payoutAmount: 50.00,
|
||||
failureMessage: 'Bank account closed',
|
||||
actionRequired: 'Please update your bank account',
|
||||
failureCode: 'account_closed',
|
||||
requiresBankUpdate: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'payoutFailedToOwner',
|
||||
expect.objectContaining({
|
||||
ownerName: 'John',
|
||||
payoutAmount: '50.00',
|
||||
failureCode: 'account_closed',
|
||||
requiresBankUpdate: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendAccountDisconnectedEmail', () => {
|
||||
it('should send account disconnected notification', async () => {
|
||||
const result = await service.sendAccountDisconnectedEmail('owner@example.com', {
|
||||
ownerName: 'John',
|
||||
hasPendingPayouts: true,
|
||||
pendingPayoutCount: 3,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'accountDisconnectedToOwner',
|
||||
expect.objectContaining({
|
||||
hasPendingPayouts: true,
|
||||
pendingPayoutCount: 3,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default values for missing params', async () => {
|
||||
await service.sendAccountDisconnectedEmail('owner@example.com', {});
|
||||
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'accountDisconnectedToOwner',
|
||||
expect.objectContaining({
|
||||
ownerName: 'there',
|
||||
hasPendingPayouts: false,
|
||||
pendingPayoutCount: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPayoutsDisabledEmail', () => {
|
||||
it('should send payouts disabled notification', async () => {
|
||||
const result = await service.sendPayoutsDisabledEmail('owner@example.com', {
|
||||
ownerName: 'John',
|
||||
disabledReason: 'Verification required',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'owner@example.com',
|
||||
'Action Required: Your payouts have been paused - Village Share',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendDisputeAlertEmail', () => {
|
||||
it('should send dispute alert to admin', async () => {
|
||||
const result = await service.sendDisputeAlertEmail({
|
||||
rentalId: 'rental-123',
|
||||
amount: 50.00,
|
||||
reason: 'fraudulent',
|
||||
evidenceDueBy: new Date(),
|
||||
renterName: 'Renter Name',
|
||||
renterEmail: 'renter@example.com',
|
||||
ownerName: 'Owner Name',
|
||||
ownerEmail: 'owner@example.com',
|
||||
itemName: 'Test Item',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||
'admin@example.com',
|
||||
'URGENT: Payment Dispute - Rental #rental-123',
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendDisputeLostAlertEmail', () => {
|
||||
it('should send dispute lost alert to admin', async () => {
|
||||
const result = await service.sendDisputeLostAlertEmail({
|
||||
rentalId: 'rental-123',
|
||||
amount: 50.00,
|
||||
ownerPayoutAmount: 45.00,
|
||||
ownerName: 'Owner Name',
|
||||
ownerEmail: 'owner@example.com',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||
'disputeLostAlertToAdmin',
|
||||
expect.objectContaining({
|
||||
rentalId: 'rental-123',
|
||||
amount: '50.00',
|
||||
ownerPayoutAmount: '45.00',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDisputeReason', () => {
|
||||
it('should format known dispute reasons', () => {
|
||||
expect(service.formatDisputeReason('fraudulent')).toBe('Fraudulent transaction');
|
||||
expect(service.formatDisputeReason('product_not_received')).toBe('Product not received');
|
||||
expect(service.formatDisputeReason('duplicate')).toBe('Duplicate charge');
|
||||
});
|
||||
|
||||
it('should return original reason for unknown reasons', () => {
|
||||
expect(service.formatDisputeReason('unknown_reason')).toBe('unknown_reason');
|
||||
});
|
||||
|
||||
it('should return "Unknown reason" for null/undefined', () => {
|
||||
expect(service.formatDisputeReason(null)).toBe('Unknown reason');
|
||||
expect(service.formatDisputeReason(undefined)).toBe('Unknown reason');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user