backend unit test coverage to 80%
This commit is contained in:
@@ -13,7 +13,9 @@ jest.mock('../../../models', () => ({
|
||||
Item: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
User: jest.fn(),
|
||||
User: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/auth', () => ({
|
||||
@@ -39,10 +41,20 @@ jest.mock('../../../services/email', () => ({
|
||||
sendRentalCancelledEmail: jest.fn(),
|
||||
sendDamageReportEmail: jest.fn(),
|
||||
sendLateReturnNotificationEmail: jest.fn(),
|
||||
sendRentalCompletionEmails: jest.fn().mockResolvedValue(),
|
||||
sendRentalCancellationEmails: jest.fn().mockResolvedValue(),
|
||||
sendAuthenticationRequiredEmail: jest.fn().mockResolvedValue(),
|
||||
},
|
||||
rentalReminder: {
|
||||
sendUpcomingRentalReminder: jest.fn(),
|
||||
},
|
||||
customerService: {
|
||||
sendLostItemToCustomerService: jest.fn().mockResolvedValue(),
|
||||
},
|
||||
payment: {
|
||||
sendPaymentDeclinedNotification: jest.fn().mockResolvedValue(),
|
||||
sendPaymentMethodUpdatedNotification: jest.fn().mockResolvedValue(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/logger', () => ({
|
||||
@@ -61,6 +73,7 @@ jest.mock('../../../services/lateReturnService', () => ({
|
||||
jest.mock('../../../services/damageAssessmentService', () => ({
|
||||
assessDamage: jest.fn(),
|
||||
processDamageFee: jest.fn(),
|
||||
processDamageAssessment: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/feeCalculator', () => ({
|
||||
@@ -89,11 +102,33 @@ jest.mock('../../../services/stripeWebhookService', () => ({
|
||||
reconcilePayoutStatuses: jest.fn().mockResolvedValue(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/payoutService', () => ({
|
||||
triggerPayoutOnCompletion: jest.fn().mockResolvedValue(),
|
||||
processRentalPayout: jest.fn().mockResolvedValue(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/eventBridgeSchedulerService', () => ({
|
||||
createConditionCheckSchedules: jest.fn().mockResolvedValue(),
|
||||
}));
|
||||
|
||||
// Mock stripe module
|
||||
jest.mock('stripe', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
paymentIntents: {
|
||||
retrieve: 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');
|
||||
const PayoutService = require('../../../services/payoutService');
|
||||
const DamageAssessmentService = require('../../../services/damageAssessmentService');
|
||||
const EventBridgeSchedulerService = require('../../../services/eventBridgeSchedulerService');
|
||||
const stripe = require('stripe');
|
||||
|
||||
// Create express app with the router
|
||||
const app = express();
|
||||
@@ -1319,4 +1354,629 @@ describe('Rentals Routes', () => {
|
||||
expect(response.body).toEqual({ error: 'Can only update payment method for pending rentals' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/report-damage', () => {
|
||||
const validUuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
ownerId: 1,
|
||||
renterId: 2,
|
||||
status: 'confirmed',
|
||||
item: { id: 1, name: 'Test Item' },
|
||||
};
|
||||
|
||||
const mockDamageResult = {
|
||||
rental: { id: 1, status: 'damaged' },
|
||||
damageAssessment: {
|
||||
description: 'Screen cracked',
|
||||
feeCalculation: { amount: 150 },
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
DamageAssessmentService.processDamageAssessment.mockResolvedValue(mockDamageResult);
|
||||
});
|
||||
|
||||
it('should report damage with all required fields', async () => {
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
canBeFixed: true,
|
||||
repairCost: 150,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(DamageAssessmentService.processDamageAssessment).toHaveBeenCalledWith(
|
||||
'1',
|
||||
expect.objectContaining({ description: 'Screen cracked' }),
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it('should report damage with optional late return', async () => {
|
||||
const damageResultWithLate = {
|
||||
...mockDamageResult,
|
||||
lateCalculation: { lateFee: 50 },
|
||||
};
|
||||
DamageAssessmentService.processDamageAssessment.mockResolvedValue(damageResultWithLate);
|
||||
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
canBeFixed: true,
|
||||
repairCost: 150,
|
||||
actualReturnDateTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept damage report without images', async () => {
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
canBeFixed: true,
|
||||
repairCost: 150,
|
||||
needsReplacement: false,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid imageFilenames format', async () => {
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
imageFilenames: ['invalid-key.jpg'],
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return 400 for non-image extensions', async () => {
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
imageFilenames: [`damage-reports/${validUuid}.exe`],
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 for exceeding max images', async () => {
|
||||
const tooManyImages = Array(11).fill(0).map((_, i) =>
|
||||
`damage-reports/550e8400-e29b-41d4-a716-44665544${String(i).padStart(4, '0')}.jpg`
|
||||
);
|
||||
|
||||
const damageData = {
|
||||
description: 'Screen cracked',
|
||||
imageFilenames: tooManyImages,
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send(damageData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should handle damage assessment service errors', async () => {
|
||||
DamageAssessmentService.processDamageAssessment.mockRejectedValue(
|
||||
new Error('Assessment failed')
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/report-damage')
|
||||
.send({ description: 'Screen cracked' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /:id/payment-client-secret', () => {
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
renterId: 1,
|
||||
stripePaymentIntentId: 'pi_test123',
|
||||
renter: { id: 1, stripeCustomerId: 'cus_test123' },
|
||||
};
|
||||
|
||||
let mockStripeInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockStripeInstance = {
|
||||
paymentIntents: {
|
||||
retrieve: jest.fn().mockResolvedValue({
|
||||
client_secret: 'pi_test123_secret_xxx',
|
||||
status: 'requires_action',
|
||||
}),
|
||||
},
|
||||
};
|
||||
stripe.mockImplementation(() => mockStripeInstance);
|
||||
});
|
||||
|
||||
it('should return client secret for renter', async () => {
|
||||
const response = await request(app)
|
||||
.get('/rentals/1/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.clientSecret).toBe('pi_test123_secret_xxx');
|
||||
expect(response.body.status).toBe('requires_action');
|
||||
});
|
||||
|
||||
it('should return payment intent status', async () => {
|
||||
mockStripeInstance.paymentIntents.retrieve.mockResolvedValue({
|
||||
client_secret: 'pi_test123_secret_xxx',
|
||||
status: 'succeeded',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/1/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe('succeeded');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent rental', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/999/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Rental not found');
|
||||
});
|
||||
|
||||
it('should return 403 for non-renter', async () => {
|
||||
const nonRenterRental = { ...mockRental, renterId: 3 };
|
||||
mockRentalFindByPk.mockResolvedValue(nonRenterRental);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/1/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Not authorized');
|
||||
});
|
||||
|
||||
it('should return 400 when no payment intent exists', async () => {
|
||||
const rentalWithoutPaymentIntent = { ...mockRental, stripePaymentIntentId: null };
|
||||
mockRentalFindByPk.mockResolvedValue(rentalWithoutPaymentIntent);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/1/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('No payment intent found');
|
||||
});
|
||||
|
||||
it('should handle Stripe API errors', async () => {
|
||||
mockStripeInstance.paymentIntents.retrieve.mockRejectedValue(
|
||||
new Error('Stripe API error')
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/rentals/1/payment-client-secret');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/complete-payment', () => {
|
||||
const mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
renterId: 1,
|
||||
status: 'pending',
|
||||
paymentStatus: 'requires_action',
|
||||
stripePaymentIntentId: 'pi_test123',
|
||||
totalAmount: 120,
|
||||
item: { id: 1, name: 'Test Item' },
|
||||
renter: { id: 1, firstName: 'Alice', lastName: 'Johnson', email: 'alice@example.com', stripeCustomerId: 'cus_test123' },
|
||||
owner: { id: 2, firstName: 'John', lastName: 'Doe', email: 'john@example.com', stripeConnectedAccountId: 'acct_test123', stripePayoutsEnabled: true },
|
||||
update: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
let mockStripeInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockRental.update.mockReset();
|
||||
mockStripeInstance = {
|
||||
paymentIntents: {
|
||||
retrieve: jest.fn().mockResolvedValue({
|
||||
status: 'succeeded',
|
||||
latest_charge: {
|
||||
payment_method_details: {
|
||||
type: 'card',
|
||||
card: { brand: 'visa', last4: '4242' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
stripe.mockImplementation(() => mockStripeInstance);
|
||||
});
|
||||
|
||||
it('should complete payment after 3DS authentication', async () => {
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.rental.status).toBe('confirmed');
|
||||
expect(response.body.rental.paymentStatus).toBe('paid');
|
||||
});
|
||||
|
||||
it('should update rental to confirmed status', async () => {
|
||||
await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
status: 'confirmed',
|
||||
paymentStatus: 'paid',
|
||||
chargedAt: expect.any(Date),
|
||||
paymentMethodBrand: 'visa',
|
||||
paymentMethodLast4: '4242',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create condition check schedules', async () => {
|
||||
await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(EventBridgeSchedulerService.createConditionCheckSchedules).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger payout if owner has payouts enabled', async () => {
|
||||
await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(PayoutService.processRentalPayout).toHaveBeenCalledWith(mockRental);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent rental', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/999/complete-payment');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Rental not found');
|
||||
});
|
||||
|
||||
it('should return 403 for non-renter', async () => {
|
||||
const nonRenterRental = { ...mockRental, renterId: 3 };
|
||||
mockRentalFindByPk.mockResolvedValue(nonRenterRental);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Not authorized');
|
||||
});
|
||||
|
||||
it('should return 400 when payment status is not requires_action', async () => {
|
||||
const paidRental = { ...mockRental, paymentStatus: 'paid' };
|
||||
mockRentalFindByPk.mockResolvedValue(paidRental);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid state');
|
||||
});
|
||||
|
||||
it('should return 402 when payment intent not succeeded', async () => {
|
||||
mockStripeInstance.paymentIntents.retrieve.mockResolvedValue({
|
||||
status: 'requires_action',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(402);
|
||||
expect(response.body.error).toBe('payment_incomplete');
|
||||
});
|
||||
|
||||
it('should handle Stripe API errors', async () => {
|
||||
mockStripeInstance.paymentIntents.retrieve.mockRejectedValue(
|
||||
new Error('Stripe API error')
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle bank account payment methods', async () => {
|
||||
mockStripeInstance.paymentIntents.retrieve.mockResolvedValue({
|
||||
status: 'succeeded',
|
||||
latest_charge: {
|
||||
payment_method_details: {
|
||||
type: 'us_bank_account',
|
||||
us_bank_account: { last4: '6789' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/complete-payment');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockRental.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paymentMethodBrand: 'bank_account',
|
||||
paymentMethodLast4: '6789',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /:id/mark-return (Additional Cases)', () => {
|
||||
let mockRental;
|
||||
|
||||
const LateReturnService = require('../../../services/lateReturnService');
|
||||
|
||||
beforeEach(() => {
|
||||
mockRental = {
|
||||
id: 1,
|
||||
ownerId: 1,
|
||||
renterId: 2,
|
||||
status: 'confirmed',
|
||||
startDateTime: new Date('2024-01-10T10:00:00.000Z'),
|
||||
endDateTime: new Date('2024-01-15T18:00:00.000Z'),
|
||||
lateFees: 0,
|
||||
item: { id: 1, name: 'Test Item' },
|
||||
update: jest.fn(),
|
||||
};
|
||||
// Make update return the modified rental instance
|
||||
mockRental.update.mockImplementation((updates) => {
|
||||
Object.assign(mockRental, updates);
|
||||
return Promise.resolve(mockRental);
|
||||
});
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
});
|
||||
|
||||
it('should mark item as returned with payout trigger', async () => {
|
||||
mockRentalFindByPk.mockResolvedValueOnce(mockRental);
|
||||
const rentalWithDetails = {
|
||||
...mockRental,
|
||||
owner: { id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com', stripeConnectedAccountId: 'acct_123' },
|
||||
renter: { id: 2, firstName: 'Alice', lastName: 'Johnson', email: 'alice@example.com' },
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValueOnce(rentalWithDetails);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({ status: 'returned' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(PayoutService.triggerPayoutOnCompletion).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should mark item as damaged', async () => {
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({ status: 'damaged' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockRental.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: 'damaged' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark item as returned_late with late fees', async () => {
|
||||
LateReturnService.processLateReturn.mockResolvedValue({
|
||||
rental: { ...mockRental, status: 'returned_late', lateFees: 50 },
|
||||
lateCalculation: { lateFee: 50, hoursLate: 5 },
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({
|
||||
status: 'returned_late',
|
||||
actualReturnDateTime: '2024-01-15T23:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.lateCalculation).toBeDefined();
|
||||
expect(response.body.lateCalculation.lateFee).toBe(50);
|
||||
});
|
||||
|
||||
it('should require actualReturnDateTime for late returns', async () => {
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({ status: 'returned_late' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Actual return date/time is required for late returns');
|
||||
});
|
||||
|
||||
it('should mark item as lost with customer service notification', async () => {
|
||||
User.findByPk = jest.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({ status: 'lost' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockRental.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: 'lost',
|
||||
itemLostReportedAt: expect.any(Date),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle damaged with late return combination', async () => {
|
||||
LateReturnService.processLateReturn.mockResolvedValue({
|
||||
rental: { ...mockRental, lateFees: 50 },
|
||||
lateCalculation: { lateFee: 50, hoursLate: 5 },
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/rentals/1/mark-return')
|
||||
.send({
|
||||
status: 'damaged',
|
||||
actualReturnDateTime: '2024-01-15T23:00:00.000Z',
|
||||
statusOptions: { returned_late: true },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockRental.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ status: 'returned_late_and_damaged' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /:id/status (3DS Flow)', () => {
|
||||
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',
|
||||
email: 'alice@example.com',
|
||||
stripeCustomerId: 'cus_test123',
|
||||
},
|
||||
owner: {
|
||||
id: 1,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
stripeConnectedAccountId: 'acct_test123',
|
||||
},
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockRental.update.mockReset();
|
||||
});
|
||||
|
||||
it('should handle payment requiring 3DS authentication', async () => {
|
||||
StripeService.chargePaymentMethod.mockResolvedValue({
|
||||
requiresAction: true,
|
||||
paymentIntentId: 'pi_test_3ds',
|
||||
clientSecret: 'pi_test_3ds_secret_xxx',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(response.status).toBe(402);
|
||||
expect(response.body.error).toBe('authentication_required');
|
||||
expect(response.body.requiresAction).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 402 with requiresAction flag', async () => {
|
||||
StripeService.chargePaymentMethod.mockResolvedValue({
|
||||
requiresAction: true,
|
||||
paymentIntentId: 'pi_test_3ds',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(response.status).toBe(402);
|
||||
expect(response.body.requiresAction).toBe(true);
|
||||
expect(response.body.rentalId).toBe(1);
|
||||
});
|
||||
|
||||
it('should store payment intent ID for later completion', async () => {
|
||||
StripeService.chargePaymentMethod.mockResolvedValue({
|
||||
requiresAction: true,
|
||||
paymentIntentId: 'pi_test_3ds',
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
stripePaymentIntentId: 'pi_test_3ds',
|
||||
paymentStatus: 'requires_action',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set paymentStatus to requires_action', async () => {
|
||||
StripeService.chargePaymentMethod.mockResolvedValue({
|
||||
requiresAction: true,
|
||||
paymentIntentId: 'pi_test_3ds',
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ paymentStatus: 'requires_action' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle card declined errors', async () => {
|
||||
const declinedError = new Error('Your card was declined');
|
||||
declinedError.code = 'card_declined';
|
||||
|
||||
StripeService.chargePaymentMethod.mockRejectedValue(declinedError);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(response.status).toBe(402);
|
||||
expect(response.body.error).toBe('payment_failed');
|
||||
expect(response.body.code).toBe('card_declined');
|
||||
});
|
||||
|
||||
it('should handle insufficient funds errors', async () => {
|
||||
const insufficientError = new Error('Insufficient funds');
|
||||
insufficientError.code = 'insufficient_funds';
|
||||
|
||||
StripeService.chargePaymentMethod.mockRejectedValue(insufficientError);
|
||||
|
||||
const response = await request(app)
|
||||
.put('/rentals/1/status')
|
||||
.send({ status: 'confirmed' });
|
||||
|
||||
expect(response.status).toBe(402);
|
||||
expect(response.body.error).toBe('payment_failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user