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); }); }); });