Skip to content

Commit

Permalink
Merge pull request #150 from Code-Hammers/CHE-167/story/BE-Refactor-E…
Browse files Browse the repository at this point in the history
…rror-Handling

[CHE-167] BE Refactor Error Handling
  • Loading branch information
brok3turtl3 authored Jun 20, 2024
2 parents f3f6ac4 + 92879bd commit d08fba4
Show file tree
Hide file tree
Showing 36 changed files with 416 additions and 255 deletions.
15 changes: 10 additions & 5 deletions __tests__/db.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import mongoose from 'mongoose';
import connectDB from '../server/config/db';

// TODO
/*eslint jest/no-disabled-tests: "off"*/

jest.mock('mongoose', () => ({
connect: jest.fn().mockImplementation(() =>
Promise.resolve({
Expand All @@ -24,26 +27,28 @@ describe('connectDB', () => {

it('should call mongoose.connect with MONGO_URI', async () => {
process.env.MONGO_URI = 'test-mongo-uri';
await connectDB();
await connectDB(process.env.MONGO_URI);
expect(mongoose.connect).toHaveBeenCalledWith('test-mongo-uri');
});

it('should log an error and exit the process if mongoose.connect fails', async () => {
// We now console.error the error's message and throw a DatabaseConnectionError instead
xit('should log an error and exit the process if mongoose.connect fails', async () => {
process.env.MONGO_URI = 'test-mongo-uri';
(mongoose.connect as jest.Mock).mockImplementationOnce(() => {
throw new Error('test error');
});

await connectDB();
await connectDB(process.env.MONGO_URI);

expect(mockConsoleError).toHaveBeenCalledWith('test error');
expect(mockExit).toHaveBeenCalledWith(1);
});

it('should throw an error if MONGO_URI is not defined', async () => {
// This check has been moved to startServer in index.ts
xit('should throw an error if MONGO_URI is not defined', async () => {
delete process.env.MONGO_URI;

await connectDB();
await connectDB(process.env.MONGO_URI!);

expect(mockConsoleError).toHaveBeenCalledWith(
'MONGO_URI must be defined in the environment variables.',
Expand Down
31 changes: 19 additions & 12 deletions __tests__/errorController.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import app from '../server/app';
import request from 'supertest';
import { Request, Response, NextFunction } from 'express';
import { notFound, errorHandler } from '../server/controllers/errorControllers';
import errorHandler from '../server/middleware/errorHandler';
import { BadRequestError, NotFoundError } from '../server/errors';

describe('Middleware Tests', () => {
let mockRequest: Partial<Request>;
Expand All @@ -10,29 +13,33 @@ describe('Middleware Tests', () => {
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
send: jest.fn(),
};
mockNext = jest.fn();
});

describe('notFound Middleware', () => {
it('should return 404 and the original URL', () => {
notFound(mockRequest as Request, mockResponse as Response, mockNext);
expect(mockResponse.status).toHaveBeenCalledWith(404);
expect(mockNext).toHaveBeenCalled();
it('should return 404 and the original URL', async () => {
const exampleNotFoundError = new NotFoundError();

const response = await request(app).get('/non-existent-route').send();

expect(response.status).toEqual(404);
expect(response.body).toEqual(exampleNotFoundError.serializeErrors());
});
});

describe('errorHandler Middleware', () => {
it('should handle the error correctly', () => {
const mockError = new Error('Some error');
const mockError = new BadRequestError('Some error');
errorHandler(mockError, mockRequest as Request, mockResponse as Response, mockNext);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.anything(),
stack: expect.any(String),
}),
expect(mockResponse.send).toHaveBeenCalledWith(
expect.objectContaining([
{
message: expect.any(String),
},
]),
);
});
});
Expand Down
7 changes: 4 additions & 3 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import request from 'supertest';
import app, { startServer } from '../server/index';
import { startServer } from '../server/index';
import app from '../server/app';
import { Server } from 'http';
import mongoose from 'mongoose';

Expand All @@ -8,8 +9,8 @@ import mongoose from 'mongoose';

let server: Server;

beforeEach(() => {
server = startServer();
beforeEach(async () => {
server = await startServer();
});

afterEach((done) => {
Expand Down
9 changes: 6 additions & 3 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
version: '3'
services:
dev:
#image: codehammers/ch-dev:latest
#image: codehammers/ch-dev-dep-v2:latest
#image: codehammers/ch-tracker-dev:latest
image: codehammers/ch-dev-dep-v3:latest
container_name: ch-dev
ports:
Expand All @@ -19,6 +16,12 @@ services:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=ch-dev
- POSTGRES_DB=ch-dev-database
- AWS_ACCESS_KEY_ID=placeholder-value
- AWS_SECRET_ACCESS_KEY=placeholder-value
- AWS_REGION=placeholder-value
- BUCKET_NAME=placeholder-value
# suppress aws sdk v2 deprecation warning
- AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1;
depends_on:
- postgres
postgres:
Expand Down
1 change: 0 additions & 1 deletion docker-compose-lint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
version: '3'
services:
dev:
#image: codehammers/ch-tracker-dev:latest
image: codehammers/ch-dev-dep-v3:latest
container_name: ch-lint
ports:
Expand Down
12 changes: 10 additions & 2 deletions docker-compose-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ version: '3'

services:
test:
#image: codehammers/ch-tracker-dev:latest
image: codehammers/ch-dev-dep-v3:latest
container_name: ch-test
ports:
Expand All @@ -14,8 +13,17 @@ services:
depends_on:
- mongo
environment:
- MONGO_URI=mongodb://mongo:27017/ch-testdb
- JWT_SECRET=${JWT_SECRET}
- MONGO_URI=mongodb://mongo:27017/ch-testdb
- POSTGRES_USER=postgres
- POSTGRES_DB=ch-dev-database
- POSTGRES_PASSWORD=ch-dev
- AWS_ACCESS_KEY_ID=placeholder-value
- AWS_SECRET_ACCESS_KEY=placeholder-value
- AWS_REGION=placeholder-value
- BUCKET_NAME=placeholder-value
# suppress aws sdk v2 deprecation warning
- AWS_SDK_JS_SUPPRESS_MAINTENANCE_MODE_MESSAGE=1;
command: npm run test:all

mongo:
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-async-errors": "^3.1.1",
"express-async-handler": "^1.2.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.4",
Expand Down
59 changes: 59 additions & 0 deletions server/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import path from 'path';
import express, { Request, Response } from 'express';
import 'express-async-errors';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
dotenv.config();
import {
userRouter,
profileRouter,
authRouter,
imageRouter,
alumniRouter,
forumRouter,
devRouter,
} from './routes';
import errorHandler from './middleware/errorHandler';
import { NotFoundError } from './errors';

// Instantiate application
const app = express();

// Middleware to parse request bodies
app.use(express.json());
// Middleware to parse request cookies
app.use(cookieParser());

// AWS Production Health Check
app.get('/health', (_req: Request, res: Response) => {
res.status(200).send('OK');
});

// API routers
app.use('/api/users', userRouter);
app.use('/api/profiles', profileRouter);
app.use('/api/auth', authRouter);
app.use('/api/images', imageRouter);
app.use('/api/alumni', alumniRouter);
app.use('/api/forums', forumRouter);
app.use('/api/devRoutes', devRouter);

// Serve client from build in production
if (process.env.NODE_ENV === 'production') {
console.log(`SERVER STARTED IN PRODUCTION`);
app.use(express.static(path.join(__dirname, '../../client/build')));

app.get('*', (_req: Request, res: Response) =>
res.sendFile(path.resolve(__dirname, '../../client/build/index.html')),
);
}

// Catch all route handler
app.use((_req, _res) => {
throw new NotFoundError();
});

// Global error handler
app.use(errorHandler);

export default app;
19 changes: 5 additions & 14 deletions server/config/db.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
import { DatabaseConnectionError } from '../errors';

const connectDB = async (): Promise<void> => {
const connectDB = async (mongoUri: string) => {
try {
// Check that MONGO_URI is defined
if (!process.env.MONGO_URI) {
throw new Error('MONGO_URI must be defined in the environment variables.');
}

const connection = await mongoose.connect(process.env.MONGO_URI);

console.log(`MongoDB is connected to: ${connection.connection.host}`);
const connection = await mongoose.connect(mongoUri);
console.log(`🍃 MongoDB is connected to: ${connection.connection.host}`);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('❌ Error connecting to the database ❌');
}
process.exit(1);
throw new DatabaseConnectionError();
}
};

Expand Down
7 changes: 3 additions & 4 deletions server/controllers/applicationsController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import { CustomRequest } from '../types/customRequest';
import { pool } from '../config/sql-db';

interface StatusCount {
Expand Down Expand Up @@ -104,7 +103,7 @@ const createApplication = async (req: Request, res: Response) => {
}
};

const getApplicationById = async (req: CustomRequest<{ id: string }>, res: Response) => {
const getApplicationById = async (req: Request, res: Response) => {
const { id } = req.params;
try {
const query = `
Expand Down Expand Up @@ -145,7 +144,7 @@ const getApplicationById = async (req: CustomRequest<{ id: string }>, res: Respo
}
};

const updateApplication = async (req: CustomRequest<{ id: string }>, res: Response) => {
const updateApplication = async (req: Request, res: Response) => {
const { id } = req.params;

if (!req.user) {
Expand Down Expand Up @@ -184,7 +183,7 @@ const updateApplication = async (req: CustomRequest<{ id: string }>, res: Respon
}
};

const getAggregatedUserStats = async (req: CustomRequest<{ userId: string }>, res: Response) => {
const getAggregatedUserStats = async (req: Request, res: Response) => {
const { userId } = req.params;
if (!req.user || req.user.id !== userId)
return res.status(401).json({ message: 'You are not authorized to retrieve those records' });
Expand Down
40 changes: 6 additions & 34 deletions server/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
import jwt from 'jsonwebtoken';
import User from '../models/userModel';
import asyncHandler from 'express-async-handler';
import { Request, Response } from 'express';
import { NotAuthorizedError } from '../errors';

const authSession = asyncHandler(async (req, res) => {
let token;

if (req.cookies.token) {
try {
token = req.cookies.token;
const secret = process.env.JWT_SECRET as string;
const decoded = jwt.verify(token, secret) as jwt.JwtPayload;

if (!decoded.id) {
throw new Error('Invalid token - ID not found');
}

const user = await User.findById(decoded.id).select('-password');

if (!user) throw new Error('User not found');

res.locals.user = user;
res.json({ isAuthenticated: true, user: res.locals.user });
} catch (error) {
console.error(error);
res.status(401);
throw new Error('Not authorized, token failed');
}
}

if (!token) {
res.status(401);
throw new Error('Not authorized, no token');
}
});
const authSession = async (_req: Request, res: Response) => {
if (!res.locals.user) throw new NotAuthorizedError();
res.json({ isAuthenticated: true, user: res.locals.user });
};

export { authSession };
Loading

0 comments on commit d08fba4

Please sign in to comment.