backend unit test coverage to 80%

This commit is contained in:
jackiettran
2026-01-19 19:22:01 -05:00
parent d4362074f5
commit 1923ffc251
8 changed files with 3183 additions and 7 deletions

View File

@@ -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');
});
});
});