updated tests

This commit is contained in:
jackiettran
2026-01-15 18:47:43 -05:00
parent 35d5050286
commit 63385e049c
13 changed files with 256 additions and 201 deletions

View File

@@ -226,7 +226,7 @@ describe('Auth Integration Tests', () => {
}) })
.expect(401); .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 () => { it('should reject login with non-existent email', async () => {
@@ -238,7 +238,7 @@ describe('Auth Integration Tests', () => {
}) })
.expect(401); .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 () => { 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 () => { it('should lock account after too many failed attempts', async () => {
// Make 5 failed login attempts // Make 10 failed login attempts (MAX_LOGIN_ATTEMPTS is 10)
for (let i = 0; i < 5; i++) { for (let i = 0; i < 10; i++) {
await request(app) await request(app)
.post('/auth/login') .post('/auth/login')
.send({ .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) const response = await request(app)
.post('/auth/login') .post('/auth/login')
.send({ .send({

View File

@@ -411,20 +411,20 @@ describe('Rental Integration Tests', () => {
expect(response.body.status).toBe('confirmed'); expect(response.body.status).toBe('confirmed');
// Step 2: Rental becomes active (typically done by system/webhook) // Step 2: Rental is now "active" because status is confirmed and startDateTime has passed
await rental.update({ status: 'active' }); // Note: "active" is a computed status, not stored. The stored status remains "confirmed"
// Verify status
await rental.reload(); 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) response = await request(app)
.post(`/rentals/${rental.id}/mark-completed`) .post(`/rentals/${rental.id}/mark-return`)
.set('Cookie', [`accessToken=${ownerToken}`]) .set('Cookie', [`accessToken=${ownerToken}`])
.send({ status: 'returned' })
.expect(200); .expect(200);
expect(response.body.status).toBe('completed'); expect(response.body.rental.status).toBe('completed');
// Verify final state // Verify final state
await rental.reload(); await rental.reload();

View File

@@ -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 = { const mockTokensInstance = {
secretSync: jest.fn().mockReturnValue('mock-secret'), secretSync: jest.fn().mockReturnValue(process.env.CSRF_SECRET),
create: jest.fn().mockReturnValue('mock-token-123'), create: jest.fn().mockReturnValue('mock-token-123'),
verify: jest.fn().mockReturnValue(true) verify: jest.fn().mockReturnValue(true)
}; };
@@ -12,6 +15,17 @@ jest.mock('cookie-parser', () => {
return jest.fn().mockReturnValue((req, res, next) => next()); 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'); const { csrfProtection, generateCSRFToken, getCSRFToken } = require('../../../middleware/csrf');
describe('CSRF Middleware', () => { describe('CSRF Middleware', () => {
@@ -77,7 +91,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled();
}); });
@@ -87,7 +101,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled();
}); });
@@ -97,7 +111,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled();
}); });
@@ -108,7 +122,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
@@ -118,7 +132,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
@@ -128,7 +142,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
}); });
@@ -244,7 +258,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({ expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token', error: 'Invalid CSRF token',
@@ -258,7 +272,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled(); expect(res.status).not.toHaveBeenCalled();
}); });
@@ -272,7 +286,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
@@ -283,7 +297,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
@@ -294,7 +308,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
@@ -305,7 +319,7 @@ describe('CSRF Middleware', () => {
csrfProtection(req, res, next); 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(next).toHaveBeenCalled();
}); });
}); });
@@ -317,7 +331,7 @@ describe('CSRF Middleware', () => {
generateCSRFToken(req, res, next); 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', { expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true, httpOnly: true,
secure: true, secure: true,
@@ -404,7 +418,7 @@ describe('CSRF Middleware', () => {
getCSRFToken(req, res); getCSRFToken(req, res);
expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret'); expect(mockTokensInstance.create).toHaveBeenCalledWith(process.env.CSRF_SECRET);
expect(res.status).toHaveBeenCalledWith(204); expect(res.status).toHaveBeenCalledWith(204);
expect(res.send).toHaveBeenCalled(); expect(res.send).toHaveBeenCalled();
}); });

View File

@@ -13,7 +13,15 @@ jest.mock('express-validator', () => ({
trim: jest.fn().mockReturnThis(), trim: jest.fn().mockReturnThis(),
optional: jest.fn().mockReturnThis(), optional: jest.fn().mockReturnThis(),
isMobilePhone: 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() validationResult: jest.fn()
})); }));

View File

