MORE UNIT TESTS - approc 90% before - 95% now?
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 45m25s

This commit is contained in:
2025-12-17 20:57:28 -08:00
parent 6c17f202ed
commit c623cddfb5
53 changed files with 2835 additions and 973 deletions

View File

@@ -322,6 +322,19 @@ describe('Auth Routes (/api/auth)', () => {
expect(response.body.message).toBe('Database connection failed');
});
it('should log a warning when passport authentication fails without a user', async () => {
// This test specifically covers the `if (!user)` debug log line in the route.
const response = await supertest(app)
.post('/api/auth/login')
.send({ email: 'notfound@test.com', password: 'any_password' });
expect(response.status).toBe(401);
expect(mockLogger.warn).toHaveBeenCalledWith(
{ info: { message: 'Login failed' } },
'[API /login] Passport reported NO USER found.'
);
});
it('should set a long-lived cookie when rememberMe is true', async () => {
// Arrange
const loginCredentials = { email: 'test@test.com', password: 'password123', rememberMe: true };
@@ -532,5 +545,14 @@ describe('Auth Routes (/api/auth)', () => {
'Failed to delete refresh token from DB during logout.'
);
});
it('should return 200 OK and clear the cookie even if no refresh token is provided', async () => {
// Act: Make a request without a cookie.
const response = await supertest(app).post('/api/auth/logout');
// Assert: The response should still be successful and attempt to clear the cookie.
expect(response.status).toBe(200);
expect(response.headers['set-cookie'][0]).toContain('refreshToken=;');
});
});
});

View File

