ADR1-3 on routes + db files
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m30s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m30s
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// src/services/db/user.db.test.ts
|
||||
// --- FIX REGISTRY ---
|
||||
//
|
||||
// 2025-12-09: Corrected transaction rollback tests to expect generic error messages.
|
||||
@@ -5,8 +6,9 @@
|
||||
// mocking strategies for internal method calls (spying on prototypes).
|
||||
//
|
||||
// --- END FIX REGISTRY ---
|
||||
// src/services/db/user.db.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { PoolClient } from 'pg';
|
||||
import { withTransaction } from './connection.db';
|
||||
|
||||
// Mock the logger to prevent stderr noise during tests
|
||||
vi.mock('../logger.server', () => ({
|
||||
@@ -21,10 +23,15 @@ vi.mock('../logger.server', () => ({
|
||||
// Un-mock the module we are testing to ensure we use the real implementation.
|
||||
vi.unmock('./user.db');
|
||||
|
||||
// Mock the withTransaction helper since we are testing a function that uses it.
|
||||
vi.mock('./connection.db', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./connection.db')>();
|
||||
return { ...actual, withTransaction: vi.fn() };
|
||||
});
|
||||
import { UserRepository, exportUserData } from './user.db';
|
||||
|
||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import { UniqueConstraintError, ForeignKeyConstraintError } from './errors.db';
|
||||
import { UniqueConstraintError, ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
||||
import type { Profile, ActivityLogItem, SearchQuery } from '../../types';
|
||||
|
||||
// Mock other db services that are used by functions in user.db.ts
|
||||
@@ -53,6 +60,9 @@ describe('User DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
userRepo = new UserRepository(mockPoolInstance as any);
|
||||
// Reset the withTransaction mock before each test
|
||||
const { withTransaction } = require('./connection.db'); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback: (client: PoolClient) => Promise<any>) => callback(mockPoolInstance as any));
|
||||
});
|
||||
|
||||
describe('findUserByEmail', () => {
|
||||
@@ -84,75 +94,55 @@ describe('User DB Service', () => {
|
||||
it('should execute a transaction to create a user and profile', async () => {
|
||||
const mockUser = { user_id: 'new-user-id', email: 'new@example.com' };
|
||||
const mockProfile = { ...mockUser, role: 'user' };
|
||||
// For transactional methods, we mock the client returned by `connect()`
|
||||
const mockClient = {
|
||||
query: vi.fn(),
|
||||
release: vi.fn(),
|
||||
};
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
|
||||
// Mock the sequence of queries within the transaction
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockResolvedValueOnce({ rows: [mockProfile] }) // SELECT profile
|
||||
.mockResolvedValueOnce({ rows: [] }) // COMMIT;
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
const mockClient = { query: vi.fn() };
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockResolvedValueOnce({ rows: [mockProfile] }); // SELECT profile
|
||||
return callback(mockClient as any);
|
||||
});
|
||||
|
||||
const result = await userRepo.createUser('new@example.com', 'hashedpass', { full_name: 'New User' });
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should rollback the transaction if creating the user fails', async () => {
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
const dbError = new Error('User insert failed');
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
const mockClient = { query: vi.fn() };
|
||||
mockClient.query.mockRejectedValueOnce(dbError); // set_config or INSERT fails
|
||||
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
|
||||
throw dbError;
|
||||
});
|
||||
|
||||
// Arrange: Mock the user insert query to fail after BEGIN and set_config
|
||||
mockClient.query
|
||||
.mockRejectedValueOnce(new Error('User insert failed')); // INSERT fails
|
||||
|
||||
// Act & Assert
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {})).rejects.toThrow('User insert failed');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('BEGIN'); // This will be called inside the try block
|
||||
// The createUser function now throws the original error, so we check for that.
|
||||
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
await expect(userRepo.createUser('fail@example.com', 'badpass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
});
|
||||
|
||||
it('should rollback the transaction if fetching the final profile fails', async () => {
|
||||
const mockUser = { user_id: 'new-user-id', email: 'new@example.com' };
|
||||
// FIX: Define mockClient within this test's scope.
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockRejectedValueOnce(new Error('Profile fetch failed')); // SELECT profile fails
|
||||
const dbError = new Error('Profile fetch failed');
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
const mockClient = { query: vi.fn() };
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // set_config
|
||||
.mockResolvedValueOnce({ rows: [mockUser] }) // INSERT user
|
||||
.mockRejectedValueOnce(dbError); // SELECT profile fails
|
||||
await expect(callback(mockClient as any)).rejects.toThrow(dbError);
|
||||
throw dbError;
|
||||
});
|
||||
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {})).rejects.toThrow('Profile fetch failed');
|
||||
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
await expect(userRepo.createUser('fail@example.com', 'pass', {})).rejects.toThrow('Failed to create user in database.');
|
||||
});
|
||||
|
||||
it('should throw UniqueConstraintError if the email already exists', async () => {
|
||||
const dbError = new Error('duplicate key value violates unique constraint');
|
||||
(dbError as any).code = '23505';
|
||||
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
|
||||
// Simulate the transaction flow:
|
||||
// 1. BEGIN (success)
|
||||
// 2. set_config (success)
|
||||
// 3. INSERT user (failure with unique violation)
|
||||
// 4. ROLLBACK (success)
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
||||
.mockRejectedValueOnce(dbError) // INSERT fails
|
||||
.mockResolvedValueOnce({ rows: [] }); // ROLLBACK
|
||||
vi.mocked(withTransaction).mockRejectedValue(dbError);
|
||||
|
||||
try {
|
||||
await userRepo.createUser('exists@example.com', 'pass', {});
|
||||
@@ -162,8 +152,7 @@ describe('User DB Service', () => {
|
||||
expect(error.message).toBe('A user with this email address already exists.');
|
||||
}
|
||||
|
||||
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
|
||||
expect(mockClient.release).toHaveBeenCalled();
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,16 +187,9 @@ describe('User DB Service', () => {
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.users WHERE user_id = $1'), ['123']);
|
||||
});
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
it('should throw NotFoundError if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserById('not-found-id');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserWithPasswordHashById('not-found-id');
|
||||
expect(result).toBeUndefined();
|
||||
await expect(userRepo.findUserById('not-found-id')).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw a generic error if the database query fails', async () => {
|
||||
@@ -239,12 +221,16 @@ describe('User DB Service', () => {
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE p.user_id = $1'), ['123']);
|
||||
});
|
||||
|
||||
it('should return undefined if user profile is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserProfileById('not-found-id');
|
||||
expect(result).toBeUndefined();
|
||||
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.');
|
||||
});
|
||||
|
||||
it('should return undefined if user is not found', async () => {
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const result = await userRepo.findUserWithPasswordHashById('not-found-id');
|
||||
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);
|
||||
@@ -370,10 +356,10 @@ describe('User DB Service', () => {
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('WHERE refresh_token = $1'), ['a-token']);
|
||||
});
|
||||
|
||||
it('should return undefined if the database query fails', async () => {
|
||||
mockPoolInstance.query.mockRejectedValue(new Error('DB Error'));
|
||||
const result = await userRepo.findUserByRefreshToken('a-token');
|
||||
expect(result).toBeUndefined();
|
||||
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.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -440,51 +426,53 @@ describe('User DB Service', () => {
|
||||
});
|
||||
|
||||
describe('exportUserData', () => {
|
||||
it('should call profile, watched items, and shopping list functions', async () => {
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
// Import the mocked withTransaction helper
|
||||
let withTransaction: any;
|
||||
beforeEach(async () => {
|
||||
const connDb = await import('./connection.db');
|
||||
withTransaction = connDb.withTransaction;
|
||||
});
|
||||
|
||||
// --- FIX: Correctly mock the methods on the prototype of the imported classes ---
|
||||
it('should call profile, watched items, and shopping list functions', async () => {
|
||||
const { ShoppingRepository } = await import('./shopping.db');
|
||||
const { PersonalizationRepository } = await import('./personalization.db');
|
||||
|
||||
// We need to spy on the prototypes because these classes are instantiated inside exportUserData
|
||||
const findProfileSpy = vi.spyOn(UserRepository.prototype, 'findUserProfileById')
|
||||
.mockResolvedValue({ user_id: '123' } as Profile);
|
||||
|
||||
const getWatchedItemsSpy = vi.spyOn(PersonalizationRepository.prototype, 'getWatchedItems')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
const getShoppingListsSpy = vi.spyOn(ShoppingRepository.prototype, 'getShoppingLists')
|
||||
.mockResolvedValue([]);
|
||||
|
||||
await exportUserData('123');
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
it('should throw an error if the user profile is not found', async () => {
|
||||
// Mock findUserProfileById to return undefined
|
||||
// This uses the same prototype spy strategy as above, or we can mock the query if we prefer.
|
||||
// Let's use the prototype spy for consistency in this block.
|
||||
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockResolvedValue(undefined);
|
||||
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
// 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.
|
||||
await expect(exportUserData('123')).rejects.toThrow('Failed to export user data.');
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should throw an error if the database query fails', async () => {
|
||||
// Force a failure in one of the calls
|
||||
// Arrange: Force a failure in one of the parallel calls
|
||||
vi.spyOn(UserRepository.prototype, 'findUserProfileById').mockRejectedValue(new Error('DB Error'));
|
||||
|
||||
const mockClient = { query: vi.fn(), release: vi.fn() };
|
||||
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
|
||||
|
||||
// Act & Assert
|
||||
await expect(exportUserData('123')).rejects.toThrow('Failed to export user data.');
|
||||
expect(withTransaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user