Files
flyer-crawler.projectium.com/src/routes/budget.routes.test.ts
Torben Sorensen 5f1901b93d
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 10m20s
unit test fixes + error refactor
2025-12-11 14:16:25 -08:00

223 lines
10 KiB
TypeScript

// src/routes/budget.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import budgetRouter from './budget.routes';
import * as db from '../services/db/index.db';
import { errorHandler } from '../middleware/errorHandler';
import { createMockUserProfile, createMockBudget, createMockSpendingByCategory } from '../tests/utils/mockFactories';
import { ForeignKeyConstraintError } from '../services/db/errors.db';
// 1. Mock the Service Layer directly.
// This decouples the route tests from the database logic.
vi.mock('../services/db/index.db', () => ({
budgetRepo: {
getBudgetsForUser: vi.fn(),
createBudget: vi.fn(),
updateBudget: vi.fn(),
deleteBudget: vi.fn(),
getSpendingByCategory: vi.fn(),
},
userRepo: {
findUserProfileById: vi.fn(),
},
}));
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
logger: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
},
}));
// Standardized mock for passport.routes
vi.mock('./passport.routes', () => ({
default: {
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
// Simulate an authenticated user for all tests in this file
(req as any).user = { user_id: 'user-123', email: 'test@test.com' };
next();
}),
initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
},
// We also need to provide mocks for any other named exports from passport.routes.ts
isAdmin: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
optionalAuth: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
}));
// Create a minimal Express app to host our router
const app = express();
app.use(express.json());
app.use('/api/budgets', budgetRouter);
app.use(errorHandler);
describe('Budget Routes (/api/budgets)', () => {
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
beforeEach(() => {
vi.clearAllMocks();
// Provide default mock implementations to prevent undefined errors.
// Individual tests can override these with more specific values.
vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue([]);
vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue([]);
});
describe('GET /', () => {
it('should return a list of budgets for the user', async () => {
const mockBudgets = [createMockBudget({ budget_id: 1, user_id: 'user-123' })];
// Mock the service function directly
vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue(mockBudgets);
const response = await supertest(app).get('/api/budgets');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockBudgets);
expect(db.budgetRepo.getBudgetsForUser).toHaveBeenCalledWith(mockUserProfile.user_id);
});
it('should return 500 if the database call fails', async () => {
vi.mocked(db.budgetRepo.getBudgetsForUser).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/budgets');
expect(response.status).toBe(500); // The custom handler will now be used
expect(response.body.message).toBe('DB Error');
});
});
describe('POST /', () => {
it('should create a new budget and return it', async () => {
const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
const mockCreatedBudget = createMockBudget({ budget_id: 2, user_id: 'user-123', ...newBudgetData });
// Mock the service function
vi.mocked(db.budgetRepo.createBudget).mockResolvedValue(mockCreatedBudget);
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
expect(response.status).toBe(201);
expect(response.body).toEqual(mockCreatedBudget);
});
it('should return 400 if the user does not exist', async () => {
const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new ForeignKeyConstraintError('User not found'));
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
expect(response.status).toBe(400);
expect(response.body.message).toBe('User not found');
});
it('should return 400 if the user does not exist', async () => {
const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new ForeignKeyConstraintError('User not found'));
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
expect(response.status).toBe(400);
expect(response.body.message).toBe('User not found');
});
it('should return 500 if a generic database error occurs', async () => {
const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' };
vi.mocked(db.budgetRepo.createBudget).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).post('/api/budgets').send(newBudgetData);
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
describe('PUT /:id', () => {
it('should update an existing budget', async () => {
const budgetUpdates = { amount_cents: 60000 };
const mockUpdatedBudget = createMockBudget({ budget_id: 1, user_id: 'user-123', ...budgetUpdates });
// Mock the service function
vi.mocked(db.budgetRepo.updateBudget).mockResolvedValue(mockUpdatedBudget);
const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
expect(response.status).toBe(200);
expect(response.body).toEqual(mockUpdatedBudget);
});
it('should return 404 if the budget is not found', async () => {
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new Error('Budget not found'));
const response = await supertest(app).put('/api/budgets/999').send({ amount_cents: 1 });
expect(response.status).toBe(404);
expect(response.body.message).toBe('Budget not found');
});
it('should return 400 for a non-numeric budget ID', async () => {
const response = await supertest(app).put('/api/budgets/abc').send({ amount_cents: 1 });
expect(response.status).toBe(400);
expect(response.body.message).toBe("Invalid ID for parameter 'id'. Must be a number.");
});
it('should return 500 if a generic database error occurs', async () => {
const budgetUpdates = { amount_cents: 60000 };
vi.mocked(db.budgetRepo.updateBudget).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates);
expect(response.status).toBe(500); // The custom handler will now be used
expect(response.body.message).toBe('DB Error');
});
});
describe('DELETE /:id', () => {
it('should delete a budget', async () => {
// Mock the service function to resolve (void)
vi.mocked(db.budgetRepo.deleteBudget).mockResolvedValue(undefined);
const response = await supertest(app).delete('/api/budgets/1');
expect(response.status).toBe(204);
expect(db.budgetRepo.deleteBudget).toHaveBeenCalledWith(1, mockUserProfile.user_id);
});
it('should return 404 if the budget is not found', async () => {
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new Error('Budget not found'));
const response = await supertest(app).delete('/api/budgets/999');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Budget not found');
});
it('should return 400 for a non-numeric budget ID', async () => {
const response = await supertest(app).delete('/api/budgets/abc');
expect(response.status).toBe(400);
expect(response.body.message).toBe("Invalid ID for parameter 'id'. Must be a number.");
});
it('should return 500 if a generic database error occurs', async () => {
vi.mocked(db.budgetRepo.deleteBudget).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).delete('/api/budgets/1');
expect(response.status).toBe(500); // The custom handler will now be used
expect(response.body.message).toBe('DB Error');
});
});
describe('GET /spending-analysis', () => {
it('should return spending analysis data for a valid date range', async () => {
const mockSpendingData = [createMockSpendingByCategory({ category_id: 1, category_name: 'Produce' })];
// Mock the service function
vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue(mockSpendingData);
const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockSpendingData);
});
it('should return 400 if startDate is missing', async () => {
const response = await supertest(app).get('/api/budgets/spending-analysis?endDate=2024-01-31');
expect(response.status).toBe(400);
expect(response.body.message).toBe('Both startDate and endDate query parameters are required.');
});
it('should return 500 if the database call fails', async () => {
// Mock the service function to throw
vi.mocked(db.budgetRepo.getSpendingByCategory).mockRejectedValue(new Error('DB Error'));
const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB Error');
});
});
});