more fixin tests
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

This commit is contained in:
2025-12-22 08:47:18 -08:00
parent fee55b0afd
commit ed857f588a
5 changed files with 1581 additions and 900 deletions

View File

@@ -33,13 +33,19 @@ import type { Profile, ActivityLogItem, SearchQuery, UserProfile } from '../../t
// Update mocks to put methods on prototype so spyOn works in exportUserData tests
vi.mock('./shopping.db', () => ({
ShoppingRepository: class {
getShoppingLists() { return Promise.resolve([]); }
createShoppingList() { return Promise.resolve({}); }
getShoppingLists() {
return Promise.resolve([]);
}
createShoppingList() {
return Promise.resolve({});
}
},
}));
vi.mock('./personalization.db', () => ({
PersonalizationRepository: class {
getWatchedItems() { return Promise.resolve([]); }
getWatchedItems() {
return Promise.resolve([]);
}
},
}));
@@ -56,7 +62,10 @@ describe('User DB Service', () => {
vi.clearAllMocks();
userRepo = new UserRepository(mockPoolInstance as unknown as PoolClient);
// Provide a default mock implementation for withTransaction for all tests.
vi.mocked(withTransaction).mockImplementation(async (callback: (client: PoolClient) => Promise<unknown>) => callback(mockPoolInstance as unknown as PoolClient));
vi.mocked(withTransaction).mockImplementation(
async (callback: (client: PoolClient) => Promise<unknown>) =>
callback(mockPoolInstance as unknown as PoolClient),
);
});
describe('findUserByEmail', () => {
@@ -66,22 +75,33 @@ describe('User DB Service', () => {
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(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('FROM public.users WHERE email = $1'),
['test@example.com'],
);
expect(result).toEqual(mockUser);
});
it('should return undefined if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
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(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('FROM public.users WHERE email = $1'),
['notfound@example.com'],
);
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.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');
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',
);
});
});
@@ -91,8 +111,15 @@ describe('User DB Service', () => {
const now = new Date().toISOString();
// This is the flat structure returned by the DB query inside createUser
const mockDbProfile = {
user_id: 'new-user-id', email: 'new@example.com', role: 'user', full_name: 'New User',
avatar_url: null, points: 0, preferences: null, created_at: now, updated_at: now
user_id: 'new-user-id',
email: 'new@example.com',
role: 'user',
full_name: 'New User',
avatar_url: null,
points: 0,
preferences: null,
created_at: now,
updated_at: now,
};
// This is the nested structure the function is expected to return
const expectedProfile: UserProfile = {
@@ -115,15 +142,26 @@ describe('User DB Service', () => {
return callback(mockClient as unknown as PoolClient);
});
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' }, mockLogger);
const result = await userRepo.createUser(
'new@example.com',
'hashedpass',
{ full_name: 'New User' },
mockLogger,
);
console.log('[TEST DEBUG] createUser - Result from function:', JSON.stringify(result, null, 2));
console.log('[TEST DEBUG] createUser - Expected result:', JSON.stringify(expectedProfile, null, 2));
console.log(
'[TEST DEBUG] createUser - Result from function:',
JSON.stringify(result, null, 2),
);
console.log(
'[TEST DEBUG] createUser - Expected result:',
JSON.stringify(expectedProfile, null, 2),
);
// Use objectContaining because the real implementation might have other DB-generated fields.
expect(result).toEqual(expect.objectContaining(expectedProfile));
expect(withTransaction).toHaveBeenCalledTimes(1);
});
});
it('should rollback the transaction if creating the user fails', async () => {
const dbError = new Error('User insert failed');
@@ -134,8 +172,13 @@ describe('User DB Service', () => {
throw dbError;
});
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');
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 () => {
@@ -151,8 +194,13 @@ describe('User DB Service', () => {
throw dbError;
});
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');
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 () => {
@@ -174,7 +222,9 @@ describe('User DB Service', () => {
}
expect(withTransaction).toHaveBeenCalledTimes(1);
expect(mockLogger.warn).toHaveBeenCalledWith(`Attempted to create a user with an existing email: exists@example.com`);
expect(mockLogger.warn).toHaveBeenCalledWith(
`Attempted to create a user with an existing email: exists@example.com`,
);
});
it('should throw an error if profile is not found after user creation', async () => {
@@ -187,12 +237,19 @@ describe('User DB Service', () => {
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user succeeds
.mockResolvedValueOnce({ rows: [] }); // SELECT profile returns nothing
// The callback will throw, which is caught and re-thrown by withTransaction
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow('Failed to create or retrieve user profile after registration.');
await expect(callback(mockClient as unknown as PoolClient)).rejects.toThrow(
'Failed to create or retrieve user profile after registration.',
);
throw new Error('Internal failure'); // Simulate re-throw from withTransaction
});
await expect(userRepo.createUser('no-profile@example.com', 'pass', {}, mockLogger)).rejects.toThrow('Failed to create user in database.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), email: 'no-profile@example.com' }, 'Error during createUser transaction');
await expect(
userRepo.createUser('no-profile@example.com', 'pass', {}, mockLogger),
).rejects.toThrow('Failed to create user in database.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: expect.any(Error), email: 'no-profile@example.com' },
'Error during createUser transaction',
);
});
});
@@ -218,7 +275,6 @@ describe('User DB Service', () => {
mockPoolInstance.query.mockResolvedValue({ rows: [mockDbResult] });
const expectedResult = {
user_id: '123',
full_name: 'Test User',
avatar_url: null,
role: 'user',
@@ -228,7 +284,6 @@ describe('User DB Service', () => {
created_at: now,
updated_at: now,
user: { user_id: '123', email: 'test@example.com' },
email: 'test@example.com',
password_hash: 'hash',
failed_login_attempts: 0,
last_failed_login: null,
@@ -237,10 +292,19 @@ describe('User DB Service', () => {
const result = await userRepo.findUserWithProfileByEmail('test@example.com', mockLogger);
console.log('[TEST DEBUG] findUserWithProfileByEmail - Result from function:', JSON.stringify(result, null, 2));
console.log('[TEST DEBUG] findUserWithProfileByEmail - Expected result:', JSON.stringify(expectedResult, null, 2));
console.log(
'[TEST DEBUG] findUserWithProfileByEmail - Result from function:',
JSON.stringify(result, null, 2),
);
console.log(
'[TEST DEBUG] findUserWithProfileByEmail - Expected result:',
JSON.stringify(expectedResult, null, 2),
);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('JOIN public.profiles'), ['test@example.com']);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('JOIN public.profiles'),
['test@example.com'],
);
expect(result).toEqual(expect.objectContaining(expectedResult));
});
@@ -253,8 +317,13 @@ 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.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');
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',
);
});
});
@@ -262,40 +331,65 @@ describe('User DB Service', () => {
it('should query for a user by their ID', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }], rowCount: 1 });
await userRepo.findUserById('123', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE user_id = $1'), ['123']);
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: [], rowCount: 0 });
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow('User with ID not-found-id not found.');
await expect(userRepo.findUserById('not-found-id', mockLogger)).rejects.toThrow(
'User with ID not-found-id not found.',
);
});
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', mockLogger)).rejects.toThrow('Failed to retrieve user by ID from database.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in findUserById');
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' }], rowCount: 1 });
mockPoolInstance.query.mockResolvedValue({
rows: [{ user_id: '123', password_hash: 'hash' }],
rowCount: 1,
});
await userRepo.findUserWithPasswordHashById('123', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('SELECT user_id, email, password_hash'), ['123']);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('SELECT user_id, email, password_hash'),
['123'],
);
});
it('should throw NotFoundError if user is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(userRepo.findUserWithPasswordHashById('not-found-id', mockLogger)).rejects.toThrow(NotFoundError);
await expect(userRepo.findUserWithPasswordHashById('not-found-id', mockLogger)).rejects.toThrow('User with ID not-found-id not found.');
await expect(
userRepo.findUserWithPasswordHashById('not-found-id', mockLogger),
).rejects.toThrow(NotFoundError);
await expect(
userRepo.findUserWithPasswordHashById('not-found-id', mockLogger),
).rejects.toThrow('User with ID not-found-id not found.');
});
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', 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');
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',
);
});
});
@@ -304,52 +398,92 @@ describe('User DB Service', () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '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']);
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.findUserProfileById('not-found-id', mockLogger)).rejects.toThrow('Profile not found for this user.');
await expect(userRepo.findUserProfileById('not-found-id', mockLogger)).rejects.toThrow(
'Profile not found for this user.',
);
});
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', mockLogger)).rejects.toThrow('Failed to retrieve user profile from database.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: '123' }, 'Database error in findUserProfileById');
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',
);
});
});
describe('updateUserProfile', () => {
it('should execute an UPDATE query for the user profile', async () => {
const mockProfile: Profile = { full_name: 'Updated Name', role: 'user', points: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
const mockProfile: Profile = {
full_name: 'Updated Name',
role: 'user',
points: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
await userRepo.updateUserProfile('123', { full_name: 'Updated Name' }, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.profiles'), expect.any(Array));
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('UPDATE public.profiles'),
expect.any(Array),
);
});
it('should execute an UPDATE query for avatar_url', async () => {
const mockProfile: Profile = { avatar_url: 'new-avatar.png', role: 'user', points: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
const mockProfile: Profile = {
avatar_url: 'new-avatar.png',
role: 'user',
points: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
await userRepo.updateUserProfile('123', { avatar_url: 'new-avatar.png' }, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('avatar_url = $1'), ['new-avatar.png', '123']);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('avatar_url = $1'),
['new-avatar.png', '123'],
);
});
it('should execute an UPDATE query for address_id', async () => {
const mockProfile: Profile = { address_id: 99, role: 'user', points: 0, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
const mockProfile: Profile = {
address_id: 99,
role: 'user',
points: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
await userRepo.updateUserProfile('123', { address_id: 99 }, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('address_id = $1'), [99, '123']);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('address_id = $1'),
[99, '123'],
);
});
it('should fetch the current profile if no update fields are provided', async () => {
const mockProfile: Profile = createMockUserProfile({ user: { user_id: '123', email: '123@example.com' }, full_name: 'Current Name' });
const mockProfile: Profile = createMockUserProfile({
user: { user_id: '123', email: '123@example.com' },
full_name: 'Current Name',
});
// FIX: Instead of mocking `mockResolvedValue` on the instance method which might fail if not spied correctly,
// we mock the underlying `db.query` call that `findUserProfileById` makes.
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile] });
@@ -357,20 +491,30 @@ describe('User DB Service', () => {
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));
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('SELECT'),
expect.any(Array),
);
expect(result).toEqual(mockProfile);
});
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' }, mockLogger)).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' }, 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');
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',
);
});
});
@@ -378,19 +522,29 @@ describe('User DB Service', () => {
it('should execute an UPDATE query for user preferences', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{}] });
await userRepo.updateUserPreferences('123', { darkMode: true }, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining("SET preferences = COALESCE(preferences, '{}'::jsonb) || $1"), [{ darkMode: true }, '123']);
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 }, mockLogger)).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 }, 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');
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',
);
});
});
@@ -398,13 +552,21 @@ describe('User DB Service', () => {
it('should execute an UPDATE query for the user password', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await userRepo.updateUserPassword('123', 'newhash', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.users SET password_hash = $1 WHERE user_id = $2', ['newhash', '123']);
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', mockLogger)).rejects.toThrow('Failed to update user password in database.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in updateUserPassword');
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',
);
});
});
@@ -412,13 +574,21 @@ describe('User DB Service', () => {
it('should execute a DELETE query for the user', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await userRepo.deleteUserById('123', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.users WHERE user_id = $1', ['123']);
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', mockLogger)).rejects.toThrow('Failed to delete user from database.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in deleteUserById');
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',
);
});
});
@@ -426,13 +596,21 @@ describe('User DB Service', () => {
it('should execute an UPDATE query to save the refresh token', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
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']);
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', mockLogger)).rejects.toThrow('Failed to save refresh token.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error), userId: '123' }, 'Database error in saveRefreshToken');
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',
);
});
});
@@ -440,22 +618,34 @@ describe('User DB Service', () => {
it('should query for a user by their refresh token', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [{ user_id: '123' }], rowCount: 1 });
await userRepo.findUserByRefreshToken('a-token', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE refresh_token = $1'), ['a-token']);
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: [], rowCount: 0 });
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.');
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.',
);
});
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.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow('Failed to find user by refresh token.');
await expect(userRepo.findUserByRefreshToken('a-token', mockLogger)).rejects.toThrow(
'Failed to find user by refresh token.',
);
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in findUserByRefreshToken');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError },
'Database error in findUserByRefreshToken',
);
});
});
@@ -464,7 +654,8 @@ describe('User DB Service', () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await userRepo.deleteRefreshToken('a-token', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'UPDATE public.users SET refresh_token = NULL WHERE refresh_token = $1', ['a-token']
'UPDATE public.users SET refresh_token = NULL WHERE refresh_token = $1',
['a-token'],
);
});
@@ -474,7 +665,10 @@ describe('User DB Service', () => {
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');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: expect.any(Error) },
'Database error in deleteRefreshToken',
);
});
});
@@ -483,23 +677,36 @@ describe('User DB Service', () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
const expires = new Date();
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]);
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],
);
});
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const dbError = new Error('violates foreign key constraint');
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(userRepo.createPasswordResetToken('non-existent-user', 'hash', new Date(), mockLogger)).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 () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
const expires = new Date();
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');
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',
);
});
});
@@ -507,13 +714,20 @@ describe('User DB Service', () => {
it('should query for tokens where expires_at > NOW()', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await userRepo.getValidResetTokens(mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE expires_at > NOW()'));
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(mockLogger)).rejects.toThrow('Failed to retrieve valid reset tokens.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: expect.any(Error) }, 'Database error in getValidResetTokens');
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',
);
});
});
@@ -521,13 +735,19 @@ describe('User DB Service', () => {
it('should execute a DELETE query for the token hash', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
await userRepo.deleteResetToken('token-hash', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.password_reset_tokens WHERE token_hash = $1', ['token-hash']);
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');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: expect.any(Error), tokenHash: 'token-hash' },
'Database error in deleteResetToken',
);
});
});
@@ -535,16 +755,25 @@ describe('User DB Service', () => {
it('should execute a DELETE query for expired tokens and return the count', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 5 });
const result = await userRepo.deleteExpiredResetTokens(mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.password_reset_tokens WHERE expires_at < NOW()');
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'DELETE FROM public.password_reset_tokens WHERE expires_at < NOW()',
);
expect(result).toBe(5);
expect(mockLogger.info).toHaveBeenCalledWith('[DB deleteExpiredResetTokens] Deleted 5 expired password reset tokens.');
expect(mockLogger.info).toHaveBeenCalledWith(
'[DB deleteExpiredResetTokens] Deleted 5 expired password reset tokens.',
);
});
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.deleteExpiredResetTokens(mockLogger)).rejects.toThrow('Failed to delete expired password reset tokens.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in deleteExpiredResetTokens');
await expect(userRepo.deleteExpiredResetTokens(mockLogger)).rejects.toThrow(
'Failed to delete expired password reset tokens.',
);
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError },
'Database error in deleteExpiredResetTokens',
);
});
});
@@ -562,7 +791,9 @@ describe('User DB Service', () => {
const { PersonalizationRepository } = await import('./personalization.db');
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById');
findProfileSpy.mockResolvedValue(createMockUserProfile({ user: { user_id: '123', email: '123@example.com' } }));
findProfileSpy.mockResolvedValue(
createMockUserProfile({ user: { user_id: '123', email: '123@example.com' } }),
);
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems');
getWatchedItemsSpy.mockResolvedValue([]);
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists');
@@ -583,19 +814,27 @@ describe('User DB Service', () => {
// Arrange: Mock findUserProfileById to throw a NotFoundError, as per its contract (ADR-001).
// The exportUserData function will catch this and re-throw a generic error.
const { NotFoundError } = await import('./errors.db');
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(new NotFoundError('Profile not found'));
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(
new NotFoundError('Profile not found'),
);
// Act & Assert: The outer function catches the NotFoundError and re-throws it.
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Failed to export user data.');
await expect(exportUserData('123', mockLogger)).rejects.toThrow(
'Failed to export user data.',
);
expect(withTransaction).toHaveBeenCalledTimes(1);
});
it('should throw an error if the database query fails', async () => {
// Arrange: Force a failure in one of the parallel calls
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(new Error('DB Error'));
// Arrange: Force a failure in one of the parallel calls
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(
new Error('DB Error'),
);
// Act & Assert
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Failed to export user data.');
await expect(exportUserData('123', mockLogger)).rejects.toThrow(
'Failed to export user data.',
);
expect(withTransaction).toHaveBeenCalledTimes(1);
});
});
@@ -606,7 +845,7 @@ describe('User DB Service', () => {
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']
['follower-1', 'following-1'],
);
});
@@ -614,14 +853,21 @@ describe('User DB Service', () => {
const dbError = new Error('violates foreign key constraint');
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(userRepo.followUser('follower-1', 'non-existent-user', mockLogger)).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', mockLogger)).rejects.toThrow('Failed to follow user.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, followerId: 'follower-1', followingId: 'following-1' }, 'Database error in followUser');
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',
);
});
});
@@ -631,15 +877,20 @@ describe('User DB Service', () => {
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']
['follower-1', 'following-1'],
);
});
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', mockLogger)).rejects.toThrow('Failed to unfollow user.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, followerId: 'follower-1', followingId: 'following-1' }, 'Database error in unfollowUser');
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',
);
});
});
@@ -662,7 +913,7 @@ describe('User DB Service', () => {
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('FROM public.activity_log al'),
['user-123', 10, 0]
['user-123', 10, 0],
);
expect(result).toEqual(mockFeedItems);
});
@@ -676,8 +927,13 @@ 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.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');
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',
);
});
});
@@ -700,12 +956,7 @@ describe('User DB Service', () => {
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'INSERT INTO public.search_queries (user_id, query_text, result_count, was_successful) VALUES ($1, $2, $3, $4) RETURNING *',
[
queryData.user_id,
queryData.query_text,
queryData.result_count,
queryData.was_successful,
]
[queryData.user_id, queryData.query_text, queryData.result_count, queryData.was_successful],
);
expect(result).toEqual(mockLoggedQuery);
});
@@ -726,14 +977,24 @@ describe('User DB Service', () => {
await userRepo.logSearchQuery(queryData, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), [null, 'anonymous search', 10, true]);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), [
null,
'anonymous search',
10,
true,
]);
});
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' }, mockLogger)).rejects.toThrow('Failed to log search query.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, queryData: { query_text: 'fail' } }, 'Database error in logSearchQuery');
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',
);
});
});
});
});