658 lines
19 KiB
JavaScript
658 lines
19 KiB
JavaScript
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'
|
|
});
|
|
});
|
|
});
|
|
}); |