Files
rentall-app/backend/tests/unit/routes/stripe.test.js
2025-09-19 19:46:41 -04:00

805 lines
25 KiB
JavaScript

const request = require('supertest');
const express = require('express');
const jwt = require('jsonwebtoken');
// Mock dependencies
jest.mock('jsonwebtoken');
jest.mock('../../../models', () => ({
User: {
findByPk: jest.fn(),
create: jest.fn(),
findOne: jest.fn()
},
Item: {}
}));
jest.mock('../../../services/stripeService', () => ({
getCheckoutSession: jest.fn(),
createConnectedAccount: jest.fn(),
createAccountLink: jest.fn(),
getAccountStatus: jest.fn(),
createCustomer: jest.fn(),
createSetupCheckoutSession: jest.fn()
}));
// Mock auth middleware
jest.mock('../../../middleware/auth', () => ({
authenticateToken: (req, res, next) => {
// Mock authenticated user
if (req.headers.authorization) {
req.user = { id: 1 };
next();
} else {
res.status(401).json({ error: 'No token provided' });
}
}
}));
const { User } = require('../../../models');
const StripeService = require('../../../services/stripeService');
const stripeRoutes = require('../../../routes/stripe');
// Set up Express app for testing
const app = express();
app.use(express.json());
app.use('/stripe', stripeRoutes);
describe('Stripe Routes', () => {
let consoleSpy, consoleErrorSpy;
beforeEach(() => {
jest.clearAllMocks();
// Set up console spies
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
});
afterEach(() => {
consoleSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('GET /checkout-session/:sessionId', () => {
it('should retrieve checkout session successfully', async () => {
const mockSession = {
status: 'complete',
payment_status: 'paid',
customer_details: {
email: 'test@example.com'
},
setup_intent: {
id: 'seti_123456789',
status: 'succeeded'
},
metadata: {
userId: '1'
}
};
StripeService.getCheckoutSession.mockResolvedValue(mockSession);
const response = await request(app)
.get('/stripe/checkout-session/cs_123456789');
expect(response.status).toBe(200);
expect(response.body).toEqual({
status: 'complete',
payment_status: 'paid',
customer_email: 'test@example.com',
setup_intent: {
id: 'seti_123456789',
status: 'succeeded'
},
metadata: {
userId: '1'
}
});
expect(StripeService.getCheckoutSession).toHaveBeenCalledWith('cs_123456789');
});
it('should handle missing customer_details gracefully', async () => {
const mockSession = {
status: 'complete',
payment_status: 'paid',
customer_details: null,
setup_intent: null,
metadata: {}
};
StripeService.getCheckoutSession.mockResolvedValue(mockSession);
const response = await request(app)
.get('/stripe/checkout-session/cs_123456789');
expect(response.status).toBe(200);
expect(response.body).toEqual({
status: 'complete',
payment_status: 'paid',
customer_email: undefined,
setup_intent: null,
metadata: {}
});
});
it('should handle checkout session retrieval errors', async () => {
const error = new Error('Session not found');
StripeService.getCheckoutSession.mockRejectedValue(error);
const response = await request(app)
.get('/stripe/checkout-session/invalid_session');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Session not found' });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error retrieving checkout session:',
error
);
});
it('should handle missing session ID', async () => {
const error = new Error('Invalid session ID');
StripeService.getCheckoutSession.mockRejectedValue(error);
const response = await request(app)
.get('/stripe/checkout-session/');
expect(response.status).toBe(404);
});
});
describe('POST /accounts', () => {
const mockUser = {
id: 1,
email: 'test@example.com',
stripeConnectedAccountId: null,
update: jest.fn()
};
beforeEach(() => {
mockUser.update.mockReset();
mockUser.stripeConnectedAccountId = null;
});
it('should create connected account successfully', async () => {
const mockAccount = {
id: 'acct_123456789',
email: 'test@example.com',
country: 'US'
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.createConnectedAccount.mockResolvedValue(mockAccount);
mockUser.update.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(200);
expect(response.body).toEqual({
stripeConnectedAccountId: 'acct_123456789',
success: true
});
expect(User.findByPk).toHaveBeenCalledWith(1);
expect(StripeService.createConnectedAccount).toHaveBeenCalledWith({
email: 'test@example.com',
country: 'US'
});
expect(mockUser.update).toHaveBeenCalledWith({
stripeConnectedAccountId: 'acct_123456789'
});
});
it('should return error if user not found', async () => {
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'User not found' });
expect(StripeService.createConnectedAccount).not.toHaveBeenCalled();
});
it('should return error if user already has connected account', async () => {
const userWithAccount = {
...mockUser,
stripeConnectedAccountId: 'acct_existing'
};
User.findByPk.mockResolvedValue(userWithAccount);
const response = await request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'User already has a connected account' });
expect(StripeService.createConnectedAccount).not.toHaveBeenCalled();
});
it('should require authentication', async () => {
const response = await request(app)
.post('/stripe/accounts');
expect(response.status).toBe(401);
expect(response.body).toEqual({ error: 'No token provided' });
});
it('should handle Stripe account creation errors', async () => {
const error = new Error('Invalid email address');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createConnectedAccount.mockRejectedValue(error);
const response = await request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Invalid email address' });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error creating connected account:',
error
);
});
it('should handle database update errors', async () => {
const mockAccount = { id: 'acct_123456789' };
const dbError = new Error('Database update failed');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createConnectedAccount.mockResolvedValue(mockAccount);
mockUser.update.mockRejectedValue(dbError);
const response = await request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database update failed' });
});
});
describe('POST /account-links', () => {
const mockUser = {
id: 1,
stripeConnectedAccountId: 'acct_123456789'
};
it('should create account link successfully', async () => {
const mockAccountLink = {
url: 'https://connect.stripe.com/setup/e/acct_123456789',
expires_at: Date.now() + 3600
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.createAccountLink.mockResolvedValue(mockAccountLink);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
refreshUrl: 'http://localhost:3000/refresh',
returnUrl: 'http://localhost:3000/return'
});
expect(response.status).toBe(200);
expect(response.body).toEqual({
url: mockAccountLink.url,
expiresAt: mockAccountLink.expires_at
});
expect(StripeService.createAccountLink).toHaveBeenCalledWith(
'acct_123456789',
'http://localhost:3000/refresh',
'http://localhost:3000/return'
);
});
it('should return error if no connected account found', async () => {
const userWithoutAccount = {
id: 1,
stripeConnectedAccountId: null
};
User.findByPk.mockResolvedValue(userWithoutAccount);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
refreshUrl: 'http://localhost:3000/refresh',
returnUrl: 'http://localhost:3000/return'
});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'No connected account found' });
expect(StripeService.createAccountLink).not.toHaveBeenCalled();
});
it('should return error if user not found', async () => {
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
refreshUrl: 'http://localhost:3000/refresh',
returnUrl: 'http://localhost:3000/return'
});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'No connected account found' });
});
it('should validate required URLs', async () => {
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
refreshUrl: 'http://localhost:3000/refresh'
// Missing returnUrl
});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'refreshUrl and returnUrl are required' });
expect(StripeService.createAccountLink).not.toHaveBeenCalled();
});
it('should validate both URLs are provided', async () => {
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
returnUrl: 'http://localhost:3000/return'
// Missing refreshUrl
});
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'refreshUrl and returnUrl are required' });
});
it('should require authentication', async () => {
const response = await request(app)
.post('/stripe/account-links')
.send({
refreshUrl: 'http://localhost:3000/refresh',
returnUrl: 'http://localhost:3000/return'
});
expect(response.status).toBe(401);
});
it('should handle Stripe account link creation errors', async () => {
const error = new Error('Account not found');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createAccountLink.mockRejectedValue(error);
const response = await request(app)
.post('/stripe/account-links')
.set('Authorization', 'Bearer valid_token')
.send({
refreshUrl: 'http://localhost:3000/refresh',
returnUrl: 'http://localhost:3000/return'
});
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Account not found' });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error creating account link:',
error
);
});
});
describe('GET /account-status', () => {
const mockUser = {
id: 1,
stripeConnectedAccountId: 'acct_123456789'
};
it('should get account status successfully', async () => {
const mockAccountStatus = {
id: 'acct_123456789',
details_submitted: true,
payouts_enabled: true,
capabilities: {
transfers: { status: 'active' }
},
requirements: {
pending_verification: [],
currently_due: [],
past_due: []
}
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.getAccountStatus.mockResolvedValue(mockAccountStatus);
const response = await request(app)
.get('/stripe/account-status')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(200);
expect(response.body).toEqual({
accountId: 'acct_123456789',
detailsSubmitted: true,
payoutsEnabled: true,
capabilities: {
transfers: { status: 'active' }
},
requirements: {
pending_verification: [],
currently_due: [],
past_due: []
}
});
expect(StripeService.getAccountStatus).toHaveBeenCalledWith('acct_123456789');
});
it('should return error if no connected account found', async () => {
const userWithoutAccount = {
id: 1,
stripeConnectedAccountId: null
};
User.findByPk.mockResolvedValue(userWithoutAccount);
const response = await request(app)
.get('/stripe/account-status')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'No connected account found' });
expect(StripeService.getAccountStatus).not.toHaveBeenCalled();
});
it('should return error if user not found', async () => {
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.get('/stripe/account-status')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(400);
expect(response.body).toEqual({ error: 'No connected account found' });
});
it('should require authentication', async () => {
const response = await request(app)
.get('/stripe/account-status');
expect(response.status).toBe(401);
});
it('should handle Stripe account status retrieval errors', async () => {
const error = new Error('Account not found');
User.findByPk.mockResolvedValue(mockUser);
StripeService.getAccountStatus.mockRejectedValue(error);
const response = await request(app)
.get('/stripe/account-status')
.set('Authorization', 'Bearer valid_token');
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Account not found' });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error getting account status:',
error
);
});
});
describe('POST /create-setup-checkout-session', () => {
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
stripeCustomerId: null,
update: jest.fn()
};
beforeEach(() => {
mockUser.update.mockReset();
mockUser.stripeCustomerId = null;
});
it('should create setup checkout session for new customer', async () => {
const mockCustomer = {
id: 'cus_123456789',
email: 'test@example.com'
};
const mockSession = {
id: 'cs_123456789',
client_secret: 'cs_123456789_secret_test'
};
const rentalData = {
itemId: '123',
startDate: '2023-12-01',
endDate: '2023-12-03'
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockResolvedValue(mockCustomer);
StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
mockUser.update.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({ rentalData });
expect(response.status).toBe(200);
expect(response.body).toEqual({
clientSecret: 'cs_123456789_secret_test',
sessionId: 'cs_123456789'
});
expect(User.findByPk).toHaveBeenCalledWith(1);
expect(StripeService.createCustomer).toHaveBeenCalledWith({
email: 'test@example.com',
name: 'John Doe',
metadata: {
userId: '1'
}
});
expect(mockUser.update).toHaveBeenCalledWith({ stripeCustomerId: 'cus_123456789' });
expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
customerId: 'cus_123456789',
metadata: {
rentalData: JSON.stringify(rentalData)
}
});
});
it('should use existing customer ID if available', async () => {
const userWithCustomer = {
...mockUser,
stripeCustomerId: 'cus_existing123'
};
const mockSession = {
id: 'cs_123456789',
client_secret: 'cs_123456789_secret_test'
};
User.findByPk.mockResolvedValue(userWithCustomer);
StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(200);
expect(response.body).toEqual({
clientSecret: 'cs_123456789_secret_test',
sessionId: 'cs_123456789'
});
expect(StripeService.createCustomer).not.toHaveBeenCalled();
expect(userWithCustomer.update).not.toHaveBeenCalled();
expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
customerId: 'cus_existing123',
metadata: {}
});
});
it('should handle session without rental data', async () => {
const mockCustomer = {
id: 'cus_123456789'
};
const mockSession = {
id: 'cs_123456789',
client_secret: 'cs_123456789_secret_test'
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockResolvedValue(mockCustomer);
StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
mockUser.update.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(200);
expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
customerId: 'cus_123456789',
metadata: {}
});
});
it('should return error if user not found', async () => {
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(404);
expect(response.body).toEqual({ error: 'User not found' });
expect(StripeService.createCustomer).not.toHaveBeenCalled();
expect(StripeService.createSetupCheckoutSession).not.toHaveBeenCalled();
});
it('should require authentication', async () => {
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.send({});
expect(response.status).toBe(401);
});
it('should handle customer creation errors', async () => {
const error = new Error('Invalid email address');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockRejectedValue(error);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Invalid email address' });
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error creating setup checkout session:',
error
);
});
it('should handle database update errors', async () => {
const mockCustomer = { id: 'cus_123456789' };
const dbError = new Error('Database update failed');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockResolvedValue(mockCustomer);
mockUser.update.mockRejectedValue(dbError);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Database update failed' });
});
it('should handle session creation errors', async () => {
const mockCustomer = { id: 'cus_123456789' };
const sessionError = new Error('Session creation failed');
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockResolvedValue(mockCustomer);
mockUser.update.mockResolvedValue(mockUser);
StripeService.createSetupCheckoutSession.mockRejectedValue(sessionError);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({});
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Session creation failed' });
});
it('should handle complex rental data', async () => {
const mockCustomer = { id: 'cus_123456789' };
const mockSession = {
id: 'cs_123456789',
client_secret: 'cs_123456789_secret_test'
};
const complexRentalData = {
itemId: '123',
startDate: '2023-12-01',
endDate: '2023-12-03',
totalAmount: 150.00,
additionalServices: ['cleaning', 'delivery'],
notes: 'Special instructions'
};
User.findByPk.mockResolvedValue(mockUser);
StripeService.createCustomer.mockResolvedValue(mockCustomer);
StripeService.createSetupCheckoutSession.mockResolvedValue(mockSession);
mockUser.update.mockResolvedValue(mockUser);
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.send({ rentalData: complexRentalData });
expect(response.status).toBe(200);
expect(StripeService.createSetupCheckoutSession).toHaveBeenCalledWith({
customerId: 'cus_123456789',
metadata: {
rentalData: JSON.stringify(complexRentalData)
}
});
});
});
describe('Error handling and edge cases', () => {
it('should handle malformed JSON in rental data', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe',
stripeCustomerId: 'cus_123456789'
};
User.findByPk.mockResolvedValue(mockUser);
// This should work fine as Express will parse valid JSON
const response = await request(app)
.post('/stripe/create-setup-checkout-session')
.set('Authorization', 'Bearer valid_token')
.set('Content-Type', 'application/json')
.send('{"rentalData":{"itemId":"123"}}');
expect(response.status).toBe(200);
});
it('should handle very large session IDs', async () => {
const longSessionId = 'cs_' + 'a'.repeat(100);
const error = new Error('Session ID too long');
StripeService.getCheckoutSession.mockRejectedValue(error);
const response = await request(app)
.get(`/stripe/checkout-session/${longSessionId}`);
expect(response.status).toBe(500);
expect(response.body).toEqual({ error: 'Session ID too long' });
});
it('should handle concurrent requests for same user', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
stripeConnectedAccountId: null,
update: jest.fn().mockResolvedValue({})
};
const mockAccount = { id: 'acct_123456789' };
User.findByPk.mockResolvedValue(mockUser);
StripeService.createConnectedAccount.mockResolvedValue(mockAccount);
// Simulate concurrent requests
const [response1, response2] = await Promise.all([
request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token'),
request(app)
.post('/stripe/accounts')
.set('Authorization', 'Bearer valid_token')
]);
// Both should succeed (in this test scenario)
expect(response1.status).toBe(200);
expect(response2.status).toBe(200);
});
});
});