@@ -44,6 +44,7 @@ vi.mock('passport-local', () => ({
import * as db from '../services/db/index.db';
import { UserProfile } from '../types';
import { createMockUserProfile } from '../tests/utils/mockFactories';
// Mock dependencies before importing the passport configuration
vi.mock('../services/db/index.db', () => ({
@@ -349,6 +350,24 @@ describe('Passport Configuration', () => {
expect(mockNext).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(403);
});
it('should return 403 Forbidden if req.user is not a valid UserProfile object', () => {
// Arrange
const mockReq: Partial<Request> = {
// An object that is not a valid UserProfile (e.g., missing 'role')
user: {
user_id: 'invalid-user-id',
} as any,
};
// Act
isAdmin(mockReq as Request, mockRes as Response, mockNext);
// Assert
expect(mockNext).not.toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Forbidden: Administrator access required.' });
});
});
describe('optionalAuth Middleware', () => {
@@ -362,7 +381,7 @@ describe('Passport Configuration', () => {
it('should populate req.user and call next() if authentication succeeds', () => {
// Arrange
const mockReq = {} as Request;
const mockUser = { user_id: 'user-123' };
const mockUser = createMockUserProfile({ user_id: 'user-123' });
// Mock passport.authenticate to call its callback with a user
vi.mocked(passport.authenticate).mockImplementation(
(_strategy, _options, callback) => () => callback?.(null, mockUser, undefined)
@@ -406,6 +425,23 @@ describe('Passport Configuration', () => {
expect(logger.info).toHaveBeenCalledWith({ info: 'Token expired' }, 'Optional auth info:');
expect(mockNext).toHaveBeenCalledTimes(1);
});
it('should call next() and not populate user if passport returns an error', () => {
// Arrange
const mockReq = {} as Request;
const authError = new Error('Malformed token');
// Mock passport.authenticate to call its callback with an error
vi.mocked(passport.authenticate).mockImplementation(
(_strategy, _options, callback) => () => callback?.(authError, false, undefined)
);
// Act
optionalAuth(mockReq, mockRes as Response, mockNext);
// Assert
expect(mockReq.user).toBeUndefined();
expect(mockNext).toHaveBeenCalledTimes(1);
});
});
// ... (Keep other describe blocks: LocalStrategy, isAdmin Middleware, optionalAuth Middleware)
@@ -458,7 +494,7 @@ describe('Passport Configuration', () => {
it('should populate req.user and call next() if authentication succeeds', () => {
const mockReq = {} as Request;
const mockUser = { user_id: 'user-123' };
const mockUser = createMockUserProfile({ user_id: 'user-123' });
vi.mocked(passport.authenticate).mockImplementation(
(_strategy, _options, callback) => () => callback?.(null, mockUser, undefined)
);

View File

@@ -52,6 +52,8 @@ passport.use(new LocalStrategy(
if (timeSinceLockout < lockoutDurationMs) {
logger.warn(`Login attempt for locked account: ${email}`);
// Refresh the lockout timestamp on each attempt to prevent probing.
await db.adminRepo.incrementFailedLoginAttempts(user.user_id, req.log);
return done(null, false, { message: `Account is temporarily locked. Please try again in ${LOCKOUT_DURATION_MINUTES} minutes.` });
}
}

View File

@@ -154,6 +154,13 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
expect(response.body.message).toContain('Profile not found');
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.userRepo.findUserProfileById).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/profile');
expect(response.status).toBe(500);
});
});
describe('GET /watched-items', () => {
@@ -164,6 +171,13 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual(mockItems);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.getWatchedItems).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/watched-items');
expect(response.status).toBe(500);
});
});
describe('POST /watched-items', () => {
@@ -177,6 +191,15 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(201);
expect(response.body).toEqual(mockAddedItem);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.addWatchedItem).mockRejectedValue(dbError);
const response = await supertest(app)
.post('/api/users/watched-items')
.send({ itemName: 'Test', category: 'Produce' });
expect(response.status).toBe(500);
});
});
describe('POST /watched-items (Validation)', () => {
@@ -214,6 +237,13 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(204);
expect(db.personalizationRepo.removeWatchedItem).toHaveBeenCalledWith(mockUserProfile.user_id, 99, expectLogger);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.removeWatchedItem).mockRejectedValue(dbError);
const response = await supertest(app).delete(`/api/users/watched-items/99`);
expect(response.status).toBe(500);
});
});
describe('Shopping List Routes', () => {
@@ -225,6 +255,13 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockLists);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.getShoppingLists).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/shopping-lists');
expect(response.status).toBe(500);
});
it('POST /shopping-lists should create a new list', async () => {
const mockNewList = createMockShoppingList({ shopping_list_id: 2, user_id: mockUserProfile.user_id, name: 'Party Supplies' });
vi.mocked(db.shoppingRepo.createShoppingList).mockResolvedValue(mockNewList);
@@ -250,6 +287,14 @@ describe('User Routes (/api/users)', () => {
expect(response.body.message).toBe('User not found');
});
it('should return 500 on a generic database error during creation', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.createShoppingList).mockRejectedValue(dbError);
const response = await supertest(app).post('/api/users/shopping-lists').send({ name: 'Failing List' });
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Connection Failed');
});
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);
@@ -271,6 +316,13 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.deleteShoppingList).mockRejectedValue(dbError);
const response = await supertest(app).delete('/api/users/shopping-lists/1');
expect(response.status).toBe(500);
});
it('should return 400 for an invalid listId', async () => {
const response = await supertest(app).delete('/api/users/shopping-lists/abc');
expect(response.status).toBe(400);
@@ -297,6 +349,15 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(400);
});
it('should return 500 on a generic database error when adding an item', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.addShoppingListItem).mockRejectedValue(dbError);
const response = await supertest(app)
.post('/api/users/shopping-lists/1/items')
.send({ customItemName: 'Test' });
expect(response.status).toBe(500);
});
it('PUT /shopping-lists/items/:itemId should update an item', async () => {
const itemId = 101;
const updates = { is_purchased: true, quantity: 2 };
@@ -316,6 +377,15 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
});
it('should return 500 on a generic database error when updating an item', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.updateShoppingListItem).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/shopping-lists/items/101')
.send({ is_purchased: true });
expect(response.status).toBe(500);
});
describe('DELETE /shopping-lists/items/:itemId', () => {
it('should delete an item', async () => {
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockResolvedValue(undefined);
@@ -328,6 +398,13 @@ describe('User Routes (/api/users)', () => {
const response = await supertest(app).delete('/api/users/shopping-lists/items/999');
expect(response.status).toBe(404);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockRejectedValue(dbError);
const response = await supertest(app).delete('/api/users/shopping-lists/items/101');
expect(response.status).toBe(500);
});
});
});
@@ -344,6 +421,15 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(updatedProfile);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/profile')
.send({ full_name: 'New Name' });
expect(response.status).toBe(500);
});
it('should return 400 if the body is empty', async () => {
const response = await supertest(app)
.put('/api/users/profile')
@@ -365,6 +451,16 @@ describe('User Routes (/api/users)', () => {
expect(response.body.message).toBe('Password updated successfully.');
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(bcrypt.hash).mockResolvedValue('hashed-password' as never);
vi.mocked(db.userRepo.updateUserPassword).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/profile/password')
.send({ newPassword: 'a-Very-Strong-Password-456!' });
expect(response.status).toBe(500);
});
it('should return 400 for a weak password', async () => {
// Use a password long enough to pass .min(8) but weak enough to fail strength check
const response = await supertest(app)
@@ -409,6 +505,17 @@ 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 on a generic database error', async () => {
const userWithHash = createMockUserWithPasswordHash({ ...mockUserProfile.user, password_hash: 'hashed-password' });
vi.mocked(db.userRepo.findUserWithPasswordHashById).mockResolvedValue(userWithHash);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
vi.mocked(db.userRepo.deleteUserById).mockRejectedValue(new Error('DB Connection Failed'));
const response = await supertest(app)
.delete('/api/users/account')
.send({ password: 'correct-password' });
expect(response.status).toBe(500);
});
});
describe('User Preferences and Personalization', () => {
@@ -427,6 +534,15 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(updatedProfile);
});
it('should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.userRepo.updateUserPreferences).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/profile/preferences')
.send({ darkMode: true });
expect(response.status).toBe(500);
});
it('should return 400 if the request body is not a valid object', async () => {
const response = await supertest(app)
.put('/api/users/profile/preferences')
@@ -447,6 +563,13 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockRestrictions);
});
it('GET should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.getUserDietaryRestrictions).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
expect(response.status).toBe(500);
});
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);
@@ -470,6 +593,15 @@ describe('User Routes (/api/users)', () => {
.send({ restrictionIds: [999] }); // Invalid ID
expect(response.status).toBe(400);
});
it('PUT should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.setUserDietaryRestrictions).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/me/dietary-restrictions')
.send({ restrictionIds: [1] });
expect(response.status).toBe(500);
});
});
describe('GET and PUT /users/me/appliances', () => {
@@ -481,6 +613,13 @@ describe('User Routes (/api/users)', () => {
expect(response.body).toEqual(mockAppliances);
});
it('GET should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.getUserAppliances).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/me/appliances');
expect(response.status).toBe(500);
});
it('PUT should successfully set the appliances', async () => {
vi.mocked(db.personalizationRepo.setUserAppliances).mockResolvedValue([]);
const applianceIds = [2, 4, 6];
@@ -496,6 +635,15 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(400);
expect(response.body.message).toBe('Invalid appliance ID');
});
it('PUT should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.personalizationRepo.setUserAppliances).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/me/appliances')
.send({ applianceIds: [1] });
expect(response.status).toBe(500);
});
});
});
@@ -511,6 +659,13 @@ describe('User Routes (/api/users)', () => {
expect(db.notificationRepo.getNotificationsForUser).toHaveBeenCalledWith('user-123', 10, 0, expectLogger);
});
it('GET /notifications should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.notificationRepo.getNotificationsForUser).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/notifications');
expect(response.status).toBe(500);
});
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');
@@ -518,6 +673,13 @@ describe('User Routes (/api/users)', () => {
expect(db.notificationRepo.markAllNotificationsAsRead).toHaveBeenCalledWith('user-123', expectLogger);
});
it('POST /notifications/mark-all-read should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.notificationRepo.markAllNotificationsAsRead).mockRejectedValue(dbError);
const response = await supertest(app).post('/api/users/notifications/mark-all-read');
expect(response.status).toBe(500);
});
it('POST /notifications/:notificationId/mark-read should return 204', async () => {
// Fix: Return a mock notification object to match the function's signature.
vi.mocked(db.notificationRepo.markNotificationAsRead).mockResolvedValue(createMockNotification({ notification_id: 1, user_id: 'user-123' }));
@@ -526,6 +688,13 @@ describe('User Routes (/api/users)', () => {
expect(db.notificationRepo.markNotificationAsRead).toHaveBeenCalledWith(1, 'user-123', expectLogger);
});
it('POST /notifications/:notificationId/mark-read should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.notificationRepo.markNotificationAsRead).mockRejectedValue(dbError);
const response = await supertest(app).post('/api/users/notifications/1/mark-read');
expect(response.status).toBe(500);
});
it('should return 400 for an invalid notificationId', async () => {
const response = await supertest(app).post('/api/users/notifications/abc/mark-read').send({});
expect(response.status).toBe(400);
@@ -569,6 +738,15 @@ describe('User Routes (/api/users)', () => {
expect(userService.upsertUserAddress).toHaveBeenCalledWith(expect.anything(), addressData, expectLogger);
});
it('PUT /profile/address should return 500 on a generic service error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(userService.upsertUserAddress).mockRejectedValue(dbError);
const response = await supertest(app)
.put('/api/users/profile/address')
.send({ address_line_1: '123 New St' });
expect(response.status).toBe(500);
});
});
describe('POST /profile/avatar', () => {
@@ -588,6 +766,16 @@ describe('User Routes (/api/users)', () => {
expect(db.userRepo.updateUserProfile).toHaveBeenCalledWith(mockUserProfile.user_id, { avatar_url: expect.any(String) }, expectLogger);
});
it('should return 500 if updating the profile fails after upload', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.userRepo.updateUserProfile).mockRejectedValue(dbError);
const dummyImagePath = 'test-avatar.png';
const response = await supertest(app)
.post('/api/users/profile/avatar')
.attach('avatar', Buffer.from('dummy-image-content'), dummyImagePath);
expect(response.status).toBe(500);
});
it('should return 400 if a non-image file is uploaded', async () => {
const dummyTextPath = 'document.txt';
@@ -622,6 +810,13 @@ describe('User Routes (/api/users)', () => {
expect(db.recipeRepo.deleteRecipe).toHaveBeenCalledWith(1, mockUserProfile.user_id, false, expectLogger);
});
it('DELETE /recipes/:recipeId should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.recipeRepo.deleteRecipe).mockRejectedValue(dbError);
const response = await supertest(app).delete('/api/users/recipes/1');
expect(response.status).toBe(500);
});
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 };
@@ -642,12 +837,26 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(404);
});
it('PUT /recipes/:recipeId should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.recipeRepo.updateRecipe).mockRejectedValue(dbError);
const response = await supertest(app).put('/api/users/recipes/1').send({ name: 'New Name' });
expect(response.status).toBe(500);
});
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');
});
it('GET /shopping-lists/:listId should return 500 on a generic database error', async () => {
const dbError = new Error('DB Connection Failed');
vi.mocked(db.shoppingRepo.getShoppingListById).mockRejectedValue(dbError);
const response = await supertest(app).get('/api/users/shopping-lists/1');
expect(response.status).toBe(500);
});
}); // End of Recipe Routes
});
});