@@ -8,7 +8,8 @@ jest.mock('sequelize', () => ({
lte: 'lte', lte: 'lte',
iLike: 'iLike', iLike: 'iLike',
or: 'or', or: 'or',
not: 'not' not: 'not',
ne: 'ne'
} }
})); }));
@@ -199,7 +200,9 @@ describe('Items Routes', () => {
{ {
model: mockUserModel, model: mockUserModel,
as: 'owner', as: 'owner',
attributes: ['id', 'firstName', 'lastName', 'imageFilename'] attributes: ['id', 'firstName', 'lastName', 'imageFilename'],
where: { isBanned: { 'ne': true } },
required: true
} }
], ],
limit: 20, limit: 20,

View File

@@ -85,6 +85,10 @@ jest.mock('../../../services/stripeService', () => ({
chargePaymentMethod: jest.fn(), chargePaymentMethod: jest.fn(),
})); }));
jest.mock('../../../services/stripeWebhookService', () => ({
reconcilePayoutStatuses: jest.fn().mockResolvedValue(),
}));
const { Rental, Item, User } = require('../../../models'); const { Rental, Item, User } = require('../../../models');
const FeeCalculator = require('../../../utils/feeCalculator'); const FeeCalculator = require('../../../utils/feeCalculator');
const RentalDurationCalculator = require('../../../utils/rentalDurationCalculator'); const RentalDurationCalculator = require('../../../utils/rentalDurationCalculator');
@@ -588,10 +592,13 @@ describe('Rentals Routes', () => {
.put('/rentals/1/status') .put('/rentals/1/status')
.send({ status: 'confirmed' }); .send({ status: 'confirmed' });
expect(response.status).toBe(400); expect(response.status).toBe(402);
expect(response.body).toEqual({ expect(response.body).toEqual({
error: 'Payment failed during approval', error: 'payment_failed',
details: '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', () => { describe('POST /calculate-fees', () => {
it('should calculate fees for given amount', async () => { it('should calculate fees for given amount', async () => {
const response = await request(app) const response = await request(app)
@@ -936,6 +886,9 @@ describe('Rentals Routes', () => {
'payoutStatus', 'payoutStatus',
'payoutProcessedAt', 'payoutProcessedAt',
'stripeTransferId', 'stripeTransferId',
'bankDepositStatus',
'bankDepositAt',
'bankDepositFailureCode',
], ],
include: [{ model: Item, as: 'item', attributes: ['name'] }], include: [{ model: Item, as: 'item', attributes: ['name'] }],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],

View File

@@ -421,6 +421,11 @@ describe("Stripe Routes", () => {
const mockUser = { const mockUser = {
id: 1, id: 1,
stripeConnectedAccountId: "acct_123456789", stripeConnectedAccountId: "acct_123456789",
stripePayoutsEnabled: true,
stripeRequirementsCurrentlyDue: [],
stripeRequirementsPastDue: [],
stripeDisabledReason: null,
update: jest.fn().mockResolvedValue(true),
}; };
it("should get account status successfully", async () => { it("should get account status successfully", async () => {

View File

@@ -23,6 +23,8 @@ jest.mock("../../../middleware/auth", () => ({
}; };
next(); next();
}), }),
optionalAuth: jest.fn((req, res, next) => next()),
requireAdmin: jest.fn((req, res, next) => next()),
})); }));
jest.mock("../../../services/UserService", () => ({ jest.mock("../../../services/UserService", () => ({
@@ -365,7 +367,7 @@ describe("Users Routes", () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual(mockUser); expect(response.body).toEqual(mockUser);
expect(mockUserFindByPk).toHaveBeenCalledWith("2", { expect(mockUserFindByPk).toHaveBeenCalledWith("2", {
attributes: { exclude: ["password", "email", "phone", "address"] }, attributes: { exclude: ["password", "email", "phone", "address", "verificationToken", "passwordResetToken", "isBanned", "bannedAt", "bannedBy", "banReason"] },
}); });
}); });

View File

@@ -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', () => { describe('GoogleMapsService', () => {
let service; let service;
let consoleSpy, consoleErrorSpy;
beforeEach(() => { beforeEach(() => {
// Clear all mocks // Clear all mocks
jest.clearAllMocks(); jest.clearAllMocks();
// Set up console spies
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
// Reset environment // Reset environment
delete process.env.GOOGLE_MAPS_API_KEY; delete process.env.GOOGLE_MAPS_API_KEY;
@@ -30,18 +38,13 @@ describe('GoogleMapsService', () => {
jest.resetModules(); jest.resetModules();
}); });
afterEach(() => {
consoleSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('Constructor', () => { describe('Constructor', () => {
it('should initialize with API key and log success', () => { it('should initialize with API key and log success', () => {
process.env.GOOGLE_MAPS_API_KEY = 'test-api-key'; process.env.GOOGLE_MAPS_API_KEY = 'test-api-key';
service = require('../../../services/googleMapsService'); service = require('../../../services/googleMapsService');
expect(consoleSpy).toHaveBeenCalledWith('Google Maps service initialized'); expect(mockLoggerInfo).toHaveBeenCalledWith('Google Maps service initialized');
expect(service.isConfigured()).toBe(true); expect(service.isConfigured()).toBe(true);
}); });
@@ -50,7 +53,7 @@ describe('GoogleMapsService', () => {
service = require('../../../services/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); expect(service.isConfigured()).toBe(false);
}); });
@@ -299,10 +302,11 @@ describe('GoogleMapsService', () => {
status: 'ZERO_RESULTS' status: 'ZERO_RESULTS'
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Places Autocomplete API error:', 'Places Autocomplete API error',
'ZERO_RESULTS', expect.objectContaining({
'No results found' status: 'ZERO_RESULTS',
})
); );
}); });
@@ -325,7 +329,7 @@ describe('GoogleMapsService', () => {
await expect(service.getPlacesAutocomplete('test input')).rejects.toThrow('Failed to fetch place predictions'); 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'); await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow('The specified place was not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Place Details API error:', 'Place Details API error',
'NOT_FOUND', expect.objectContaining({
'Place not found' status: 'NOT_FOUND',
})
); );
}); });
@@ -582,7 +587,7 @@ describe('GoogleMapsService', () => {
await expect(service.getPlaceDetails('ChIJ123')).rejects.toThrow(originalError); 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' status: 'ZERO_RESULTS'
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Geocoding API error:', 'Geocoding API error',
'ZERO_RESULTS', expect.objectContaining({
'No results found' status: 'ZERO_RESULTS',
})
); );
}); });
@@ -796,7 +802,7 @@ describe('GoogleMapsService', () => {
await expect(service.geocodeAddress('123 Test St')).rejects.toThrow('Failed to geocode address'); 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) }));
}); });
}); });
}); });

