const request = require('supertest'); const express = require('express'); const usersRouter = require('../../../routes/users'); // Mock all dependencies jest.mock('../../../models', () => ({ User: { findByPk: jest.fn(), update: jest.fn(), }, UserAddress: { findAll: jest.fn(), findByPk: jest.fn(), create: jest.fn(), }, })); jest.mock('../../../middleware/auth', () => ({ authenticateToken: jest.fn((req, res, next) => { req.user = { id: 1, update: jest.fn() }; next(); }), })); jest.mock('../../../middleware/upload', () => ({ uploadProfileImage: jest.fn((req, res, callback) => { // Mock successful upload req.file = { filename: 'test-profile.jpg' }; callback(null); }), })); jest.mock('fs', () => ({ promises: { unlink: jest.fn(), }, })); jest.mock('path'); const { User, UserAddress } = require('../../../models'); const { uploadProfileImage } = require('../../../middleware/upload'); const fs = require('fs').promises; // Create express app with the router const app = express(); app.use(express.json()); app.use('/users', usersRouter); // Mock models const mockUserFindByPk = User.findByPk; const mockUserUpdate = User.update; const mockUserAddressFindAll = UserAddress.findAll; const mockUserAddressFindByPk = UserAddress.findByPk; const mockUserAddressCreate = UserAddress.create; describe('Users Routes', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('GET /profile', () => { it('should get user profile for authenticated user', async () => { const mockUser = { id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com', phone: '555-1234', imageFilename: 'profile.jpg', }; mockUserFindByPk.mockResolvedValue(mockUser); const response = await request(app) .get('/users/profile'); expect(response.status).toBe(200); expect(response.body).toEqual(mockUser); expect(mockUserFindByPk).toHaveBeenCalledWith(1, { attributes: { exclude: ['password'] } }); }); it('should handle database errors', async () => { mockUserFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/users/profile'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('GET /addresses', () => { it('should get user addresses', async () => { const mockAddresses = [ { id: 1, userId: 1, address1: '123 Main St', city: 'New York', isPrimary: true, }, { id: 2, userId: 1, address1: '456 Oak Ave', city: 'Boston', isPrimary: false, }, ]; mockUserAddressFindAll.mockResolvedValue(mockAddresses); const response = await request(app) .get('/users/addresses'); expect(response.status).toBe(200); expect(response.body).toEqual(mockAddresses); expect(mockUserAddressFindAll).toHaveBeenCalledWith({ where: { userId: 1 }, order: [['isPrimary', 'DESC'], ['createdAt', 'ASC']] }); }); it('should handle database errors', async () => { mockUserAddressFindAll.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/users/addresses'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('POST /addresses', () => { it('should create a new address', async () => { const addressData = { address1: '789 Pine St', address2: 'Apt 4B', city: 'Chicago', state: 'IL', zipCode: '60601', country: 'USA', isPrimary: false, }; const mockCreatedAddress = { id: 3, ...addressData, userId: 1, }; mockUserAddressCreate.mockResolvedValue(mockCreatedAddress); const response = await request(app) .post('/users/addresses') .send(addressData); expect(response.status).toBe(201); expect(response.body).toEqual(mockCreatedAddress); expect(mockUserAddressCreate).toHaveBeenCalledWith({ ...addressData, userId: 1 }); }); it('should handle database errors during creation', async () => { mockUserAddressCreate.mockRejectedValue(new Error('Database error')); const response = await request(app) .post('/users/addresses') .send({ address1: '789 Pine St', city: 'Chicago', }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('PUT /addresses/:id', () => { const mockAddress = { id: 1, userId: 1, address1: '123 Main St', city: 'New York', update: jest.fn(), }; beforeEach(() => { mockUserAddressFindByPk.mockResolvedValue(mockAddress); }); it('should update user address', async () => { const updateData = { address1: '123 Updated St', city: 'Updated City', }; mockAddress.update.mockResolvedValue(); const response = await request(app) .put('/users/addresses/1') .send(updateData); expect(response.status).toBe(200); expect(response.body).toEqual({ id: 1, userId: 1, address1: '123 Main St', city: 'New York', }); expect(mockAddress.update).toHaveBeenCalledWith(updateData); }); it('should return 404 for non-existent address', async () => { mockUserAddressFindByPk.mockResolvedValue(null); const response = await request(app) .put('/users/addresses/999') .send({ address1: 'Updated St' }); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Address not found' }); }); it('should return 403 for unauthorized user', async () => { const unauthorizedAddress = { ...mockAddress, userId: 2 }; mockUserAddressFindByPk.mockResolvedValue(unauthorizedAddress); const response = await request(app) .put('/users/addresses/1') .send({ address1: 'Updated St' }); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Unauthorized' }); }); it('should handle database errors', async () => { mockUserAddressFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .put('/users/addresses/1') .send({ address1: 'Updated St' }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('DELETE /addresses/:id', () => { const mockAddress = { id: 1, userId: 1, address1: '123 Main St', destroy: jest.fn(), }; beforeEach(() => { mockUserAddressFindByPk.mockResolvedValue(mockAddress); }); it('should delete user address', async () => { mockAddress.destroy.mockResolvedValue(); const response = await request(app) .delete('/users/addresses/1'); expect(response.status).toBe(204); expect(mockAddress.destroy).toHaveBeenCalled(); }); it('should return 404 for non-existent address', async () => { mockUserAddressFindByPk.mockResolvedValue(null); const response = await request(app) .delete('/users/addresses/999'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'Address not found' }); }); it('should return 403 for unauthorized user', async () => { const unauthorizedAddress = { ...mockAddress, userId: 2 }; mockUserAddressFindByPk.mockResolvedValue(unauthorizedAddress); const response = await request(app) .delete('/users/addresses/1'); expect(response.status).toBe(403); expect(response.body).toEqual({ error: 'Unauthorized' }); }); it('should handle database errors', async () => { mockUserAddressFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .delete('/users/addresses/1'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('GET /availability', () => { it('should get user availability settings', async () => { const mockUser = { defaultAvailableAfter: '09:00', defaultAvailableBefore: '17:00', defaultSpecifyTimesPerDay: true, defaultWeeklyTimes: { monday: '09:00-17:00', tuesday: '10:00-16:00' }, }; mockUserFindByPk.mockResolvedValue(mockUser); const response = await request(app) .get('/users/availability'); expect(response.status).toBe(200); expect(response.body).toEqual({ generalAvailableAfter: '09:00', generalAvailableBefore: '17:00', specifyTimesPerDay: true, weeklyTimes: { monday: '09:00-17:00', tuesday: '10:00-16:00' }, }); expect(mockUserFindByPk).toHaveBeenCalledWith(1, { attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes'] }); }); it('should handle database errors', async () => { mockUserFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/users/availability'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('PUT /availability', () => { it('should update user availability settings', async () => { const availabilityData = { generalAvailableAfter: '08:00', generalAvailableBefore: '18:00', specifyTimesPerDay: false, weeklyTimes: { monday: '08:00-18:00' }, }; mockUserUpdate.mockResolvedValue([1]); const response = await request(app) .put('/users/availability') .send(availabilityData); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'Availability updated successfully' }); expect(mockUserUpdate).toHaveBeenCalledWith({ defaultAvailableAfter: '08:00', defaultAvailableBefore: '18:00', defaultSpecifyTimesPerDay: false, defaultWeeklyTimes: { monday: '08:00-18:00' }, }, { where: { id: 1 } }); }); it('should handle database errors', async () => { mockUserUpdate.mockRejectedValue(new Error('Database error')); const response = await request(app) .put('/users/availability') .send({ generalAvailableAfter: '08:00', generalAvailableBefore: '18:00', }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('GET /:id', () => { it('should get public user profile by ID', async () => { const mockUser = { id: 2, firstName: 'Jane', lastName: 'Smith', username: 'janesmith', imageFilename: 'jane.jpg', }; mockUserFindByPk.mockResolvedValue(mockUser); const response = await request(app) .get('/users/2'); expect(response.status).toBe(200); expect(response.body).toEqual(mockUser); expect(mockUserFindByPk).toHaveBeenCalledWith('2', { attributes: { exclude: ['password', 'email', 'phone', 'address'] } }); }); it('should return 404 for non-existent user', async () => { mockUserFindByPk.mockResolvedValue(null); const response = await request(app) .get('/users/999'); expect(response.status).toBe(404); expect(response.body).toEqual({ error: 'User not found' }); }); it('should handle database errors', async () => { mockUserFindByPk.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/users/2'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('PUT /profile', () => { const mockUpdatedUser = { id: 1, firstName: 'Updated', lastName: 'User', email: 'updated@example.com', phone: '555-9999', }; beforeEach(() => { mockUserFindByPk.mockResolvedValue(mockUpdatedUser); }); it('should update user profile', async () => { const profileData = { firstName: 'Updated', lastName: 'User', email: 'updated@example.com', phone: '555-9999', address1: '123 New St', city: 'New City', }; const response = await request(app) .put('/users/profile') .send(profileData); expect(response.status).toBe(200); expect(response.body).toEqual(mockUpdatedUser); }); it('should exclude empty email from update', async () => { const profileData = { firstName: 'Updated', lastName: 'User', email: '', phone: '555-9999', }; const response = await request(app) .put('/users/profile') .send(profileData); expect(response.status).toBe(200); // Verify email was not included in the update call // (This would need to check the actual update call if we spy on req.user.update) }); it('should handle validation errors', async () => { const mockValidationError = new Error('Validation error'); mockValidationError.errors = [ { path: 'email', message: 'Invalid email format' } ]; // Mock req.user.update to throw validation error const { authenticateToken } = require('../../../middleware/auth'); authenticateToken.mockImplementation((req, res, next) => { req.user = { id: 1, update: jest.fn().mockRejectedValue(mockValidationError) }; next(); }); const response = await request(app) .put('/users/profile') .send({ firstName: 'Test', email: 'invalid-email', }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Validation error', details: [{ field: 'email', message: 'Invalid email format' }] }); }); it('should handle general database errors', async () => { // Reset the authenticateToken mock to use default user const { authenticateToken } = require('../../../middleware/auth'); authenticateToken.mockImplementation((req, res, next) => { req.user = { id: 1, update: jest.fn().mockRejectedValue(new Error('Database error')) }; next(); }); const response = await request(app) .put('/users/profile') .send({ firstName: 'Test', }); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Database error' }); }); }); describe('POST /profile/image', () => { const mockUser = { id: 1, imageFilename: 'old-image.jpg', update: jest.fn(), }; beforeEach(() => { mockUserFindByPk.mockResolvedValue(mockUser); }); it('should upload profile image successfully', async () => { mockUser.update.mockResolvedValue(); fs.unlink.mockResolvedValue(); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'Profile image uploaded successfully', filename: 'test-profile.jpg', imageUrl: '/uploads/profiles/test-profile.jpg' }); expect(fs.unlink).toHaveBeenCalled(); // Old image deleted expect(mockUser.update).toHaveBeenCalledWith({ imageFilename: 'test-profile.jpg' }); }); it('should handle upload errors', async () => { uploadProfileImage.mockImplementation((req, res, callback) => { callback(new Error('File too large')); }); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'File too large' }); }); it('should handle missing file', async () => { uploadProfileImage.mockImplementation((req, res, callback) => { req.file = null; callback(null); }); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(400); expect(response.body).toEqual({ error: 'No file uploaded' }); }); it('should handle database update errors', async () => { // Mock upload to succeed but database update to fail uploadProfileImage.mockImplementation((req, res, callback) => { req.file = { filename: 'test-profile.jpg' }; callback(null); }); const userWithError = { ...mockUser, update: jest.fn().mockRejectedValue(new Error('Database error')) }; mockUserFindByPk.mockResolvedValue(userWithError); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(500); expect(response.body).toEqual({ error: 'Failed to update profile image' }); }); it('should handle case when user has no existing profile image', async () => { // Mock upload to succeed uploadProfileImage.mockImplementation((req, res, callback) => { req.file = { filename: 'test-profile.jpg' }; callback(null); }); const userWithoutImage = { id: 1, imageFilename: null, update: jest.fn().mockResolvedValue() }; mockUserFindByPk.mockResolvedValue(userWithoutImage); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(200); expect(fs.unlink).not.toHaveBeenCalled(); // No old image to delete }); it('should continue if old image deletion fails', async () => { // Mock upload to succeed uploadProfileImage.mockImplementation((req, res, callback) => { req.file = { filename: 'test-profile.jpg' }; callback(null); }); const userWithImage = { id: 1, imageFilename: 'old-image.jpg', update: jest.fn().mockResolvedValue() }; mockUserFindByPk.mockResolvedValue(userWithImage); fs.unlink.mockRejectedValue(new Error('File not found')); const response = await request(app) .post('/users/profile/image'); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'Profile image uploaded successfully', filename: 'test-profile.jpg', imageUrl: '/uploads/profiles/test-profile.jpg' }); }); }); });