All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 10m20s
223 lines
10 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
}); |