route testing refactoring using zod - ADR-003
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m3s

This commit is contained in:
2025-12-12 13:16:58 -08:00
parent d004efb84b
commit e37a32c890
27 changed files with 481 additions and 856 deletions

View File

@@ -148,13 +148,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
expect(response.body.message).toContain('Profile not found');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/profile');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('GET /watched-items', () => {
@@ -165,13 +158,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual(mockItems);
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.getWatchedItems).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/watched-items');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('POST /watched-items', () => {
@@ -185,15 +171,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(201);
expect(response.body).toEqual(mockAddedItem);
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.addWatchedItem).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.post('/api/users/watched-items')
.send({ itemName: 'Failing Item', category: 'Errors' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('POST /watched-items (Validation)', () => {
@@ -229,13 +206,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(204);
expect(db.personalizationRepo.removeWatchedItem).toHaveBeenCalledWith(mockUserProfile.user_id, 99);
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.removeWatchedItem).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).delete('/api/users/watched-items/99');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('Shopping List Routes', () => {
@@ -271,15 +241,6 @@ describe('User Routes (/api/users)', () => {
expect(response.body.message).toBe('User not found');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.shoppingRepo.createShoppingList).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.post('/api/users/shopping-lists')
.send({ name: 'New List' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 400 for an invalid listId on DELETE', async () => {
const response = await supertest(app).delete('/api/users/shopping-lists/abc');
expect(response.status).toBe(400);
@@ -301,9 +262,7 @@ describe('User Routes (/api/users)', () => {
});
});
});
describe('Shopping List Item Routes', () => {
it('POST /shopping-lists/:listId/items should add an item to a list', async () => {
describe('Shopping List Item Routes', () => { it('POST /shopping-lists/:listId/items should add an item to a list', async () => {
const listId = 1;
const itemData = { customItemName: 'Paper Towels' };
const mockAddedItem = createMockShoppingListItem({ shopping_list_item_id: 101, shopping_list_id: listId, ...itemData });
@@ -322,19 +281,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(400);
});
it('should return 500 if DB fails when adding an item', async () => {
vi.mocked(db.shoppingRepo.addShoppingListItem).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).post('/api/users/shopping-lists/1/items').send({ customItemName: 'Test' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 400 for an invalid listId on POST', async () => {
const response = await supertest(app).post('/api/users/shopping-lists/abc/items').send({ customItemName: 'Test' });
expect(response.status).toBe(400);
expect(response.body.message).toBe("Invalid ID for parameter 'listId'. Must be a number.");
});
it('PUT /shopping-lists/items/:itemId should update an item', async () => {
const itemId = 101;
const updates = { is_purchased: true, quantity: 2 };
@@ -348,43 +294,12 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockUpdatedItem);
});
it('should return 400 for an invalid itemId on PUT', async () => {
const response = await supertest(app).put('/api/users/shopping-lists/items/abc').send({ is_purchased: true });
expect(response.status).toBe(400);
expect(response.body.message).toBe("Invalid ID for parameter 'itemId'. Must be a number.");
});
it('should return 404 if item to update is not found', async () => {
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(new Error('not found'));
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(new NotFoundError('not found'));
const response = await supertest(app).put('/api/users/shopping-lists/items/999').send({ is_purchased: true });
expect(response.status).toBe(404);
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/shopping-lists/items/101')
.send({ is_purchased: true });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/shopping-lists/items/101')
.send({ is_purchased: true });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 400 for an invalid itemId on DELETE', async () => {
const response = await supertest(app).delete('/api/users/shopping-lists/items/abc');
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid item ID.');
});
describe('DELETE /shopping-lists/items/:itemId', () => {
it('should delete an item', async () => {
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockResolvedValue(undefined);
@@ -393,7 +308,6 @@ describe('User Routes (/api/users)', () => {
});
it('should return 404 if item to delete is not found', async () => {
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(new Error('not found'));
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(new NotFoundError('not found'));
const response = await supertest(app).delete('/api/users/shopping-lists/items/999');
expect(response.status).toBe(404);
@@ -413,23 +327,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual(updatedProfile);
});
it('should return 400 if no update fields are provided', async () => {
const response = await supertest(app)
.put('/api/users/profile')
.send({});
expect(response.status).toBe(400);
expect(response.body.message).toBe('At least one field to update must be provided.');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/profile')
.send({ full_name: 'Failing Name' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('PUT /profile/password', () => {
@@ -452,16 +349,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(400);
expect(response.body.message).toContain('New password is too weak.');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(bcrypt.hash).mockResolvedValue('hashed-password' as never);
vi.mocked(db.userRepo.updateUserPassword).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/profile/password')
.send({ newPassword: 'a-Very-Strong-Password-456!' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('DELETE /account', () => {
@@ -507,23 +394,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
expect(response.body.message).toBe('User not found or password not set.');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.userRepo.findUserWithPasswordHashById).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.delete('/api/users/account')
.send({ password: 'any-password' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 400 if password is not provided', async () => {
const response = await supertest(app)
.delete('/api/users/account')
.send({}); // Empty body
expect(response.status).toBe(400);
expect(response.body.message).toBe("Field 'password' is required.");
});
});
describe('User Preferences and Personalization', () => {
@@ -561,26 +431,12 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockRestrictions);
});
it('GET should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 400 for an invalid masterItemId', async () => {
const response = await supertest(app).delete('/api/users/watched-items/abc');
expect(response.status).toBe(400);
expect(response.body.message).toBe("Invalid ID for parameter 'masterItemId'. Must be a number.");
});
it('GET should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('PUT should successfully set the restrictions', async () => {
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockResolvedValue(undefined);
const restrictionIds = [1, 3, 5];
@@ -590,24 +446,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(204);
});
it('PUT should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/me/dietary-restrictions')
.send({ restrictionIds: [1] });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('PUT should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/me/dietary-restrictions')
.send({ restrictionIds: [1] });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('PUT should return 400 on foreign key constraint error', async () => {
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(new ForeignKeyConstraintError('Invalid restriction ID'));
const response = await supertest(app)
@@ -626,13 +464,6 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockAppliances);
});
it('GET should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.getUserAppliances).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/me/appliances');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('PUT should successfully set the appliances', async () => {
vi.mocked(db.personalizationRepo.setUserAppliances).mockResolvedValue([]);
const applianceIds = [2, 4, 6];
@@ -640,23 +471,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(204);
});
it('PUT should return 500 if the database call fails', async () => {
vi.mocked(db.personalizationRepo.setUserAppliances).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/me/appliances')
.send({ applianceIds: [1] });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('PUT should return 400 if applianceIds is not an array', async () => {
const response = await supertest(app)
.put('/api/users/me/appliances')
.send({ applianceIds: 'not-an-array' });
expect(response.status).toBe(400);
expect(response.body.message).toBe('applianceIds must be an array.');
});
it('PUT should return 400 on foreign key constraint error', async () => {
vi.mocked(db.personalizationRepo.setUserAppliances).mockRejectedValue(new ForeignKeyConstraintError('Invalid appliance ID'));
const response = await supertest(app)
@@ -670,7 +484,7 @@ describe('User Routes (/api/users)', () => {
describe('Notification Routes', () => {
it('GET /notifications should return notifications for the user', async () => {
const mockNotifications: Notification[] = [{ notification_id: 1, user_id: 'user-123', content: 'Test', is_read: false, created_at: '' }];
const mockNotifications: Notification[] = [{ notification_id: 1, user_id: 'user-123', content: 'Test', is_read: false, created_at: '', link_url: null }];
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
const response = await supertest(app).get('/api/users/notifications?limit=10&offset=0');
@@ -680,20 +494,6 @@ describe('User Routes (/api/users)', () => {
expect(db.notificationRepo.getNotificationsForUser).toHaveBeenCalledWith('user-123', 10, 0);
});
it('GET /notifications should return 500 if the database call fails', async () => {
vi.mocked(db.notificationRepo.getNotificationsForUser).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/notifications');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('POST /notifications/mark-all-read should return 500 if the database call fails', async () => {
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).post('/api/users/notifications/mark-all-read');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('POST /notifications/mark-all-read should return 204', async () => {
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockResolvedValue(undefined);
const response = await supertest(app).post('/api/users/notifications/mark-all-read');
@@ -729,14 +529,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(403);
});
it('should return 500 if database call fails', async () => {
const appWithUser = createApp({ ...mockUserProfile, address_id: 1 } as any);
vi.mocked(db.addressRepo.getAddressById).mockRejectedValue(new Error('DB Error'));
const response = await supertest(appWithUser).get('/api/users/addresses/1');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('GET /addresses/:addressId should return 404 if address not found', async () => {
const appWithUser = createApp({ ...mockUserProfile, address_id: 1 } as any);
vi.mocked(db.addressRepo.getAddressById).mockResolvedValue(undefined);
@@ -760,26 +552,6 @@ describe('User Routes (/api/users)', () => {
expect(db.userRepo.updateUserProfile).toHaveBeenCalledWith('user-123', { address_id: 5 });
});
it('should return 500 if upsertAddress fails', async () => {
const appWithUser = createApp({ ...mockUserProfile, address_id: null } as any);
const addressData = { address_line_1: '123 New St' };
vi.mocked(db.addressRepo.upsertAddress).mockRejectedValue(new Error('DB Error'));
const response = await supertest(appWithUser).put('/api/users/profile/address').send(addressData);
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('should return 500 if upsertAddress succeeds but updateUserProfile fails', async () => {
const appWithUser = createApp({ ...mockUserProfile, address_id: null } as any);
const addressData = { address_line_1: '123 Failing St' };
vi.mocked(db.addressRepo.upsertAddress).mockResolvedValue(6); // upsert succeeds
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(new Error('DB link error')); // update fails
const response = await supertest(appWithUser).put('/api/users/profile/address').send(addressData);
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB link error');
});
});
describe('POST /profile/avatar', () => {
@@ -817,16 +589,6 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(400);
expect(response.body.message).toBe('No avatar file uploaded.');
});
it('should return 500 if the database call fails', async () => {
vi.mocked(bcrypt.hash).mockResolvedValue('hashed-password' as never);
vi.mocked(db.userRepo.updateUserPassword).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app)
.put('/api/users/profile/password')
.send({ newPassword: 'a-Very-Strong-Password-456!' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('Recipe Routes', () => {
@@ -837,20 +599,6 @@ describe('User Routes (/api/users)', () => {
expect(db.recipeRepo.deleteRecipe).toHaveBeenCalledWith(1, mockUserProfile.user_id, false);
});
it('DELETE /recipes/:recipeId should return 500 if the database call fails', async () => {
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).delete('/api/users/recipes/1');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('DELETE /recipes/:recipeId should return 404 if recipe to delete is not found', async () => {
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(new Error('not found'));
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(new NotFoundError('not found'));
const response = await supertest(app).delete('/api/users/recipes/999');
expect(response.status).toBe(404);
});
it('PUT /recipes/:recipeId should update a user\'s own recipe', async () => {
const updates = { description: 'A new delicious description.' };
const mockUpdatedRecipe = { ...createMockRecipe({ recipe_id: 1 }), ...updates };
@@ -866,38 +614,17 @@ describe('User Routes (/api/users)', () => {
});
it('PUT /recipes/:recipeId should return 404 if recipe not found', async () => {
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new Error('not found'));
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new NotFoundError('not found'));
const response = await supertest(app).put('/api/users/recipes/999').send({ name: 'New Name' });
expect(response.status).toBe(404);
});
it('PUT /recipes/:recipeId should return 400 if no update fields are provided', async () => {
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new Error('No fields provided'));
const response = await supertest(app).put('/api/users/recipes/1').send({});
expect(response.status).toBe(400);
});
it('PUT /recipes/:recipeId should return 500 if database call fails', async () => {
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).put('/api/users/recipes/1').send({ name: 'New Name' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('GET /shopping-lists/:listId should return 500 if the database call fails', async () => {
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/users/shopping-lists/1');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
it('GET /shopping-lists/:listId should return 404 if list is not found', async () => {
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(new NotFoundError('Shopping list not found'));
const response = await supertest(app).get('/api/users/shopping-lists/999');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Shopping list not found');
});
});
}); // End of Recipe Routes
});
});