diff --git a/backend/tests/integration/auth.integration.test.js b/backend/tests/integration/auth.integration.test.js index 726c858..4a774e8 100644 --- a/backend/tests/integration/auth.integration.test.js +++ b/backend/tests/integration/auth.integration.test.js @@ -226,7 +226,7 @@ describe('Auth Integration Tests', () => { }) .expect(401); - expect(response.body.error).toBe('Unable to log in. Please check your email and password, or create an account.'); + expect(response.body.error).toBe('Please check your email and password, or create an account.'); }); it('should reject login with non-existent email', async () => { @@ -238,7 +238,7 @@ describe('Auth Integration Tests', () => { }) .expect(401); - expect(response.body.error).toBe('Unable to log in. Please check your email and password, or create an account.'); + expect(response.body.error).toBe('Please check your email and password, or create an account.'); }); it('should increment login attempts on failed login', async () => { @@ -255,8 +255,8 @@ describe('Auth Integration Tests', () => { }); it('should lock account after too many failed attempts', async () => { - // Make 5 failed login attempts - for (let i = 0; i < 5; i++) { + // Make 10 failed login attempts (MAX_LOGIN_ATTEMPTS is 10) + for (let i = 0; i < 10; i++) { await request(app) .post('/auth/login') .send({ @@ -265,7 +265,7 @@ describe('Auth Integration Tests', () => { }); } - // 6th attempt should return locked error + // 11th attempt should return locked error const response = await request(app) .post('/auth/login') .send({ diff --git a/backend/tests/integration/rental.integration.test.js b/backend/tests/integration/rental.integration.test.js index efa1706..1fb7a20 100644 --- a/backend/tests/integration/rental.integration.test.js +++ b/backend/tests/integration/rental.integration.test.js @@ -411,20 +411,20 @@ describe('Rental Integration Tests', () => { expect(response.body.status).toBe('confirmed'); - // Step 2: Rental becomes active (typically done by system/webhook) - await rental.update({ status: 'active' }); - - // Verify status + // Step 2: Rental is now "active" because status is confirmed and startDateTime has passed + // Note: "active" is a computed status, not stored. The stored status remains "confirmed" await rental.reload(); - expect(rental.status).toBe('active'); + expect(rental.status).toBe('confirmed'); // Stored status is still 'confirmed' + // isActive() returns true because status='confirmed' and startDateTime is in the past - // Step 3: Owner marks rental as completed + // Step 3: Owner marks rental as completed (via mark-return with status='returned') response = await request(app) - .post(`/rentals/${rental.id}/mark-completed`) + .post(`/rentals/${rental.id}/mark-return`) .set('Cookie', [`accessToken=${ownerToken}`]) + .send({ status: 'returned' }) .expect(200); - expect(response.body.status).toBe('completed'); + expect(response.body.rental.status).toBe('completed'); // Verify final state await rental.reload(); diff --git a/backend/tests/unit/middleware/csrf.test.js b/backend/tests/unit/middleware/csrf.test.js index 6218a44..87151f3 100644 --- a/backend/tests/unit/middleware/csrf.test.js +++ b/backend/tests/unit/middleware/csrf.test.js @@ -1,5 +1,8 @@ +// Set CSRF_SECRET before requiring the middleware +process.env.CSRF_SECRET = 'test-csrf-secret-that-is-at-least-32-chars-long'; + const mockTokensInstance = { - secretSync: jest.fn().mockReturnValue('mock-secret'), + secretSync: jest.fn().mockReturnValue(process.env.CSRF_SECRET), create: jest.fn().mockReturnValue('mock-token-123'), verify: jest.fn().mockReturnValue(true) }; @@ -12,6 +15,17 @@ jest.mock('cookie-parser', () => { return jest.fn().mockReturnValue((req, res, next) => next()); }); +jest.mock('../../../utils/logger', () => ({ + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + withRequestId: jest.fn(() => ({ + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })), +})); + const { csrfProtection, generateCSRFToken, getCSRFToken } = require('../../../middleware/csrf'); describe('CSRF Middleware', () => { @@ -77,7 +91,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); @@ -87,7 +101,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); @@ -97,7 +111,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); @@ -108,7 +122,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); @@ -118,7 +132,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); @@ -128,7 +142,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); }); @@ -244,7 +258,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(res.status).toHaveBeenCalledWith(403); expect(res.json).toHaveBeenCalledWith({ error: 'Invalid CSRF token', @@ -258,7 +272,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled(); }); @@ -272,7 +286,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); @@ -283,7 +297,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); @@ -294,7 +308,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); @@ -305,7 +319,7 @@ describe('CSRF Middleware', () => { csrfProtection(req, res, next); - expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123'); + expect(mockTokensInstance.verify).toHaveBeenCalledWith(process.env.CSRF_SECRET, 'mock-token-123'); expect(next).toHaveBeenCalled(); }); }); @@ -317,7 +331,7 @@ describe('CSRF Middleware', () => { generateCSRFToken(req, res, next); - expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret'); + expect(mockTokensInstance.create).toHaveBeenCalledWith(process.env.CSRF_SECRET); expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', { httpOnly: true, secure: true, @@ -404,7 +418,7 @@ describe('CSRF Middleware', () => { getCSRFToken(req, res); - expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret'); + expect(mockTokensInstance.create).toHaveBeenCalledWith(process.env.CSRF_SECRET); expect(res.status).toHaveBeenCalledWith(204); expect(res.send).toHaveBeenCalled(); }); diff --git a/backend/tests/unit/middleware/validation.test.js b/backend/tests/unit/middleware/validation.test.js index db8ffc1..d14174c 100644 --- a/backend/tests/unit/middleware/validation.test.js +++ b/backend/tests/unit/middleware/validation.test.js @@ -13,7 +13,15 @@ jest.mock('express-validator', () => ({ trim: jest.fn().mockReturnThis(), optional: jest.fn().mockReturnThis(), isMobilePhone: jest.fn().mockReturnThis(), - notEmpty: jest.fn().mockReturnThis() + notEmpty: jest.fn().mockReturnThis(), + isFloat: jest.fn().mockReturnThis(), + toFloat: jest.fn().mockReturnThis() + })), + query: jest.fn(() => ({ + optional: jest.fn().mockReturnThis(), + isFloat: jest.fn().mockReturnThis(), + withMessage: jest.fn().mockReturnThis(), + toFloat: jest.fn().mockReturnThis() })), validationResult: jest.fn() })); diff --git a/backend/tests/unit/routes/items.test.js b/backend/tests/unit/routes/items.test.js index e4b2dc1..6c4820a 100644 --- a/backend/tests/unit/routes/items.test.js +++ b/backend/tests/unit/routes/items.test.js @@ -8,7 +8,8 @@ jest.mock('sequelize', () => ({ lte: 'lte', iLike: 'iLike', or: 'or', - not: 'not' + not: 'not', + ne: 'ne' } })); @@ -199,7 +200,9 @@ describe('Items Routes', () => { { model: mockUserModel, as: 'owner', - attributes: ['id', 'firstName', 'lastName', 'imageFilename'] + attributes: ['id', 'firstName', 'lastName', 'imageFilename'], + where: { isBanned: { 'ne': true } }, + required: true } ], limit: 20, diff --git a/backend/tests/unit/routes/rentals.test.js b/backend/tests/unit/routes/rentals.test.js index 2686a4a..4141588 100644 --- a/backend/tests/unit/routes/rentals.test.js +++ b/backend/tests/unit/routes/rentals.test.js @@ -85,6 +85,10 @@ jest.mock('../../../services/stripeService', () => ({ chargePaymentMethod: jest.fn(), })); +jest.mock('../../../services/stripeWebhookService', () => ({ + reconcilePayoutStatuses: jest.fn().mockResolvedValue(), +})); + const { Rental, Item, User } = require('../../../models'); const FeeCalculator = require('../../../utils/feeCalculator'); const RentalDurationCalculator = require('../../../utils/rentalDurationCalculator'); @@ -588,10 +592,13 @@ describe('Rentals Routes', () => { .put('/rentals/1/status') .send({ status: 'confirmed' }); - expect(response.status).toBe(400); + expect(response.status).toBe(402); expect(response.body).toEqual({ - error: 'Payment failed during approval', - details: 'Payment failed', + error: 'payment_failed', + code: 'unknown_error', + ownerMessage: 'The payment could not be processed.', + renterMessage: 'Your payment could not be processed. Please try a different payment method.', + rentalId: 1, }); }); @@ -798,63 +805,6 @@ describe('Rentals Routes', () => { }); }); - describe('POST /:id/mark-completed', () => { - // Active status is computed: confirmed + startDateTime in the past - const pastDate = new Date(); - pastDate.setHours(pastDate.getHours() - 1); // 1 hour ago - const mockRental = { - id: 1, - ownerId: 1, - renterId: 2, - status: 'confirmed', - startDateTime: pastDate, - 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', payoutStatus: 'pending' }); - }); - - 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 rentals as completed', - }); - }); - }); - describe('POST /calculate-fees', () => { it('should calculate fees for given amount', async () => { const response = await request(app) @@ -936,6 +886,9 @@ describe('Rentals Routes', () => { 'payoutStatus', 'payoutProcessedAt', 'stripeTransferId', + 'bankDepositStatus', + 'bankDepositAt', + 'bankDepositFailureCode', ], include: [{ model: Item, as: 'item', attributes: ['name'] }], order: [['createdAt', 'DESC']], diff --git a/backend/tests/unit/routes/stripe.test.js b/backend/tests/unit/routes/stripe.test.js index e089f93..630f5eb 100644 --- a/backend/tests/unit/routes/stripe.test.js +++ b/backend/tests/unit/routes/stripe.test.js @@ -421,6 +421,11 @@ describe("Stripe Routes", () => { const mockUser = { id: 1, stripeConnectedAccountId: "acct_123456789", + stripePayoutsEnabled: true, + stripeRequirementsCurrentlyDue: [], + stripeRequirementsPastDue: [], + stripeDisabledReason: null, + update: jest.fn().mockResolvedValue(true), }; it("should get account status successfully", async () => { diff --git a/backend/tests/unit/routes/users.test.js b/backend/tests/unit/routes/users.test.js index 5ab512e..fa21c61 100644 --- a/backend/tests/unit/routes/users.test.js +++ b/backend/tests/unit/routes/users.test.js @@ -23,6 +23,8 @@ jest.mock("../../../middleware/auth", () => ({ }; next(); }), + optionalAuth: jest.fn((req, res, next) => next()), + requireAdmin: jest.fn((req, res, next) => next()), })); jest.mock("../../../services/UserService", () => ({ @@ -365,7 +367,7 @@ describe("Users Routes", () => { expect(response.status).toBe(200); expect(response.body).toEqual(mockUser); expect(mockUserFindByPk).toHaveBeenCalledWith("2", { - attributes: { exclude: ["password", "email", "phone", "address"] }, + attributes: { exclude: ["password", "email", "phone", "address", "verificationToken", "passwordResetToken", "isBanned", "bannedAt", "bannedBy", "banReason"] }, }); }); diff --git a/backend/tests/unit/services/googleMapsService.test.js b/backend/tests/unit/services/googleMapsService.test.js index b1b150a..6c2e07f 100644 --- a/backend/tests/unit/services/googleMapsService.test.js +++ b/backend/tests/unit/services/googleMapsService.test.js @@ -11,18 +11,26 @@ jest.mock('@googlemaps/google-maps-services-js', () => ({ })) })); +const mockLoggerInfo = jest.fn(); +const mockLoggerError = jest.fn(); +jest.mock('../../../utils/logger', () => ({ + info: mockLoggerInfo, + error: mockLoggerError, + warn: jest.fn(), + withRequestId: jest.fn(() => ({ + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + })), +})); + describe('GoogleMapsService', () => { let service; - let consoleSpy, consoleErrorSpy; beforeEach(() => { // Clear all mocks jest.clearAllMocks(); - // Set up console spies - consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - // Reset environment delete process.env.GOOGLE_MAPS_API_KEY; @@ -30,18 +38,13 @@ describe('GoogleMapsService', () => { jest.resetModules(); }); - afterEach(() => { - consoleSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - describe('Constructor', () => { it('should initialize with API key and log success', () => { process.env.GOOGLE_MAPS_API_KEY = 'test-api-key'; service = require('../../../services/googleMapsService'); - expect(consoleSpy).toHaveBeenCalledWith('✅ Google Maps service initialized'); + expect(mockLoggerInfo).toHaveBeenCalledWith('Google Maps service initialized'); expect(service.isConfigured()).toBe(true); }); @@ -50,7 +53,7 @@ describe('GoogleMapsService', () => { service = require('../../../services/googleMapsService'); - expect(consoleErrorSpy).toHaveBeenCalledWith('❌ Google Maps API key not configured in environment variables'); + expect(mockLoggerError).toHaveBeenCalledWith('Google Maps API key not configured in environment variables'); expect(service.isConfigured()).toBe(false); }); @@ -299,10 +302,11 @@ describe('GoogleMapsService', () => { status: 'ZERO_RESULTS' }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Places Autocomplete API error:', - 'ZERO_RESULTS', - 'No results found' + expect(mockLoggerError).toHaveBeenCalledWith( + 'Places Autocomplete API error', + expect.objectContaining({ + status: 'ZERO_RESULTS', + }) ); }); @@ -325,7 +329,7 @@ describe('GoogleMapsService', () => { await expect(service.getPlacesAutocomplete('test input')).rejects.toThrow('Failed to fetch place predictions'); - expect(consoleErrorSpy).toHaveBeenCalledWith('Places Autocomplete service error:', 'Network error'); + expect(mockLoggerError).toHaveBeenCalledWith('Places Autocomplete service error', expect.objectContaining({ error: expect.any(Error) })); }); }); }); @@ -557,10 +561,11 @@ describe('GoogleMapsService', () => { await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('The specified place was not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Place Details API error:', - 'NOT_FOUND', - 'Place not found' + expect(mockLoggerError).toHaveBeenCalledWith( + 'Place Details API error', + expect.objectContaining({ + status: 'NOT_FOUND', + }) ); }); @@ -582,7 +587,7 @@ describe('GoogleMapsService', () => { await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow(originalError); - expect(consoleErrorSpy).toHaveBeenCalledWith('Place Details service error:', 'Network error'); + expect(mockLoggerError).toHaveBeenCalledWith('Place Details service error', expect.objectContaining({ error: expect.any(Error) })); }); }); }); @@ -769,10 +774,11 @@ describe('GoogleMapsService', () => { status: 'ZERO_RESULTS' }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Geocoding API error:', - 'ZERO_RESULTS', - 'No results found' + expect(mockLoggerError).toHaveBeenCalledWith( + 'Geocoding API error', + expect.objectContaining({ + status: 'ZERO_RESULTS', + }) ); }); @@ -796,7 +802,7 @@ describe('GoogleMapsService', () => { await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Failed to geocode address'); - expect(consoleErrorSpy).toHaveBeenCalledWith('Geocoding service error:', 'Network error'); + expect(mockLoggerError).toHaveBeenCalledWith('Geocoding service error', expect.objectContaining({ error: expect.any(Error) })); }); }); }); diff --git a/backend/tests/unit/services/payoutService.test.js b/backend/tests/unit/services/payoutService.test.js index 550f0f6..27051b1 100644 --- a/backend/tests/unit/services/payoutService.test.js +++ b/backend/tests/unit/services/payoutService.test.js @@ -18,6 +18,19 @@ jest.mock('sequelize', () => ({ } })); +const mockLoggerError = jest.fn(); +const mockLoggerInfo = jest.fn(); +jest.mock('../../../utils/logger', () => ({ + error: mockLoggerError, + info: mockLoggerInfo, + warn: jest.fn(), + withRequestId: jest.fn(() => ({ + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })), +})); + const PayoutService = require('../../../services/payoutService'); const { Rental, User, Item } = require('../../../models'); const StripeService = require('../../../services/stripeService'); @@ -30,19 +43,15 @@ const mockItemModel = Item; const mockCreateTransfer = StripeService.createTransfer; describe('PayoutService', () => { - let consoleSpy, consoleErrorSpy; + let consoleSpy; 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('getEligiblePayouts', () => { @@ -87,7 +96,8 @@ describe('PayoutService', () => { where: { stripeConnectedAccountId: { 'not': null - } + }, + stripePayoutsEnabled: true } }, { @@ -106,7 +116,7 @@ describe('PayoutService', () => { await expect(PayoutService.getEligiblePayouts()).rejects.toThrow('Database connection failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith('Error getting eligible payouts:', dbError); + expect(mockLoggerError).toHaveBeenCalledWith('Error getting eligible payouts', expect.objectContaining({ error: dbError.message })); }); it('should return empty array when no eligible rentals found', async () => { @@ -269,9 +279,9 @@ describe('PayoutService', () => { payoutStatus: 'failed' }); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing payout for rental 1:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing payout for rental', + expect.objectContaining({ rentalId: 1 }) ); }); @@ -287,9 +297,9 @@ describe('PayoutService', () => { await expect(PayoutService.processRentalPayout(mockRental)) .rejects.toThrow('Database update failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing payout for rental 1:', - dbError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing payout for rental', + expect.objectContaining({ rentalId: 1 }) ); }); @@ -306,9 +316,9 @@ describe('PayoutService', () => { .rejects.toThrow('Database completion update failed'); expect(mockCreateTransfer).toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing payout for rental 1:', - dbError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing payout for rental', + expect.objectContaining({ rentalId: 1 }) ); }); @@ -438,9 +448,9 @@ describe('PayoutService', () => { await expect(PayoutService.processAllEligiblePayouts()) .rejects.toThrow('Database connection failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing all eligible payouts:', - dbError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing all eligible payouts', + expect.objectContaining({ error: dbError.message }) ); }); @@ -520,7 +530,8 @@ describe('PayoutService', () => { where: { stripeConnectedAccountId: { 'not': null - } + }, + stripePayoutsEnabled: true } }, { @@ -613,9 +624,9 @@ describe('PayoutService', () => { await expect(PayoutService.retryFailedPayouts()) .rejects.toThrow('Database query failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error retrying failed payouts:', - dbError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error retrying failed payouts', + expect.objectContaining({ error: dbError.message }) ); }); @@ -655,9 +666,9 @@ describe('PayoutService', () => { await expect(PayoutService.processRentalPayout(mockRental)) .rejects.toThrow('Update failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing payout for rental 123:', - expect.any(Error) + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing payout for rental', + expect.objectContaining({ rentalId: 123 }) ); }); diff --git a/backend/tests/unit/services/refundService.test.js b/backend/tests/unit/services/refundService.test.js index 8bd7e4a..43140fa 100644 --- a/backend/tests/unit/services/refundService.test.js +++ b/backend/tests/unit/services/refundService.test.js @@ -13,6 +13,19 @@ jest.mock('../../../services/stripeService', () => ({ createRefund: mockCreateRefund })); +const mockLoggerError = jest.fn(); +const mockLoggerWarn = jest.fn(); +jest.mock('../../../utils/logger', () => ({ + error: mockLoggerError, + warn: mockLoggerWarn, + info: jest.fn(), + withRequestId: jest.fn(() => ({ + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })), +})); + const RefundService = require('../../../services/refundService'); describe('RefundService', () => { @@ -540,8 +553,9 @@ describe('RefundService', () => { const result = await RefundService.processCancellation(1, 200); expect(mockCreateRefund).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Refund amount calculated but no payment intent ID for rental 1' + expect(mockLoggerWarn).toHaveBeenCalledWith( + 'Refund amount calculated but no payment intent ID for rental', + { rentalId: 1 } ); expect(result.refund).toEqual({ @@ -605,9 +619,9 @@ describe('RefundService', () => { await expect(RefundService.processCancellation(1, 200)) .rejects.toThrow('Failed to process refund: Refund failed'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error processing Stripe refund:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error processing Stripe refund', + expect.objectContaining({ error: stripeError }) ); }); diff --git a/backend/tests/unit/services/stripeService.test.js b/backend/tests/unit/services/stripeService.test.js index decd74b..91f48d6 100644 --- a/backend/tests/unit/services/stripeService.test.js +++ b/backend/tests/unit/services/stripeService.test.js @@ -41,27 +41,28 @@ jest.mock('stripe', () => { })); }); +const mockLoggerError = jest.fn(); +jest.mock('../../../utils/logger', () => ({ + error: mockLoggerError, + info: jest.fn(), + warn: jest.fn(), + withRequestId: jest.fn(() => ({ + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })), +})); + const StripeService = require('../../../services/stripeService'); describe('StripeService', () => { - let consoleSpy, consoleErrorSpy; - beforeEach(() => { jest.clearAllMocks(); - // Set up console spies - consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - // Set environment variables for tests process.env.FRONTEND_URL = 'http://localhost:3000'; }); - afterEach(() => { - consoleSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - describe('getCheckoutSession', () => { it('should retrieve checkout session successfully', async () => { const mockSession = { @@ -93,9 +94,11 @@ describe('StripeService', () => { await expect(StripeService.getCheckoutSession('invalid_session')) .rejects.toThrow('Session not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error retrieving checkout session:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error retrieving checkout session', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -174,9 +177,11 @@ describe('StripeService', () => { email: 'invalid-email' })).rejects.toThrow('Invalid email address'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating connected account:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating connected account', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -225,9 +230,11 @@ describe('StripeService', () => { 'http://localhost:3000/return' )).rejects.toThrow('Account not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating account link:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating account link', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -287,9 +294,11 @@ describe('StripeService', () => { await expect(StripeService.getAccountStatus('invalid_account')) .rejects.toThrow('Account not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error retrieving account status:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error retrieving account status', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -420,9 +429,11 @@ describe('StripeService', () => { destination: 'acct_123456789' })).rejects.toThrow('Insufficient funds'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating transfer:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating transfer', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -558,9 +569,11 @@ describe('StripeService', () => { amount: 50.00 })).rejects.toThrow('Payment intent not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating refund:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating refund', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -617,9 +630,11 @@ describe('StripeService', () => { await expect(StripeService.getRefund('re_invalid')) .rejects.toThrow('Refund not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error retrieving refund:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error retrieving refund', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -698,11 +713,13 @@ describe('StripeService', () => { 'pm_invalid', 50.00, 'cus_123456789' - )).rejects.toThrow('Payment method declined'); + )).rejects.toThrow('The payment could not be processed.'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error charging payment method:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Payment failed', + expect.objectContaining({ + code: expect.any(String), + }) ); }); @@ -839,9 +856,11 @@ describe('StripeService', () => { email: 'invalid-email' })).rejects.toThrow('Invalid email format'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating customer:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating customer', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -886,6 +905,11 @@ describe('StripeService', () => { mode: 'setup', ui_mode: 'embedded', redirect_on_completion: 'never', + payment_method_options: { + card: { + request_three_d_secure: 'any', + }, + }, metadata: { type: 'payment_method_setup', userId: '123' @@ -919,6 +943,11 @@ describe('StripeService', () => { mode: 'setup', ui_mode: 'embedded', redirect_on_completion: 'never', + payment_method_options: { + card: { + request_three_d_secure: 'any', + }, + }, metadata: { type: 'payment_method_setup' } @@ -934,9 +963,11 @@ describe('StripeService', () => { customerId: 'cus_invalid' })).rejects.toThrow('Customer not found'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating setup checkout session:', - stripeError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating setup checkout session', + expect.objectContaining({ + error: stripeError.message, + }) ); }); @@ -1015,9 +1046,11 @@ describe('StripeService', () => { destination: 'acct_123456789' })).rejects.toThrow('Request timeout'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating transfer:', - timeoutError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating transfer', + expect.objectContaining({ + error: timeoutError.message, + }) ); }); @@ -1030,9 +1063,11 @@ describe('StripeService', () => { email: 'test@example.com' })).rejects.toThrow('Invalid API key'); - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Error creating customer:', - apiKeyError + expect(mockLoggerError).toHaveBeenCalledWith( + 'Error creating customer', + expect.objectContaining({ + error: apiKeyError.message, + }) ); }); }); diff --git a/backend/tests/unit/services/stripeWebhookService.test.js b/backend/tests/unit/services/stripeWebhookService.test.js index 041edfc..65fbe35 100644 --- a/backend/tests/unit/services/stripeWebhookService.test.js +++ b/backend/tests/unit/services/stripeWebhookService.test.js @@ -442,6 +442,10 @@ describe("StripeWebhookService", () => { expect(mockUser.update).toHaveBeenCalledWith({ stripeConnectedAccountId: null, stripePayoutsEnabled: false, + stripeDisabledReason: null, + stripeRequirementsCurrentlyDue: [], + stripeRequirementsPastDue: [], + stripeRequirementsLastUpdated: null, }); }); @@ -525,12 +529,12 @@ describe("StripeWebhookService", () => { ); }); - it("should use name fallback when firstName is not available", async () => { + it("should use lastName fallback when firstName is not available", async () => { const mockUser = { id: 1, email: "owner@test.com", firstName: null, - name: "Full Name", + lastName: "Smith", update: jest.fn().mockResolvedValue(true), }; @@ -545,7 +549,7 @@ describe("StripeWebhookService", () => { expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith( "owner@test.com", expect.objectContaining({ - ownerName: "Full Name", + ownerName: "Smith", }) ); });