Files
rentall-app/backend/tests/unit/routes/rentals.test.js
2025-11-24 17:36:18 -05:00

1016 lines
30 KiB
JavaScript

const request = require('supertest');
const express = require('express');
const rentalsRouter = require('../../../routes/rentals');
// Mock all dependencies
jest.mock('../../../models', () => ({
Rental: {
findAll: jest.fn(),
findByPk: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
},
Item: {
findByPk: jest.fn(),
},
User: jest.fn(),
}));
jest.mock('../../../middleware/auth', () => ({
authenticateToken: jest.fn((req, res, next) => {
req.user = { id: 1 };
next();
}),
requireVerifiedEmail: jest.fn((req, res, next) => next()),
}));
jest.mock('../../../utils/rentalDurationCalculator', () => ({
calculateRentalCost: jest.fn(() => 100),
}));
jest.mock('../../../services/emailService', () => ({
sendRentalRequestEmail: jest.fn(),
sendRentalApprovalEmail: jest.fn(),
sendRentalDeclinedEmail: jest.fn(),
sendRentalCompletedEmail: jest.fn(),
sendRentalCancelledEmail: jest.fn(),
sendDamageReportEmail: jest.fn(),
sendLateReturnNotificationEmail: jest.fn(),
}));
jest.mock('../../../utils/logger', () => ({
withRequestId: jest.fn(() => ({
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
})),
}));
jest.mock('../../../services/lateReturnService', () => ({
calculateLateFee: jest.fn(),
processLateReturn: jest.fn(),
}));
jest.mock('../../../services/damageAssessmentService', () => ({
assessDamage: jest.fn(),
processDamageFee: jest.fn(),
}));
jest.mock('../../../utils/feeCalculator', () => ({
calculateRentalFees: jest.fn(() => ({
totalChargedAmount: 120,
platformFee: 20,
payoutAmount: 100,
})),
formatFeesForDisplay: jest.fn(() => ({
baseAmount: '$100.00',
platformFee: '$20.00',
totalAmount: '$120.00',
})),
}));
jest.mock('../../../services/refundService', () => ({
getRefundPreview: jest.fn(),
processCancellation: jest.fn(),
}));
jest.mock('../../../services/stripeService', () => ({
chargePaymentMethod: jest.fn(),
}));
const { Rental, Item, User } = require('../../../models');
const FeeCalculator = require('../../../utils/feeCalculator');
const RentalDurationCalculator = require('../../../utils/rentalDurationCalculator');
const RefundService = require('../../../services/refundService');
const StripeService = require('../../../services/stripeService');
// Create express app with the router
const app = express();
app.use(express.json());
app.use('/rentals', rentalsRouter);
// Mock models
const mockRentalFindAll = Rental.findAll;
const mockRentalFindByPk = Rental.findByPk;
const mockRentalFindOne = Rental.findOne;
const mockRentalCreate = Rental.create;
describe('Rentals Routes', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET /renting', () => {
it('should get rentals for authenticated user', async () => {
const mockRentals = [
{
id: 1,
renterId: 1,
item: { id: 1, name: 'Test Item' },
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
},
{
id: 2,
renterId: 1,
item: { id: 2, name: 'Another Item' },
owner: { id: 3, username: 'owner2', firstName: 'Jane', lastName: 'Smith' },
},
];
mockRentalFindAll.mockResolvedValue(mockRentals);
const response = await request(app)
.get('/rentals/renting');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockRentals);
expect(mockRentalFindAll).toHaveBeenCalledWith({
where: { renterId: 1 },
include: [
{ model: Item, as: 'item' },
{
model: User,
as: 'owner',
attributes: ['id', 'username', 'firstName', 'lastName'],
},
],
order: [['createdAt', 'DESC']],
});
});
it('should handle database errors', async () => {
mockRentalFindAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/rentals/renting');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to fetch rentals' });
});
});
describe('GET /owning', () => {
it('should get listings for authenticated user', async () => {
const mockListings = [
{
id: 1,
ownerId: 1,
item: { id: 1, name: 'My Item' },
renter: { id: 2, username: 'renter1', firstName: 'Alice', lastName: 'Johnson' },
},
];
mockRentalFindAll.mockResolvedValue(mockListings);
const response = await request(app)
.get('/rentals/owning');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockListings);
expect(mockRentalFindAll).toHaveBeenCalledWith({
where: { ownerId: 1 },
include: [
{ model: Item, as: 'item' },
{
model: User,
as: 'renter',
attributes: ['id', 'username', 'firstName', 'lastName'],
},
],
order: [['createdAt', 'DESC']],
});
});
it('should handle database errors', async () => {
mockRentalFindAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/rentals/owning');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to fetch listings' });
});
});
describe('GET /:id', () => {
const mockRental = {
id: 1,
ownerId: 2,
renterId: 1,
item: { id: 1, name: 'Test Item' },
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
renter: { id: 1, username: 'renter1', firstName: 'Alice', lastName: 'Johnson' },
};
it('should get rental by ID for authorized user (renter)', async () => {
mockRentalFindByPk.mockResolvedValue(mockRental);
const response = await request(app)
.get('/rentals/1');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockRental);
});
it('should get rental by ID for authorized user (owner)', async () => {
const ownerRental = { ...mockRental, ownerId: 1, renterId: 2 };
mockRentalFindByPk.mockResolvedValue(ownerRental);
const response = await request(app)
.get('/rentals/1');
expect(response.status).toBe(200);
expect(response.body).toEqual(ownerRental);
});
it('should return 404 for non-existent rental', async () => {
mockRentalFindByPk.mockResolvedValue(null);
const response = await request(app)
.get('/rentals/999');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Rental not found' });
});
it('should return 403 for unauthorized user', async () => {
const unauthorizedRental = { ...mockRental, ownerId: 3, renterId: 4 };
mockRentalFindByPk.mockResolvedValue(unauthorizedRental);
const response = await request(app)
.get('/rentals/1');
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Unauthorized to view this rental' });
});
it('should handle database errors', async () => {
mockRentalFindByPk.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/rentals/1');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to fetch rental' });
});
});
describe('POST /', () => {
const mockItem = {
id: 1,
name: 'Test Item',
ownerId: 2,
isAvailable: true,
pricePerHour: 10,
pricePerDay: 50,
};
const mockCreatedRental = {
id: 1,
itemId: 1,
renterId: 1,
ownerId: 2,
totalAmount: 120,
platformFee: 20,
payoutAmount: 100,
status: 'pending',
};
const mockRentalWithDetails = {
...mockCreatedRental,
item: mockItem,
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
renter: { id: 1, username: 'renter1', firstName: 'Alice', lastName: 'Johnson' },
};
const rentalData = {
itemId: 1,
startDateTime: '2024-01-15T10:00:00.000Z',
endDateTime: '2024-01-15T18:00:00.000Z',
deliveryMethod: 'pickup',
deliveryAddress: null,
notes: 'Test rental',
stripePaymentMethodId: 'pm_test123',
};
beforeEach(() => {
Item.findByPk.mockResolvedValue(mockItem);
mockRentalFindOne.mockResolvedValue(null); // No overlapping rentals
mockRentalCreate.mockResolvedValue(mockCreatedRental);
mockRentalFindByPk.mockResolvedValue(mockRentalWithDetails);
});
it('should create a new rental with hourly pricing', async () => {
RentalDurationCalculator.calculateRentalCost.mockReturnValue(80); // 8 hours * 10/hour
const response = await request(app)
.post('/rentals')
.send(rentalData);
expect(response.status).toBe(201);
expect(response.body).toEqual(mockRentalWithDetails);
expect(FeeCalculator.calculateRentalFees).toHaveBeenCalledWith(80); // 8 hours * 10/hour
});
it('should create a new rental with daily pricing', async () => {
RentalDurationCalculator.calculateRentalCost.mockReturnValue(150); // 3 days * 50/day
const dailyRentalData = {
...rentalData,
endDateTime: '2024-01-17T18:00:00.000Z', // 3 days
};
const response = await request(app)
.post('/rentals')
.send(dailyRentalData);
expect(response.status).toBe(201);
expect(FeeCalculator.calculateRentalFees).toHaveBeenCalledWith(150); // 3 days * 50/day
});
it('should return 404 for non-existent item', async () => {
Item.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/rentals')
.send(rentalData);
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Item not found' });
});
it('should return 400 for unavailable item', async () => {
Item.findByPk.mockResolvedValue({ ...mockItem, isAvailable: false });
const response = await request(app)
.post('/rentals')
.send(rentalData);
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Item is not available' });
});
it('should return 400 for overlapping rental', async () => {
mockRentalFindOne.mockResolvedValue({ id: 999 }); // Overlapping rental exists
const response = await request(app)
.post('/rentals')
.send(rentalData);
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Item is already booked for these dates' });
});
it('should return 400 when payment method is missing for paid rentals', async () => {
RentalDurationCalculator.calculateRentalCost.mockReturnValue(100); // Paid rental
const dataWithoutPayment = { ...rentalData };
delete dataWithoutPayment.stripePaymentMethodId;
const response = await request(app)
.post('/rentals')
.send(dataWithoutPayment);
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Payment method is required for paid rentals' });
});
it('should create a free rental without payment method', async () => {
RentalDurationCalculator.calculateRentalCost.mockReturnValue(0); // Free rental
// Set up a free item (both prices are 0)
Item.findByPk.mockResolvedValue({
id: 1,
ownerId: 2,
isAvailable: true,
pricePerHour: 0,
pricePerDay: 0
});
const freeRentalData = { ...rentalData };
delete freeRentalData.stripePaymentMethodId;
const createdRental = {
id: 1,
...freeRentalData,
renterId: 1,
ownerId: 2,
totalAmount: 0,
platformFee: 0,
payoutAmount: 0,
paymentStatus: 'not_required',
status: 'pending'
};
Rental.create.mockResolvedValue(createdRental);
Rental.findByPk.mockResolvedValue({
...createdRental,
item: { id: 1, name: 'Free Item' },
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
renter: { id: 1, username: 'renter1', firstName: 'Jane', lastName: 'Smith' }
});
const response = await request(app)
.post('/rentals')
.send(freeRentalData);
expect(response.status).toBe(201);
expect(response.body.paymentStatus).toBe('not_required');
expect(response.body.totalAmount).toBe(0);
});
it('should handle database errors during creation', async () => {
mockRentalCreate.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/rentals')
.send(rentalData);
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to create rental' });
});
});
describe('PUT /:id/status', () => {
const mockRental = {
id: 1,
ownerId: 1,
renterId: 2,
status: 'pending',
stripePaymentMethodId: 'pm_test123',
totalAmount: 120,
item: { id: 1, name: 'Test Item' },
renter: {
id: 2,
username: 'renter1',
firstName: 'Alice',
lastName: 'Johnson',
stripeCustomerId: 'cus_test123'
},
update: jest.fn(),
};
beforeEach(() => {
mockRentalFindByPk.mockResolvedValue(mockRental);
});
it('should update rental status to confirmed without payment processing', async () => {
const nonPendingRental = { ...mockRental, status: 'active' };
mockRentalFindByPk.mockResolvedValueOnce(nonPendingRental);
const updatedRental = { ...nonPendingRental, status: 'confirmed' };
mockRentalFindByPk.mockResolvedValueOnce(updatedRental);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(200);
expect(nonPendingRental.update).toHaveBeenCalledWith({ status: 'confirmed' });
});
it('should process payment when owner approves pending rental', async () => {
// Use the original mockRental (status: 'pending') for this test
mockRentalFindByPk.mockResolvedValueOnce(mockRental);
StripeService.chargePaymentMethod.mockResolvedValue({
paymentIntentId: 'pi_test123',
paymentMethod: {
brand: 'visa',
last4: '4242'
},
chargedAt: new Date('2024-01-15T10:00:00.000Z')
});
const updatedRental = {
...mockRental,
status: 'confirmed',
paymentStatus: 'paid',
stripePaymentIntentId: 'pi_test123'
};
mockRentalFindByPk.mockResolvedValueOnce(updatedRental);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(200);
expect(StripeService.chargePaymentMethod).toHaveBeenCalledWith(
'pm_test123',
120,
'cus_test123',
expect.objectContaining({
rentalId: 1,
itemName: 'Test Item',
})
);
expect(mockRental.update).toHaveBeenCalledWith({
status: 'confirmed',
paymentStatus: 'paid',
stripePaymentIntentId: 'pi_test123',
paymentMethodBrand: 'visa',
paymentMethodLast4: '4242',
chargedAt: new Date('2024-01-15T10:00:00.000Z'),
});
});
it('should approve free rental without payment processing', async () => {
const freeRental = {
...mockRental,
totalAmount: 0,
paymentStatus: 'not_required',
stripePaymentMethodId: null,
update: jest.fn().mockResolvedValue(true)
};
mockRentalFindByPk.mockResolvedValueOnce(freeRental);
const updatedFreeRental = {
...freeRental,
status: 'confirmed'
};
mockRentalFindByPk.mockResolvedValueOnce(updatedFreeRental);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(200);
expect(StripeService.chargePaymentMethod).not.toHaveBeenCalled();
expect(freeRental.update).toHaveBeenCalledWith({
status: 'confirmed'
});
});
it('should return 400 when renter has no Stripe customer ID', async () => {
const rentalWithoutStripeCustomer = {
...mockRental,
renter: { ...mockRental.renter, stripeCustomerId: null }
};
mockRentalFindByPk.mockResolvedValue(rentalWithoutStripeCustomer);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(400);
expect(response.body).toEqual({
error: 'Renter does not have a Stripe customer account'
});
});
it('should handle payment failure during approval', async () => {
StripeService.chargePaymentMethod.mockRejectedValue(
new Error('Payment failed')
);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(400);
expect(response.body).toEqual({
error: 'Payment failed during approval',
details: 'Payment failed',
});
});
it('should return 404 for non-existent rental', async () => {
mockRentalFindByPk.mockResolvedValue(null);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Rental not found' });
});
it('should return 403 for unauthorized user', async () => {
const unauthorizedRental = { ...mockRental, ownerId: 3, renterId: 4 };
mockRentalFindByPk.mockResolvedValue(unauthorizedRental);
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Unauthorized to update this rental' });
});
it('should handle database errors', async () => {
mockRentalFindByPk.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.put('/rentals/1/status')
.send({ status: 'confirmed' });
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to update rental status' });
});
});
describe('POST /:id/review-renter', () => {
const mockRental = {
id: 1,
ownerId: 1,
renterId: 2,
status: 'completed',
renterReviewSubmittedAt: null,
update: jest.fn(),
};
beforeEach(() => {
mockRentalFindByPk.mockResolvedValue(mockRental);
});
it('should allow owner to review renter', async () => {
const reviewData = {
rating: 5,
review: 'Great renter!',
privateMessage: 'Thanks for taking care of my item',
};
mockRental.update.mockResolvedValue();
const response = await request(app)
.post('/rentals/1/review-renter')
.send(reviewData);
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: true,
});
expect(mockRental.update).toHaveBeenCalledWith({
renterRating: 5,
renterReview: 'Great renter!',
renterReviewSubmittedAt: expect.any(Date),
renterPrivateMessage: 'Thanks for taking care of my item',
});
});
it('should return 404 for non-existent rental', async () => {
mockRentalFindByPk.mockResolvedValue(null);
const response = await request(app)
.post('/rentals/1/review-renter')
.send({ rating: 5, review: 'Great!' });
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'Rental not found' });
});
it('should return 403 for non-owner', async () => {
const nonOwnerRental = { ...mockRental, ownerId: 3 };
mockRentalFindByPk.mockResolvedValue(nonOwnerRental);
const response = await request(app)
.post('/rentals/1/review-renter')
.send({ rating: 5, review: 'Great!' });
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Only owners can review renters' });
});
it('should return 400 for non-completed rental', async () => {
const activeRental = { ...mockRental, status: 'active' };
mockRentalFindByPk.mockResolvedValue(activeRental);
const response = await request(app)
.post('/rentals/1/review-renter')
.send({ rating: 5, review: 'Great!' });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Can only review completed rentals' });
});
it('should return 400 if review already submitted', async () => {
const reviewedRental = {
...mockRental,
renterReviewSubmittedAt: new Date()
};
mockRentalFindByPk.mockResolvedValue(reviewedRental);
const response = await request(app)
.post('/rentals/1/review-renter')
.send({ rating: 5, review: 'Great!' });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Renter review already submitted' });
});
it('should handle database errors', async () => {
mockRentalFindByPk.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/rentals/1/review-renter')
.send({ rating: 5, review: 'Great!' });
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to submit review' });
});
});
describe('POST /:id/review-item', () => {
const mockRental = {
id: 1,
ownerId: 2,
renterId: 1,
status: 'completed',
itemReviewSubmittedAt: null,
update: jest.fn(),
};
beforeEach(() => {
mockRentalFindByPk.mockResolvedValue(mockRental);
});
it('should allow renter to review item', async () => {
const reviewData = {
rating: 4,
review: 'Good item!',
privateMessage: 'Item was as described',
};
mockRental.update.mockResolvedValue();
const response = await request(app)
.post('/rentals/1/review-item')
.send(reviewData);
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: true,
});
expect(mockRental.update).toHaveBeenCalledWith({
itemRating: 4,
itemReview: 'Good item!',
itemReviewSubmittedAt: expect.any(Date),
itemPrivateMessage: 'Item was as described',
});
});
it('should return 403 for non-renter', async () => {
const nonRenterRental = { ...mockRental, renterId: 3 };
mockRentalFindByPk.mockResolvedValue(nonRenterRental);
const response = await request(app)
.post('/rentals/1/review-item')
.send({ rating: 4, review: 'Good!' });
expect(response.status).toBe(403);
expect(response.body).toEqual({ error: 'Only renters can review items' });
});
it('should return 400 if review already submitted', async () => {
const reviewedRental = {
...mockRental,
itemReviewSubmittedAt: new Date()
};
mockRentalFindByPk.mockResolvedValue(reviewedRental);
const response = await request(app)
.post('/rentals/1/review-item')
.send({ rating: 4, review: 'Good!' });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Item review already submitted' });
});
});
describe('POST /:id/mark-completed', () => {
const mockRental = {
id: 1,
ownerId: 1,
renterId: 2,
status: 'active',
update: jest.fn(),
};
beforeEach(() => {
mockRentalFindByPk.mockResolvedValue(mockRental);
});
it('should allow owner to mark rental as completed', async () => {
const completedRental = { ...mockRental, status: 'completed' };
mockRentalFindByPk
.mockResolvedValueOnce(mockRental)
.mockResolvedValueOnce(completedRental);
const response = await request(app)
.post('/rentals/1/mark-completed');
expect(response.status).toBe(200);
expect(mockRental.update).toHaveBeenCalledWith({ status: 'completed' });
});
it('should return 403 for non-owner', async () => {
const nonOwnerRental = { ...mockRental, ownerId: 3 };
mockRentalFindByPk.mockResolvedValue(nonOwnerRental);
const response = await request(app)
.post('/rentals/1/mark-completed');
expect(response.status).toBe(403);
expect(response.body).toEqual({
error: 'Only owners can mark rentals as completed'
});
});
it('should return 400 for invalid status', async () => {
const pendingRental = { ...mockRental, status: 'pending' };
mockRentalFindByPk.mockResolvedValue(pendingRental);
const response = await request(app)
.post('/rentals/1/mark-completed');
expect(response.status).toBe(400);
expect(response.body).toEqual({
error: 'Can only mark active or confirmed rentals as completed',
});
});
});
describe('POST /calculate-fees', () => {
it('should calculate fees for given amount', async () => {
const response = await request(app)
.post('/rentals/calculate-fees')
.send({ totalAmount: 100 });
expect(response.status).toBe(200);
expect(response.body).toEqual({
fees: {
totalChargedAmount: 120,
platformFee: 20,
payoutAmount: 100,
},
display: {
baseAmount: '$100.00',
platformFee: '$20.00',
totalAmount: '$120.00',
},
});
expect(FeeCalculator.calculateRentalFees).toHaveBeenCalledWith(100);
expect(FeeCalculator.formatFeesForDisplay).toHaveBeenCalled();
});
it('should return 400 for invalid amount', async () => {
const response = await request(app)
.post('/rentals/calculate-fees')
.send({ totalAmount: 0 });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Valid base amount is required' });
});
it('should handle calculation errors', async () => {
FeeCalculator.calculateRentalFees.mockImplementation(() => {
throw new Error('Calculation error');
});
const response = await request(app)
.post('/rentals/calculate-fees')
.send({ totalAmount: 100 });
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Failed to calculate fees' });
});
});
describe('GET /earnings/status', () => {
it('should get earnings status for owner', async () => {
const mockEarnings = [
{
id: 1,
totalAmount: 120,
platformFee: 20,
payoutAmount: 100,
payoutStatus: 'completed',
payoutProcessedAt: '2024-01-15T10:00:00.000Z',
stripeTransferId: 'tr_test123',
item: { name: 'Test Item' },
},
];
mockRentalFindAll.mockResolvedValue(mockEarnings);
const response = await request(app)
.get('/rentals/earnings/status');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockEarnings);
expect(mockRentalFindAll).toHaveBeenCalledWith({
where: {
ownerId: 1,
status: 'completed',
},
attributes: [
'id',
'totalAmount',
'platformFee',
'payoutAmount',
'payoutStatus',
'payoutProcessedAt',
'stripeTransferId',
],
include: [{ model: Item, as: 'item', attributes: ['name'] }],
order: [['createdAt', 'DESC']],
});
});
it('should handle database errors', async () => {
mockRentalFindAll.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/rentals/earnings/status');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database error' });
});
});
describe('GET /:id/refund-preview', () => {
it('should get refund preview', async () => {
const mockPreview = {
refundAmount: 80,
refundPercentage: 80,
reason: 'Cancelled more than 24 hours before start',
};
RefundService.getRefundPreview.mockResolvedValue(mockPreview);
const response = await request(app)
.get('/rentals/1/refund-preview');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockPreview);
expect(RefundService.getRefundPreview).toHaveBeenCalledWith('1', 1);
});
it('should handle refund service errors', async () => {
RefundService.getRefundPreview.mockRejectedValue(
new Error('Rental not found')
);
const response = await request(app)
.get('/rentals/1/refund-preview');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Rental not found' });
});
});
describe('POST /:id/cancel', () => {
it('should cancel rental with refund', async () => {
const mockResult = {
rental: {
id: 1,
status: 'cancelled',
},
refund: {
amount: 80,
stripeRefundId: 'rf_test123',
},
};
const mockUpdatedRental = {
id: 1,
status: 'cancelled',
item: { id: 1, name: 'Test Item' },
owner: { id: 2, username: 'owner1', firstName: 'John', lastName: 'Doe' },
renter: { id: 1, username: 'renter1', firstName: 'Alice', lastName: 'Johnson' },
};
RefundService.processCancellation.mockResolvedValue(mockResult);
mockRentalFindByPk.mockResolvedValue(mockUpdatedRental);
const response = await request(app)
.post('/rentals/1/cancel')
.send({ reason: 'Change of plans' });
expect(response.status).toBe(200);
expect(response.body).toEqual({
rental: mockUpdatedRental,
refund: mockResult.refund,
});
expect(RefundService.processCancellation).toHaveBeenCalledWith(
'1',
1,
'Change of plans'
);
});
it('should handle cancellation errors', async () => {
RefundService.processCancellation.mockRejectedValue(
new Error('Cannot cancel completed rental')
);
const response = await request(app)
.post('/rentals/1/cancel')
.send({ reason: 'Change of plans' });
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'Cannot cancel completed rental' });
});
});
});