more unit tests
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
// 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 AlphaInvitationEmailService = require('../../../../../services/email/domain/AlphaInvitationEmailService');
|
||||||
|
|
||||||
|
describe('AlphaInvitationEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||||
|
service = new AlphaInvitationEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendAlphaInvitation', () => {
|
||||||
|
const email = 'john@example.com';
|
||||||
|
const code = 'ALPHA-123-XYZ';
|
||||||
|
|
||||||
|
it('should send alpha invitation email with correct variables', async () => {
|
||||||
|
const result = await service.sendAlphaInvitation(email, code);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'alphaInvitationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
code: 'ALPHA-123-XYZ',
|
||||||
|
email: 'john@example.com',
|
||||||
|
frontendUrl: 'http://localhost:3000',
|
||||||
|
title: 'Welcome to Alpha Testing!',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Your Alpha Access Code - Village Share',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include code in message variable', async () => {
|
||||||
|
await service.sendAlphaInvitation(email, code);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'alphaInvitationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
message: expect.stringContaining('ALPHA-123-XYZ'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendAlphaInvitation(email, code);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
// 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 CustomerServiceEmailService = require('../../../../../services/email/domain/CustomerServiceEmailService');
|
||||||
|
|
||||||
|
describe('CustomerServiceEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = {
|
||||||
|
...originalEnv,
|
||||||
|
CUSTOMER_SUPPORT_EMAIL: 'support@example.com',
|
||||||
|
};
|
||||||
|
service = new CustomerServiceEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendLateReturnToCustomerService', () => {
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
actualReturnDateTime: '2024-01-16T15:00:00Z',
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
};
|
||||||
|
const owner = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
};
|
||||||
|
const renter = {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Smith',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
};
|
||||||
|
const lateCalculation = {
|
||||||
|
lateHours: 5,
|
||||||
|
lateFee: 25.00,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send late return notification with correct variables', async () => {
|
||||||
|
const result = await service.sendLateReturnToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
lateCalculation
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'lateReturnToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
rentalId: 123,
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
ownerName: 'John Doe',
|
||||||
|
ownerEmail: 'john@example.com',
|
||||||
|
renterName: 'Jane Smith',
|
||||||
|
renterEmail: 'jane@example.com',
|
||||||
|
hoursLate: '5.0',
|
||||||
|
lateFee: '25.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'support@example.com',
|
||||||
|
'Late Return Detected - Action Required',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when customer service email is not configured', async () => {
|
||||||
|
delete process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
|
||||||
|
const result = await service.sendLateReturnToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
lateCalculation
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('No customer service email configured');
|
||||||
|
expect(service.emailClient.sendEmail).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendLateReturnToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
lateCalculation
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendDamageReportToCustomerService', () => {
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
};
|
||||||
|
const owner = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
};
|
||||||
|
const renter = {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Smith',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
};
|
||||||
|
const damageAssessment = {
|
||||||
|
description: 'Drill bit broken',
|
||||||
|
canBeFixed: true,
|
||||||
|
repairCost: 30,
|
||||||
|
needsReplacement: false,
|
||||||
|
replacementCost: null,
|
||||||
|
feeCalculation: {
|
||||||
|
type: 'repair',
|
||||||
|
amount: 30,
|
||||||
|
},
|
||||||
|
proofOfOwnership: ['receipt.jpg'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send damage report with correct variables', async () => {
|
||||||
|
const result = await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
damageAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'damageReportToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
rentalId: 123,
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
ownerName: 'John Doe',
|
||||||
|
ownerEmail: 'john@example.com',
|
||||||
|
renterName: 'Jane Smith',
|
||||||
|
renterEmail: 'jane@example.com',
|
||||||
|
damageDescription: 'Drill bit broken',
|
||||||
|
canBeFixed: 'Yes',
|
||||||
|
repairCost: '30.00',
|
||||||
|
needsReplacement: 'No',
|
||||||
|
feeTypeDescription: 'Repair Cost',
|
||||||
|
damageFee: '30.00',
|
||||||
|
lateFee: '0.00',
|
||||||
|
totalFees: '30.00',
|
||||||
|
hasProofOfOwnership: 'Yes',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'support@example.com',
|
||||||
|
'Damage Report Filed - Action Required',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include late fee when provided', async () => {
|
||||||
|
const lateCalculation = { lateFee: 15 };
|
||||||
|
|
||||||
|
await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
damageAssessment,
|
||||||
|
lateCalculation
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'damageReportToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
lateFee: '15.00',
|
||||||
|
totalFees: '45.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle replacement fee type', async () => {
|
||||||
|
const replacementAssessment = {
|
||||||
|
...damageAssessment,
|
||||||
|
canBeFixed: false,
|
||||||
|
needsReplacement: true,
|
||||||
|
replacementCost: 150,
|
||||||
|
feeCalculation: {
|
||||||
|
type: 'replacement',
|
||||||
|
amount: 150,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
replacementAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'damageReportToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
feeTypeDescription: 'Replacement Cost',
|
||||||
|
needsReplacement: 'Yes',
|
||||||
|
replacementCost: '150.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown fee type', async () => {
|
||||||
|
const unknownTypeAssessment = {
|
||||||
|
...damageAssessment,
|
||||||
|
feeCalculation: {
|
||||||
|
type: 'other',
|
||||||
|
amount: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
unknownTypeAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'damageReportToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
feeTypeDescription: 'Damage Assessment Fee',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate no proof of ownership when not provided', async () => {
|
||||||
|
const noProofAssessment = {
|
||||||
|
...damageAssessment,
|
||||||
|
proofOfOwnership: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
noProofAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'damageReportToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
hasProofOfOwnership: 'No',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when customer service email is not configured', async () => {
|
||||||
|
delete process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
|
||||||
|
const result = await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
damageAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('No customer service email configured');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error'));
|
||||||
|
|
||||||
|
const result = await service.sendDamageReportToCustomerService(
|
||||||
|
rental,
|
||||||
|
owner,
|
||||||
|
renter,
|
||||||
|
damageAssessment
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Send error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendLostItemToCustomerService', () => {
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
itemLostReportedAt: '2024-01-17T10:00:00Z',
|
||||||
|
item: {
|
||||||
|
name: 'Power Drill',
|
||||||
|
replacementCost: 200,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const owner = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
};
|
||||||
|
const renter = {
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Smith',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send lost item notification with correct variables', async () => {
|
||||||
|
const result = await service.sendLostItemToCustomerService(rental, owner, renter);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'lostItemToCS',
|
||||||
|
expect.objectContaining({
|
||||||
|
rentalId: 123,
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
ownerName: 'John Doe',
|
||||||
|
ownerEmail: 'john@example.com',
|
||||||
|
renterName: 'Jane Smith',
|
||||||
|
renterEmail: 'jane@example.com',
|
||||||
|
replacementCost: '200.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'support@example.com',
|
||||||
|
'Lost Item Claim Filed - Action Required',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error when customer service email is not configured', async () => {
|
||||||
|
delete process.env.CUSTOMER_SUPPORT_EMAIL;
|
||||||
|
|
||||||
|
const result = await service.sendLostItemToCustomerService(rental, owner, renter);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('No customer service email configured');
|
||||||
|
expect(service.emailClient.sendEmail).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendLostItemToCustomerService(rental, owner, renter);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// 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 MessagingEmailService = require('../../../../../services/email/domain/MessagingEmailService');
|
||||||
|
|
||||||
|
describe('MessagingEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||||
|
service = new MessagingEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendNewMessageNotification', () => {
|
||||||
|
const receiver = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const sender = { id: 456, firstName: 'Jane', lastName: 'Smith' };
|
||||||
|
const message = {
|
||||||
|
content: 'Hello, is the power drill still available?',
|
||||||
|
createdAt: '2024-01-15T10:00:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send new message notification with correct variables', async () => {
|
||||||
|
const result = await service.sendNewMessageNotification(receiver, sender, message);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'newMessageToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
recipientName: 'John',
|
||||||
|
senderName: 'Jane Smith',
|
||||||
|
messageContent: 'Hello, is the power drill still available?',
|
||||||
|
conversationUrl: 'http://localhost:3000/messages/conversations/456',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'New message from Jane Smith',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when receiver firstName is missing', async () => {
|
||||||
|
const receiverNoName = { email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendNewMessageNotification(receiverNoName, sender, message);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'newMessageToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use A user fallback when sender names produce empty string', async () => {
|
||||||
|
const senderEmptyNames = { id: 456, firstName: '', lastName: '' };
|
||||||
|
|
||||||
|
await service.sendNewMessageNotification(receiver, senderEmptyNames, message);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'newMessageToUser',
|
||||||
|
expect.objectContaining({ senderName: 'A user' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendNewMessageNotification(receiver, sender, message);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,986 @@
|
|||||||
|
// 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 RentalFlowEmailService = require('../../../../../services/email/domain/RentalFlowEmailService');
|
||||||
|
|
||||||
|
describe('RentalFlowEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||||
|
service = new RentalFlowEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendRentalRequestEmail', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const renter = { firstName: 'Jane', lastName: 'Smith' };
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
intendedUse: 'Home project',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send rental request email with correct variables', async () => {
|
||||||
|
const result = await service.sendRentalRequestEmail(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerName: 'John',
|
||||||
|
renterName: 'Jane Smith',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
intendedUse: 'Home project',
|
||||||
|
approveUrl: 'http://localhost:3000/owning?rentalId=123',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Rental Request for Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default values when data is missing', async () => {
|
||||||
|
const minimalRental = { id: 123, item: null };
|
||||||
|
const minimalRenter = {};
|
||||||
|
|
||||||
|
await service.sendRentalRequestEmail(owner, minimalRenter, minimalRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
itemName: 'your item',
|
||||||
|
startDate: 'Not specified',
|
||||||
|
endDate: 'Not specified',
|
||||||
|
totalAmount: '0.00',
|
||||||
|
payoutAmount: '0.00',
|
||||||
|
deliveryMethod: 'Not specified',
|
||||||
|
intendedUse: 'Not specified',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use A renter fallback when names produce empty string', async () => {
|
||||||
|
const minimalRental = { id: 123, item: null };
|
||||||
|
const renterWithEmptyNames = { firstName: '', lastName: '' };
|
||||||
|
|
||||||
|
await service.sendRentalRequestEmail(owner, renterWithEmptyNames, minimalRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
renterName: 'A renter',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendRentalRequestEmail(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalRequestConfirmationEmail', () => {
|
||||||
|
const renter = { firstName: 'Jane', email: 'jane@example.com' };
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send rental request confirmation with correct variables', async () => {
|
||||||
|
const result = await service.sendRentalRequestConfirmationEmail(renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestConfirmationToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
renterName: 'Jane',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
viewRentalsUrl: 'http://localhost:3000/renting',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'jane@example.com',
|
||||||
|
'Rental Request Submitted - Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const renterNoName = { email: 'jane@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalRequestConfirmationEmail(renterNoName, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestConfirmationToRenter',
|
||||||
|
expect.objectContaining({ renterName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show different payment message for free rentals', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalRequestConfirmationEmail(renter, freeRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestConfirmationToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentMessage: 'The owner will review your request and respond soon.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show payment pending message for paid rentals', async () => {
|
||||||
|
await service.sendRentalRequestConfirmationEmail(renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalRequestConfirmationToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentMessage: "The owner will review your request. You'll only be charged if they approve it.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error'));
|
||||||
|
|
||||||
|
const result = await service.sendRentalRequestConfirmationEmail(renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Send error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalApprovalConfirmationEmail', () => {
|
||||||
|
const owner = {
|
||||||
|
firstName: 'John',
|
||||||
|
email: 'john@example.com',
|
||||||
|
stripeConnectedAccountId: 'acct_123',
|
||||||
|
};
|
||||||
|
const renter = { firstName: 'Jane', lastName: 'Smith' };
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
deliveryMethod: 'delivery',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
platformFee: '5.00',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send approval confirmation with correct variables', async () => {
|
||||||
|
const result = await service.sendRentalApprovalConfirmationEmail(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalApprovalConfirmationToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerName: 'John',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
renterName: 'Jane Smith',
|
||||||
|
deliveryMethod: 'Delivery',
|
||||||
|
paymentMessage: 'their payment has been processed successfully.',
|
||||||
|
rentalDetailsUrl: 'http://localhost:3000/owning?rentalId=123',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Rental Approved - Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show stripe setup reminder when no stripe account for paid rental', async () => {
|
||||||
|
const ownerNoStripe = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalApprovalConfirmationEmail(ownerNoStripe, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalApprovalConfirmationToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
stripeSection: expect.stringContaining('Action Required'),
|
||||||
|
stripeSection: expect.stringContaining('Set Up Your Earnings Account'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show earnings active message when stripe account exists for paid rental', async () => {
|
||||||
|
await service.sendRentalApprovalConfirmationEmail(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalApprovalConfirmationToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
stripeSection: expect.stringContaining('Earnings Account Active'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle free rentals correctly', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0', payoutAmount: '0', platformFee: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalApprovalConfirmationEmail(owner, renter, freeRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalApprovalConfirmationToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentMessage: 'this is a free rental (no payment required).',
|
||||||
|
earningsSection: '',
|
||||||
|
stripeSection: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com', stripeConnectedAccountId: 'acct_123' };
|
||||||
|
|
||||||
|
await service.sendRentalApprovalConfirmationEmail(ownerNoName, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalApprovalConfirmationToOwner',
|
||||||
|
expect.objectContaining({ ownerName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendRentalApprovalConfirmationEmail(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalDeclinedEmail', () => {
|
||||||
|
const renter = { firstName: 'Jane', email: 'jane@example.com' };
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send declined email with correct variables', async () => {
|
||||||
|
const result = await service.sendRentalDeclinedEmail(renter, rental, 'Item unavailable');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
renterName: 'Jane',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
deliveryMethod: 'pickup',
|
||||||
|
browseItemsUrl: 'http://localhost:3000/',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'jane@example.com',
|
||||||
|
'Rental Request Declined - Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include owner message when decline reason is provided', async () => {
|
||||||
|
await service.sendRentalDeclinedEmail(renter, rental, 'Item is broken');
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerMessage: expect.stringContaining('Item is broken'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include owner message when no decline reason', async () => {
|
||||||
|
await service.sendRentalDeclinedEmail(renter, rental, null);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerMessage: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show no charge message for paid rentals', async () => {
|
||||||
|
await service.sendRentalDeclinedEmail(renter, rental, null);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentMessage: 'Since your request was declined before payment was processed, you will not be charged.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show no payment message for free rentals', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalDeclinedEmail(renter, freeRental, null);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentMessage: 'No payment was required for this rental request.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const renterNoName = { email: 'jane@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalDeclinedEmail(renterNoName, rental, null);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalDeclinedToRenter',
|
||||||
|
expect.objectContaining({ renterName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error'));
|
||||||
|
|
||||||
|
const result = await service.sendRentalDeclinedEmail(renter, rental, null);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Send error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalConfirmation', () => {
|
||||||
|
const userEmail = 'jane@example.com';
|
||||||
|
const notification = {
|
||||||
|
title: 'Rental Confirmed',
|
||||||
|
message: 'Your rental has been confirmed.',
|
||||||
|
};
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
paymentStatus: 'paid',
|
||||||
|
paymentMethodBrand: 'visa',
|
||||||
|
paymentMethodLast4: '4242',
|
||||||
|
stripePaymentIntentId: 'pi_123',
|
||||||
|
chargedAt: '2024-01-15T09:00:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send rental confirmation with correct variables', async () => {
|
||||||
|
const result = await service.sendRentalConfirmation(
|
||||||
|
userEmail,
|
||||||
|
notification,
|
||||||
|
rental,
|
||||||
|
'Jane',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
recipientName: 'Jane',
|
||||||
|
title: 'Rental Confirmed',
|
||||||
|
message: 'Your rental has been confirmed.',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
isRenter: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'jane@example.com',
|
||||||
|
'Rental Confirmation - Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include payment receipt for renter with paid rental', async () => {
|
||||||
|
await service.sendRentalConfirmation(userEmail, notification, rental, 'Jane', true);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentSection: expect.stringContaining('Payment Receipt'),
|
||||||
|
paymentSection: expect.stringContaining('Visa ending in 4242'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show free rental message for zero amount', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalConfirmation(userEmail, notification, freeRental, 'Jane', true);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentSection: expect.stringContaining('No Payment Required'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include payment section for owner', async () => {
|
||||||
|
await service.sendRentalConfirmation(userEmail, notification, rental, 'John', false);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
paymentSection: '',
|
||||||
|
isRenter: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when recipientName is null', async () => {
|
||||||
|
await service.sendRentalConfirmation(userEmail, notification, rental, null, true);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing rental data', async () => {
|
||||||
|
const minimalRental = {};
|
||||||
|
|
||||||
|
await service.sendRentalConfirmation(userEmail, notification, minimalRental, 'Jane', true);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
itemName: 'Unknown Item',
|
||||||
|
startDate: 'Not specified',
|
||||||
|
endDate: 'Not specified',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendRentalConfirmation(
|
||||||
|
userEmail,
|
||||||
|
notification,
|
||||||
|
rental,
|
||||||
|
'Jane',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalConfirmationEmails', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const renter = { firstName: 'Jane', email: 'jane@example.com' };
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
ownerId: 1,
|
||||||
|
renterId: 2,
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
paymentStatus: 'paid',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send confirmation emails to both owner and renter', async () => {
|
||||||
|
const result = await service.sendRentalConfirmationEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(true);
|
||||||
|
expect(result.renterEmailSent).toBe(true);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send owner email with isRenter false', async () => {
|
||||||
|
await service.sendRentalConfirmationEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
// First call is for owner
|
||||||
|
const firstCall = service.templateManager.renderTemplate.mock.calls[0];
|
||||||
|
expect(firstCall[1]).toMatchObject({ isRenter: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send renter email with isRenter true', async () => {
|
||||||
|
await service.sendRentalConfirmationEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
// Second call is for renter
|
||||||
|
const secondCall = service.templateManager.renderTemplate.mock.calls[1];
|
||||||
|
expect(secondCall[1]).toMatchObject({ isRenter: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing owner email', async () => {
|
||||||
|
const ownerNoEmail = { firstName: 'John' };
|
||||||
|
|
||||||
|
const result = await service.sendRentalConfirmationEmails(ownerNoEmail, renter, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(false);
|
||||||
|
expect(result.renterEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing renter email', async () => {
|
||||||
|
const renterNoEmail = { firstName: 'Jane' };
|
||||||
|
|
||||||
|
const result = await service.sendRentalConfirmationEmails(owner, renterNoEmail, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(true);
|
||||||
|
expect(result.renterEmailSent).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue sending renter email if owner email fails', async () => {
|
||||||
|
service.emailClient.sendEmail
|
||||||
|
.mockResolvedValueOnce({ success: false, error: 'Owner send failed' })
|
||||||
|
.mockResolvedValueOnce({ success: true, messageId: 'msg-456' });
|
||||||
|
|
||||||
|
const result = await service.sendRentalConfirmationEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(false);
|
||||||
|
expect(result.renterEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors in owner email without crashing', async () => {
|
||||||
|
service.emailClient.sendEmail
|
||||||
|
.mockRejectedValueOnce(new Error('Network error'))
|
||||||
|
.mockResolvedValueOnce({ success: true, messageId: 'msg-456' });
|
||||||
|
|
||||||
|
const result = await service.sendRentalConfirmationEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(false);
|
||||||
|
expect(result.renterEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalCancellationEmails', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const renter = { firstName: 'Jane', email: 'jane@example.com' };
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
cancelledBy: 'renter',
|
||||||
|
cancelledAt: '2024-01-14T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
};
|
||||||
|
const refundInfo = {
|
||||||
|
amount: 50,
|
||||||
|
percentage: 1,
|
||||||
|
reason: 'Full refund - cancelled before rental start',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send cancellation emails when renter cancels', async () => {
|
||||||
|
const result = await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo);
|
||||||
|
|
||||||
|
expect(result.confirmationEmailSent).toBe(true);
|
||||||
|
expect(result.notificationEmailSent).toBe(true);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send confirmation to renter and notification to owner when renter cancels', async () => {
|
||||||
|
await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo);
|
||||||
|
|
||||||
|
// Confirmation to renter (canceller)
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'Jane' })
|
||||||
|
);
|
||||||
|
// Notification to owner
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationNotificationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'John' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send confirmation to owner and notification to renter when owner cancels', async () => {
|
||||||
|
const ownerCancelledRental = { ...rental, cancelledBy: 'owner' };
|
||||||
|
|
||||||
|
await service.sendRentalCancellationEmails(owner, renter, ownerCancelledRental, refundInfo);
|
||||||
|
|
||||||
|
// Confirmation to owner (canceller)
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'John' })
|
||||||
|
);
|
||||||
|
// Notification to renter
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationNotificationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'Jane' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include refund section for paid rentals', async () => {
|
||||||
|
await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
refundSection: expect.stringContaining('Refund Information'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show no refund message when refund amount is zero', async () => {
|
||||||
|
const noRefundInfo = { amount: 0, percentage: 0, reason: 'Cancelled after rental period' };
|
||||||
|
|
||||||
|
await service.sendRentalCancellationEmails(owner, renter, rental, noRefundInfo);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
refundSection: expect.stringContaining('No Refund Available'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include refund section for free rentals', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalCancellationEmails(owner, renter, freeRental, refundInfo);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
refundSection: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com' };
|
||||||
|
const renterNoName = { email: 'jane@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalCancellationEmails(ownerNoName, renterNoName, rental, refundInfo);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCancellationConfirmationToUser',
|
||||||
|
expect.objectContaining({ recipientName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue if confirmation email fails', async () => {
|
||||||
|
service.emailClient.sendEmail
|
||||||
|
.mockResolvedValueOnce({ success: false, error: 'Send failed' })
|
||||||
|
.mockResolvedValueOnce({ success: true, messageId: 'msg-456' });
|
||||||
|
|
||||||
|
const result = await service.sendRentalCancellationEmails(owner, renter, rental, refundInfo);
|
||||||
|
|
||||||
|
expect(result.confirmationEmailSent).toBe(false);
|
||||||
|
expect(result.notificationEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendRentalCompletionEmails', () => {
|
||||||
|
const owner = {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
email: 'john@example.com',
|
||||||
|
stripeConnectedAccountId: 'acct_123',
|
||||||
|
};
|
||||||
|
const renter = { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' };
|
||||||
|
const rental = {
|
||||||
|
id: 123,
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
actualReturnDateTime: '2024-01-16T09:30:00Z',
|
||||||
|
itemReviewSubmittedAt: null,
|
||||||
|
totalAmount: '50.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
platformFee: '5.00',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send completion emails to both owner and renter', async () => {
|
||||||
|
const result = await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.ownerEmailSent).toBe(true);
|
||||||
|
expect(result.renterEmailSent).toBe(true);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send renter email with review prompt when not yet reviewed', async () => {
|
||||||
|
await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionThankYouToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
reviewSection: expect.stringContaining('Share Your Experience'),
|
||||||
|
reviewSection: expect.stringContaining('Leave a Review'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show thank you message when review already submitted', async () => {
|
||||||
|
const reviewedRental = { ...rental, itemReviewSubmittedAt: '2024-01-16T12:00:00Z' };
|
||||||
|
|
||||||
|
await service.sendRentalCompletionEmails(owner, renter, reviewedRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionThankYouToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
reviewSection: expect.stringContaining('Thank You for Your Review'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send owner email with earnings for paid rental', async () => {
|
||||||
|
await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionCongratsToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
earningsSection: expect.stringContaining('Your Earnings'),
|
||||||
|
earningsSection: expect.stringContaining('$45.00'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show payout initiated message when owner has stripe account', async () => {
|
||||||
|
await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionCongratsToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
stripeSection: expect.stringContaining('Payout Initiated'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show stripe setup reminder when owner has no stripe account for paid rental', async () => {
|
||||||
|
const ownerNoStripe = { firstName: 'John', lastName: 'Doe', email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalCompletionEmails(ownerNoStripe, renter, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionCongratsToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
stripeSection: expect.stringContaining('Action Required'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show earnings section for free rentals', async () => {
|
||||||
|
const freeRental = { ...rental, totalAmount: '0', payoutAmount: '0', platformFee: '0' };
|
||||||
|
|
||||||
|
await service.sendRentalCompletionEmails(owner, renter, freeRental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionCongratsToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
earningsSection: '',
|
||||||
|
stripeSection: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com', stripeConnectedAccountId: 'acct_123' };
|
||||||
|
const renterNoName = { email: 'jane@example.com' };
|
||||||
|
|
||||||
|
await service.sendRentalCompletionEmails(ownerNoName, renterNoName, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionThankYouToRenter',
|
||||||
|
expect.objectContaining({ renterName: 'there' })
|
||||||
|
);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'rentalCompletionCongratsToOwner',
|
||||||
|
expect.objectContaining({ ownerName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue if renter email fails', async () => {
|
||||||
|
service.emailClient.sendEmail
|
||||||
|
.mockResolvedValueOnce({ success: false, error: 'Send failed' })
|
||||||
|
.mockResolvedValueOnce({ success: true, messageId: 'msg-456' });
|
||||||
|
|
||||||
|
const result = await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.renterEmailSent).toBe(false);
|
||||||
|
expect(result.ownerEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors in renter email without crashing', async () => {
|
||||||
|
service.emailClient.sendEmail
|
||||||
|
.mockRejectedValueOnce(new Error('Network error'))
|
||||||
|
.mockResolvedValueOnce({ success: true, messageId: 'msg-456' });
|
||||||
|
|
||||||
|
const result = await service.sendRentalCompletionEmails(owner, renter, rental);
|
||||||
|
|
||||||
|
expect(result.renterEmailSent).toBe(false);
|
||||||
|
expect(result.ownerEmailSent).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendPayoutReceivedEmail', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
startDateTime: '2024-01-15T10:00:00Z',
|
||||||
|
endDateTime: '2024-01-16T10:00:00Z',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
platformFee: '5.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
stripeTransferId: 'tr_123',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send payout email with correct variables', async () => {
|
||||||
|
const result = await service.sendPayoutReceivedEmail(owner, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'payoutReceivedToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerName: 'John',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
totalAmount: '50.00',
|
||||||
|
platformFee: '5.00',
|
||||||
|
payoutAmount: '45.00',
|
||||||
|
stripeTransferId: 'tr_123',
|
||||||
|
earningsDashboardUrl: 'http://localhost:3000/earnings',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Earnings Received - $45.00 for Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendPayoutReceivedEmail(ownerNoName, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'payoutReceivedToOwner',
|
||||||
|
expect.objectContaining({ ownerName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing item name', async () => {
|
||||||
|
const rentalNoItem = { ...rental, item: null };
|
||||||
|
|
||||||
|
await service.sendPayoutReceivedEmail(owner, rentalNoItem);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'payoutReceivedToOwner',
|
||||||
|
expect.objectContaining({ itemName: 'your item' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendPayoutReceivedEmail(owner, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendAuthenticationRequiredEmail', () => {
|
||||||
|
const email = 'jane@example.com';
|
||||||
|
const data = {
|
||||||
|
renterName: 'Jane',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
ownerName: 'John',
|
||||||
|
amount: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send authentication required email with correct variables', async () => {
|
||||||
|
const result = await service.sendAuthenticationRequiredEmail(email, data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'authenticationRequiredToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
renterName: 'Jane',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
ownerName: 'John',
|
||||||
|
amount: '50.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'jane@example.com',
|
||||||
|
'Action Required: Complete payment for Power Drill',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default values when data is missing', async () => {
|
||||||
|
const minimalData = {};
|
||||||
|
|
||||||
|
await service.sendAuthenticationRequiredEmail(email, minimalData);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'authenticationRequiredToRenter',
|
||||||
|
expect.objectContaining({
|
||||||
|
renterName: 'there',
|
||||||
|
itemName: 'the item',
|
||||||
|
ownerName: 'The owner',
|
||||||
|
amount: '0.00',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error'));
|
||||||
|
|
||||||
|
const result = await service.sendAuthenticationRequiredEmail(email, data);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Send error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// 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 RentalReminderEmailService = require('../../../../../services/email/domain/RentalReminderEmailService');
|
||||||
|
|
||||||
|
describe('RentalReminderEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = { ...originalEnv, FRONTEND_URL: 'http://localhost:3000' };
|
||||||
|
service = new RentalReminderEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendConditionCheckReminder', () => {
|
||||||
|
const userEmail = 'john@example.com';
|
||||||
|
const notification = {
|
||||||
|
title: 'Condition Check Required',
|
||||||
|
message: 'Please complete the condition check for your rental.',
|
||||||
|
metadata: {
|
||||||
|
deadline: '2024-01-16T10:00:00Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const rental = {
|
||||||
|
item: { name: 'Power Drill' },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send condition check reminder with correct variables', async () => {
|
||||||
|
const result = await service.sendConditionCheckReminder(userEmail, notification, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'conditionCheckReminderToUser',
|
||||||
|
expect.objectContaining({
|
||||||
|
title: 'Condition Check Required',
|
||||||
|
message: 'Please complete the condition check for your rental.',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Village Share: Condition Check Required',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default item name when rental item is missing', async () => {
|
||||||
|
const rentalNoItem = {};
|
||||||
|
|
||||||
|
await service.sendConditionCheckReminder(userEmail, notification, rentalNoItem);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'conditionCheckReminderToUser',
|
||||||
|
expect.objectContaining({ itemName: 'Unknown Item' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default deadline when metadata is missing', async () => {
|
||||||
|
const notificationNoDeadline = {
|
||||||
|
...notification,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.sendConditionCheckReminder(userEmail, notificationNoDeadline, rental);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'conditionCheckReminderToUser',
|
||||||
|
expect.objectContaining({ deadline: 'Not specified' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendConditionCheckReminder(userEmail, notification, rental);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// 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 UserEngagementEmailService = require('../../../../../services/email/domain/UserEngagementEmailService');
|
||||||
|
|
||||||
|
describe('UserEngagementEmailService', () => {
|
||||||
|
let service;
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env = {
|
||||||
|
...originalEnv,
|
||||||
|
FRONTEND_URL: 'http://localhost:3000',
|
||||||
|
SUPPORT_EMAIL: 'support@villageshare.com',
|
||||||
|
};
|
||||||
|
service = new UserEngagementEmailService();
|
||||||
|
});
|
||||||
|
|
||||||
|
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('sendFirstListingCelebrationEmail', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const item = { id: 123, name: 'Power Drill' };
|
||||||
|
|
||||||
|
it('should send first listing celebration email with correct variables', async () => {
|
||||||
|
const result = await service.sendFirstListingCelebrationEmail(owner, item);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'firstListingCelebrationToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerName: 'John',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
itemId: 123,
|
||||||
|
viewItemUrl: 'http://localhost:3000/items/123',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Congratulations! Your first item is live on Village Share',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendFirstListingCelebrationEmail(ownerNoName, item);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'firstListingCelebrationToOwner',
|
||||||
|
expect.objectContaining({ ownerName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendFirstListingCelebrationEmail(owner, item);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendItemDeletionNotificationToOwner', () => {
|
||||||
|
const owner = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const item = { id: 123, name: 'Power Drill' };
|
||||||
|
const deletionReason = 'Violated community guidelines';
|
||||||
|
|
||||||
|
it('should send item deletion notification with correct variables', async () => {
|
||||||
|
const result = await service.sendItemDeletionNotificationToOwner(
|
||||||
|
owner,
|
||||||
|
item,
|
||||||
|
deletionReason
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'itemDeletionToOwner',
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerName: 'John',
|
||||||
|
itemName: 'Power Drill',
|
||||||
|
deletionReason: 'Violated community guidelines',
|
||||||
|
supportEmail: 'support@villageshare.com',
|
||||||
|
dashboardUrl: 'http://localhost:3000/owning',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Important: Your listing "Power Drill" has been removed',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const ownerNoName = { email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendItemDeletionNotificationToOwner(ownerNoName, item, deletionReason);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'itemDeletionToOwner',
|
||||||
|
expect.objectContaining({ ownerName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.emailClient.sendEmail.mockRejectedValueOnce(new Error('Send error'));
|
||||||
|
|
||||||
|
const result = await service.sendItemDeletionNotificationToOwner(
|
||||||
|
owner,
|
||||||
|
item,
|
||||||
|
deletionReason
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Send error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendUserBannedNotification', () => {
|
||||||
|
const bannedUser = { firstName: 'John', email: 'john@example.com' };
|
||||||
|
const admin = { firstName: 'Admin', lastName: 'User' };
|
||||||
|
const banReason = 'Multiple policy violations';
|
||||||
|
|
||||||
|
it('should send user banned notification with correct variables', async () => {
|
||||||
|
const result = await service.sendUserBannedNotification(bannedUser, admin, banReason);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'userBannedNotification',
|
||||||
|
expect.objectContaining({
|
||||||
|
userName: 'John',
|
||||||
|
banReason: 'Multiple policy violations',
|
||||||
|
supportEmail: 'support@villageshare.com',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(service.emailClient.sendEmail).toHaveBeenCalledWith(
|
||||||
|
'john@example.com',
|
||||||
|
'Important: Your Village Share Account Has Been Suspended',
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default name when firstName is missing', async () => {
|
||||||
|
const bannedUserNoName = { email: 'john@example.com' };
|
||||||
|
|
||||||
|
await service.sendUserBannedNotification(bannedUserNoName, admin, banReason);
|
||||||
|
|
||||||
|
expect(service.templateManager.renderTemplate).toHaveBeenCalledWith(
|
||||||
|
'userBannedNotification',
|
||||||
|
expect.objectContaining({ userName: 'there' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors gracefully', async () => {
|
||||||
|
service.templateManager.renderTemplate.mockRejectedValueOnce(new Error('Template error'));
|
||||||
|
|
||||||
|
const result = await service.sendUserBannedNotification(bannedUser, admin, banReason);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Template error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user