Commit e3cd5ac0 authored by Harold Boissenin's avatar Harold Boissenin
Browse files

initial commit

No related merge requests found
Showing with 439 additions and 0 deletions
+439 -0
import { HttpErrorResponse } from '@angular/common/http';
import { AppError } from './app.error';
import { ConstraintViolations } from './constraint-violations.class';
import { AccessDeniedError, AuthenticationError, BadRequestError, HTTPError } from './http.error';
import { normalizeError } from './normalize-error.function';
describe('normalizeError', () => {
it('should use string as message', () => {
const err = 'Error';
expect(normalizeError(err)).toEqual(new AppError(err, err, undefined, false));
});
describe('should return NullError on empty values', () => {
const falsies = [null, false, {}, [], '', 0];
for (const falsy of falsies) {
it(JSON.stringify(falsy), () => expect(normalizeError(falsy).isNullError).toBeTruthy());
}
});
it('should reuse Error properties', () => {
const err = new Error('BAM!');
expect(normalizeError(err)).toEqual(new AppError(err, err.message, err.stack, false));
});
it('should return AppErrors as is', () => {
const orig = new Error('BAM!');
const err = new AppError(orig, orig.message);
expect(normalizeError(err)).toBe(err);
});
it('should classify HttpErrorResponse as HTTPError', () => {
const err = new HttpErrorResponse({
error: 'BAM!',
status: 404,
statusText: 'Not found',
url: 'http://example.com',
});
const appErr = normalizeError(err);
expect(appErr instanceof HTTPError).toBeTruthy();
if (appErr instanceof HTTPError) {
expect(appErr.message).toContain('BAM!');
expect(appErr.transient).toBeFalsy();
expect(appErr.original).toBe(err);
}
});
it('should classify 5xx HttpErrorResponse as transient Error', () => {
const err = new HttpErrorResponse({ status: 500 });
const appErr = normalizeError(err);
expect(appErr instanceof HTTPError).toBeTruthy();
if (appErr instanceof HTTPError) {
expect(appErr.transient).toBeTruthy();
}
});
it('should classify 401 HttpErrorResponse as AuthenticationError', () => {
const err = new HttpErrorResponse({ status: 401 });
const appErr = normalizeError(err);
expect(appErr instanceof AuthenticationError).toBeTruthy();
});
it('should classify 403 HttpErrorResponse as AccessDeniedError', () => {
const err = new HttpErrorResponse({ status: 403 });
const appErr = normalizeError(err);
expect(appErr instanceof AccessDeniedError).toBeTruthy();
});
it('should classify 400 HttpErrorResponse as BadRequestError', () => {
const err = new HttpErrorResponse({ status: 400 });
const appErr = normalizeError(err);
expect(appErr instanceof BadRequestError).toBeTruthy();
});
it('should classify sepcial 400 HttpErrorResponse as ConstraintViolations', () => {
const violations = [
{
propertyPath: 'properties',
message: 'The product must have the minimal properties required ("description", "price")',
},
];
const err = new HttpErrorResponse({
error: {
'@context': '/contexts/ConstraintViolationList',
'@type': 'ConstraintViolationList',
'hydra:title': 'An error occurred',
'hydra:description':
'properties: The product must have the minimal properties required ("description",' + ' "price")',
violations,
},
status: 400,
});
const appErr = normalizeError(err);
expect(appErr instanceof ConstraintViolations).toBeTruthy();
if (appErr instanceof ConstraintViolations) {
expect(appErr.violations).toEqual(violations);
}
});
});
import { HttpErrorResponse } from '@angular/common/http';
import * as _ from 'lodash';
import { AppError } from './app.error';
import { ConstraintViolations } from './constraint-violations.class';
import { AccessDeniedError, AuthenticationError, BadRequestError, HTTPError } from './http.error';
import { DispatchableError } from './interfaces';
import { NullError } from './null.error';
/**
* Transforme une valeur en erreur.
*
* Les informations intéressantes d'HttpErrorResponse et Error sont conservés.
*/
export function normalizeError(err: DispatchableError | HttpErrorResponse | Error | any): DispatchableError {
if (err instanceof AppError || err === NullError) {
return err;
}
if (err instanceof HttpErrorResponse) {
return normalizeHttpErrorResponse(err);
}
if (err instanceof Error) {
return new AppError(err, err.message, err.stack, false);
}
if (_.isEmpty(err)) {
return NullError;
}
if (typeof err === 'object') {
return new AppError(err, extractMessage(err), err.stack, err.transient || false);
}
return new AppError(err, extractMessage(err));
}
function normalizeHttpErrorResponse(err: HttpErrorResponse): HTTPError {
const body: any = 'error' in err ? err.error : {};
const message = extractMessage(body) || extractMessage(err);
switch (err.status) {
case 400:
if (_.has(body, 'violations') && _.isArray(body.violations)) {
return new ConstraintViolations(err, message, body.violations);
}
return new BadRequestError(err, message);
case 401:
return new AuthenticationError(err, message);
case 403:
return new AccessDeniedError(err, message);
default:
return new HTTPError(err, message);
}
}
function extractMessage(err: any): string | undefined {
if (!err) {
return undefined;
}
switch (typeof err) {
case 'object':
if ('message' in err) {
return err.message;
}
if ('hydra:description' in err) {
return err['hydra:description'];
}
if ('hydra:title' in err) {
return err['hydra:title'];
}
if ('error' in err) {
return extractMessage(err.error);
}
break;
case 'string':
case 'number':
return `${err}`;
}
return undefined;
}
import { NullError } from './null.error';
describe('NullError', () => {
it('.isNullError should be true', () => {
expect(NullError.isNullError).toBe(true);
});
describe('.dispatch()', () => {
it('should clear error message', () => {
const handler = jasmine.createSpyObj('handler', ['setError']);
handler.setError.and.returnValue(true);
expect(NullError.dispatch(handler)).toBeTruthy();
expect(handler.setError).toHaveBeenCalledWith(null);
});
it('should clear constraint violations', () => {
const handler = jasmine.createSpyObj('handler', ['setConstraintViolations']);
handler.setConstraintViolations.and.returnValue(true);
expect(NullError.dispatch(handler)).toBeTruthy();
expect(handler.setConstraintViolations).toHaveBeenCalledWith(null);
});
it('should clear validation errors', () => {
const handler = jasmine.createSpyObj('handler', ['setErrors']);
handler.setErrors.and.returnValue(true);
expect(NullError.dispatch(handler)).toBeTruthy();
expect(handler.setErrors).toHaveBeenCalledWith(null);
});
it('should not log error', () => {
const handler = jasmine.createSpyObj('handler', ['logError']);
handler.logError.and.returnValue(true);
expect(NullError.dispatch(handler)).toBeFalsy();
expect(handler.logError).not.toHaveBeenCalled();
});
});
});
import { clearErrors } from './clear-errors.function';
import { DispatchableError, ErrorHandler } from './interfaces';
/**
* Singleton d'erreur null.
*/
export const NullError: DispatchableError = {
isNullError: true,
name: 'NullError',
message: 'No error',
dispatch: (handler: ErrorHandler) => {
return clearErrors(handler);
},
};
import * as _ from 'lodash';
import {
ConstraintViolationHandler,
DispatchableError,
ErrorClearer,
ErrorHandler,
ErrorLogger,
ErrorMessageHandler,
ValidationErrorHandler,
} from './interfaces';
export function isErrorMessageHandler(that: any): that is ErrorMessageHandler {
return _.hasIn(that, 'setError') && _.isFunction(that.setError);
}
export function isConstraintViolationHandler(that: any): that is ConstraintViolationHandler {
return _.hasIn(that, 'setConstraintViolations') && _.isFunction(that.setConstraintViolations);
}
export function isValidationErrorHandler(that: any): that is ValidationErrorHandler {
return _.hasIn(that, 'setErrors') && _.isFunction(that.setErrors);
}
export function isErrorLogger(that: any): that is ErrorLogger {
return _.hasIn(that, 'logError') && _.isFunction(that.logError);
}
export function isErrorHandler(that: any): that is ErrorHandler {
return (
isErrorMessageHandler(that) ||
isConstraintViolationHandler(that) ||
isValidationErrorHandler(that) ||
isErrorLogger(that)
);
}
export function isErrorClearer(that: any): that is ErrorClearer {
return _.hasIn(that, 'clearErrors') && _.isFunction(that.clearErrors);
}
export function isDispatchableError(that: any): that is DispatchableError {
return _.hasIn(that, 'dispatch') && _.isFunction(that.dispatch);
}
import { AppError } from './app.error';
import { DispatchableError } from './interfaces';
/**
* Erreur émise lorsqu'une exception ne peut pas être dispatchée.
*/
export class UnhandledError extends AppError<DispatchableError> {
public constructor(original: DispatchableError) {
super(original, `Unhandled error: ${original.message}`, original.stack, false);
}
}
import { ValidationError } from './validation.error';
describe('ValidationError', () => {
const validationErrors = {
message: 'This field is required',
required: true,
};
let err: ValidationError;
beforeEach(() => {
err = new ValidationError(validationErrors);
});
it('.isNullError should be false', () => {
expect(err.isNullError).toBe(false);
});
describe('.dispatch()', () => {
it('should set error message', () => {
const handler = jasmine.createSpyObj('handler', ['setError']);
handler.setError.and.returnValue(true);
expect(err.dispatch(handler)).toBeTruthy();
expect(handler.setError).toHaveBeenCalledWith('This field is required');
});
it('should set validation errors', () => {
const handler = jasmine.createSpyObj('handler', ['setErrors']);
handler.setErrors.and.returnValue(true);
expect(err.dispatch(handler)).toBeTruthy();
expect(handler.setErrors).toHaveBeenCalledWith(validationErrors);
});
});
});
import { ValidationErrors } from '@angular/forms';
import { AppError } from './app.error';
import { isValidationErrorHandler } from './type-guards.function';
export class ValidationError extends AppError<ValidationErrors> {
public constructor(public readonly errors: ValidationErrors) {
super(errors, errors.message || 'Validation error');
}
public dispatch(handler: any): boolean {
return (isValidationErrorHandler(handler) && handler.setErrors(this.errors)) || super.dispatch(handler);
}
}
src/src.ts 0 → 100644
/*
* Public API Surface of error-types
*/
export * from './lib/error-handler.service';
export * from './lib/error.module';
export * from './lib/generic-error.component';
export * from './lib/not-found-error.component';
export * from './lib/types/app.error';
export * from './lib/types/clear-errors.function';
export * from './lib/types/constraint-violations.class';
export * from './lib/types/http.error';
export * from './lib/types/interfaces';
export * from './lib/types/normalize-error.function';
export * from './lib/types/null.error';
export * from './lib/types/type-guards.function';
export * from './lib/types/unhandled.error';
export * from './lib/types/validation.error';
src/test.ts 0 → 100644
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
{
"compilerOptions": {
"outDir": "out-tsc/lib",
"target": "es2015",
"declaration": true,
"sourceMap": true,
"inlineSources": false,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}
{
"compilerOptions": {
"outDir": "out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}
tslint.json 0 → 100644
{
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment