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