better errors for routes
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 8m2s

This commit is contained in:
2025-12-10 22:53:30 -08:00
parent b929925a6e
commit 99d0dba296
20 changed files with 181 additions and 93 deletions

View File

@@ -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'));
});
});
});

View File

@@ -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

View File

@@ -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,
});
};

View File

@@ -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);
});
});
});

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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(() => {

View File

@@ -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)', () => {

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});
});

View File

@@ -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.');
});
});

View File

@@ -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 });
}
});

View File

@@ -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);

View File

@@ -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(() => {

View File

@@ -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');
});
});
});