Files
flyer-crawler.projectium.com/src/services/db/budget.db.test.ts
Torben Sorensen 6aa72dd90b
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m50s
unit test fixin
2025-12-11 00:53:24 -08:00

208 lines
9.9 KiB
TypeScript

// src/services/db/budget.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ForeignKeyConstraintError } from './errors.db';
// Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./budget.db');
import { BudgetRepository } from './budget.db';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
import type { Budget, SpendingByCategory } from '../../types';
// Mock the logger to prevent console output during tests
vi.mock('../logger.server', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
// Mock the gamification repository, as createBudget calls it.
vi.mock('./gamification.db', () => ({
GamificationRepository: class { awardAchievement = vi.fn(); },
}));
describe('Budget DB Service', () => {
let budgetRepo: BudgetRepository;
beforeEach(() => {
vi.clearAllMocks();
// Instantiate the repository with the mock pool for each test
budgetRepo = new BudgetRepository(mockPoolInstance as any);
});
describe('getBudgetsForUser', () => {
it('should execute the correct SELECT query and return budgets', async () => {
const mockBudgets: Budget[] = [{ budget_id: 1, user_id: 'user-123', name: 'Groceries', amount_cents: 50000, period: 'monthly', start_date: '2024-01-01' }];
mockPoolInstance.query.mockResolvedValue({ rows: mockBudgets });
const result = await budgetRepo.getBudgetsForUser('user-123');
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.budgets WHERE user_id = $1 ORDER BY start_date DESC', ['user-123']);
expect(result).toEqual(mockBudgets);
});
it('should return an empty array if the user has no budgets', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
const result = await budgetRepo.getBudgetsForUser('user-123');
expect(result).toEqual([]);
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.any(String), ['user-123']);
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(budgetRepo.getBudgetsForUser('user-123')).rejects.toThrow('Failed to retrieve budgets.');
});
});
describe('createBudget', () => {
it('should execute an INSERT query and return the new budget', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const mockCreatedBudget: Budget = { budget_id: 1, user_id: 'user-123', ...budgetData };
// 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: [] }) // For BEGIN
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // For the INSERT...RETURNING
.mockResolvedValueOnce({ rows: [] }); // For both award_achievement and COMMIT
const result = await budgetRepo.createBudget('user-123', budgetData);
expect(mockClient.query).toHaveBeenCalledWith('BEGIN');
expect(mockClient.query).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO public.budgets'), expect.any(Array));
expect(mockClient.query).toHaveBeenCalledWith('COMMIT');
expect(mockClient.release).toHaveBeenCalled();
expect(result).toEqual(mockCreatedBudget);
});
it('should throw ForeignKeyConstraintError if user does not exist', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const dbError = new Error('violates foreign key constraint');
(dbError as any).code = '23503';
const mockClient = {
query: vi.fn()
.mockResolvedValueOnce({ rows: [] }) // Allow BEGIN to succeed
.mockRejectedValueOnce(dbError), // Have the INSERT fail
release: vi.fn(),
};
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
// The function should now correctly throw the custom error.
await expect(budgetRepo.createBudget('non-existent-user', budgetData))
.rejects.toThrow(new ForeignKeyConstraintError('The specified user does not exist.'));
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK'); // Verify rollback was called
});
it('should rollback the transaction if awarding an achievement fails', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const mockCreatedBudget: Budget = { budget_id: 1, user_id: 'user-123', ...budgetData };
const achievementError = new Error('Achievement award failed');
const mockClient = { query: vi.fn(), release: vi.fn() };
vi.mocked(mockPoolInstance.connect).mockResolvedValue(mockClient as any);
mockClient.query
.mockResolvedValueOnce({ rows: [] }) // BEGIN
.mockResolvedValueOnce({ rows: [mockCreatedBudget] }) // INSERT...RETURNING
.mockRejectedValueOnce(achievementError); // award_achievement fails
await expect(budgetRepo.createBudget('user-123', budgetData)).rejects.toThrow('Failed to create budget.');
expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK');
expect(mockClient.query).not.toHaveBeenCalledWith('COMMIT');
expect(mockClient.release).toHaveBeenCalled();
});
it('should throw a generic error if the database query fails', async () => {
const budgetData = { name: 'Groceries', amount_cents: 50000, period: 'monthly' as const, start_date: '2024-01-01' };
const dbError = new Error('DB Error');
// Mock BEGIN to succeed, but the INSERT to fail
mockPoolInstance.query.mockResolvedValueOnce({ rows: [] }).mockRejectedValueOnce(dbError);
await expect(budgetRepo.createBudget('user-123', budgetData)).rejects.toThrow('Failed to create budget.');
});
});
describe('updateBudget', () => {
it('should execute an UPDATE query with COALESCE and return the updated budget', async () => {
const budgetUpdates = { name: 'Updated Groceries', amount_cents: 55000 };
const mockUpdatedBudget: Budget = { budget_id: 1, user_id: 'user-123', name: 'Updated Groceries', amount_cents: 55000, period: 'monthly', start_date: '2024-01-01' };
mockPoolInstance.query.mockResolvedValue({ rows: [mockUpdatedBudget], rowCount: 1 });
const result = await budgetRepo.updateBudget(1, 'user-123', budgetUpdates);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('UPDATE public.budgets SET'),
[budgetUpdates.name, budgetUpdates.amount_cents, undefined, undefined, 1, 'user-123']
);
expect(result).toEqual(mockUpdatedBudget);
});
it('should throw an error if no rows are updated', async () => {
// Arrange: Mock the query to return 0 rows affected
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(budgetRepo.updateBudget(999, 'user-123', { name: 'Fail' })).rejects.toThrow('Budget not found or user does not have permission to update.');
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(budgetRepo.updateBudget(1, 'user-123', { name: 'Fail' })).rejects.toThrow('Failed to update budget.');
});
});
describe('deleteBudget', () => {
it('should execute a DELETE query with user ownership check', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 1, command: 'DELETE', rows: [] });
await budgetRepo.deleteBudget(1, 'user-123');
expect(mockPoolInstance.query).toHaveBeenCalledWith('DELETE FROM public.budgets WHERE budget_id = $1 AND user_id = $2', [1, 'user-123']);
});
it('should throw an error if no rows are deleted', async () => {
// Arrange: Mock the query to return 0 rows affected
mockPoolInstance.query.mockResolvedValue({ rows: [], rowCount: 0 });
await expect(budgetRepo.deleteBudget(999, 'user-123')).rejects.toThrow('Budget not found or user does not have permission to delete.');
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(budgetRepo.deleteBudget(1, 'user-123')).rejects.toThrow('Failed to delete budget.');
});
});
describe('getSpendingByCategory', () => {
it('should call the correct database function and return spending data', async () => {
const mockSpendingData: SpendingByCategory[] = [{ category_id: 1, category_name: 'Produce', total_spent_cents: 12345 }];
mockPoolInstance.query.mockResolvedValue({ rows: mockSpendingData });
const result = await budgetRepo.getSpendingByCategory('user-123', '2024-01-01', '2024-01-31');
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.get_spending_by_category($1, $2, $3)', ['user-123', '2024-01-01', '2024-01-31']);
expect(result).toEqual(mockSpendingData);
});
it('should return an empty array if there is no spending data', async () => {
mockPoolInstance.query.mockResolvedValue({ rows: [] });
const result = await budgetRepo.getSpendingByCategory('user-123', '2024-01-01', '2024-01-31');
expect(result).toEqual([]);
});
it('should throw an error if the database query fails', async () => {
const dbError = new Error('DB Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(budgetRepo.getSpendingByCategory('user-123', '2024-01-01', '2024-01-31')).rejects.toThrow('Failed to get spending analysis.');
});
});
});