Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
- Refactored AIService to integrate with the latest GoogleGenAI SDK, updating the generateContent method signature and response handling. - Adjusted error handling and logging for improved clarity and consistency. - Enhanced mock implementations in tests to align with the new SDK structure. refactor: Modify Admin DB service to use Profile type - Updated AdminRepository to replace User type with Profile in relevant methods. - Enhanced test cases to utilize mock factories for creating Profile and AdminUserView objects. fix: Improve error handling in BudgetRepository - Implemented type-safe checks for PostgreSQL error codes to enhance error handling in createBudget method. test: Refactor Deals DB tests for type safety - Updated DealsRepository tests to use Pool type for mock instances, ensuring type safety. chore: Add new mock factories for testing - Introduced mock factories for UserWithPasswordHash, Profile, WatchedItemDeal, LeaderboardUser, and UnmatchedFlyerItem to streamline test data creation. style: Clean up queue service tests - Refactored queue service tests to improve readability and maintainability, including better handling of mock worker instances. docs: Update types to include UserWithPasswordHash - Added UserWithPasswordHash interface to types for better clarity on user authentication data structure. chore: Remove deprecated Google AI SDK references - Updated code and documentation to reflect the migration to the new Google Generative AI SDK, removing references to the deprecated SDK.
214 lines
9.4 KiB
TypeScript
214 lines
9.4 KiB
TypeScript
// src/routes/budget.routes.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import budgetRouter from './budget.routes';
|
|
import * as db from '../services/db/index.db';
|
|
import { createMockUserProfile, createMockBudget, createMockSpendingByCategory } from '../tests/utils/mockFactories';
|
|
import { mockLogger } from '../tests/utils/mockLogger';
|
|
import { createTestApp } from '../tests/utils/createTestApp';
|
|
import { ForeignKeyConstraintError, NotFoundError } 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: mockLogger,
|
|
}));
|
|
|
|
const mockUser = createMockUserProfile({
|
|
user_id: 'user-123',
|
|
user: { user_id: 'user-123', email: 'test@test.com' }
|
|
});
|
|
|
|
// Standardized mock for passport.routes
|
|
vi.mock('./passport.routes', () => ({
|
|
default: {
|
|
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
|
req.user = mockUser;
|
|
next();
|
|
}),
|
|
initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
|
|
},
|
|
}));
|
|
|
|
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([]);
|
|
});
|
|
|
|
const app = createTestApp({ router: budgetRouter, basePath: '/api/budgets', authenticatedUser: mockUser });
|
|
|
|
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 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');
|
|
});
|
|
|
|
it('should return 400 for invalid budget data', async () => {
|
|
const invalidData = {
|
|
name: '', // empty name
|
|
amount_cents: -100, // negative amount
|
|
period: 'yearly', // invalid period
|
|
start_date: 'not-a-date', // invalid date
|
|
};
|
|
|
|
const response = await supertest(app).post('/api/budgets').send(invalidData);
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors).toHaveLength(4);
|
|
});
|
|
});
|
|
|
|
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 NotFoundError('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 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');
|
|
});
|
|
|
|
it('should return 400 if no update fields are provided', async () => {
|
|
const response = await supertest(app).put('/api/budgets/1').send({});
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors[0].message).toBe('At least one field to update must be provided.');
|
|
});
|
|
});
|
|
|
|
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 NotFoundError('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 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 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');
|
|
});
|
|
|
|
it('should return 400 for invalid date formats', async () => {
|
|
const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024/01/01&endDate=invalid');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors).toHaveLength(2);
|
|
});
|
|
});
|
|
}); |