Skip to content

Commit

Permalink
BC-7082 Add domain error handler (#4904)
Browse files Browse the repository at this point in the history
To allow passing errors to our domain workflow a domain error handler is outsource from the error pipline.
  • Loading branch information
CeEv authored Apr 22, 2024
1 parent e2b5b63 commit 2566232
Show file tree
Hide file tree
Showing 20 changed files with 455 additions and 124 deletions.
2 changes: 1 addition & 1 deletion apps/server/src/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import { ValidationModule } from './validation';
*/
@Module({
imports: [LoggerModule, ErrorModule, ValidationModule, InterceptorModule],
exports: [LoggerModule],
exports: [LoggerModule, ErrorModule],
})
export class CoreModule {}
205 changes: 205 additions & 0 deletions apps/server/src/core/error/domain/domainErrorHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { BadRequestException, HttpStatus, InternalServerErrorException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BusinessError } from '@shared/common';
import { ErrorLogger, ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger';
import util from 'util';
import { ErrorLoggable } from '../loggable/error.loggable';
import { ErrorUtils } from '../utils';
import { DomainErrorHandler } from './domainErrorHandler';

class SampleLoggableException extends BadRequestException implements Loggable {
constructor(private testData: string) {
super();
}

getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage {
const message = {
type: 'BAD_REQUEST_EXCEPTION',
stack: this.stack,
data: {
testData: this.testData,
},
};

return message;
}
}

class SampleLoggableExceptionWithCause extends InternalServerErrorException implements Loggable {
constructor(private readonly testValue: string, error?: unknown) {
super(ErrorUtils.createHttpExceptionOptions(error));
}

getLogMessage(): ErrorLogMessage {
const message: ErrorLogMessage = {
type: 'WITH_CAUSE',
stack: this.stack,
data: {
testValue: this.testValue,
},
};

return message;
}
}

class SampleLoggableFromBusinessException extends BusinessError implements Loggable {
constructor(private readonly testValue: string) {
super(
{
type: 'xyu',
title: 'test_title',
defaultMessage: 'test_defaultMessage',
},
HttpStatus.INTERNAL_SERVER_ERROR
);
}

getLogMessage(): ErrorLogMessage {
const message: ErrorLogMessage = {
type: 'WITH_CAUSE',
stack: this.stack,
data: {
testValue: this.testValue,
},
};

return message;
}
}

describe('GlobalErrorFilter', () => {
let module: TestingModule;
let domainErrorHandler: DomainErrorHandler;
let logger: DeepMocked<ErrorLogger>;

beforeAll(async () => {
module = await Test.createTestingModule({
providers: [
DomainErrorHandler,
{
provide: ErrorLogger,
useValue: createMock<ErrorLogger>(),
},
],
}).compile();

domainErrorHandler = module.get(DomainErrorHandler);
logger = module.get(ErrorLogger);
});

afterEach(() => {
jest.resetAllMocks();
});

afterAll(async () => {
await module.close();
});

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

describe('exec', () => {
describe('when random non error object is passed', () => {
const setup = () => {
const rndObject = { abc: '123' };

return { rndObject };
};

it('should call logger with error', () => {
const { rndObject } = setup();

domainErrorHandler.exec(rndObject);

expect(logger.error).toBeCalledWith(expect.any(ErrorLoggable));
});
});

describe('when error implements Loggable', () => {
const setup = () => {
const error = new SampleLoggableException('test');

return { error };
};

it('should call logger with error', () => {
const { error } = setup();

domainErrorHandler.exec(error);

expect(logger.error).toBeCalledWith(error);
});
});

describe('when error is a generic error', () => {
const setup = () => {
const error = new Error('test');
const loggable = new ErrorLoggable(error);

return { error, loggable };
};

it('should call logger with ErrorLoggable', () => {
const { error, loggable } = setup();

domainErrorHandler.exec(error);

expect(logger.error).toBeCalledWith(loggable);
});
});

describe('when error is some random object', () => {
const setup = () => {
const randomObject = { foo: 'bar' };
const error = new Error(util.inspect(randomObject));
const loggable = new ErrorLoggable(error);

return { error, loggable };
};

it('should call logger with ErrorLoggable', () => {
const { error, loggable } = setup();

domainErrorHandler.exec(error);

expect(logger.error).toBeCalledWith(loggable);
});
});

describe('when error is loggable exception with cause', () => {
const setup = () => {
const error = new Error('test');
const loggable = new SampleLoggableExceptionWithCause('test', error);

return { error, loggable };
};

it('should call logger with ErrorLoggable', () => {
const { loggable } = setup();

domainErrorHandler.exec(loggable);

expect(logger.error).toBeCalledWith(loggable);
});
});

describe('when error is a business exception', () => {
const setup = () => {
const loggable = new SampleLoggableFromBusinessException('test');

return { loggable };
};

it('should call logger with ErrorLoggable', () => {
const { loggable } = setup();

domainErrorHandler.exec(loggable);

expect(logger.error).toBeCalledWith(loggable);
});
});
});
});
29 changes: 29 additions & 0 deletions apps/server/src/core/error/domain/domainErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import util from 'util';
import { ErrorLogger, Loggable, LoggingUtils } from '@src/core/logger';
import { ErrorLoggable } from '../loggable';

@Injectable()
export class DomainErrorHandler {
constructor(private readonly logger: ErrorLogger) {}

public exec(error: unknown): void {
const loggable = this.createErrorLoggable(error);
this.logger.error(loggable);
}

private createErrorLoggable(error: unknown): Loggable {
let loggable: Loggable;

if (LoggingUtils.isInstanceOfLoggable(error)) {
loggable = error;
} else if (error instanceof Error) {
loggable = new ErrorLoggable(error);
} else {
const unknownError = new Error(util.inspect(error));
loggable = new ErrorLoggable(unknownError);
}

return loggable;
}
}
1 change: 1 addition & 0 deletions apps/server/src/core/error/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DomainErrorHandler } from './domainErrorHandler';
3 changes: 3 additions & 0 deletions apps/server/src/core/error/error.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { LoggerModule } from '../logger';
import { GlobalErrorFilter } from './filter/global-error.filter';
import { DomainErrorHandler } from './domain';

/**
* Overrides the default global Exception Filter of NestJS provided by @APP_FILTER
Expand All @@ -13,6 +14,8 @@ import { GlobalErrorFilter } from './filter/global-error.filter';
provide: APP_FILTER,
useClass: GlobalErrorFilter,
},
DomainErrorHandler,
],
exports: [DomainErrorHandler],
})
export class ErrorModule {}
Loading

0 comments on commit 2566232

Please sign in to comment.