Files
rentall-app/backend/tests/unit/services/UserService.test.js
2025-12-20 14:59:09 -05:00

353 lines
11 KiB
JavaScript

const UserService = require('../../../services/UserService');
const { User, UserAddress } = require('../../../models');
const emailServices = require('../../../services/email');
const logger = require('../../../utils/logger');
// Mock dependencies
jest.mock('../../../models', () => ({
User: {
findByPk: jest.fn(),
},
UserAddress: {
create: jest.fn(),
findOne: jest.fn(),
},
}));
jest.mock('../../../services/email', () => ({
auth: {
sendPersonalInfoChangedEmail: jest.fn().mockResolvedValue(),
},
}));
jest.mock('../../../utils/logger', () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
}));
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env.NODE_ENV = 'test';
});
describe('updateProfile', () => {
const mockUser = {
id: 'user-123',
email: 'original@example.com',
firstName: 'John',
lastName: 'Doe',
address1: '123 Main St',
address2: null,
city: 'New York',
state: 'NY',
zipCode: '10001',
country: 'USA',
update: jest.fn().mockResolvedValue(),
};
it('should update user profile successfully', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser) // First call to find user
.mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' }); // Second call for return
const updateData = { firstName: 'Jane' };
const result = await UserService.updateProfile('user-123', updateData);
expect(User.findByPk).toHaveBeenCalledWith('user-123');
expect(mockUser.update).toHaveBeenCalledWith({ firstName: 'Jane' }, {});
expect(result.firstName).toBe('Jane');
});
it('should throw error when user not found', async () => {
User.findByPk.mockResolvedValue(null);
await expect(UserService.updateProfile('non-existent', { firstName: 'Test' }))
.rejects.toThrow('User not found');
});
it('should trim email and ignore empty email', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce(mockUser);
await UserService.updateProfile('user-123', { email: ' new@example.com ' });
expect(mockUser.update).toHaveBeenCalledWith(
expect.objectContaining({ email: 'new@example.com' }),
{}
);
});
it('should not update email if empty string', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce(mockUser);
await UserService.updateProfile('user-123', { email: ' ' });
// Email should not be in the update call
expect(mockUser.update).toHaveBeenCalledWith({}, {});
});
it('should convert empty phone to null', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce(mockUser);
await UserService.updateProfile('user-123', { phone: '' });
expect(mockUser.update).toHaveBeenCalledWith(
expect.objectContaining({ phone: null }),
{}
);
});
it('should trim phone number', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce(mockUser);
await UserService.updateProfile('user-123', { phone: ' 555-1234 ' });
expect(mockUser.update).toHaveBeenCalledWith(
expect.objectContaining({ phone: '555-1234' }),
{}
);
});
it('should pass options to update call', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce(mockUser);
const mockTransaction = { id: 'tx-123' };
await UserService.updateProfile('user-123', { firstName: 'Jane' }, { transaction: mockTransaction });
expect(mockUser.update).toHaveBeenCalledWith(
expect.any(Object),
{ transaction: mockTransaction }
);
});
it('should not send email notification in test environment', async () => {
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' });
await UserService.updateProfile('user-123', { firstName: 'Jane' });
// Email should not be sent in test environment
expect(emailServices.auth.sendPersonalInfoChangedEmail).not.toHaveBeenCalled();
});
it('should send email notification in production when personal info changes', async () => {
process.env.NODE_ENV = 'production';
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce({ ...mockUser, firstName: 'Jane' });
await UserService.updateProfile('user-123', { firstName: 'Jane' });
expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
expect(logger.info).toHaveBeenCalledWith(
'Personal information changed notification sent',
expect.objectContaining({
userId: 'user-123',
changedFields: ['firstName'],
})
);
});
it('should handle email notification failure gracefully', async () => {
process.env.NODE_ENV = 'production';
emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
new Error('Email service down')
);
User.findByPk
.mockResolvedValueOnce(mockUser)
.mockResolvedValueOnce({ ...mockUser, email: 'new@example.com' });
// Should not throw despite email failure
const result = await UserService.updateProfile('user-123', { email: 'new@example.com' });
expect(result).toBeDefined();
expect(logger.error).toHaveBeenCalledWith(
'Failed to send personal information changed notification',
expect.objectContaining({
error: 'Email service down',
userId: 'user-123',
})
);
});
});
describe('createUserAddress', () => {
const mockUser = {
id: 'user-123',
email: 'user@example.com',
};
const addressData = {
label: 'Home',
address1: '456 Oak Ave',
city: 'Boston',
state: 'MA',
zipCode: '02101',
country: 'USA',
};
it('should create a new address successfully', async () => {
const mockAddress = { id: 'addr-123', ...addressData, userId: 'user-123' };
User.findByPk.mockResolvedValue(mockUser);
UserAddress.create.mockResolvedValue(mockAddress);
const result = await UserService.createUserAddress('user-123', addressData);
expect(User.findByPk).toHaveBeenCalledWith('user-123');
expect(UserAddress.create).toHaveBeenCalledWith({
...addressData,
userId: 'user-123',
});
expect(result.id).toBe('addr-123');
});
it('should throw error when user not found', async () => {
User.findByPk.mockResolvedValue(null);
await expect(UserService.createUserAddress('non-existent', addressData))
.rejects.toThrow('User not found');
});
it('should send notification in production', async () => {
process.env.NODE_ENV = 'production';
const mockAddress = { id: 'addr-123', ...addressData };
User.findByPk.mockResolvedValue(mockUser);
UserAddress.create.mockResolvedValue(mockAddress);
await UserService.createUserAddress('user-123', addressData);
expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
});
});
describe('updateUserAddress', () => {
const mockAddress = {
id: 'addr-123',
userId: 'user-123',
address1: '123 Old St',
update: jest.fn().mockResolvedValue(),
};
it('should update address successfully', async () => {
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });
const result = await UserService.updateUserAddress('user-123', 'addr-123', {
address1: '789 New St',
});
expect(UserAddress.findOne).toHaveBeenCalledWith({
where: { id: 'addr-123', userId: 'user-123' },
});
expect(mockAddress.update).toHaveBeenCalledWith({ address1: '789 New St' });
expect(result.id).toBe('addr-123');
});
it('should throw error when address not found', async () => {
UserAddress.findOne.mockResolvedValue(null);
await expect(
UserService.updateUserAddress('user-123', 'non-existent', { address1: 'New' })
).rejects.toThrow('Address not found');
});
it('should send notification in production', async () => {
process.env.NODE_ENV = 'production';
const mockUser = { id: 'user-123', email: 'user@example.com' };
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue(mockUser);
await UserService.updateUserAddress('user-123', 'addr-123', { city: 'Chicago' });
expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
});
it('should handle email failure gracefully', async () => {
process.env.NODE_ENV = 'production';
emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
new Error('Email failed')
);
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });
const result = await UserService.updateUserAddress('user-123', 'addr-123', { city: 'Chicago' });
expect(result).toBeDefined();
expect(logger.error).toHaveBeenCalled();
});
});
describe('deleteUserAddress', () => {
const mockAddress = {
id: 'addr-123',
userId: 'user-123',
destroy: jest.fn().mockResolvedValue(),
};
it('should delete address successfully', async () => {
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });
await UserService.deleteUserAddress('user-123', 'addr-123');
expect(UserAddress.findOne).toHaveBeenCalledWith({
where: { id: 'addr-123', userId: 'user-123' },
});
expect(mockAddress.destroy).toHaveBeenCalled();
});
it('should throw error when address not found', async () => {
UserAddress.findOne.mockResolvedValue(null);
await expect(
UserService.deleteUserAddress('user-123', 'non-existent')
).rejects.toThrow('Address not found');
});
it('should send notification in production', async () => {
process.env.NODE_ENV = 'production';
const mockUser = { id: 'user-123', email: 'user@example.com' };
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue(mockUser);
await UserService.deleteUserAddress('user-123', 'addr-123');
expect(emailServices.auth.sendPersonalInfoChangedEmail).toHaveBeenCalledWith(mockUser);
});
it('should handle email failure gracefully', async () => {
process.env.NODE_ENV = 'production';
emailServices.auth.sendPersonalInfoChangedEmail.mockRejectedValueOnce(
new Error('Email failed')
);
UserAddress.findOne.mockResolvedValue(mockAddress);
User.findByPk.mockResolvedValue({ id: 'user-123', email: 'user@example.com' });
// Should not throw
await UserService.deleteUserAddress('user-123', 'addr-123');
expect(logger.error).toHaveBeenCalled();
});
});
});