View File

@@ -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 PayoutService = require('../../../services/payoutService');
const { Rental, User, Item } = require('../../../models'); const { Rental, User, Item } = require('../../../models');
const StripeService = require('../../../services/stripeService'); const StripeService = require('../../../services/stripeService');
@@ -30,19 +43,15 @@ const mockItemModel = Item;
const mockCreateTransfer = StripeService.createTransfer; const mockCreateTransfer = StripeService.createTransfer;
describe('PayoutService', () => { describe('PayoutService', () => {
let consoleSpy, consoleErrorSpy; let consoleSpy;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
// Set up console spies
consoleSpy = jest.spyOn(console, 'log').mockImplementation(); consoleSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
}); });
afterEach(() => { afterEach(() => {
consoleSpy.mockRestore(); consoleSpy.mockRestore();
consoleErrorSpy.mockRestore();
}); });
describe('getEligiblePayouts', () => { describe('getEligiblePayouts', () => {
@@ -87,7 +96,8 @@ describe('PayoutService', () => {
where: { where: {
stripeConnectedAccountId: { stripeConnectedAccountId: {
'not': null 'not': null
} },
stripePayoutsEnabled: true
} }
}, },
{ {
@@ -106,7 +116,7 @@ describe('PayoutService', () => {
await expect(PayoutService.getEligiblePayouts()).rejects.toThrow('Database connection failed'); 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 () => { it('should return empty array when no eligible rentals found', async () => {
@@ -269,9 +279,9 @@ describe('PayoutService', () => {
payoutStatus: 'failed' payoutStatus: 'failed'
}); });
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing payout for rental 1:', 'Error processing payout for rental',
stripeError expect.objectContaining({ rentalId: 1 })
); );
}); });
@@ -287,9 +297,9 @@ describe('PayoutService', () => {
await expect(PayoutService.processRentalPayout(mockRental)) await expect(PayoutService.processRentalPayout(mockRental))
.rejects.toThrow('Database update failed'); .rejects.toThrow('Database update failed');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing payout for rental 1:', 'Error processing payout for rental',
dbError expect.objectContaining({ rentalId: 1 })
); );
}); });
@@ -306,9 +316,9 @@ describe('PayoutService', () => {
.rejects.toThrow('Database completion update failed'); .rejects.toThrow('Database completion update failed');
expect(mockCreateTransfer).toHaveBeenCalled(); expect(mockCreateTransfer).toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing payout for rental 1:', 'Error processing payout for rental',
dbError expect.objectContaining({ rentalId: 1 })
); );
}); });
@@ -438,9 +448,9 @@ describe('PayoutService', () => {
await expect(PayoutService.processAllEligiblePayouts()) await expect(PayoutService.processAllEligiblePayouts())
.rejects.toThrow('Database connection failed'); .rejects.toThrow('Database connection failed');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing all eligible payouts:', 'Error processing all eligible payouts',
dbError expect.objectContaining({ error: dbError.message })
); );
}); });
@@ -520,7 +530,8 @@ describe('PayoutService', () => {
where: { where: {
stripeConnectedAccountId: { stripeConnectedAccountId: {
'not': null 'not': null
} },
stripePayoutsEnabled: true
} }
}, },
{ {
@@ -613,9 +624,9 @@ describe('PayoutService', () => {
await expect(PayoutService.retryFailedPayouts()) await expect(PayoutService.retryFailedPayouts())
.rejects.toThrow('Database query failed'); .rejects.toThrow('Database query failed');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error retrying failed payouts:', 'Error retrying failed payouts',
dbError expect.objectContaining({ error: dbError.message })
); );
}); });
@@ -655,9 +666,9 @@ describe('PayoutService', () => {
await expect(PayoutService.processRentalPayout(mockRental)) await expect(PayoutService.processRentalPayout(mockRental))
.rejects.toThrow('Update failed'); .rejects.toThrow('Update failed');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing payout for rental 123:', 'Error processing payout for rental',
expect.any(Error) expect.objectContaining({ rentalId: 123 })
); );
}); });

