From 9a9e96d007cfd430b798a3168c526b1e3b65ccd2 Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:05:29 -0400 Subject: [PATCH] tests --- backend/services/emailService.js | 23 -- .../services/conditionCheckService.test.js | 165 ++++++++++++ .../services/damageAssessmentService.test.js | 219 +++++++++++++++ .../tests/unit/services/emailService.test.js | 251 ++++++++++++++++++ .../unit/services/lateReturnService.test.js | 163 ++++++++++++ 5 files changed, 798 insertions(+), 23 deletions(-) create mode 100644 backend/tests/unit/services/conditionCheckService.test.js create mode 100644 backend/tests/unit/services/damageAssessmentService.test.js create mode 100644 backend/tests/unit/services/emailService.test.js create mode 100644 backend/tests/unit/services/lateReturnService.test.js diff --git a/backend/services/emailService.js b/backend/services/emailService.js index 4b3f103..8733444 100644 --- a/backend/services/emailService.js +++ b/backend/services/emailService.js @@ -184,29 +184,6 @@ class EmailService {

Thank you for using RentAll!

` ), - - damageClaimNotification: baseTemplate.replace( - "{{content}}", - ` -

{{title}}

-

{{message}}

-

Item: {{itemName}}

-

Claim Amount: ${{ claimAmount }}

-

Description: {{description}}

-

Please review this claim and respond accordingly through your account.

- ` - ), - - returnIssueNotification: baseTemplate.replace( - "{{content}}", - ` -

{{title}}

-

{{message}}

-

Item: {{itemName}}

-

Return Status: {{returnStatus}}

-

Please check your account for more details and take appropriate action.

- ` - ), }; return ( diff --git a/backend/tests/unit/services/conditionCheckService.test.js b/backend/tests/unit/services/conditionCheckService.test.js new file mode 100644 index 0000000..5b84e4f --- /dev/null +++ b/backend/tests/unit/services/conditionCheckService.test.js @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/unit/services/damageAssessmentService.test.js b/backend/tests/unit/services/damageAssessmentService.test.js new file mode 100644 index 0000000..5b419cc --- /dev/null +++ b/backend/tests/unit/services/damageAssessmentService.test.js @@ -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'); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/unit/services/emailService.test.js b/backend/tests/unit/services/emailService.test.js new file mode 100644 index 0000000..b9590ce --- /dev/null +++ b/backend/tests/unit/services/emailService.test.js @@ -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', + '

Test HTML

', + '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', '

Content

'); + + 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', + '

Content

' + ); + + 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', '

Content

'); + + 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', '

Content

'); + + 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', '

Content

'); + + 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 = '

Hello {{name}}

Your order {{orderId}} is ready.

'; + emailService.templates.set('test', template); + + const rendered = emailService.renderTemplate('test', { + name: 'John Doe', + orderId: '12345' + }); + + expect(rendered).toBe('

Hello John Doe

Your order 12345 is ready.

'); + }); + + it('should handle missing variables by replacing with empty string', () => { + const template = '

Hello {{name}}

Your order {{orderId}} is ready.

'; + 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); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/unit/services/lateReturnService.test.js b/backend/tests/unit/services/lateReturnService.test.js new file mode 100644 index 0000000..73989eb --- /dev/null +++ b/backend/tests/unit/services/lateReturnService.test.js @@ -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'); + }); + }); +}); \ No newline at end of file