backend unit test coverage to 80%
This commit is contained in:
@@ -29,18 +29,27 @@ jest.mock('../../../models', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
// Track whether to simulate admin user
|
||||
let mockIsAdmin = true;
|
||||
|
||||
// Mock auth middleware
|
||||
jest.mock('../../../middleware/auth', () => ({
|
||||
authenticateToken: (req, res, next) => {
|
||||
if (req.headers.authorization) {
|
||||
req.user = { id: 1 };
|
||||
req.user = { id: 1, role: mockIsAdmin ? 'admin' : 'user' };
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
},
|
||||
requireVerifiedEmail: (req, res, next) => next(),
|
||||
requireAdmin: (req, res, next) => next(),
|
||||
requireAdmin: (req, res, next) => {
|
||||
if (req.user && req.user.role === 'admin') {
|
||||
next();
|
||||
} else {
|
||||
res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
},
|
||||
optionalAuth: (req, res, next) => next()
|
||||
}));
|
||||
|
||||
@@ -76,6 +85,7 @@ const mockItemCreate = Item.create;
|
||||
const mockItemFindAll = Item.findAll;
|
||||
const mockItemCount = Item.count;
|
||||
const mockRentalFindAll = Rental.findAll;
|
||||
const mockRentalCount = Rental.count;
|
||||
const mockUserModel = User;
|
||||
|
||||
// Set up Express app for testing
|
||||
@@ -96,6 +106,7 @@ describe('Items Routes', () => {
|
||||
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
mockItemCount.mockResolvedValue(1); // Default to not first listing
|
||||
mockIsAdmin = true; // Default to admin user
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1404,4 +1415,303 @@ describe('Items Routes', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /admin/:id (Admin Soft Delete)', () => {
|
||||
const mockItem = {
|
||||
id: 1,
|
||||
name: 'Test Item',
|
||||
ownerId: 2,
|
||||
isDeleted: false,
|
||||
owner: { id: 2, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
const mockUpdatedItem = {
|
||||
id: 1,
|
||||
name: 'Test Item',
|
||||
ownerId: 2,
|
||||
isDeleted: true,
|
||||
deletedBy: 1,
|
||||
deletedAt: expect.any(Date),
|
||||
deletionReason: 'Violates terms of service',
|
||||
owner: { id: 2, firstName: 'John', lastName: 'Doe' },
|
||||
deleter: { id: 1, firstName: 'Admin', lastName: 'User' }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockItem.update.mockReset();
|
||||
mockRentalCount.mockResolvedValue(0); // No active rentals by default
|
||||
});
|
||||
|
||||
it('should soft delete item as admin with valid reason', async () => {
|
||||
mockItemFindByPk
|
||||
.mockResolvedValueOnce(mockItem)
|
||||
.mockResolvedValueOnce(mockUpdatedItem);
|
||||
mockItem.update.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockItem.update).toHaveBeenCalledWith({
|
||||
isDeleted: true,
|
||||
deletedBy: 1,
|
||||
deletedAt: expect.any(Date),
|
||||
deletionReason: 'Violates terms of service'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return updated item with deleter information', async () => {
|
||||
mockItemFindByPk
|
||||
.mockResolvedValueOnce(mockItem)
|
||||
.mockResolvedValueOnce(mockUpdatedItem);
|
||||
mockItem.update.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.deleter).toBeDefined();
|
||||
expect(response.body.isDeleted).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 400 when reason is missing', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Deletion reason is required');
|
||||
});
|
||||
|
||||
it('should return 400 when reason is empty', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: ' ' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Deletion reason is required');
|
||||
});
|
||||
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('No token provided');
|
||||
});
|
||||
|
||||
it('should return 403 when user is not admin', async () => {
|
||||
mockIsAdmin = false;
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Admin access required');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent item', async () => {
|
||||
mockItemFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/999')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Item not found');
|
||||
});
|
||||
|
||||
it('should return 400 when item is already deleted', async () => {
|
||||
const deletedItem = { ...mockItem, isDeleted: true };
|
||||
mockItemFindByPk.mockResolvedValue(deletedItem);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Item is already deleted');
|
||||
});
|
||||
|
||||
it('should return 400 when item has active rentals', async () => {
|
||||
mockItemFindByPk.mockResolvedValue(mockItem);
|
||||
mockRentalCount.mockResolvedValue(2);
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Cannot delete item with active or upcoming rentals');
|
||||
expect(response.body.code).toBe('ACTIVE_RENTALS_EXIST');
|
||||
expect(response.body.activeRentalsCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Database error');
|
||||
});
|
||||
|
||||
it('should handle update errors', async () => {
|
||||
mockItemFindByPk.mockResolvedValue(mockItem);
|
||||
mockItem.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/items/admin/1')
|
||||
.set('Authorization', 'Bearer valid_token')
|
||||
.send({ reason: 'Violates terms of service' });
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Update failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /admin/:id/restore (Admin Restore)', () => {
|
||||
const mockDeletedItem = {
|
||||
id: 1,
|
||||
name: 'Test Item',
|
||||
ownerId: 2,
|
||||
isDeleted: true,
|
||||
deletedBy: 1,
|
||||
deletedAt: new Date(),
|
||||
deletionReason: 'Violates terms of service',
|
||||
update: jest.fn()
|
||||
};
|
||||
|
||||
const mockRestoredItem = {
|
||||
id: 1,
|
||||
name: 'Test Item',
|
||||
ownerId: 2,
|
||||
isDeleted: false,
|
||||
deletedBy: null,
|
||||
deletedAt: null,
|
||||
deletionReason: null,
|
||||
owner: { id: 2, firstName: 'John', lastName: 'Doe' }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeletedItem.update.mockReset();
|
||||
});
|
||||
|
||||
it('should restore soft-deleted item as admin', async () => {
|
||||
mockItemFindByPk
|
||||
.mockResolvedValueOnce(mockDeletedItem)
|
||||
.mockResolvedValueOnce(mockRestoredItem);
|
||||
mockDeletedItem.update.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockDeletedItem.update).toHaveBeenCalledWith({
|
||||
isDeleted: false,
|
||||
deletedBy: null,
|
||||
deletedAt: null,
|
||||
deletionReason: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear deletion fields after restore', async () => {
|
||||
mockItemFindByPk
|
||||
.mockResolvedValueOnce(mockDeletedItem)
|
||||
.mockResolvedValueOnce(mockRestoredItem);
|
||||
mockDeletedItem.update.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.isDeleted).toBe(false);
|
||||
expect(response.body.deletedBy).toBeNull();
|
||||
expect(response.body.deletedAt).toBeNull();
|
||||
expect(response.body.deletionReason).toBeNull();
|
||||
});
|
||||
|
||||
it('should return 401 when not authenticated', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('No token provided');
|
||||
});
|
||||
|
||||
it('should return 403 when user is not admin', async () => {
|
||||
mockIsAdmin = false;
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.error).toBe('Admin access required');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent item', async () => {
|
||||
mockItemFindByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/999/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Item not found');
|
||||
});
|
||||
|
||||
it('should return 400 when item is not deleted', async () => {
|
||||
const activeItem = { ...mockDeletedItem, isDeleted: false };
|
||||
mockItemFindByPk.mockResolvedValue(activeItem);
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Item is not deleted');
|
||||
});
|
||||
|
||||
it('should handle database errors', async () => {
|
||||
mockItemFindByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Database error');
|
||||
});
|
||||
|
||||
it('should handle update errors', async () => {
|
||||
mockItemFindByPk.mockResolvedValue(mockDeletedItem);
|
||||
mockDeletedItem.update.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.patch('/items/admin/1/restore')
|
||||
.set('Authorization', 'Bearer valid_token');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toBe('Update failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user