better errors for routes
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 8m2s
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 8m2s
This commit is contained in:
122
src/App.test.tsx
122
src/App.test.tsx
@@ -1,6 +1,6 @@
|
||||
// src/App.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, fireEvent, act, within } from '@testing-library/react';
|
||||
import { render, screen, waitFor, fireEvent, act, within, renderHook } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { MemoryRouter, Outlet } from 'react-router-dom';
|
||||
import App from './App';
|
||||
@@ -15,7 +15,7 @@ vi.mock('./hooks/useAuth', () => ({
|
||||
|
||||
// Mock top-level components rendered by App's routes
|
||||
vi.mock('./components/Header', () => ({ Header: (props: any) => <header data-testid="header-mock"><button onClick={props.onOpenProfile}>Open Profile</button><button onClick={props.onOpenVoiceAssistant}>Open Voice Assistant</button></header> }));
|
||||
vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: ({ onClose }: { onClose: () => void }) => <div data-testid="profile-manager-mock"><button onClick={onClose}>Close Profile</button></div> }));
|
||||
vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: ({ onClose, onProfileUpdate, onLoginSuccess }: { onClose: () => void, onProfileUpdate: (p: any) => void, onLoginSuccess: (u: any, t: string) => void }) => <div data-testid="profile-manager-mock"><button onClick={onClose}>Close Profile</button><button onClick={() => onProfileUpdate({ full_name: 'Updated' })}>Update Profile</button><button onClick={() => onLoginSuccess({}, 'token')}>Login</button></div> }));
|
||||
vi.mock('./features/voice-assistant/VoiceAssistant', () => ({ VoiceAssistant: ({ onClose }: { onClose: () => void }) => <div data-testid="voice-assistant-mock"><button onClick={onClose}>Close Voice Assistant</button></div> }));
|
||||
vi.mock('./pages/admin/AdminPage', () => ({ AdminPage: () => <div data-testid="admin-page-mock">Admin Page</div> }));
|
||||
// In react-router v6, wrapper routes must render an <Outlet /> for nested routes to appear.
|
||||
@@ -24,9 +24,9 @@ vi.mock('./pages/admin/CorrectionsPage', () => ({ CorrectionsPage: () => <div da
|
||||
vi.mock('./pages/admin/AdminStatsPage', () => ({ AdminStatsPage: () => <div data-testid="admin-stats-page-mock">Admin Stats Page</div> }));
|
||||
vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () => <div data-testid="reset-password-page-mock">Reset Password Page</div> }));
|
||||
vi.mock('./pages/admin/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> })); // Corrected path
|
||||
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: ({ onClose }: { onClose: () => void }) => <div data-testid="whats-new-modal-mock"><button onClick={onClose}>Close What's New</button></div> }));
|
||||
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => isOpen ? <div data-testid="whats-new-modal-mock"><button onClick={onClose}>Close What's New</button></div> : null }));
|
||||
|
||||
vi.mock('./components/FlyerCorrectionTool', () => ({ FlyerCorrectionTool: ({ onClose, onDataExtracted }: { onClose: () => void, onDataExtracted: (type: 'store_name' | 'dates', value: string) => void }) => <div data-testid="flyer-correction-tool-mock"><button onClick={onClose}>Close Correction</button><button onClick={() => onDataExtracted('store_name', 'New Store Name')}>Extract Data</button></div> }));
|
||||
vi.mock('./components/FlyerCorrectionTool', () => ({ FlyerCorrectionTool: ({ onClose, onDataExtracted }: { onClose: () => void, onDataExtracted: (type: 'store_name' | 'dates', value: string) => void }) => <div data-testid="flyer-correction-tool-mock"><button onClick={onClose}>Close Correction</button><button onClick={() => onDataExtracted('store_name', 'New Store Name')}>Extract Store</button><button onClick={() => onDataExtracted('dates', '2023-01-01')}>Extract Dates</button></div> }));
|
||||
// Mock the new layout and page components
|
||||
vi.mock('./layouts/MainLayout', () => ({ MainLayout: () => <div data-testid="main-layout-mock"><Outlet /></div> }));
|
||||
vi.mock('./pages/HomePage', () => ({ HomePage: (props: any) => <div data-testid="home-page-mock"><button onClick={props.onOpenCorrectionTool}>Open Correction Tool</button></div> }));
|
||||
@@ -34,6 +34,7 @@ vi.mock('./pages/HomePage', () => ({ HomePage: (props: any) => <div data-testid=
|
||||
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
|
||||
// This must be done in any test file that imports App.tsx.
|
||||
vi.mock('pdfjs-dist', () => ({
|
||||
// pdfjsLib: { GlobalWorkerOptions: { workerSrc: '' } },
|
||||
GlobalWorkerOptions: { workerSrc: '' },
|
||||
getDocument: vi.fn(() => ({
|
||||
promise: Promise.resolve({ getPage: vi.fn() }),
|
||||
@@ -327,6 +328,17 @@ describe('App Component', () => {
|
||||
expect(mockLogin).toHaveBeenCalledWith({ user_id: '', email: '' }, 'test-github-token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should log an error if login with a token fails', async () => {
|
||||
const mockLogin = vi.fn().mockRejectedValue(new Error('Token login failed'));
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: null, profile: null, authStatus: 'SIGNED_OUT', isLoading: false,
|
||||
login: mockLogin, logout: vi.fn(), updateProfile: vi.fn(),
|
||||
});
|
||||
|
||||
renderApp(['/?googleAuthToken=bad-token']);
|
||||
await waitFor(() => expect(mockLogin).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flyer Selection from URL', () => {
|
||||
@@ -424,14 +436,98 @@ describe('App Component', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle data extraction from the correction tool', async () => {
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Correction Tool'));
|
||||
const correctionTool = await screen.findByTestId('flyer-correction-tool-mock');
|
||||
// The logic for updating the flyer is inside App.tsx, but we can't see the state.
|
||||
// We trigger the callback from the mock and ensure it doesn't crash.
|
||||
// A more integrated test would be needed to see the visual change.
|
||||
fireEvent.click(within(correctionTool).getByText('Extract Data'));
|
||||
// The test passes if no errors are thrown here.
|
||||
describe('Flyer Correction Tool Data Handling', () => {
|
||||
it('should handle store name extraction from the correction tool', async () => {
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Correction Tool'));
|
||||
const correctionTool = await screen.findByTestId('flyer-correction-tool-mock');
|
||||
// We trigger the callback from the mock and ensure it doesn't crash.
|
||||
fireEvent.click(within(correctionTool).getByText('Extract Store'));
|
||||
// The test passes if no errors are thrown here.
|
||||
});
|
||||
|
||||
it('should handle date extraction from the correction tool', async () => {
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Correction Tool'));
|
||||
const correctionTool = await screen.findByTestId('flyer-correction-tool-mock');
|
||||
fireEvent.click(within(correctionTool).getByText('Extract Dates'));
|
||||
// The test passes if no errors are thrown here, covering the 'dates' branch.
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version Display and What\'s New', () => {
|
||||
const mockEnv = {
|
||||
VITE_APP_VERSION: '2.0.0',
|
||||
VITE_APP_COMMIT_MESSAGE: 'A new version!',
|
||||
VITE_APP_COMMIT_URL: 'http://example.com/commit/2.0.0',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(import.meta, 'env', {
|
||||
value: mockEnv,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the version number and commit link', () => {
|
||||
renderApp();
|
||||
const versionLink = screen.getByText(`Version: ${mockEnv.VITE_APP_VERSION}`);
|
||||
expect(versionLink).toBeInTheDocument();
|
||||
expect(versionLink).toHaveAttribute('href', mockEnv.VITE_APP_COMMIT_URL);
|
||||
});
|
||||
|
||||
it('should open the "What\'s New" modal when the question mark icon is clicked', async () => {
|
||||
renderApp();
|
||||
expect(screen.queryByTestId('whats-new-modal-mock')).not.toBeInTheDocument();
|
||||
|
||||
const openButton = screen.getByTitle("Show what's new in this version");
|
||||
fireEvent.click(openButton);
|
||||
|
||||
expect(await screen.findByTestId('whats-new-modal-mock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dynamic Toaster Styles', () => {
|
||||
it('should render the correct CSS variables for toast styling in light mode', () => {
|
||||
renderApp();
|
||||
const styleTag = document.querySelector('style');
|
||||
expect(styleTag).not.toBeNull();
|
||||
expect(styleTag!.innerHTML).toContain('--toast-bg: #FFFFFF');
|
||||
expect(styleTag!.innerHTML).toContain('--toast-color: #1F2937');
|
||||
});
|
||||
|
||||
it('should render the correct CSS variables for toast styling in dark mode', () => {
|
||||
localStorageMock.setItem('darkMode', 'true');
|
||||
renderApp();
|
||||
const styleTag = document.querySelector('style');
|
||||
expect(styleTag).not.toBeNull();
|
||||
expect(styleTag!.innerHTML).toContain('--toast-bg: #4B5563');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile and Login Handlers', () => {
|
||||
it('should call updateProfile when handleProfileUpdate is triggered', async () => {
|
||||
const mockUpdateProfile = vi.fn();
|
||||
mockUseAuth.mockReturnValue({
|
||||
...mockUseAuth(),
|
||||
updateProfile: mockUpdateProfile,
|
||||
});
|
||||
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Profile'));
|
||||
const profileManager = await screen.findByTestId('profile-manager-mock');
|
||||
fireEvent.click(within(profileManager).getByText('Update Profile'));
|
||||
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith({ full_name: 'Updated' });
|
||||
});
|
||||
|
||||
it('should set an error state if login fails inside handleLoginSuccess', async () => {
|
||||
const mockLogin = vi.fn().mockRejectedValue(new Error('Login failed'));
|
||||
mockUseAuth.mockReturnValue({ ...mockUseAuth(), login: mockLogin });
|
||||
|
||||
renderApp();
|
||||
fireEvent.click(screen.getByText('Open Profile'));
|
||||
fireEvent.click(await screen.findByText('Login'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { errorHandler } from './errorHandler';
|
||||
import { DatabaseError, ForeignKeyConstraintError, UniqueConstraintError } from '../services/db/errors.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
// Mock the logger to prevent console output during tests and to verify it's called
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
@@ -58,8 +59,6 @@ app.get('/unauthorized-error-with-status', (req, res, next) => {
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('errorHandler Middleware', () => {
|
||||
const { logger } = require('../services/logger.server'); // Get the mocked logger
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleErrorSpy.mockClear(); // Clear spy for console.error
|
||||
|
||||
@@ -41,6 +41,10 @@ export const errorHandler = (err: HttpError, req: Request, res: Response, next:
|
||||
}
|
||||
|
||||
res.status(statusCode).json({
|
||||
message: statusCode >= 500 ? 'An unexpected server error occurred.' : message,
|
||||
// In production, send a generic message for 5xx errors.
|
||||
// In dev/test, send the actual error message for easier debugging.
|
||||
message: (statusCode >= 500 && process.env.NODE_ENV === 'production')
|
||||
? 'An unexpected server error occurred.'
|
||||
: message,
|
||||
});
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { MemoryRouter, useOutletContext } from 'react-router-dom';
|
||||
import { HomePage } from './HomePage';
|
||||
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
import type { Flyer, FlyerItem } from '../types';
|
||||
import { ExtractedDataTable } from '../features/flyer/ExtractedDataTable';
|
||||
|
||||
// Mock child components to isolate the HomePage logic
|
||||
vi.mock('../features/flyer/FlyerDisplay', () => ({
|
||||
@@ -109,12 +110,12 @@ describe('HomePage Component', () => {
|
||||
|
||||
// We can also check the props passed to the mock component by inspecting the mock itself.
|
||||
// This is a more robust way to test prop passing.
|
||||
const ExtractedDataTableMock = vi.mocked(require('../features/flyer/ExtractedDataTable').ExtractedDataTable);
|
||||
const props = ExtractedDataTableMock.mock.calls[0][0];
|
||||
const ExtractedDataTableMock = vi.mocked(ExtractedDataTable);
|
||||
const props = ExtractedDataTableMock.mock.calls[ExtractedDataTableMock.mock.calls.length - 1][0];
|
||||
|
||||
expect(props.items).toEqual(mockItems);
|
||||
expect(props.masterItems).toEqual(mockContext.masterItems);
|
||||
expect(props.addWatchedItem).toBe(mockContext.addWatchedItem);
|
||||
expect(props.onAddItem).toBe(mockContext.addWatchedItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile, createMockSuggestedCorrection, createMockBrand, createMockRecipe, createMockRecipeComment, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
import { SuggestedCorrection, Brand, UserProfile, UnmatchedFlyerItem } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -114,9 +115,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { Job } from 'bullmq';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -102,9 +103,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile, createMockActivityLogItem } from '../tests/utils/mockFactories';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -111,9 +112,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
const { mockedDb } = vi.hoisted(() => {
|
||||
return {
|
||||
@@ -80,9 +81,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../services/geocodingService.server', () => ({
|
||||
@@ -52,9 +53,7 @@ const createApp = () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { User, UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
|
||||
const { mockedDb } = vi.hoisted(() => {
|
||||
return {
|
||||
@@ -89,9 +90,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import express, { type Request, type Response, type NextFunction } from 'express
|
||||
import path from 'node:path';
|
||||
import aiRouter from './ai.routes';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import * as flyerDb from '../services/db/flyer.db';
|
||||
import * as db from '../services/db/index.db';
|
||||
import * as aiService from '../services/aiService.server';
|
||||
@@ -59,13 +60,7 @@ vi.mock('./passport.routes', () => ({
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
app.use('/api/ai', aiRouter);
|
||||
|
||||
// FIX: Add a generic error handler with the correct 4-argument signature.
|
||||
// This ensures that errors passed via `next(error)` in the routes are caught
|
||||
// and formatted into a JSON response that the tests can assert against.
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('AI Routes (/api/ai)', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import passport from 'passport';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { UserProfile } from '../types';
|
||||
|
||||
// --- FIX: Hoist passport mocks to be available for vi.mock ---
|
||||
@@ -151,11 +152,7 @@ app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
});
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
|
||||
// Add error handler
|
||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(err.status || 500).json({ message: err.message });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
// --- 5. Tests ---
|
||||
describe('Auth Routes (/api/auth)', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -51,11 +52,7 @@ vi.mock('./passport.routes', () => ({
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/budgets', budgetRouter);
|
||||
|
||||
// Add a basic error handler to return JSON errors instead of Express default HTML
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Budget Routes (/api/budgets)', () => {
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
@@ -84,8 +81,8 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
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);
|
||||
expect(response.body.message).toBe('Internal Server Error');
|
||||
expect(response.status).toBe(500); // The custom handler will now be used
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,8 +152,8 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
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);
|
||||
expect(response.body.message).toBe('Internal Server Error');
|
||||
expect(response.status).toBe(500); // The custom handler will now be used
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,8 +183,8 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
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);
|
||||
expect(response.body.message).toBe('Internal Server Error');
|
||||
expect(response.status).toBe(500); // The custom handler will now be used
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import flyerRouter from './flyer.routes';
|
||||
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
|
||||
@@ -32,11 +33,7 @@ const app = express();
|
||||
app.use(express.json());
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/flyers', flyerRouter);
|
||||
|
||||
// Add a generic error handler to catch errors passed via next()
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Flyer Routes (/api/flyers)', () => {
|
||||
beforeEach(() => {
|
||||
@@ -64,6 +61,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
vi.mocked(db.flyerRepo.getFlyers).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/flyers');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,6 +94,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
vi.mocked(db.flyerRepo.getFlyerById).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/flyers/123');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +119,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
vi.mocked(db.flyerRepo.getFlyerItems).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/flyers/123/items');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,6 +155,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
vi.mocked(db.flyerRepo.getFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).post('/api/flyers/items/batch-fetch').send({ flyerIds: [1] });
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,6 +190,7 @@ describe('Flyer Routes (/api/flyers)', () => {
|
||||
vi.mocked(db.flyerRepo.countFlyerItemsForFlyers).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).post('/api/flyers/items/batch-count').send({ flyerIds: [1] });
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import gamificationRouter from './gamification.routes';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockUserProfile, createMockAchievement, createMockUserAchievement } from '../tests/utils/mockFactories';
|
||||
import { ForeignKeyConstraintError } from '../services/db/errors.db';
|
||||
|
||||
@@ -44,6 +45,7 @@ vi.mock('./passport.routes', () => ({
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
app.use('/api/achievements', gamificationRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Gamification Routes (/api/achievements)', () => {
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
@@ -78,6 +80,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
|
||||
const response = await supertest(app).get('/api/achievements');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Connection Failed');
|
||||
});
|
||||
|
||||
it('should return 400 if awarding an achievement to a non-existent user', async () => {
|
||||
@@ -127,6 +130,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockRejectedValue(dbError);
|
||||
const response = await supertest(app).get('/api/achievements/me');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,6 +193,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send(awardPayload);
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
|
||||
it('should return 400 if awarding an achievement to a non-existent user', async () => {
|
||||
@@ -221,6 +226,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/achievements/leaderboard');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import healthRouter from './health.routes';
|
||||
import * as dbConnection from '../services/db/connection.db';
|
||||
import { connection as redisConnection } from '../services/queueService.server';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
// 1. Mock the dependencies of the health router.
|
||||
@@ -46,10 +47,7 @@ const mockedFs = fs as Mocked<typeof fs>;
|
||||
// 2. Create a minimal Express app to host the router for testing.
|
||||
const app = express();
|
||||
app.use('/api/health', healthRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
const statusCode = (err as any).status || 500;
|
||||
res.status(statusCode).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Health Routes (/api/health)', () => {
|
||||
beforeEach(() => {
|
||||
@@ -115,6 +113,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.message).toBe('Failed to connect to Redis.');
|
||||
expect(response.body.error).toContain('Unexpected Redis ping response: OK');
|
||||
});
|
||||
@@ -171,6 +170,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
const response = await supertest(app).get('/api/health/db-schema');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('An error occurred while checking the database schema.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,5 +248,6 @@ describe('Health Routes (/api/health)', () => {
|
||||
const response = await supertest(app).get('/api/health/db-pool');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('An error occurred while checking the database pool status.');
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import { getSimpleWeekAndYear } from '../utils/dateUtils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/ping', (req: Request, res: Response) => {
|
||||
router.get('/ping', (_req: Request, res: Response) => {
|
||||
res.status(200).send('pong');
|
||||
});
|
||||
|
||||
@@ -21,19 +21,19 @@ router.get('/db-schema', async (req, res) => {
|
||||
return res.status(500).json({ success: false, message: `Database schema check failed. Missing tables: ${missingTables.join(', ')}.` });
|
||||
}
|
||||
return res.status(200).json({ success: true, message: 'All required database tables exist.' });
|
||||
} catch (error) {
|
||||
logger.error('Error during DB schema check:', { error });
|
||||
return res.status(500).json({ success: false, message: 'An error occurred while checking the database schema.' });
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error during DB schema check:', { error: error instanceof Error ? error.message : error });
|
||||
return res.status(500).json({ success: false, message: 'An error occurred while checking the database schema.' }); // No change to next(error) as this is a health check, not a standard API route
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/storage', async (req, res) => {
|
||||
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets';
|
||||
try {
|
||||
await fs.access(storagePath, fs.constants.W_OK);
|
||||
await fs.access(storagePath, fs.constants.W_OK); // Use fs.promises
|
||||
return res.status(200).json({ success: true, message: `Storage directory '${storagePath}' is accessible and writable.` });
|
||||
} catch (error) {
|
||||
logger.error(`Storage check failed for path: ${storagePath}`, { error });
|
||||
} catch (error: unknown) {
|
||||
logger.error(`Storage check failed for path: ${storagePath}`, { error: error instanceof Error ? error.message : error });
|
||||
return res.status(500).json({ success: false, message: `Storage check failed. Ensure the directory '${storagePath}' exists and is writable by the application.` });
|
||||
}
|
||||
});
|
||||
@@ -50,9 +50,9 @@ router.get('/db-pool', (req: Request, res: Response) => {
|
||||
logger.warn(`Database pool health check shows high waiting count: ${status.waitingCount}`);
|
||||
return res.status(500).json({ success: false, message: `Pool may be under stress. ${message}` });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error during DB pool health check:', { error });
|
||||
return res.status(500).json({ success: false, message: 'An error occurred while checking the database pool status.' });
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error during DB pool health check:', { error: error instanceof Error ? error.message : error });
|
||||
return res.status(500).json({ success: false, message: 'An error occurred while checking the database pool status.' }); // No change to next(error) as this is a health check
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,9 +75,9 @@ router.get('/redis', async (req: Request, res: Response) => {
|
||||
if (reply === 'PONG') {
|
||||
return res.status(200).json({ success: true, message: 'Redis connection is healthy.' });
|
||||
}
|
||||
throw new Error(`Unexpected Redis ping response: ${reply}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
|
||||
throw new Error(`Unexpected Redis ping response: ${reply}`); // This will be caught below
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return res.status(500).json({ success: false, message: 'Failed to connect to Redis.', error: errorMessage });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ vi.mock('../services/db/index.db', () => ({
|
||||
const mockedDb = db as Mocked<typeof db>;
|
||||
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
// This mock is used by the module under test and can be imported in the test file.
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
|
||||
@@ -87,6 +88,7 @@ vi.mock('passport', () => {
|
||||
|
||||
// Now, import the passport configuration which will use our mocks
|
||||
import passport, { isAdmin, optionalAuth } from './passport.routes';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
describe('Passport Configuration', () => {
|
||||
beforeEach(() => {
|
||||
@@ -390,7 +392,6 @@ describe('Passport Configuration', () => {
|
||||
vi.mocked(passport.authenticate).mockImplementation(
|
||||
(_strategy, _options, callback) => () => callback?.(null, false, mockInfo)
|
||||
);
|
||||
const { logger } = require('../services/logger.server');
|
||||
|
||||
// Act
|
||||
optionalAuth(mockReq, mockRes as Response, mockNext);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import recipeRouter from './recipe.routes';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockRecipe, createMockRecipeComment } from '../tests/utils/mockFactories';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
@@ -31,11 +32,7 @@ const app = express();
|
||||
app.use(express.json());
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/recipes', recipeRouter);
|
||||
|
||||
// Add a generic error handler to catch errors passed via next()
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Recipe Routes (/api/recipes)', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import statsRouter from './stats.routes';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
@@ -26,11 +27,7 @@ const app = express();
|
||||
app.use(express.json());
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/stats', statsRouter);
|
||||
|
||||
// Add a generic error handler to catch errors passed via next()
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Stats Routes (/api/stats)', () => {
|
||||
beforeEach(() => {
|
||||
@@ -67,6 +64,7 @@ describe('Stats Routes (/api/stats)', () => {
|
||||
vi.mocked(db.adminRepo.getMostFrequentSaleItems).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/stats/most-frequent-sales');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user