353 lines
11 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|