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

@@ -455,6 +455,13 @@ describe('Admin DB Service', () => {
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users u JOIN public.profiles p'));
expect(result).toEqual(mockUsers);
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(adminRepo.getAllUsers(mockLogger)).rejects.toThrow('Failed to retrieve all users.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in getAllUsers');
});
});
describe('updateUserRole', () => {

View File

@@ -184,6 +184,16 @@ describe('Personalization DB Service', () => {
});
});
describe('getDietaryRestrictions', () => {
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockQuery.mockRejectedValue(dbError);
await expect(personalizationRepo.getDietaryRestrictions(mockLogger)).rejects.toThrow('Failed to get dietary restrictions.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in getDietaryRestrictions');
});
});
describe('removeWatchedItem', () => {
it('should execute a DELETE query', async () => {
mockQuery.mockResolvedValue({ rows: [] });
@@ -311,6 +321,21 @@ describe('Personalization DB Service', () => {
});
});
describe('getUserDietaryRestrictions', () => {
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockQuery.mockRejectedValue(dbError);
await expect(personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger)).rejects.toThrow('Failed to get user dietary restrictions.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123' }, 'Database error in getUserDietaryRestrictions');
});
it('should return an empty array if user has no restrictions', async () => {
mockQuery.mockResolvedValue({ rows: [] });
const result = await personalizationRepo.getUserDietaryRestrictions('user-123', mockLogger);
expect(result).toEqual([]);
});
});
describe('findPantryItemOwner', () => {
it('should execute a SELECT query to find the owner', async () => {
mockQuery.mockResolvedValue({ rows: [{ user_id: 'user-123' }] });

View File

@@ -166,6 +166,24 @@ describe('User DB Service', () => {
expect(withTransaction).toHaveBeenCalledTimes(1);
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 () => {
const mockUser = { user_id: 'new-user-id', email: 'no-profile@example.com' };
vi.mocked(withTransaction).mockImplementation(async (callback) => {
const mockClient = { query: vi.fn() };
mockClient.query
.mockResolvedValueOnce({ rows: [] }) // set_config
.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.');
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');
});
});
describe('findUserWithProfileByEmail', () => {
@@ -383,6 +401,15 @@ describe('User DB Service', () => {
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.');
expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in findUserByRefreshToken');
});
});
describe('deleteRefreshToken', () => {
@@ -457,6 +484,23 @@ describe('User DB Service', () => {
});
});
describe('deleteExpiredResetTokens', () => {
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(result).toBe(5);
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');
});
});
describe('exportUserData', () => {
// Import the mocked withTransaction helper
let withTransaction: Mock;
@@ -488,13 +532,13 @@ describe('User DB Service', () => {
expect(getShoppingListsSpy).toHaveBeenCalledWith('123', expect.any(Object));
});
it('should throw an error if the user profile is not found', async () => {
it('should throw NotFoundError if the user profile is not found', async () => {
// 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'));
// Act & Assert: The outer function catches the NotFoundError and re-throws a generic one.
// Act & Assert: The outer function catches the NotFoundError and re-throws it.
await expect(exportUserData('123', mockLogger)).rejects.toThrow('Failed to export user data.');
expect(withTransaction).toHaveBeenCalledTimes(1);
});

View File

@@ -444,6 +444,23 @@ export class UserRepository {
}
}
/**
* Deletes all expired password reset tokens from the database.
* This is intended for a periodic cleanup job.
* @returns A promise that resolves to the number of deleted tokens.
*/
async deleteExpiredResetTokens(logger: Logger): Promise<number> {
try {
const res = await this.db.query(
"DELETE FROM public.password_reset_tokens WHERE expires_at < NOW()"
);
logger.info(`[DB deleteExpiredResetTokens] Deleted ${res.rowCount ?? 0} expired password reset tokens.`);
return res.rowCount ?? 0;
} catch (error) {
logger.error({ err: error }, 'Database error in deleteExpiredResetTokens');
throw new Error('Failed to delete expired password reset tokens.');
}
}
/**
* Creates a following relationship between two users.
* @param followerId The ID of the user who is following.