Skip to content

Commit

Permalink
add: error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
kennysghub committed Nov 13, 2024
1 parent e655f7b commit 7baa3fa
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 21 deletions.
6 changes: 3 additions & 3 deletions user-service/package-lock.json

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

6 changes: 3 additions & 3 deletions user-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/testing": "^10.4.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/jest": "^29.5.14",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"jest": "^29.7.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
Expand Down
9 changes: 9 additions & 0 deletions user-service/src/common/exceptions/http-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class CustomHttpException extends Error {
constructor(
public readonly message: string,
public readonly statusCode: number,
) {
super(message);
this.name = this.constructor.name;
}
}
13 changes: 13 additions & 0 deletions user-service/src/common/exceptions/user.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CustomHttpException } from './http-exception';

export class UserNotFoundException extends CustomHttpException {
constructor(userId: string) {
super(`User with ID ${userId} not found`, 404);
}
}

export class DuplicateEmailException extends CustomHttpException {
constructor(email: string) {
super(`User with email ${email} already exists`, 409);
}
}
7 changes: 7 additions & 0 deletions user-service/src/common/exceptions/validation.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CustomHttpException } from './http-exception';

export class ValidationException extends CustomHttpException {
constructor(message: string) {
super(message, 400);
}
}
52 changes: 52 additions & 0 deletions user-service/src/common/filters/http-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { CustomHttpException } from '../exceptions/http-exception';
import { Logger } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);

catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let error = 'Internal Server Error';

if (exception instanceof CustomHttpException) {
status = exception.statusCode;
message = exception.message;
error = exception.name;
} else if (exception instanceof HttpException) {
status = exception.getStatus();
const errorResponse = exception.getResponse();
message = (errorResponse as any).message || exception.message;
error = (errorResponse as any).error || 'Http Exception';
}

this.logger.error(`${request.method} ${request.url}`, {
status,
error,
message,
timestamp: new Date().toISOString(),
path: request.url,
});

response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error,
message,
});
}
}
38 changes: 35 additions & 3 deletions user-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
// user-service/src/main.ts
// // user-service/src/main.ts
// import { NestFactory } from '@nestjs/core';
// import { AppModule } from './app.module';
// import { ValidationPipe } from '@nestjs/common';

// async function bootstrap() {
// const app = await NestFactory.create(AppModule);
// app.useGlobalPipes(new ValidationPipe());
// await app.listen(3000);
// console.log('User service is running on port 3000');
// }
// bootstrap();
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
import { Logger } from '@nestjs/common';
import { ValidationException } from './common/exceptions/validation.exception';

async function bootstrap() {
const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());

app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
exceptionFactory: (errors) => {
const messages = errors.map((error) =>
Object.values(error.constraints).join(', '),
);
return new ValidationException(messages.join('; '));
},
}),
);

app.useGlobalFilters(new AllExceptionsFilter());

await app.listen(3000);
console.log('User service is running on port 3000');
logger.log('Application is running on: http://localhost:3000');
}
bootstrap();
129 changes: 129 additions & 0 deletions user-service/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { ClientProxy } from '@nestjs/microservices';
import {
UserNotFoundException,
DuplicateEmailException,
} from '../common/exceptions/user.exception';

describe('UsersService', () => {
let service: UsersService;
let clientProxyMock: jest.Mocked<ClientProxy>;

beforeEach(async () => {
// Create a mock for ClientProxy
clientProxyMock = {
emit: jest.fn().mockReturnValue({ toPromise: () => Promise.resolve() }),
} as any;

const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: 'NOTIFICATION_SERVICE',
useValue: clientProxyMock,
},
],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('create', () => {
it('should create a new user and emit event', async () => {
const createUserDto = {
email: '[email protected]',
name: 'Test User',
};

const user = await service.create(createUserDto);

expect(user).toMatchObject(createUserDto);
expect(user.id).toBeDefined();
expect(user.createdAt).toBeDefined();
expect(clientProxyMock.emit).toHaveBeenCalledWith('user_created', user);
});

it('should throw DuplicateEmailException for duplicate email', async () => {
const createUserDto = {
email: '[email protected]',
name: 'Test User',
};

await service.create(createUserDto);

await expect(service.create(createUserDto)).rejects.toThrow(
DuplicateEmailException,
);
});
});

describe('findOne', () => {
it('should return a user if it exists', async () => {
const createUserDto = {
email: '[email protected]',
name: 'Test User',
};

const createdUser = await service.create(createUserDto);
const foundUser = service.findOne(createdUser.id);

expect(foundUser).toMatchObject(createUserDto);
});

it('should throw UserNotFoundException if user does not exist', () => {
expect(() => service.findOne('nonexistent-id')).toThrow(
UserNotFoundException,
);
});
});

describe('update', () => {
it('should update a user if it exists', async () => {
const createUserDto = {
email: '[email protected]',
name: 'Test User',
};

const createdUser = await service.create(createUserDto);
const updateUserDto = { name: 'Updated Name' };

const updatedUser = service.update(createdUser.id, updateUserDto);

expect(updatedUser.name).toBe(updateUserDto.name);
expect(updatedUser.email).toBe(createUserDto.email);
});

it('should throw UserNotFoundException if user does not exist', () => {
expect(() =>
service.update('nonexistent-id', { name: 'Updated Name' }),
).toThrow(UserNotFoundException);
});
});

describe('remove', () => {
it('should remove a user if it exists', async () => {
const createUserDto = {
email: '[email protected]',
name: 'Test User',
};

const createdUser = await service.create(createUserDto);
service.remove(createdUser.id);

expect(() => service.findOne(createdUser.id)).toThrow(
UserNotFoundException,
);
});

it('should throw UserNotFoundException if user does not exist', () => {
expect(() => service.remove('nonexistent-id')).toThrow(
UserNotFoundException,
);
});
});
});
Loading

0 comments on commit 7baa3fa

Please sign in to comment.