Refactor geocoding services and improve logging
- Updated the Nominatim geocoding service to use a class-based structure and accept a logger instance for better logging control. - Modified tests for the Nominatim service to align with the new structure and improved logging assertions. - Removed the disabled notification service test file. - Added a new GeocodingFailedError class to handle geocoding failures more explicitly. - Enhanced error logging in the queue service to include structured error objects. - Updated user service to accept a logger instance for better logging in address upsert operations. - Added request-scoped logger to Express Request interface for type-safe logging in route handlers. - Improved logging in utility functions for better debugging and error tracking. - Created a new GoogleGeocodingService class for Google Maps geocoding with structured logging. - Added tests for the useAiAnalysis hook to ensure proper functionality and error handling.
This commit is contained in:
@@ -19,6 +19,7 @@ vi.mock('../logger.server', () => ({
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
import { logger as mockLogger } from '../logger.server';
|
||||
|
||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||
vi.unmock('./user.db');
|
||||
@@ -70,7 +71,7 @@ describe('User DB Service', () => {
|
||||
const mockUser = { user_id: '123', email: 'test@example.com' };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockUser] });
|
||||
|
||||
const result = await userRepo.findUserByEmail('test@example.com');
|
||||
const result = await userRepo.findUserByEmail('test@example.com', mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE email = $1'), ['test@example.com']);
|
||||
expect(result).toEqual(mockUser);
|
||||
@@ -78,7 +79,7 @@ describe('User DB Service', () => {
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserByEmail('notfound@example.com');
|
||||
const result = await userRepo.findUserByEmail('notfound@example.com', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE email = $1'), ['notfound@example.com']);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
@@ -86,7 +87,8 @@ describe('User DB Service', () => {
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.findUserByEmail('test@example.com')).rejects.toThrow('Failed to retrieve user from database.');
|
||||
await expect(userRepo.findUserByEmail('test@example.com', mockLogger)).rejects.toThrow('Failed to retrieve user from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'test@example.com' }, 'Database error in findUserByEmail');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +106,7 @@ describe('User DB Service', () => {
|
||||
return callback(mockClient as any);
|
||||
});
|
||||
|
||||
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' });
|
||||
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' }, mockLogger);
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
@@ -119,7 +121,8 @@ describe('User DB Service', () => {
|
||||
throw dbError;
|
||||
});
|
||||
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {}, mockLogger)).rejects.toThrow('Failed to create user in database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'fail@example.com' }, 'Error during createUser transaction');
|
||||
});
|
||||
|
||||
it('should rollback the transaction if fetching the final profile fails', async () => {
|
||||
@@ -135,7 +138,8 @@ describe('User DB Service', () => {
|
||||
throw dbError;
|
||||
});
|
||||
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {}, mockLogger)).rejects.toThrow('Failed to create user in database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'fail@example.com' }, 'Error during createUser transaction');
|
||||
});
|
||||
|
||||
it('should throw UniqueConstraintError if the email already exists', async () => {
|
||||
@@ -145,7 +149,7 @@ describe('User DB Service', () => {
|
||||
vi.mocked(withTransaction).mockRejectedValue(dbError);
|
||||
|
||||
try {
|
||||
await userRepo.createUser('exists@example.com', 'pass', {});
|
||||
await userRepo.createUser('exists@example.com', 'pass', {}, mockLogger);
|
||||
expect.fail('Expected createUser to throw UniqueConstraintError');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(UniqueConstraintError);
|
||||
@@ -153,6 +157,7 @@ describe('User DB Service', () => {
|
||||
}
|
||||
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'exists@example.com' }, 'Error during createUser transaction');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +166,7 @@ describe('User DB Service', () => {
|
||||
const mockUserWithProfile = { user_id: '123', email: 'test@example.com', full_name: 'Test User', role: 'user' };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockUserWithProfile] });
|
||||
|
||||
const result = await userRepo.findUserWithProfileByEmail('test@example.com');
|
||||
const result = await userRepo.findUserWithProfileByEmail('test@example.com', mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('JOIN public.profiles'), ['test@example.com']);
|
||||
expect(result).toEqual(mockUserWithProfile);
|
||||
@@ -169,72 +174,76 @@ describe('User DB Service', () => {
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserWithProfileByEmail('notfound@example.com');
|
||||
const result = await userRepo.findUserWithProfileByEmail('notfound@example.com', mockLogger);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.findUserWithProfileByEmail('test@example.com')).rejects.toThrow('Failed to retrieve user with profile from database.');
|
||||
await expect(userRepo.findUserWithProfileByEmail('test@example.com', mockLogger)).rejects.toThrow('Failed to retrieve user with profile from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, email: 'test@example.com' }, 'Database error in findUserWithProfileByEmail');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserById', () => {
|
||||
it('should query for a user by their ID', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }] });
|
||||
await userRepo.findUserById('123');
|
||||
await userRepo.findUserById('123', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE user_id = $1'), ['123']);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await expect(userRepo.findUserById('not-found-id')).rejects.toThrow(NotFoundError);
|
||||
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.findUserById('123')).rejects.toThrow('Failed to retrieve user by ID from database.');
|
||||
await expect(userRepo.findUserById('123', mockLogger)).rejects.toThrow('Failed to retrieve user by ID from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in findUserById');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserWithPasswordHashById', () => {
|
||||
it('should query for a user and their password hash by ID', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123', password_hash: 'hash' }] });
|
||||
await userRepo.findUserWithPasswordHashById('123');
|
||||
await userRepo.findUserWithPasswordHashById('123', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('SELECT user_id, email, password_hash'), ['123']);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.findUserWithPasswordHashById('123')).rejects.toThrow('Failed to retrieve user with sensitive data by ID from database.');
|
||||
await expect(userRepo.findUserWithPasswordHashById('123', mockLogger)).rejects.toThrow('Failed to retrieve user with sensitive data by ID from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in findUserWithPasswordHashById');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserProfileById', () => {
|
||||
it('should query for a user profile by user ID', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }] });
|
||||
await userRepo.findUserProfileById('123');
|
||||
await userRepo.findUserProfileById('123', mockLogger);
|
||||
// The actual query uses 'p.user_id' due to the join alias
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE p.user_id = $1'), ['123']);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if user profile is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
|
||||
await expect(userRepo.findUserById('not-found-id')).rejects.toThrow('User with ID not-found-id not found.');
|
||||
await expect(userRepo.findUserProfileById('not-found-id', mockLogger)).rejects.toThrow('Profile not found for this user.');
|
||||
});
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserWithPasswordHashById('not-found-id');
|
||||
const result = await userRepo.findUserWithPasswordHashById('not-found-id', mockLogger);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Connection Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.findUserProfileById('123')).rejects.toThrow('Failed to retrieve user profile from database.');
|
||||
await expect(userRepo.findUserProfileById('123', mockLogger)).rejects.toThrow('Failed to retrieve user profile from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in findUserProfileById');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,7 +252,7 @@ describe('User DB Service', () => {
|
||||
const mockProfile: Profile = { user_id: '123', full_name: 'Updated Name', role: 'user', points: 0 };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
|
||||
|
||||
await userRepo.updateUserProfile('123', { full_name: 'Updated Name' });
|
||||
await userRepo.updateUserProfile('123', { full_name: 'Updated Name' }, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.profiles'), expect.any(Array));
|
||||
});
|
||||
@@ -252,7 +261,7 @@ describe('User DB Service', () => {
|
||||
const mockProfile: Profile = { user_id: '123', avatar_url: 'new-avatar.png', role: 'user', points: 0 };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
|
||||
|
||||
await userRepo.updateUserProfile('123', { avatar_url: 'new-avatar.png' });
|
||||
await userRepo.updateUserProfile('123', { avatar_url: 'new-avatar.png' }, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('avatar_url = $1'), ['new-avatar.png', '123']);
|
||||
});
|
||||
@@ -261,7 +270,7 @@ describe('User DB Service', () => {
|
||||
const mockProfile: Profile = { user_id: '123', address_id: 99, role: 'user', points: 0 };
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
|
||||
|
||||
await userRepo.updateUserProfile('123', { address_id: 99 });
|
||||
await userRepo.updateUserProfile('123', { address_id: 99 }, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('address_id = $1'), [99, '123']);
|
||||
});
|
||||
@@ -272,7 +281,7 @@ describe('User DB Service', () => {
|
||||
// we mock the underlying `db.query` call that `findUserProfileById` makes.
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
|
||||
|
||||
const result = await userRepo.updateUserProfile('123', { full_name: undefined });
|
||||
const result = await userRepo.updateUserProfile('123', { full_name: undefined }, mockLogger);
|
||||
|
||||
// Check that it calls query for finding profile (since no updates were made)
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('SELECT'), expect.any(Array));
|
||||
@@ -282,91 +291,96 @@ describe('User DB Service', () => {
|
||||
it('should throw an error if the user to update is not found', async () => {
|
||||
// Simulate the DB returning 0 rows affected
|
||||
mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [] });
|
||||
await expect(userRepo.updateUserProfile('999', { full_name: 'Fail' })).rejects.toThrow('User not found or user does not have permission to update.');
|
||||
await expect(userRepo.updateUserProfile('999', { full_name: 'Fail' }, mockLogger)).rejects.toThrow('User not found or user does not have permission to update.');
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.updateUserProfile('123', { full_name: 'Fail' })).rejects.toThrow('Failed to update user profile in database.');
|
||||
await expect(userRepo.updateUserProfile('123', { full_name: 'Fail' }, mockLogger)).rejects.toThrow('Failed to update user profile in database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123', profileData: { full_name: 'Fail' } }, 'Database error in updateUserProfile');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserPreferences', () => {
|
||||
it('should execute an UPDATE query for user preferences', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [{}] });
|
||||
await userRepo.updateUserPreferences('123', { darkMode: true });
|
||||
await userRepo.updateUserPreferences('123', { darkMode: true }, mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining("SET preferences = COALESCE(preferences, '{}'::jsonb) || $1"), [{ darkMode: true }, '123']);
|
||||
});
|
||||
|
||||
it('should throw an error if the user to update is not found', async () => {
|
||||
// Simulate the DB returning 0 rows affected
|
||||
mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [] });
|
||||
await expect(userRepo.updateUserPreferences('999', { darkMode: true })).rejects.toThrow('User not found or user does not have permission to update.');
|
||||
await expect(userRepo.updateUserPreferences('999', { darkMode: true }, mockLogger)).rejects.toThrow('User not found or user does not have permission to update.');
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.updateUserPreferences('123', { darkMode: true })).rejects.toThrow('Failed to update user preferences in database.');
|
||||
await expect(userRepo.updateUserPreferences('123', { darkMode: true }, mockLogger)).rejects.toThrow('Failed to update user preferences in database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123', preferences: { darkMode: true } }, 'Database error in updateUserPreferences');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserPassword', () => {
|
||||
it('should execute an UPDATE query for the user password', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.updateUserPassword('123', 'newhash');
|
||||
await userRepo.updateUserPassword('123', 'newhash', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.users SET password_hash = $1 WHERE user_id = $2', ['newhash', '123']);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.updateUserPassword('123', 'newhash')).rejects.toThrow('Failed to update user password in database.');
|
||||
await expect(userRepo.updateUserPassword('123', 'newhash', mockLogger)).rejects.toThrow('Failed to update user password in database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in updateUserPassword');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserById', () => {
|
||||
it('should execute a DELETE query for the user', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.deleteUserById('123');
|
||||
await userRepo.deleteUserById('123', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.users WHERE user_id = $1', ['123']);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.deleteUserById('123')).rejects.toThrow('Failed to delete user from database.');
|
||||
await expect(userRepo.deleteUserById('123', mockLogger)).rejects.toThrow('Failed to delete user from database.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in deleteUserById');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveRefreshToken', () => {
|
||||
it('should execute an UPDATE query to save the refresh token', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.saveRefreshToken('123', 'new-token');
|
||||
await userRepo.saveRefreshToken('123', 'new-token', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.users SET refresh_token = $1 WHERE user_id = $2', ['new-token', '123']);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.saveRefreshToken('123', 'new-token')).rejects.toThrow('Failed to save refresh token.');
|
||||
await expect(userRepo.saveRefreshToken('123', 'new-token', mockLogger)).rejects.toThrow('Failed to save refresh token.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in saveRefreshToken');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserByRefreshToken', () => {
|
||||
it('should query for a user by their refresh token', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }] });
|
||||
await userRepo.findUserByRefreshToken('a-token');
|
||||
await userRepo.findUserByRefreshToken('a-token', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE refresh_token = $1'), ['a-token']);
|
||||
});
|
||||
|
||||
it('should throw NotFoundError if token is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await expect(userRepo.findUserByRefreshToken('a-token')).rejects.toThrow(NotFoundError);
|
||||
await expect(userRepo.findUserByRefreshToken('a-token')).rejects.toThrow('User not found for the given refresh token.');
|
||||
await expect(userRepo.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow(NotFoundError);
|
||||
await expect(userRepo.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow('User not found for the given refresh token.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRefreshToken', () => {
|
||||
it('should execute an UPDATE query to set the refresh token to NULL', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.deleteRefreshToken('a-token');
|
||||
await userRepo.deleteRefreshToken('a-token', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
'UPDATE public.users SET refresh_token = NULL WHERE refresh_token = $1', ['a-token']
|
||||
);
|
||||
@@ -375,9 +389,10 @@ describe('User DB Service', () => {
|
||||
it('should not throw an error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
// The function is designed to swallow errors, so we expect it to resolve.
|
||||
await expect(userRepo.deleteRefreshToken('a-token')).resolves.toBeUndefined();
|
||||
await expect(userRepo.deleteRefreshToken('a-token', mockLogger)).resolves.toBeUndefined();
|
||||
// We can still check that the query was attempted.
|
||||
expect(mockPoolInstance.query).toHaveBeenCalled();
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error) }, 'Database error in deleteRefreshToken');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -385,7 +400,7 @@ describe('User DB Service', () => {
|
||||
it('should execute DELETE and INSERT queries', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const expires = new Date();
|
||||
await userRepo.createPasswordResetToken('123', 'token-hash', expires);
|
||||
await userRepo.createPasswordResetToken('123', 'token-hash', expires, mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.password_reset_tokens WHERE user_id = $1', ['123']);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.password_reset_tokens'), ['123', 'token-hash', expires]);
|
||||
});
|
||||
@@ -394,35 +409,44 @@ describe('User DB Service', () => {
|
||||
const dbError = new Error('violates foreign key constraint');
|
||||
(dbError as any).code = '23503';
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.createPasswordResetToken('non-existent-user', 'hash', new Date())).rejects.toThrow(ForeignKeyConstraintError);
|
||||
await expect(userRepo.createPasswordResetToken('non-existent-user', 'hash', new Date(), mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
const dbError = new Error('DB Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
const expires = new Date();
|
||||
await expect(userRepo.createPasswordResetToken('123', 'token-hash', expires)).rejects.toThrow('Failed to create password reset token.');
|
||||
await expect(userRepo.createPasswordResetToken('123', 'token-hash', expires, mockLogger)).rejects.toThrow('Failed to create password reset token.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in createPasswordResetToken');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidResetTokens', () => {
|
||||
it('should query for tokens where expires_at > NOW()', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.getValidResetTokens();
|
||||
await userRepo.getValidResetTokens(mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE expires_at > NOW()'));
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await expect(userRepo.getValidResetTokens()).rejects.toThrow('Failed to retrieve valid reset tokens.');
|
||||
await expect(userRepo.getValidResetTokens(mockLogger)).rejects.toThrow('Failed to retrieve valid reset tokens.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error) }, 'Database error in getValidResetTokens');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteResetToken', () => {
|
||||
it('should execute a DELETE query for the token hash', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.deleteResetToken('token-hash');
|
||||
await userRepo.deleteResetToken('token-hash', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.password_reset_tokens WHERE token_hash = $1', ['token-hash']);
|
||||
});
|
||||
|
||||
it('should log an error if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
await userRepo.deleteResetToken('token-hash', mockLogger);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), tokenHash: 'token-hash' }, 'Database error in deleteResetToken');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportUserData', () => {
|
||||
@@ -438,21 +462,21 @@ describe('User DB Service', () => {
|
||||
const { PersonalizationRepository } = await import('./personalization.db');
|
||||
|
||||
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById')
|
||||
.mockResolvedValue({ user_id: '123' } as Profile);
|
||||
.mockResolvedValue({ user_id: '123' } as any);
|
||||
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems')
|
||||
.mockResolvedValue([]);
|
||||
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await exportUserData('123');
|
||||
await exportUserData('123', mockLogger);
|
||||
|
||||
// Verify that withTransaction was called
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify the repository methods were called inside the transaction
|
||||
expect(findProfileSpy).toHaveBeenCalledWith('123');
|
||||
expect(getWatchedItemsSpy).toHaveBeenCalledWith('123');
|
||||
expect(getShoppingListsSpy).toHaveBeenCalledWith('123');
|
||||
expect(findProfileSpy).toHaveBeenCalledWith('123', expect.any(Object));
|
||||
expect(getWatchedItemsSpy).toHaveBeenCalledWith('123', expect.any(Object));
|
||||
expect(getShoppingListsSpy).toHaveBeenCalledWith('123', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should throw an error if the user profile is not found', async () => {
|
||||
@@ -462,7 +486,7 @@ describe('User DB Service', () => {
|
||||
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(new NotFoundError('Profile not found'));
|
||||
|
||||
// Act & Assert: The outer function catches the NotFoundError and re-throws a generic one.
|
||||
await expect(exportUserData('123')).rejects.toThrow('Failed to export user data.');
|
||||
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Failed to export user data.');
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -471,7 +495,7 @@ describe('User DB Service', () => {
|
||||
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(new Error('DB Error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(exportUserData('123')).rejects.toThrow('Failed to export user data.');
|
||||
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Failed to export user data.');
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -479,7 +503,7 @@ describe('User DB Service', () => {
|
||||
describe('followUser', () => {
|
||||
it('should execute an INSERT query to create a follow relationship', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.followUser('follower-1', 'following-1');
|
||||
await userRepo.followUser('follower-1', 'following-1', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
'INSERT INTO public.user_follows (follower_id, following_id) VALUES ($1, $2) ON CONFLICT (follower_id, following_id) DO NOTHING',
|
||||
['follower-1', 'following-1']
|
||||
@@ -490,20 +514,21 @@ describe('User DB Service', () => {
|
||||
const dbError = new Error('violates foreign key constraint');
|
||||
(dbError as any).code = '23503';
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.followUser('follower-1', 'non-existent-user')).rejects.toThrow(ForeignKeyConstraintError);
|
||||
await expect(userRepo.followUser('follower-1', 'non-existent-user', mockLogger)).rejects.toThrow(ForeignKeyConstraintError);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.followUser('follower-1', 'following-1')).rejects.toThrow('Failed to follow user.');
|
||||
await expect(userRepo.followUser('follower-1', 'following-1', mockLogger)).rejects.toThrow('Failed to follow user.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, followerId: 'follower-1', followingId: 'following-1' }, 'Database error in followUser');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfollowUser', () => {
|
||||
it('should execute a DELETE query to remove a follow relationship', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await userRepo.unfollowUser('follower-1', 'following-1');
|
||||
await userRepo.unfollowUser('follower-1', 'following-1', mockLogger);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
'DELETE FROM public.user_follows WHERE follower_id = $1 AND following_id = $2',
|
||||
['follower-1', 'following-1']
|
||||
@@ -513,7 +538,8 @@ describe('User DB Service', () => {
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.unfollowUser('follower-1', 'following-1')).rejects.toThrow('Failed to unfollow user.');
|
||||
await expect(userRepo.unfollowUser('follower-1', 'following-1', mockLogger)).rejects.toThrow('Failed to unfollow user.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, followerId: 'follower-1', followingId: 'following-1' }, 'Database error in unfollowUser');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -531,7 +557,7 @@ describe('User DB Service', () => {
|
||||
];
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockFeedItems });
|
||||
|
||||
const result = await userRepo.getUserFeed('user-123', 10, 0);
|
||||
const result = await userRepo.getUserFeed('user-123', 10, 0, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('FROM public.activity_log al'),
|
||||
@@ -542,14 +568,15 @@ describe('User DB Service', () => {
|
||||
|
||||
it('should return an empty array if the user feed is empty', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.getUserFeed('user-123', 10, 0);
|
||||
const result = await userRepo.getUserFeed('user-123', 10, 0, mockLogger);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.getUserFeed('user-123', 10, 0)).rejects.toThrow('Failed to retrieve user feed.');
|
||||
await expect(userRepo.getUserFeed('user-123', 10, 0, mockLogger)).rejects.toThrow('Failed to retrieve user feed.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', limit: 10, offset: 0 }, 'Database error in getUserFeed');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -568,7 +595,7 @@ describe('User DB Service', () => {
|
||||
};
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockLoggedQuery] });
|
||||
|
||||
const result = await userRepo.logSearchQuery(queryData);
|
||||
const result = await userRepo.logSearchQuery(queryData, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
'INSERT INTO public.search_queries (user_id, query_text, result_count, was_successful) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||
@@ -596,7 +623,7 @@ describe('User DB Service', () => {
|
||||
};
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockLoggedQuery] });
|
||||
|
||||
await userRepo.logSearchQuery(queryData);
|
||||
await userRepo.logSearchQuery(queryData, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), [null, 'anonymous search', 10, true]);
|
||||
});
|
||||
@@ -604,7 +631,8 @@ describe('User DB Service', () => {
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
const dbError = new Error('DB Error');
|
||||
mockPoolInstance.query.mockRejectedValue(dbError);
|
||||
await expect(userRepo.logSearchQuery({ query_text: 'fail' })).rejects.toThrow('Failed to log search query.');
|
||||
await expect(userRepo.logSearchQuery({ query_text: 'fail' }, mockLogger)).rejects.toThrow('Failed to log search query.');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, queryData: { query_text: 'fail' } }, 'Database error in logSearchQuery');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user