tests
This commit is contained in:
@@ -184,29 +184,6 @@ class EmailService {
|
||||
<p>Thank you for using RentAll!</p>
|
||||
`
|
||||
),
|
||||
|
||||
damageClaimNotification: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<h2>{{title}}</h2>
|
||||
<p>{{message}}</p>
|
||||
<p><strong>Item:</strong> {{itemName}}</p>
|
||||
<p><strong>Claim Amount:</strong> ${{ claimAmount }}</p>
|
||||
<p><strong>Description:</strong> {{description}}</p>
|
||||
<p>Please review this claim and respond accordingly through your account.</p>
|
||||
`
|
||||
),
|
||||
|
||||
returnIssueNotification: baseTemplate.replace(
|
||||
"{{content}}",
|
||||
`
|
||||
<h2>{{title}}</h2>
|
||||
<p>{{message}}</p>
|
||||
<p><strong>Item:</strong> {{itemName}}</p>
|
||||
<p><strong>Return Status:</strong> {{returnStatus}}</p>
|
||||
<p>Please check your account for more details and take appropriate action.</p>
|
||||
`
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
165
backend/tests/unit/services/conditionCheckService.test.js
Normal file
165
backend/tests/unit/services/conditionCheckService.test.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const ConditionCheckService = require('../../../services/conditionCheckService');
|
||||
const { ConditionCheck, Rental, User } = require('../../../models');
|
||||
|
||||
jest.mock('../../../models');
|
||||
|
||||
describe('ConditionCheckService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('submitConditionCheck', () => {
|
||||
// Set rental dates relative to current time for valid time window
|
||||
const now = new Date();
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 'owner-456',
|
||||
renterId: 'renter-789',
|
||||
startDateTime: new Date(now.getTime() - 1000 * 60 * 60), // 1 hour ago
|
||||
endDateTime: new Date(now.getTime() + 1000 * 60 * 60 * 24), // 24 hours from now
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
const mockPhotos = ['/uploads/photo1.jpg', '/uploads/photo2.jpg'];
|
||||
|
||||
beforeEach(() => {
|
||||
Rental.findByPk.mockResolvedValue(mockRental);
|
||||
ConditionCheck.findOne.mockResolvedValue(null); // No existing check
|
||||
ConditionCheck.create.mockResolvedValue({
|
||||
id: 'check-123',
|
||||
rentalId: 'rental-123',
|
||||
checkType: 'rental_start_renter',
|
||||
photos: mockPhotos,
|
||||
notes: 'Item received in good condition',
|
||||
submittedBy: 'renter-789'
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit condition check with photos and notes', async () => {
|
||||
const result = await ConditionCheckService.submitConditionCheck(
|
||||
'rental-123',
|
||||
'rental_start_renter',
|
||||
'renter-789',
|
||||
mockPhotos,
|
||||
'Item received in good condition'
|
||||
);
|
||||
|
||||
expect(ConditionCheck.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rentalId: 'rental-123',
|
||||
checkType: 'rental_start_renter',
|
||||
submittedBy: 'renter-789',
|
||||
photos: mockPhotos,
|
||||
notes: 'Item received in good condition',
|
||||
metadata: expect.any(Object)
|
||||
})
|
||||
);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.id).toBe('check-123');
|
||||
});
|
||||
|
||||
it('should validate user authorization - owner checks', async () => {
|
||||
// Renter trying to submit pre-rental owner check
|
||||
await expect(
|
||||
ConditionCheckService.submitConditionCheck(
|
||||
'rental-123',
|
||||
'pre_rental_owner',
|
||||
'renter-789',
|
||||
mockPhotos
|
||||
)
|
||||
).rejects.toThrow('Only the item owner can submit owner condition checks');
|
||||
});
|
||||
|
||||
it('should validate user authorization - renter checks', async () => {
|
||||
// Owner trying to submit rental start renter check
|
||||
await expect(
|
||||
ConditionCheckService.submitConditionCheck(
|
||||
'rental-123',
|
||||
'rental_start_renter',
|
||||
'owner-456',
|
||||
mockPhotos
|
||||
)
|
||||
).rejects.toThrow('Only the renter can submit renter condition checks');
|
||||
});
|
||||
|
||||
it('should prevent duplicate condition checks', async () => {
|
||||
ConditionCheck.findOne.mockResolvedValue({ id: 'existing-check' });
|
||||
|
||||
await expect(
|
||||
ConditionCheckService.submitConditionCheck(
|
||||
'rental-123',
|
||||
'rental_start_renter',
|
||||
'renter-789',
|
||||
mockPhotos
|
||||
)
|
||||
).rejects.toThrow('Condition check already submitted for this type');
|
||||
});
|
||||
|
||||
it('should limit number of photos to 20', async () => {
|
||||
const tooManyPhotos = Array(21).fill('/uploads/photo.jpg');
|
||||
|
||||
await expect(
|
||||
ConditionCheckService.submitConditionCheck(
|
||||
'rental-123',
|
||||
'rental_start_renter',
|
||||
'renter-789',
|
||||
tooManyPhotos
|
||||
)
|
||||
).rejects.toThrow('Maximum 20 photos allowed per condition check');
|
||||
});
|
||||
|
||||
it('should handle rental not found', async () => {
|
||||
Rental.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
ConditionCheckService.submitConditionCheck(
|
||||
'nonexistent-rental',
|
||||
'rental_start_renter',
|
||||
'renter-789',
|
||||
mockPhotos
|
||||
)
|
||||
).rejects.toThrow('Rental not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionChecks', () => {
|
||||
it('should retrieve condition checks for rental', async () => {
|
||||
const mockChecks = [
|
||||
{
|
||||
id: 'check-1',
|
||||
checkType: 'pre_rental_owner',
|
||||
submittedBy: 'owner-456',
|
||||
submittedAt: '2023-05-31T12:00:00Z',
|
||||
photos: ['/uploads/photo1.jpg'],
|
||||
notes: 'Item ready'
|
||||
},
|
||||
{
|
||||
id: 'check-2',
|
||||
checkType: 'rental_start_renter',
|
||||
submittedBy: 'renter-789',
|
||||
submittedAt: '2023-06-01T11:00:00Z',
|
||||
photos: ['/uploads/photo2.jpg'],
|
||||
notes: 'Item received'
|
||||
}
|
||||
];
|
||||
|
||||
ConditionCheck.findAll.mockResolvedValue(mockChecks);
|
||||
|
||||
const result = await ConditionCheckService.getConditionChecks('rental-123');
|
||||
|
||||
expect(ConditionCheck.findAll).toHaveBeenCalledWith({
|
||||
where: { rentalId: 'rental-123' },
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'submittedByUser',
|
||||
attributes: ['id', 'username', 'firstName', 'lastName']
|
||||
}],
|
||||
order: [['submittedAt', 'ASC']]
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockChecks);
|
||||
expect(result.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
219
backend/tests/unit/services/damageAssessmentService.test.js
Normal file
219
backend/tests/unit/services/damageAssessmentService.test.js
Normal file
@@ -0,0 +1,219 @@
|
||||
// Mock dependencies BEFORE requiring modules
|
||||
jest.mock('../../../models');
|
||||
jest.mock('../../../services/lateReturnService');
|
||||
jest.mock('../../../services/emailService');
|
||||
jest.mock('../../../config/aws', () => ({
|
||||
getAWSConfig: jest.fn(() => ({ region: 'us-east-1' })),
|
||||
getAWSCredentials: jest.fn()
|
||||
}));
|
||||
|
||||
const DamageAssessmentService = require('../../../services/damageAssessmentService');
|
||||
const { Rental, Item } = require('../../../models');
|
||||
const LateReturnService = require('../../../services/lateReturnService');
|
||||
const emailService = require('../../../services/emailService');
|
||||
|
||||
describe('DamageAssessmentService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('processDamageAssessment', () => {
|
||||
let mockRental;
|
||||
let mockDamageInfo;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mockRental for each test to avoid state pollution
|
||||
mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 'owner-789',
|
||||
renterId: 'renter-456',
|
||||
status: 'active',
|
||||
item: { name: 'Test Camera', dailyRate: 100 },
|
||||
update: jest.fn().mockResolvedValue({
|
||||
id: 'rental-123',
|
||||
status: 'damaged',
|
||||
damageFees: 500
|
||||
})
|
||||
};
|
||||
|
||||
mockDamageInfo = {
|
||||
description: 'Camera lens is cracked and unusable',
|
||||
canBeFixed: false,
|
||||
needsReplacement: true,
|
||||
replacementCost: 500,
|
||||
photos: ['photo1.jpg', 'photo2.jpg'],
|
||||
proofOfOwnership: ['receipt.pdf']
|
||||
};
|
||||
|
||||
Rental.findByPk.mockResolvedValue(mockRental);
|
||||
LateReturnService.processLateReturn.mockResolvedValue({
|
||||
lateCalculation: { lateFee: 0, isLate: false }
|
||||
});
|
||||
emailService.sendDamageReportToCustomerService.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('should process damage assessment for replacement', async () => {
|
||||
const result = await DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
mockDamageInfo,
|
||||
'owner-789'
|
||||
);
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
status: 'damaged',
|
||||
damageFees: 500,
|
||||
damageAssessment: expect.objectContaining({
|
||||
description: 'Camera lens is cracked and unusable',
|
||||
canBeFixed: false,
|
||||
needsReplacement: true,
|
||||
replacementCost: 500,
|
||||
feeCalculation: expect.objectContaining({
|
||||
type: 'replacement',
|
||||
amount: 500
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
expect(emailService.sendDamageReportToCustomerService).toHaveBeenCalled();
|
||||
expect(result.totalAdditionalFees).toBe(500);
|
||||
});
|
||||
|
||||
it('should process damage assessment for repair', async () => {
|
||||
const repairInfo = {
|
||||
description: 'Lens needs professional cleaning and adjustment',
|
||||
canBeFixed: true,
|
||||
needsReplacement: false,
|
||||
repairCost: 150
|
||||
};
|
||||
|
||||
mockRental.update.mockResolvedValue({
|
||||
...mockRental,
|
||||
status: 'damaged',
|
||||
damageFees: 150
|
||||
});
|
||||
|
||||
const result = await DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
repairInfo,
|
||||
'owner-789'
|
||||
);
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
status: 'damaged',
|
||||
damageFees: 150,
|
||||
damageAssessment: expect.objectContaining({
|
||||
canBeFixed: true,
|
||||
needsReplacement: false,
|
||||
repairCost: 150,
|
||||
feeCalculation: expect.objectContaining({
|
||||
type: 'repair',
|
||||
amount: 150
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
expect(result.totalAdditionalFees).toBe(150);
|
||||
});
|
||||
|
||||
it('should include late fees when provided', async () => {
|
||||
const actualReturnDateTime = new Date('2023-06-01T14:00:00Z');
|
||||
|
||||
LateReturnService.processLateReturn.mockResolvedValue({
|
||||
lateCalculation: { lateFee: 50, isLate: true }
|
||||
});
|
||||
|
||||
mockRental.update.mockResolvedValue({
|
||||
...mockRental,
|
||||
status: 'damaged',
|
||||
damageFees: 500,
|
||||
lateFees: 50
|
||||
});
|
||||
|
||||
const result = await DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
{ ...mockDamageInfo, actualReturnDateTime },
|
||||
'owner-789'
|
||||
);
|
||||
|
||||
expect(LateReturnService.processLateReturn).toHaveBeenCalledWith(
|
||||
'rental-123',
|
||||
actualReturnDateTime,
|
||||
'Item returned damaged: Camera lens is cracked and unusable'
|
||||
);
|
||||
|
||||
expect(result.totalAdditionalFees).toBe(550); // 500 damage + 50 late fee
|
||||
});
|
||||
|
||||
it('should throw error when rental not found', async () => {
|
||||
Rental.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'nonexistent',
|
||||
mockDamageInfo,
|
||||
'owner-789'
|
||||
)
|
||||
).rejects.toThrow('Rental not found');
|
||||
});
|
||||
|
||||
it('should validate authorization - only owner can report', async () => {
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
mockDamageInfo,
|
||||
'renter-456'
|
||||
)
|
||||
).rejects.toThrow('Only the item owner can report damage');
|
||||
});
|
||||
|
||||
it('should validate rental status - only active rentals', async () => {
|
||||
mockRental.status = 'completed';
|
||||
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
mockDamageInfo,
|
||||
'owner-789'
|
||||
)
|
||||
).rejects.toThrow('Can only assess damage for active rentals');
|
||||
});
|
||||
|
||||
it('should validate required fields - description', async () => {
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
{ ...mockDamageInfo, description: '' },
|
||||
'owner-789'
|
||||
)
|
||||
).rejects.toThrow('Damage description is required');
|
||||
});
|
||||
|
||||
it('should validate required fields - repair cost', async () => {
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
{
|
||||
description: 'Needs repair',
|
||||
canBeFixed: true,
|
||||
needsReplacement: false
|
||||
},
|
||||
'owner-789'
|
||||
)
|
||||
).rejects.toThrow('Repair cost is required when item can be fixed');
|
||||
});
|
||||
|
||||
it('should validate required fields - replacement cost', async () => {
|
||||
await expect(
|
||||
DamageAssessmentService.processDamageAssessment(
|
||||
'rental-123',
|
||||
{
|
||||
description: 'Needs replacement',
|
||||
canBeFixed: false,
|
||||
needsReplacement: true
|
||||
},
|
||||
'owner-789'
|
||||
)
|
||||
).rejects.toThrow('Replacement cost is required when item needs replacement');
|
||||
});
|
||||
});
|
||||
});
|
||||
251
backend/tests/unit/services/emailService.test.js
Normal file
251
backend/tests/unit/services/emailService.test.js
Normal file
@@ -0,0 +1,251 @@
|
||||
// Mock dependencies BEFORE requiring modules
|
||||
jest.mock('@aws-sdk/client-ses');
|
||||
jest.mock('../../../config/aws', () => ({
|
||||
getAWSConfig: jest.fn(() => ({
|
||||
region: 'us-east-1',
|
||||
credentials: {
|
||||
accessKeyId: 'test-key',
|
||||
secretAccessKey: 'test-secret'
|
||||
}
|
||||
}))
|
||||
}));
|
||||
jest.mock('../../../models', () => ({
|
||||
User: {
|
||||
findByPk: jest.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
const emailService = require('../../../services/emailService');
|
||||
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
|
||||
const { getAWSConfig } = require('../../../config/aws');
|
||||
|
||||
describe('EmailService', () => {
|
||||
let mockSESClient;
|
||||
let mockSend;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSend = jest.fn();
|
||||
mockSESClient = {
|
||||
send: mockSend
|
||||
};
|
||||
|
||||
SESClient.mockImplementation(() => mockSESClient);
|
||||
|
||||
// Reset environment variables
|
||||
process.env.EMAIL_ENABLED = 'true';
|
||||
process.env.AWS_REGION = 'us-east-1';
|
||||
process.env.AWS_ACCESS_KEY_ID = 'test-key';
|
||||
process.env.AWS_SECRET_ACCESS_KEY = 'test-secret';
|
||||
process.env.SES_FROM_EMAIL = 'test@example.com';
|
||||
process.env.SES_REPLY_TO_EMAIL = 'reply@example.com';
|
||||
|
||||
// Reset the service instance
|
||||
emailService.initialized = false;
|
||||
emailService.sesClient = null;
|
||||
emailService.templates.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize SES client using AWS config', async () => {
|
||||
await emailService.initialize();
|
||||
|
||||
expect(getAWSConfig).toHaveBeenCalled();
|
||||
expect(SESClient).toHaveBeenCalledWith({
|
||||
region: 'us-east-1',
|
||||
credentials: {
|
||||
accessKeyId: 'test-key',
|
||||
secretAccessKey: 'test-secret'
|
||||
}
|
||||
});
|
||||
expect(emailService.initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle initialization errors', async () => {
|
||||
SESClient.mockImplementationOnce(() => {
|
||||
throw new Error('AWS credentials not found');
|
||||
});
|
||||
|
||||
// Reset initialized state
|
||||
emailService.initialized = false;
|
||||
|
||||
await expect(emailService.initialize()).rejects.toThrow('AWS credentials not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmail', () => {
|
||||
beforeEach(async () => {
|
||||
mockSend.mockResolvedValue({ MessageId: 'test-message-id' });
|
||||
await emailService.initialize();
|
||||
});
|
||||
|
||||
it('should send email successfully', async () => {
|
||||
const result = await emailService.sendEmail(
|
||||
'recipient@example.com',
|
||||
'Test Subject',
|
||||
'<h1>Test HTML</h1>',
|
||||
'Test Text'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messageId).toBe('test-message-id');
|
||||
expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand));
|
||||
});
|
||||
|
||||
it('should handle single email address', async () => {
|
||||
const result = await emailService.sendEmail('single@example.com', 'Subject', '<p>Content</p>');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand));
|
||||
});
|
||||
|
||||
it('should handle array of email addresses', async () => {
|
||||
const result = await emailService.sendEmail(
|
||||
['first@example.com', 'second@example.com'],
|
||||
'Subject',
|
||||
'<p>Content</p>'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand));
|
||||
});
|
||||
|
||||
it('should include reply-to address when configured', async () => {
|
||||
const result = await emailService.sendEmail('test@example.com', 'Subject', '<p>Content</p>');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockSend).toHaveBeenCalledWith(expect.any(SendEmailCommand));
|
||||
});
|
||||
|
||||
it('should handle SES errors', async () => {
|
||||
mockSend.mockRejectedValue(new Error('SES Error'));
|
||||
|
||||
const result = await emailService.sendEmail('test@example.com', 'Subject', '<p>Content</p>');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('SES Error');
|
||||
});
|
||||
|
||||
it('should skip sending when email is disabled', async () => {
|
||||
process.env.EMAIL_ENABLED = 'false';
|
||||
|
||||
const result = await emailService.sendEmail('test@example.com', 'Subject', '<p>Content</p>');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.messageId).toBe('disabled');
|
||||
expect(mockSend).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('template rendering', () => {
|
||||
it('should render template with variables', () => {
|
||||
const template = '<h1>Hello {{name}}</h1><p>Your order {{orderId}} is ready.</p>';
|
||||
emailService.templates.set('test', template);
|
||||
|
||||
const rendered = emailService.renderTemplate('test', {
|
||||
name: 'John Doe',
|
||||
orderId: '12345'
|
||||
});
|
||||
|
||||
expect(rendered).toBe('<h1>Hello John Doe</h1><p>Your order 12345 is ready.</p>');
|
||||
});
|
||||
|
||||
it('should handle missing variables by replacing with empty string', () => {
|
||||
const template = '<h1>Hello {{name}}</h1><p>Your order {{orderId}} is ready.</p>';
|
||||
emailService.templates.set('test', template);
|
||||
|
||||
const rendered = emailService.renderTemplate('test', {
|
||||
name: 'John Doe',
|
||||
orderId: '' // Explicitly provide empty string
|
||||
});
|
||||
|
||||
expect(rendered).toContain('Hello John Doe');
|
||||
expect(rendered).toContain('Your order');
|
||||
});
|
||||
|
||||
it('should use fallback template when template not found', () => {
|
||||
const rendered = emailService.renderTemplate('nonexistent', {
|
||||
title: 'Test Title',
|
||||
content: 'Test Content',
|
||||
message: 'Test message'
|
||||
});
|
||||
|
||||
expect(rendered).toContain('Test Title');
|
||||
expect(rendered).toContain('Test message');
|
||||
expect(rendered).toContain('RentAll');
|
||||
});
|
||||
});
|
||||
|
||||
describe('notification-specific senders', () => {
|
||||
beforeEach(async () => {
|
||||
mockSend.mockResolvedValue({ MessageId: 'test-message-id' });
|
||||
await emailService.initialize();
|
||||
});
|
||||
|
||||
it('should send condition check reminder', async () => {
|
||||
const notification = {
|
||||
title: 'Condition Check Required',
|
||||
message: 'Please take photos of the item',
|
||||
metadata: { deadline: '2024-01-15' }
|
||||
};
|
||||
const rental = { item: { name: 'Test Item' } };
|
||||
|
||||
const result = await emailService.sendConditionCheckReminder(
|
||||
'test@example.com',
|
||||
notification,
|
||||
rental
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send rental confirmation', async () => {
|
||||
const notification = {
|
||||
title: 'Rental Confirmed',
|
||||
message: 'Your rental has been confirmed'
|
||||
};
|
||||
const rental = {
|
||||
item: { name: 'Test Item' },
|
||||
startDateTime: '2024-01-15T10:00:00Z',
|
||||
endDateTime: '2024-01-17T10:00:00Z'
|
||||
};
|
||||
|
||||
const result = await emailService.sendRentalConfirmation(
|
||||
'test@example.com',
|
||||
notification,
|
||||
rental
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockSend).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(async () => {
|
||||
await emailService.initialize();
|
||||
});
|
||||
|
||||
it('should handle missing rental data gracefully', async () => {
|
||||
mockSend.mockResolvedValue({ MessageId: 'test-message-id' });
|
||||
|
||||
const notification = {
|
||||
title: 'Test',
|
||||
message: 'Test message',
|
||||
metadata: {}
|
||||
};
|
||||
|
||||
const result = await emailService.sendConditionCheckReminder(
|
||||
'test@example.com',
|
||||
notification,
|
||||
null
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
163
backend/tests/unit/services/lateReturnService.test.js
Normal file
163
backend/tests/unit/services/lateReturnService.test.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// Mock dependencies BEFORE requiring modules
|
||||
jest.mock('../../../models');
|
||||
jest.mock('../../../services/emailService');
|
||||
jest.mock('../../../config/aws', () => ({
|
||||
getAWSConfig: jest.fn(() => ({ region: 'us-east-1' })),
|
||||
getAWSCredentials: jest.fn()
|
||||
}));
|
||||
|
||||
const LateReturnService = require('../../../services/lateReturnService');
|
||||
const { Rental, Item, User } = require('../../../models');
|
||||
const emailService = require('../../../services/emailService');
|
||||
|
||||
describe('LateReturnService', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('calculateLateFee', () => {
|
||||
it('should return no late fee when returned on time or early', () => {
|
||||
const rental = {
|
||||
endDateTime: new Date('2023-06-01T10:00:00Z'),
|
||||
item: { pricePerHour: 10, pricePerDay: 50 }
|
||||
};
|
||||
const actualReturn = new Date('2023-06-01T09:30:00Z'); // 30 minutes early
|
||||
|
||||
const result = LateReturnService.calculateLateFee(rental, actualReturn);
|
||||
|
||||
expect(result.isLate).toBe(false);
|
||||
expect(result.lateFee).toBe(0);
|
||||
expect(result.lateHours).toBe(0);
|
||||
});
|
||||
|
||||
it('should calculate late fee using hourly rate when available', () => {
|
||||
const rental = {
|
||||
endDateTime: new Date('2023-06-01T10:00:00Z'),
|
||||
item: { pricePerHour: 10, pricePerDay: 50 }
|
||||
};
|
||||
const actualReturn = new Date('2023-06-01T14:00:00Z'); // 4 hours late
|
||||
|
||||
const result = LateReturnService.calculateLateFee(rental, actualReturn);
|
||||
|
||||
expect(result.isLate).toBe(true);
|
||||
expect(result.lateFee).toBe(40); // 4 hours * $10
|
||||
expect(result.lateHours).toBe(4);
|
||||
expect(result.pricingType).toBe('hourly');
|
||||
});
|
||||
|
||||
it('should calculate late fee using daily rate when no hourly rate', () => {
|
||||
const rental = {
|
||||
endDateTime: new Date('2023-06-01T10:00:00Z'),
|
||||
item: { pricePerDay: 100 }
|
||||
};
|
||||
const actualReturn = new Date('2023-06-02T14:00:00Z'); // 28 hours late (2 days rounded up)
|
||||
|
||||
const result = LateReturnService.calculateLateFee(rental, actualReturn);
|
||||
|
||||
expect(result.isLate).toBe(true);
|
||||
expect(result.lateFee).toBe(200); // 2 days * $100
|
||||
expect(result.pricingType).toBe('daily');
|
||||
});
|
||||
|
||||
it('should use free borrow fallback rates when item has no pricing', () => {
|
||||
const rental = {
|
||||
startDateTime: new Date('2023-06-01T08:00:00Z'),
|
||||
endDateTime: new Date('2023-06-01T10:00:00Z'), // 2 hour rental
|
||||
item: {}
|
||||
};
|
||||
const actualReturn = new Date('2023-06-01T14:00:00Z'); // 4 hours late
|
||||
|
||||
const result = LateReturnService.calculateLateFee(rental, actualReturn);
|
||||
|
||||
expect(result.isLate).toBe(true);
|
||||
expect(result.lateFee).toBe(40); // 4 hours * $10 (free borrow hourly rate)
|
||||
expect(result.pricingType).toBe('hourly');
|
||||
});
|
||||
});
|
||||
|
||||
describe('processLateReturn', () => {
|
||||
let mockRental;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRental = {
|
||||
id: '123',
|
||||
status: 'active',
|
||||
endDateTime: new Date('2023-06-01T10:00:00Z'),
|
||||
item: { pricePerHour: 10, name: 'Test Item' },
|
||||
renterId: 'renter-123',
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
Rental.findByPk.mockResolvedValue(mockRental);
|
||||
emailService.sendLateReturnToCustomerService = jest.fn().mockResolvedValue();
|
||||
});
|
||||
|
||||
it('should process late return and send email to customer service', async () => {
|
||||
const actualReturn = new Date('2023-06-01T14:00:00Z'); // 4 hours late
|
||||
|
||||
mockRental.update.mockResolvedValue({
|
||||
...mockRental,
|
||||
status: 'returned_late',
|
||||
actualReturnDateTime: actualReturn
|
||||
});
|
||||
|
||||
const result = await LateReturnService.processLateReturn('123', actualReturn, 'Test notes');
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
actualReturnDateTime: actualReturn,
|
||||
status: 'returned_late',
|
||||
notes: 'Test notes'
|
||||
});
|
||||
|
||||
expect(emailService.sendLateReturnToCustomerService).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'returned_late'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
isLate: true,
|
||||
lateFee: 40,
|
||||
lateHours: 4
|
||||
})
|
||||
);
|
||||
|
||||
expect(result.lateCalculation.isLate).toBe(true);
|
||||
expect(result.lateCalculation.lateFee).toBe(40);
|
||||
});
|
||||
|
||||
it('should mark as completed when returned on time', async () => {
|
||||
const actualReturn = new Date('2023-06-01T09:30:00Z'); // Returned early
|
||||
|
||||
mockRental.update.mockResolvedValue({
|
||||
...mockRental,
|
||||
status: 'completed',
|
||||
actualReturnDateTime: actualReturn
|
||||
});
|
||||
|
||||
const result = await LateReturnService.processLateReturn('123', actualReturn);
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
actualReturnDateTime: actualReturn,
|
||||
status: 'completed'
|
||||
});
|
||||
|
||||
expect(emailService.sendLateReturnToCustomerService).not.toHaveBeenCalled();
|
||||
expect(result.lateCalculation.isLate).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw error when rental not found', async () => {
|
||||
Rental.findByPk.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
LateReturnService.processLateReturn('nonexistent', new Date())
|
||||
).rejects.toThrow('Rental not found');
|
||||
});
|
||||
|
||||
it('should throw error when rental is not active', async () => {
|
||||
mockRental.status = 'completed';
|
||||
|
||||
await expect(
|
||||
LateReturnService.processLateReturn('123', new Date())
|
||||
).rejects.toThrow('Can only process late returns for active rentals');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user