View File

@@ -13,6 +13,19 @@ jest.mock('../../../services/stripeService', () => ({
createRefund: mockCreateRefund 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'); const RefundService = require('../../../services/refundService');
describe('RefundService', () => { describe('RefundService', () => {
@@ -540,8 +553,9 @@ describe('RefundService', () => {
const result = await RefundService.processCancellation(1, 200); const result = await RefundService.processCancellation(1, 200);
expect(mockCreateRefund).not.toHaveBeenCalled(); expect(mockCreateRefund).not.toHaveBeenCalled();
expect(consoleWarnSpy).toHaveBeenCalledWith( expect(mockLoggerWarn).toHaveBeenCalledWith(
'Refund amount calculated but no payment intent ID for rental 1' 'Refund amount calculated but no payment intent ID for rental',
{ rentalId: 1 }
); );
expect(result.refund).toEqual({ expect(result.refund).toEqual({
@@ -605,9 +619,9 @@ describe('RefundService', () => {
await expect(RefundService.processCancellation(1, 200)) await expect(RefundService.processCancellation(1, 200))
.rejects.toThrow('Failed to process refund: Refund failed'); .rejects.toThrow('Failed to process refund: Refund failed');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error processing Stripe refund:', 'Error processing Stripe refund',
stripeError expect.objectContaining({ error: stripeError })
); );
}); });

View File

@@ -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'); const StripeService = require('../../../services/stripeService');
describe('StripeService', () => { describe('StripeService', () => {
let consoleSpy, consoleErrorSpy;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
// Set up console spies
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
// Set environment variables for tests // Set environment variables for tests
process.env.FRONTEND_URL = 'http://localhost:3000'; process.env.FRONTEND_URL = 'http://localhost:3000';
}); });
afterEach(() => {
consoleSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('getCheckoutSession', () => { describe('getCheckoutSession', () => {
it('should retrieve checkout session successfully', async () => { it('should retrieve checkout session successfully', async () => {
const mockSession = { const mockSession = {
@@ -93,9 +94,11 @@ describe('StripeService', () => {
await expect(StripeService.getCheckoutSession('invalid_session')) await expect(StripeService.getCheckoutSession('invalid_session'))
.rejects.toThrow('Session not found'); .rejects.toThrow('Session not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error retrieving checkout session:', 'Error retrieving checkout session',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -174,9 +177,11 @@ describe('StripeService', () => {
email: 'invalid-email' email: 'invalid-email'
})).rejects.toThrow('Invalid email address'); })).rejects.toThrow('Invalid email address');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating connected account:', 'Error creating connected account',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -225,9 +230,11 @@ describe('StripeService', () => {
'http://localhost:3000/return' 'http://localhost:3000/return'
)).rejects.toThrow('Account not found'); )).rejects.toThrow('Account not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating account link:', 'Error creating account link',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -287,9 +294,11 @@ describe('StripeService', () => {
await expect(StripeService.getAccountStatus('invalid_account')) await expect(StripeService.getAccountStatus('invalid_account'))
.rejects.toThrow('Account not found'); .rejects.toThrow('Account not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error retrieving account status:', 'Error retrieving account status',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -420,9 +429,11 @@ describe('StripeService', () => {
destination: 'acct_123456789' destination: 'acct_123456789'
})).rejects.toThrow('Insufficient funds'); })).rejects.toThrow('Insufficient funds');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating transfer:', 'Error creating transfer',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -558,9 +569,11 @@ describe('StripeService', () => {
amount: 50.00 amount: 50.00
})).rejects.toThrow('Payment intent not found'); })).rejects.toThrow('Payment intent not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating refund:', 'Error creating refund',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -617,9 +630,11 @@ describe('StripeService', () => {
await expect(StripeService.getRefund('re_invalid')) await expect(StripeService.getRefund('re_invalid'))
.rejects.toThrow('Refund not found'); .rejects.toThrow('Refund not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error retrieving refund:', 'Error retrieving refund',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -698,11 +713,13 @@ describe('StripeService', () => {
'pm_invalid', 'pm_invalid',
50.00, 50.00,
'cus_123456789' 'cus_123456789'
)).rejects.toThrow('Payment method declined'); )).rejects.toThrow('The payment could not be processed.');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error charging payment method:', 'Payment failed',
stripeError expect.objectContaining({
code: expect.any(String),
})
); );
}); });
@@ -839,9 +856,11 @@ describe('StripeService', () => {
email: 'invalid-email' email: 'invalid-email'
})).rejects.toThrow('Invalid email format'); })).rejects.toThrow('Invalid email format');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating customer:', 'Error creating customer',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -886,6 +905,11 @@ describe('StripeService', () => {
mode: 'setup', mode: 'setup',
ui_mode: 'embedded', ui_mode: 'embedded',
redirect_on_completion: 'never', redirect_on_completion: 'never',
payment_method_options: {
card: {
request_three_d_secure: 'any',
},
},
metadata: { metadata: {
type: 'payment_method_setup', type: 'payment_method_setup',
userId: '123' userId: '123'
@@ -919,6 +943,11 @@ describe('StripeService', () => {
mode: 'setup', mode: 'setup',
ui_mode: 'embedded', ui_mode: 'embedded',
redirect_on_completion: 'never', redirect_on_completion: 'never',
payment_method_options: {
card: {
request_three_d_secure: 'any',
},
},
metadata: { metadata: {
type: 'payment_method_setup' type: 'payment_method_setup'
} }
@@ -934,9 +963,11 @@ describe('StripeService', () => {
customerId: 'cus_invalid' customerId: 'cus_invalid'
})).rejects.toThrow('Customer not found'); })).rejects.toThrow('Customer not found');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating setup checkout session:', 'Error creating setup checkout session',
stripeError expect.objectContaining({
error: stripeError.message,
})
); );
}); });
@@ -1015,9 +1046,11 @@ describe('StripeService', () => {
destination: 'acct_123456789' destination: 'acct_123456789'
})).rejects.toThrow('Request timeout'); })).rejects.toThrow('Request timeout');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating transfer:', 'Error creating transfer',
timeoutError expect.objectContaining({
error: timeoutError.message,
})
); );
}); });
@@ -1030,9 +1063,11 @@ describe('StripeService', () => {
email: 'test@example.com' email: 'test@example.com'
})).rejects.toThrow('Invalid API key'); })).rejects.toThrow('Invalid API key');
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(mockLoggerError).toHaveBeenCalledWith(
'Error creating customer:', 'Error creating customer',
apiKeyError expect.objectContaining({
error: apiKeyError.message,
})
); );
}); });
}); });

View File

@@ -442,6 +442,10 @@ describe("StripeWebhookService", () => {
expect(mockUser.update).toHaveBeenCalledWith({ expect(mockUser.update).toHaveBeenCalledWith({
stripeConnectedAccountId: null, stripeConnectedAccountId: null,
stripePayoutsEnabled: false, 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 = { const mockUser = {
id: 1, id: 1,
email: "owner@test.com", email: "owner@test.com",
firstName: null, firstName: null,
name: "Full Name", lastName: "Smith",
update: jest.fn().mockResolvedValue(true), update: jest.fn().mockResolvedValue(true),
}; };
@@ -545,7 +549,7 @@ describe("StripeWebhookService", () => {
expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith( expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
"owner@test.com", "owner@test.com",
expect.objectContaining({ expect.objectContaining({
ownerName: "Full Name", ownerName: "Smith",
}) })
); );
}); });