mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
Clean up old errors (#3633)
This PR attempts to improve the error handling introduced in #3607. ## About the changes ## **tl;dr:** - Make `UnleashError` constructor protected - Make all custom errors inherit from `UnleashError`. - Add tests to ensure that all special error cases include their relevant data - Remove `PasswordMismatchError` and `BadRequestError`. These don't exist. - Add a few new error types: `ContentTypeError`, `NotImplementedError`, `UnauthorizedError` - Remove the `...rest` parameter from error constructor - Add an unexported `GenericUnleashError` class - Move OpenAPI conversion function to `BadDataError` clas - Remove explicit `Error.captureStackTrace`. This is done automatically. - Extract `getPropFromString` function and add tests ### **In a more verbose fashion** The main thing is that all our internal errors now inherit from`UnleashError`. This allows us to simplify the `UnleashError` constructor and error handling in general while still giving us the extra benefits we added to that class. However, it _does_ also mean that I've had to update **all** existing error classes. The constructor for `UnleashError` is now protected and all places that called that constructor directly have been updated. Because the base error isn't available anymore, I've added three new errors to cover use cases that we didn't already have covered: `NotImplementedError`, `UnauthorizedError`, `ContentTypeError`. This is to stay consistent in how we report errors to the user. There is also an internal class, `GenericUnleashError` that inherits from the base error. This class is only used in conversions for cases where we don't know what the error is. It is not exported. In making all the errors inherit, I've also removed the `...rest` parameter from the `UnleashError` constructor. We don't need this anymore. Following on from the fixes with missing properties in #3638, I have added tests for all errors that contain extra data. Some of the error names that were originally used when creating the list don't exist in the backend. `BadRequestError` and `PasswordMismatchError` have been removed. The `BadDataError` class now contains the conversion code for OpenAPI validation errors. In doing so, I extracted and tested the `getPropFromString` function. ### Main files Due to the nature of the changes, there's a lot of files to look at. So to make it easier to know where to turn your attention: The changes in `api-error.ts` contain the main changes: protected constructor, removal of OpenAPI conversion (moved into `BadDataError`. `api-error.test.ts` contains tests to make sure that errors work as expected. Aside from `get-prop-from-string.ts` and the tests, everything else is just the required updates to go through with the changes. ## Discussion points I've gone for inheritance of the Error type over composition. This is in large part because throwing actual Error instances instead of just objects is preferable (because they collect stack traces, for instance). However, it's quite possible that we could solve the same thing in a more elegant fashion using composition. ## For later / suggestions for further improvements The `api-error` files still contain a lot of code. I think it might be beneficial to break each Error into a separate folder that includes the error, its tests, and its schema (if required). It would help decouple it a bit. We don't currently expose the schema anywhere, so it's not available in the openapi spec. We should look at exposing it too. Finally, it would be good to go through each individual error message and update each one to be as helpful as possible.
This commit is contained in:
parent
191e40c95e
commit
9943179393
@ -119,6 +119,7 @@
|
|||||||
"json-schema-to-ts": "2.7.2",
|
"json-schema-to-ts": "2.7.2",
|
||||||
"json2csv": "^5.0.7",
|
"json2csv": "^5.0.7",
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
"log4js": "^6.0.0",
|
"log4js": "^6.0.0",
|
||||||
"make-fetch-happen": "^11.0.0",
|
"make-fetch-happen": "^11.0.0",
|
||||||
"memoizee": "^0.4.15",
|
"memoizee": "^0.4.15",
|
||||||
|
@ -28,7 +28,7 @@ import { Knex } from 'knex';
|
|||||||
import maintenanceMiddleware from './middleware/maintenance-middleware';
|
import maintenanceMiddleware from './middleware/maintenance-middleware';
|
||||||
import { unless } from './middleware/unless-middleware';
|
import { unless } from './middleware/unless-middleware';
|
||||||
import { catchAllErrorHandler } from './middleware/catch-all-error-handler';
|
import { catchAllErrorHandler } from './middleware/catch-all-error-handler';
|
||||||
import { UnleashError } from './error/api-error';
|
import NotFoundError from './error/notfound-error';
|
||||||
|
|
||||||
export default async function getApp(
|
export default async function getApp(
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -186,10 +186,9 @@ export default async function getApp(
|
|||||||
|
|
||||||
// handle all API 404s
|
// handle all API 404s
|
||||||
app.use(`${baseUriPath}/api`, (req, res) => {
|
app.use(`${baseUriPath}/api`, (req, res) => {
|
||||||
const error = new UnleashError({
|
const error = new NotFoundError(
|
||||||
name: 'NotFoundError',
|
`The path you were looking for (${baseUriPath}/api${req.path}) is not available.`,
|
||||||
message: `The path you were looking for (${baseUriPath}/api${req.path}) is not available.`,
|
);
|
||||||
});
|
|
||||||
res.status(error.statusCode).send(error);
|
res.status(error.statusCode).send(error);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
@ -1,335 +0,0 @@
|
|||||||
import owasp from 'owasp-password-strength-test';
|
|
||||||
import { ErrorObject } from 'ajv';
|
|
||||||
import {
|
|
||||||
ApiErrorSchema,
|
|
||||||
fromLegacyError,
|
|
||||||
fromOpenApiValidationError,
|
|
||||||
fromOpenApiValidationErrors,
|
|
||||||
UnleashApiErrorNameWithoutExtraData,
|
|
||||||
UnleashApiErrorTypes,
|
|
||||||
UnleashError,
|
|
||||||
} from './api-error';
|
|
||||||
import BadDataError from './bad-data-error';
|
|
||||||
import OwaspValidationError from './owasp-validation-error';
|
|
||||||
|
|
||||||
describe('v5 deprecation: backwards compatibility', () => {
|
|
||||||
it.each(UnleashApiErrorTypes)(
|
|
||||||
'Adds details to error type: "%s"',
|
|
||||||
(name: UnleashApiErrorNameWithoutExtraData) => {
|
|
||||||
const message = `Error type: ${name}`;
|
|
||||||
const error = new UnleashError({ name, message }).toJSON();
|
|
||||||
|
|
||||||
expect(error.message).toBe(message);
|
|
||||||
expect(error.details).toStrictEqual([
|
|
||||||
{
|
|
||||||
message,
|
|
||||||
description: message,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Standard/legacy error conversion', () => {
|
|
||||||
it('Moves message to the details list for baddataerror', () => {
|
|
||||||
const message = `: message!`;
|
|
||||||
const result = fromLegacyError(new BadDataError(message)).toJSON();
|
|
||||||
|
|
||||||
expect(result.message.includes('`details`'));
|
|
||||||
expect(result.details).toStrictEqual([
|
|
||||||
{
|
|
||||||
message,
|
|
||||||
description: message,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('OpenAPI error conversion', () => {
|
|
||||||
it('Gives useful error messages for missing properties', () => {
|
|
||||||
const error = {
|
|
||||||
keyword: 'required',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body',
|
|
||||||
schemaPath: '#/components/schemas/addonCreateUpdateSchema/required',
|
|
||||||
params: {
|
|
||||||
missingProperty: 'enabled',
|
|
||||||
},
|
|
||||||
message: "should have required property 'enabled'",
|
|
||||||
};
|
|
||||||
|
|
||||||
const { description } = fromOpenApiValidationError({})(error);
|
|
||||||
|
|
||||||
// it tells the user that the property is required
|
|
||||||
expect(description.includes('required')).toBeTruthy();
|
|
||||||
// it tells the user the name of the missing property
|
|
||||||
expect(description.includes(error.params.missingProperty)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Gives useful error messages for type errors', () => {
|
|
||||||
const error = {
|
|
||||||
keyword: 'type',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body.parameters',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/parameters/type',
|
|
||||||
params: {
|
|
||||||
type: 'object',
|
|
||||||
},
|
|
||||||
message: 'should be object',
|
|
||||||
};
|
|
||||||
|
|
||||||
const parameterValue = [];
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
parameters: parameterValue,
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it provides the message
|
|
||||||
expect(description.includes(error.message)).toBeTruthy();
|
|
||||||
// it tells the user what they provided
|
|
||||||
expect(
|
|
||||||
description.includes(JSON.stringify(parameterValue)),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Gives useful pattern error messages', () => {
|
|
||||||
const error = {
|
|
||||||
instancePath: '',
|
|
||||||
keyword: 'pattern',
|
|
||||||
dataPath: '.body.description',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/description/pattern',
|
|
||||||
params: {
|
|
||||||
pattern: '^this is',
|
|
||||||
},
|
|
||||||
message: 'should match pattern "^this is"',
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestDescription = 'A pattern that does not match.';
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
description: requestDescription,
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it tells the user what the pattern it should match is
|
|
||||||
expect(description.includes(error.params.pattern)).toBeTruthy();
|
|
||||||
// it tells the user which property it pertains to
|
|
||||||
expect(description.includes('description')).toBeTruthy();
|
|
||||||
// it tells the user what they provided
|
|
||||||
expect(description.includes(requestDescription)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Gives useful min/maxlength error messages', () => {
|
|
||||||
const error = {
|
|
||||||
instancePath: '',
|
|
||||||
keyword: 'maxLength',
|
|
||||||
dataPath: '.body.description',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/description/maxLength',
|
|
||||||
params: {
|
|
||||||
limit: 5,
|
|
||||||
},
|
|
||||||
message: 'should NOT be longer than 5 characters',
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestDescription = 'Longer than the max length';
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
description: requestDescription,
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it tells the user what the pattern it should match is
|
|
||||||
expect(
|
|
||||||
description.includes(error.params.limit.toString()),
|
|
||||||
).toBeTruthy();
|
|
||||||
// it tells the user which property it pertains to
|
|
||||||
expect(description.includes('description')).toBeTruthy();
|
|
||||||
// it tells the user what they provided
|
|
||||||
expect(description.includes(requestDescription)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Handles numerical min/max errors', () => {
|
|
||||||
const error = {
|
|
||||||
keyword: 'maximum',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body.newprop',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/newprop/maximum',
|
|
||||||
params: {
|
|
||||||
comparison: '<=',
|
|
||||||
limit: 5,
|
|
||||||
exclusive: false,
|
|
||||||
},
|
|
||||||
message: 'should be <= 5',
|
|
||||||
};
|
|
||||||
|
|
||||||
const propertyValue = 6;
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
newprop: propertyValue,
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it tells the user what the limit is
|
|
||||||
expect(
|
|
||||||
description.includes(error.params.limit.toString()),
|
|
||||||
).toBeTruthy();
|
|
||||||
// it tells the user what kind of comparison it performed
|
|
||||||
expect(description.includes(error.params.comparison)).toBeTruthy();
|
|
||||||
// it tells the user which property it pertains to
|
|
||||||
expect(description.includes('newprop')).toBeTruthy();
|
|
||||||
// it tells the user what they provided
|
|
||||||
expect(description.includes(propertyValue.toString())).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Handles multiple errors', () => {
|
|
||||||
const errors: [ErrorObject, ...ErrorObject[]] = [
|
|
||||||
{
|
|
||||||
keyword: 'maximum',
|
|
||||||
instancePath: '',
|
|
||||||
// @ts-expect-error
|
|
||||||
dataPath: '.body.newprop',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/newprop/maximum',
|
|
||||||
params: {
|
|
||||||
comparison: '<=',
|
|
||||||
limit: 5,
|
|
||||||
exclusive: false,
|
|
||||||
},
|
|
||||||
message: 'should be <= 5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyword: 'required',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/required',
|
|
||||||
params: {
|
|
||||||
missingProperty: 'enabled',
|
|
||||||
},
|
|
||||||
message: "should have required property 'enabled'",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// create an error and serialize it as it would be shown to the end user.
|
|
||||||
const serializedUnleashError: ApiErrorSchema =
|
|
||||||
fromOpenApiValidationErrors({ newprop: 7 }, errors).toJSON();
|
|
||||||
|
|
||||||
expect(serializedUnleashError.name).toBe('ValidationError');
|
|
||||||
expect(serializedUnleashError.message).toContain('`details`');
|
|
||||||
expect(
|
|
||||||
serializedUnleashError.details!![0].description.includes('newprop'),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
serializedUnleashError.details!![1].description.includes('enabled'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Disallowed additional properties', () => {
|
|
||||||
it('gives useful messages for base-level properties', () => {
|
|
||||||
const openApiError = {
|
|
||||||
keyword: 'additionalProperties',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/additionalProperties',
|
|
||||||
params: { additionalProperty: 'bogus' },
|
|
||||||
message: 'should NOT have additional properties',
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = fromOpenApiValidationError({ bogus: 5 })(
|
|
||||||
openApiError,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
error.description.includes(
|
|
||||||
openApiError.params.additionalProperty,
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(error.description).toMatch(/\broot\b/i);
|
|
||||||
expect(error.description).toMatch(/\badditional properties\b/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gives useful messages for nested properties', () => {
|
|
||||||
const request2 = {
|
|
||||||
nestedObject: {
|
|
||||||
nested2: { extraPropertyName: 'illegal property' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const openApiError = {
|
|
||||||
keyword: 'additionalProperties',
|
|
||||||
instancePath: '',
|
|
||||||
dataPath: '.body.nestedObject.nested2',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/nestedObject/properties/nested2/additionalProperties',
|
|
||||||
params: { additionalProperty: 'extraPropertyName' },
|
|
||||||
message: 'should NOT have additional properties',
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = fromOpenApiValidationError(request2)(openApiError);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
error.description.includes('nestedObject.nested2'),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
error.description.includes(
|
|
||||||
openApiError.params.additionalProperty,
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
error.description
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('additional properties'),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Handles deeply nested properties gracefully', () => {
|
|
||||||
const error = {
|
|
||||||
keyword: 'type',
|
|
||||||
dataPath: '.body.nestedObject.a.b',
|
|
||||||
schemaPath:
|
|
||||||
'#/components/schemas/addonCreateUpdateSchema/properties/nestedObject/properties/a/properties/b/type',
|
|
||||||
params: { type: 'string' },
|
|
||||||
message: 'should be string',
|
|
||||||
instancePath: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
nestedObject: { a: { b: [] } },
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it should hold the full path to the error
|
|
||||||
expect(description).toMatch(/\bnestedObject.a.b\b/);
|
|
||||||
// it should include the value that the user sent
|
|
||||||
expect(description.includes('[]')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Handles deeply nested properties on referenced schemas', () => {
|
|
||||||
const error = {
|
|
||||||
keyword: 'type',
|
|
||||||
dataPath: '.body.nestedObject.a.b',
|
|
||||||
schemaPath: '#/components/schemas/parametersSchema/type',
|
|
||||||
params: { type: 'object' },
|
|
||||||
message: 'should be object',
|
|
||||||
instancePath: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const illegalValue = 'illegal string';
|
|
||||||
const { description } = fromOpenApiValidationError({
|
|
||||||
nestedObject: { a: { b: illegalValue } },
|
|
||||||
})(error);
|
|
||||||
|
|
||||||
// it should hold the full path to the error
|
|
||||||
expect(description).toMatch(/\bnestedObject.a.b\b/);
|
|
||||||
// it should include the value that the user sent
|
|
||||||
expect(description.includes(illegalValue)).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Error serialization special cases', () => {
|
|
||||||
it('OwaspValidationErrors: adds `validationErrors` to `details`', () => {
|
|
||||||
const results = owasp.test('123');
|
|
||||||
const error = new OwaspValidationError(results);
|
|
||||||
const json = fromLegacyError(error).toJSON();
|
|
||||||
|
|
||||||
expect(json.details!![0].message).toBe(results.errors[0]);
|
|
||||||
expect(json.details!![0].validationErrors).toBe(results.errors);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,342 +0,0 @@
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
|
||||||
import { FromSchema } from 'json-schema-to-ts';
|
|
||||||
import { ErrorObject } from 'ajv';
|
|
||||||
import OwaspValidationError from './owasp-validation-error';
|
|
||||||
|
|
||||||
export const UnleashApiErrorTypes = [
|
|
||||||
'ContentTypeError',
|
|
||||||
'DisabledError',
|
|
||||||
'FeatureHasTagError',
|
|
||||||
'IncompatibleProjectError',
|
|
||||||
'InvalidOperationError',
|
|
||||||
'MinimumOneEnvironmentError',
|
|
||||||
'NameExistsError',
|
|
||||||
'NoAccessError',
|
|
||||||
'NotFoundError',
|
|
||||||
'NotImplementedError',
|
|
||||||
'OperationDeniedError',
|
|
||||||
'PasswordMismatch',
|
|
||||||
'PasswordMismatchError',
|
|
||||||
'PasswordUndefinedError',
|
|
||||||
'ProjectWithoutOwnerError',
|
|
||||||
'RoleInUseError',
|
|
||||||
'UnknownError',
|
|
||||||
'UsedTokenError',
|
|
||||||
|
|
||||||
// server errors; not the end user's fault
|
|
||||||
'InternalError',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const UnleashApiErrorTypesWithExtraData = [
|
|
||||||
'BadDataError',
|
|
||||||
'BadRequestError',
|
|
||||||
'ValidationError',
|
|
||||||
'AuthenticationRequired',
|
|
||||||
'NoAccessError',
|
|
||||||
'InvalidTokenError',
|
|
||||||
'OwaspValidationError',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const AllUnleashApiErrorTypes = [
|
|
||||||
...UnleashApiErrorTypes,
|
|
||||||
...UnleashApiErrorTypesWithExtraData,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type UnleashApiErrorName = typeof AllUnleashApiErrorTypes[number];
|
|
||||||
export type UnleashApiErrorNameWithoutExtraData =
|
|
||||||
typeof UnleashApiErrorTypes[number];
|
|
||||||
|
|
||||||
const statusCode = (errorName: UnleashApiErrorName): number => {
|
|
||||||
switch (errorName) {
|
|
||||||
case 'ContentTypeError':
|
|
||||||
return 415;
|
|
||||||
case 'ValidationError':
|
|
||||||
return 400;
|
|
||||||
case 'BadDataError':
|
|
||||||
return 400;
|
|
||||||
case 'BadRequestError':
|
|
||||||
return 400;
|
|
||||||
case 'OwaspValidationError':
|
|
||||||
return 400;
|
|
||||||
case 'PasswordUndefinedError':
|
|
||||||
return 400;
|
|
||||||
case 'MinimumOneEnvironmentError':
|
|
||||||
return 400;
|
|
||||||
case 'InvalidTokenError':
|
|
||||||
return 401;
|
|
||||||
case 'NoAccessError':
|
|
||||||
return 403;
|
|
||||||
case 'UsedTokenError':
|
|
||||||
return 403;
|
|
||||||
case 'InvalidOperationError':
|
|
||||||
return 403;
|
|
||||||
case 'IncompatibleProjectError':
|
|
||||||
return 403;
|
|
||||||
case 'OperationDeniedError':
|
|
||||||
return 403;
|
|
||||||
case 'NotFoundError':
|
|
||||||
return 404;
|
|
||||||
case 'NameExistsError':
|
|
||||||
return 409;
|
|
||||||
case 'FeatureHasTagError':
|
|
||||||
return 409;
|
|
||||||
case 'RoleInUseError':
|
|
||||||
return 400;
|
|
||||||
case 'ProjectWithoutOwnerError':
|
|
||||||
return 409;
|
|
||||||
case 'UnknownError':
|
|
||||||
return 500;
|
|
||||||
case 'InternalError':
|
|
||||||
return 500;
|
|
||||||
case 'PasswordMismatch':
|
|
||||||
case 'PasswordMismatchError':
|
|
||||||
return 401;
|
|
||||||
case 'DisabledError':
|
|
||||||
return 422;
|
|
||||||
case 'NotImplementedError':
|
|
||||||
return 405;
|
|
||||||
case 'NoAccessError':
|
|
||||||
return 403;
|
|
||||||
case 'AuthenticationRequired':
|
|
||||||
return 401;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValidationErrorDescription = {
|
|
||||||
description: string;
|
|
||||||
message: string;
|
|
||||||
path?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UnleashErrorData =
|
|
||||||
| {
|
|
||||||
message: string;
|
|
||||||
documentationLink?: string;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
name: UnleashApiErrorNameWithoutExtraData;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'NoAccessError';
|
|
||||||
permission: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'AuthenticationRequired';
|
|
||||||
path: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name:
|
|
||||||
| 'ValidationError'
|
|
||||||
| 'BadDataError'
|
|
||||||
| 'BadRequestError'
|
|
||||||
| 'MinimumOneEnvironmentError'
|
|
||||||
| 'InvalidTokenError';
|
|
||||||
details: [
|
|
||||||
ValidationErrorDescription,
|
|
||||||
...ValidationErrorDescription[],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: 'OwaspValidationError';
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
validationErrors: string[];
|
|
||||||
message: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export class UnleashError extends Error {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
name: UnleashApiErrorName;
|
|
||||||
|
|
||||||
statusCode: number;
|
|
||||||
|
|
||||||
additionalParameters: object;
|
|
||||||
|
|
||||||
constructor({
|
|
||||||
name,
|
|
||||||
message,
|
|
||||||
documentationLink,
|
|
||||||
...rest
|
|
||||||
}: UnleashErrorData) {
|
|
||||||
super();
|
|
||||||
this.id = uuidV4();
|
|
||||||
this.name = name;
|
|
||||||
super.message = message;
|
|
||||||
|
|
||||||
this.statusCode = statusCode(name);
|
|
||||||
|
|
||||||
this.additionalParameters = rest;
|
|
||||||
}
|
|
||||||
|
|
||||||
help(): string {
|
|
||||||
return `Get help for id ${this.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): ApiErrorSchema {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
name: this.name,
|
|
||||||
message: this.message,
|
|
||||||
details: [{ message: this.message, description: this.message }],
|
|
||||||
...this.additionalParameters,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return `${this.name}: ${this.message}.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const apiErrorSchema = {
|
|
||||||
$id: '#/components/schemas/apiError',
|
|
||||||
type: 'object',
|
|
||||||
required: ['id', 'name', 'message'],
|
|
||||||
description:
|
|
||||||
'An Unleash API error. Contains information about what went wrong.',
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
enum: AllUnleashApiErrorTypes,
|
|
||||||
description:
|
|
||||||
'The kind of error that occurred. Meant for machine consumption.',
|
|
||||||
example: 'ValidationError',
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
type: 'string',
|
|
||||||
description:
|
|
||||||
'A unique identifier for this error instance. Can be used to search logs etc.',
|
|
||||||
example: '0b84c7fd-5278-4087-832d-0b502c7929b3',
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'A human-readable explanation of what went wrong.',
|
|
||||||
example:
|
|
||||||
"We couldn't find an addon provider with the name that you are trying to add ('bogus-addon')",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const fromLegacyError = (e: Error): UnleashError => {
|
|
||||||
const name = AllUnleashApiErrorTypes.includes(e.name as UnleashApiErrorName)
|
|
||||||
? (e.name as UnleashApiErrorName)
|
|
||||||
: 'UnknownError';
|
|
||||||
|
|
||||||
if (name === 'NoAccessError') {
|
|
||||||
return new UnleashError({
|
|
||||||
name,
|
|
||||||
message: e.message,
|
|
||||||
permission: 'unknown',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'BadDataError',
|
|
||||||
'BadRequestError',
|
|
||||||
'InvalidTokenError',
|
|
||||||
'ValidationError',
|
|
||||||
].includes(name)
|
|
||||||
) {
|
|
||||||
return new UnleashError({
|
|
||||||
name: name as
|
|
||||||
| 'ValidationError'
|
|
||||||
| 'BadRequestError'
|
|
||||||
| 'BadDataError'
|
|
||||||
| 'InvalidTokenError',
|
|
||||||
message:
|
|
||||||
'Request validation failed: your request body failed to validate. Refer to the `details` list to see what happened.',
|
|
||||||
details: [{ description: e.message, message: e.message }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'OwaspValidationError') {
|
|
||||||
return new UnleashError({
|
|
||||||
name,
|
|
||||||
message:
|
|
||||||
'Password validation failed. Refer to the `details` property.',
|
|
||||||
details: (e as OwaspValidationError).toJSON().details,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'AuthenticationRequired') {
|
|
||||||
return new UnleashError({
|
|
||||||
name,
|
|
||||||
message: `You must be authenticated to view this content. Please log in.`,
|
|
||||||
path: `/err/maybe/login?`,
|
|
||||||
type: 'password',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UnleashError({
|
|
||||||
name: name as UnleashApiErrorNameWithoutExtraData,
|
|
||||||
message: e.message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fromOpenApiValidationError =
|
|
||||||
(requestBody: object) =>
|
|
||||||
(validationError: ErrorObject): ValidationErrorDescription => {
|
|
||||||
// @ts-expect-error Unsure why, but the `dataPath` isn't listed on the type definition for error objects. However, it's always there. Suspect this is a bug in the library.
|
|
||||||
const propertyName = validationError.dataPath.substring(
|
|
||||||
'.body.'.length,
|
|
||||||
);
|
|
||||||
if (validationError.keyword === 'required') {
|
|
||||||
const path =
|
|
||||||
propertyName + '.' + validationError.params.missingProperty;
|
|
||||||
const description = `The ${path} property is required. It was not present on the data you sent.`;
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
description,
|
|
||||||
message: description,
|
|
||||||
};
|
|
||||||
} else if (validationError.keyword === 'additionalProperties') {
|
|
||||||
const path =
|
|
||||||
(propertyName ? propertyName + '.' : '') +
|
|
||||||
validationError.params.additionalProperty;
|
|
||||||
const description = `The ${
|
|
||||||
propertyName ? `\`${propertyName}\`` : 'root'
|
|
||||||
} object of the request body does not allow additional properties. Your request included the \`${path}\` property.`;
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
description,
|
|
||||||
message: description,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const input = propertyName
|
|
||||||
.split('.')
|
|
||||||
.reduce((x, prop) => x[prop], requestBody);
|
|
||||||
|
|
||||||
const youSent = JSON.stringify(input);
|
|
||||||
const description = `The .${propertyName} property ${validationError.message}. You sent ${youSent}.`;
|
|
||||||
return {
|
|
||||||
description,
|
|
||||||
message: description,
|
|
||||||
path: propertyName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fromOpenApiValidationErrors = (
|
|
||||||
requestBody: object,
|
|
||||||
validationErrors: [ErrorObject, ...ErrorObject[]],
|
|
||||||
): UnleashError => {
|
|
||||||
const details = validationErrors.map(
|
|
||||||
fromOpenApiValidationError(requestBody),
|
|
||||||
);
|
|
||||||
|
|
||||||
return new UnleashError({
|
|
||||||
name: 'ValidationError',
|
|
||||||
message:
|
|
||||||
"Request validation failed: the payload you provided doesn't conform to the schema. Check the `details` property for a list of errors that we found.",
|
|
||||||
// @ts-expect-error We know that the list is non-empty
|
|
||||||
details,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ApiErrorSchema = FromSchema<typeof apiErrorSchema>;
|
|
@ -1,23 +1,123 @@
|
|||||||
class BadDataError extends Error {
|
import { ErrorObject } from 'ajv';
|
||||||
constructor(message: string) {
|
import getProp from 'lodash.get';
|
||||||
super();
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
type ValidationErrorDescription = {
|
||||||
this.message = message;
|
description: string;
|
||||||
|
message: string;
|
||||||
|
path?: string;
|
||||||
|
};
|
||||||
|
class BadDataError extends UnleashError {
|
||||||
|
details: ValidationErrorDescription[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
errors?: [ValidationErrorDescription, ...ValidationErrorDescription[]],
|
||||||
|
) {
|
||||||
|
const topLevelMessage =
|
||||||
|
'Request validation failed: your request body contains invalid data' +
|
||||||
|
(errors
|
||||||
|
? '. Refer to the `details` list to see what happened.'
|
||||||
|
: `: ${message}`);
|
||||||
|
super(topLevelMessage);
|
||||||
|
|
||||||
|
this.details = errors ?? [{ message: message, description: message }];
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): object {
|
toJSON(): ApiErrorSchema {
|
||||||
return {
|
return {
|
||||||
isJoi: true,
|
...super.toJSON(),
|
||||||
name: this.constructor.name,
|
details: this.details,
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BadDataError;
|
export default BadDataError;
|
||||||
|
|
||||||
|
const constructPath = (pathToParent: string, propertyName: string) =>
|
||||||
|
[pathToParent, propertyName].filter(Boolean).join('.');
|
||||||
|
|
||||||
|
const missingRequiredPropertyMessage = (
|
||||||
|
pathToParentObject: string,
|
||||||
|
missingPropertyName: string,
|
||||||
|
) => {
|
||||||
|
const path = constructPath(pathToParentObject, missingPropertyName);
|
||||||
|
const description = `The ${path} property is required. It was not present on the data you sent.`;
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
description,
|
||||||
|
message: description,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const additionalPropertiesMessage = (
|
||||||
|
pathToParentObject: string,
|
||||||
|
additionalPropertyName: string,
|
||||||
|
) => {
|
||||||
|
const path = constructPath(pathToParentObject, additionalPropertyName);
|
||||||
|
const description = `The ${
|
||||||
|
pathToParentObject ? `\`${pathToParentObject}\`` : 'root'
|
||||||
|
} object of the request body does not allow additional properties. Your request included the \`${path}\` property.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
description,
|
||||||
|
message: description,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const genericErrorMessage = (
|
||||||
|
requestBody: object,
|
||||||
|
propertyName: string,
|
||||||
|
errorMessage: string = 'is invalid',
|
||||||
|
) => {
|
||||||
|
const input = getProp(requestBody, propertyName);
|
||||||
|
|
||||||
|
const youSent = JSON.stringify(input);
|
||||||
|
const description = `The .${propertyName} property ${errorMessage}. You sent ${youSent}.`;
|
||||||
|
return {
|
||||||
|
description,
|
||||||
|
message: description,
|
||||||
|
path: propertyName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromOpenApiValidationError =
|
||||||
|
(requestBody: object) =>
|
||||||
|
(validationError: ErrorObject): ValidationErrorDescription => {
|
||||||
|
// @ts-expect-error Unsure why, but the `dataPath` isn't listed on the type definition for error objects. However, it's always there. Suspect this is a bug in the library.
|
||||||
|
const dataPath = validationError.dataPath.substring('.body.'.length);
|
||||||
|
|
||||||
|
switch (validationError.keyword) {
|
||||||
|
case 'required':
|
||||||
|
return missingRequiredPropertyMessage(
|
||||||
|
dataPath,
|
||||||
|
validationError.params.missingProperty,
|
||||||
|
);
|
||||||
|
case 'additionalProperties':
|
||||||
|
return additionalPropertiesMessage(
|
||||||
|
dataPath,
|
||||||
|
validationError.params.additionalProperty,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return genericErrorMessage(
|
||||||
|
requestBody,
|
||||||
|
dataPath,
|
||||||
|
validationError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromOpenApiValidationErrors = (
|
||||||
|
requestBody: object,
|
||||||
|
validationErrors: [ErrorObject, ...ErrorObject[]],
|
||||||
|
): BadDataError => {
|
||||||
|
const [firstDetail, ...remainingDetails] = validationErrors.map(
|
||||||
|
fromOpenApiValidationError(requestBody),
|
||||||
|
);
|
||||||
|
|
||||||
|
return new BadDataError(
|
||||||
|
"Request validation failed: the payload you provided doesn't conform to the schema. Check the `details` property for a list of errors that we found.",
|
||||||
|
[firstDetail, ...remainingDetails],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
class BaseError extends Error {
|
|
||||||
readonly statusCode: number;
|
|
||||||
|
|
||||||
constructor(message: string, statusCode: number, name: string) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.message = message;
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BaseError;
|
|
19
src/lib/error/content-type-error.ts
Normal file
19
src/lib/error/content-type-error.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { UnleashError } from './unleash-error';
|
||||||
|
|
||||||
|
class ContentTypeError extends UnleashError {
|
||||||
|
constructor(
|
||||||
|
acceptedContentTypes: [string, ...string[]],
|
||||||
|
providedContentType?: string,
|
||||||
|
) {
|
||||||
|
const message = `We do not accept the content-type you provided (${
|
||||||
|
providedContentType || "you didn't provide one"
|
||||||
|
}). Try using one of the content-types we do accept instead (${acceptedContentTypes.join(
|
||||||
|
', ',
|
||||||
|
)}) and make sure the body is in the corresponding format.`;
|
||||||
|
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentTypeError;
|
||||||
|
module.exports = ContentTypeError;
|
@ -1,10 +1,5 @@
|
|||||||
import BaseError from './base-error';
|
import { UnleashError } from './unleash-error';
|
||||||
|
|
||||||
class DisabledError extends BaseError {
|
class DisabledError extends UnleashError {}
|
||||||
constructor(message: string) {
|
|
||||||
super(message, 422, 'DisabledError');
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DisabledError;
|
export default DisabledError;
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
class FeatureHasTagError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class FeatureHasTagError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default FeatureHasTagError;
|
export default FeatureHasTagError;
|
||||||
module.exports = FeatureHasTagError;
|
module.exports = FeatureHasTagError;
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
export default class IncompatibleProjectError extends Error {
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
constructor(targetProject: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
export default class IncompatibleProjectError extends UnleashError {
|
||||||
this.message = `${targetProject} is not a compatible target`;
|
constructor(targetProject: string) {
|
||||||
|
super(`${targetProject} is not a compatible target`);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): ApiErrorSchema {
|
||||||
const obj = {
|
return {
|
||||||
isJoi: true,
|
...super.toJSON(),
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
message: this.message,
|
message: this.message,
|
||||||
|
description: this.message,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import BadDataError from './bad-data-error';
|
import BadDataError from './bad-data-error';
|
||||||
import BaseError from './base-error';
|
|
||||||
import { UNIQUE_CONSTRAINT_VIOLATION, FOREIGN_KEY_VIOLATION } from './db-error';
|
import { UNIQUE_CONSTRAINT_VIOLATION, FOREIGN_KEY_VIOLATION } from './db-error';
|
||||||
import DisabledError from './disabled-error';
|
import DisabledError from './disabled-error';
|
||||||
import FeatureHasTagError from './feature-has-tag-error';
|
import FeatureHasTagError from './feature-has-tag-error';
|
||||||
@ -18,7 +17,6 @@ import PasswordMismatchError from './password-mismatch';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
BadDataError,
|
BadDataError,
|
||||||
BaseError,
|
|
||||||
UNIQUE_CONSTRAINT_VIOLATION,
|
UNIQUE_CONSTRAINT_VIOLATION,
|
||||||
FOREIGN_KEY_VIOLATION,
|
FOREIGN_KEY_VIOLATION,
|
||||||
DisabledError,
|
DisabledError,
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
class InvalidOperationError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class InvalidOperationError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default InvalidOperationError;
|
export default InvalidOperationError;
|
||||||
module.exports = InvalidOperationError;
|
module.exports = InvalidOperationError;
|
||||||
|
@ -1,22 +1,8 @@
|
|||||||
class InvalidTokenError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
this.message = 'Token was not valid';
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): any {
|
class InvalidTokenError extends UnleashError {
|
||||||
const obj = {
|
constructor() {
|
||||||
isJoi: true,
|
super('Token was not valid');
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,6 @@
|
|||||||
class MinimumOneEnvironmentError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class MinimumOneEnvironmentError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default MinimumOneEnvironmentError;
|
export default MinimumOneEnvironmentError;
|
||||||
module.exports = MinimumOneEnvironmentError;
|
module.exports = MinimumOneEnvironmentError;
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
class NameExistsError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class NameExistsError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default NameExistsError;
|
export default NameExistsError;
|
||||||
module.exports = NameExistsError;
|
module.exports = NameExistsError;
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { UnleashError } from './api-error';
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
|
|
||||||
class NoAccessError extends UnleashError {
|
class NoAccessError extends UnleashError {
|
||||||
|
permission: string;
|
||||||
|
|
||||||
constructor(permission: string, environment?: string) {
|
constructor(permission: string, environment?: string) {
|
||||||
const message =
|
const message =
|
||||||
`You don't have the required permissions to perform this operation. You need the "${permission}" permission to perform this action` +
|
`You don't have the required permissions to perform this operation. You need the "${permission}" permission to perform this action` +
|
||||||
(environment ? ` in the "${environment}" environment.` : `.`);
|
(environment ? ` in the "${environment}" environment.` : `.`);
|
||||||
|
|
||||||
super({
|
super(message);
|
||||||
name: 'NoAccessError',
|
|
||||||
message,
|
this.permission = permission;
|
||||||
permission,
|
}
|
||||||
});
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
toJSON(): ApiErrorSchema {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
permission: this.permission,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
src/lib/error/not-implemented-error.ts
Normal file
6
src/lib/error/not-implemented-error.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { UnleashError } from './unleash-error';
|
||||||
|
|
||||||
|
class NotImplementedError extends UnleashError {}
|
||||||
|
|
||||||
|
export default NotImplementedError;
|
||||||
|
module.exports = NotImplementedError;
|
@ -1,10 +1,8 @@
|
|||||||
class NotFoundError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message?: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class NotFoundError extends UnleashError {
|
||||||
this.message = message;
|
constructor(message: string = 'The requested resource could not be found') {
|
||||||
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default NotFoundError;
|
export default NotFoundError;
|
||||||
|
@ -1,21 +1,3 @@
|
|||||||
export class OperationDeniedError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
export class OperationDeniedError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,29 +1,30 @@
|
|||||||
import { TestResult } from 'owasp-password-strength-test';
|
import { TestResult } from 'owasp-password-strength-test';
|
||||||
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
|
|
||||||
class OwaspValidationError extends Error {
|
type ValidationError = {
|
||||||
private errors: string[];
|
validationErrors: string[];
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OwaspValidationError extends UnleashError {
|
||||||
|
private details: [ValidationError];
|
||||||
|
|
||||||
constructor(testResult: TestResult) {
|
constructor(testResult: TestResult) {
|
||||||
|
const details = {
|
||||||
|
validationErrors: testResult.errors,
|
||||||
|
message: testResult.errors[0],
|
||||||
|
};
|
||||||
super(testResult.errors[0]);
|
super(testResult.errors[0]);
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
this.name = this.constructor.name;
|
this.details = [details];
|
||||||
this.errors = testResult.errors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): ApiErrorSchema {
|
||||||
const obj = {
|
return {
|
||||||
isJoi: true,
|
...super.toJSON(),
|
||||||
name: this.constructor.name,
|
details: this.details,
|
||||||
details: [
|
|
||||||
{
|
|
||||||
validationErrors: this.errors,
|
|
||||||
message: this.errors[0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OwaspValidationError;
|
export default OwaspValidationError;
|
||||||
module.exports = OwaspValidationError;
|
module.exports = OwaspValidationError;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import BaseError from './base-error';
|
import { UnleashError } from './unleash-error';
|
||||||
|
|
||||||
class PasswordMismatch extends BaseError {
|
class PasswordMismatch extends UnleashError {
|
||||||
constructor(message: string = 'Wrong password, try again.') {
|
constructor(message: string = 'Wrong password, try again.') {
|
||||||
super(message, 401, 'PasswordMismatch');
|
super(message);
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
export default class PasswordUndefinedError extends Error {
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
export default class PasswordUndefinedError extends UnleashError {
|
||||||
this.message = 'Password cannot be empty or undefined';
|
constructor() {
|
||||||
|
super('Password cannot be empty or undefined');
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): ApiErrorSchema {
|
||||||
const obj = {
|
return {
|
||||||
isJoi: true,
|
...super.toJSON(),
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
validationErrors: [],
|
validationErrors: [],
|
||||||
message: 'Password cannot be empty or undefined',
|
message: this.message,
|
||||||
|
description: this.message,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,20 @@
|
|||||||
export default class ProjectWithoutOwnerError extends Error {
|
import { ApiErrorSchema, UnleashError } from './unleash-error';
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
export default class ProjectWithoutOwnerError extends UnleashError {
|
||||||
this.message = 'A project must have at least one owner';
|
constructor() {
|
||||||
|
super('A project must have at least one owner');
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): ApiErrorSchema {
|
||||||
const obj = {
|
return {
|
||||||
isJoi: true,
|
...super.toJSON(),
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
details: [
|
||||||
{
|
{
|
||||||
validationErrors: [],
|
description: this.message,
|
||||||
message: this.message,
|
message: this.message,
|
||||||
|
validationErrors: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
class RoleInUseError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(message: string) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
class RoleInUseError extends UnleashError {}
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return {
|
|
||||||
isJoi: true,
|
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RoleInUseError;
|
export default RoleInUseError;
|
||||||
|
6
src/lib/error/unauthorized-error.ts
Normal file
6
src/lib/error/unauthorized-error.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { UnleashError } from './unleash-error';
|
||||||
|
|
||||||
|
class UnauthorizedError extends UnleashError {}
|
||||||
|
|
||||||
|
export default UnauthorizedError;
|
||||||
|
module.exports = UnauthorizedError;
|
460
src/lib/error/unleash-error.test.ts
Normal file
460
src/lib/error/unleash-error.test.ts
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
import owasp from 'owasp-password-strength-test';
|
||||||
|
import { ErrorObject } from 'ajv';
|
||||||
|
import AuthenticationRequired from '../types/authentication-required';
|
||||||
|
import { ApiErrorSchema, fromLegacyError } from './unleash-error';
|
||||||
|
import BadDataError, {
|
||||||
|
fromOpenApiValidationError,
|
||||||
|
fromOpenApiValidationErrors,
|
||||||
|
} from './bad-data-error';
|
||||||
|
import NoAccessError from './no-access-error';
|
||||||
|
import OwaspValidationError from './owasp-validation-error';
|
||||||
|
import IncompatibleProjectError from './incompatible-project-error';
|
||||||
|
import PasswordUndefinedError from './password-undefined';
|
||||||
|
import ProjectWithoutOwnerError from './project-without-owner-error';
|
||||||
|
import NotFoundError from './notfound-error';
|
||||||
|
|
||||||
|
describe('v5 deprecation: backwards compatibility', () => {
|
||||||
|
it(`Adds details to error types that don't specify it`, () => {
|
||||||
|
const message = `Error!`;
|
||||||
|
const error = new NotFoundError(message).toJSON();
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
message,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
description: message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Standard/legacy error conversion', () => {
|
||||||
|
it('Moves message to the details list for baddataerror', () => {
|
||||||
|
const message = `: message!`;
|
||||||
|
const result = fromLegacyError(new BadDataError(message)).toJSON();
|
||||||
|
|
||||||
|
expect(result.details).toStrictEqual([
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
description: message,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OpenAPI error conversion', () => {
|
||||||
|
it('Gives useful error messages for missing properties', () => {
|
||||||
|
const error = {
|
||||||
|
keyword: 'required',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body',
|
||||||
|
schemaPath: '#/components/schemas/addonCreateUpdateSchema/required',
|
||||||
|
params: {
|
||||||
|
missingProperty: 'enabled',
|
||||||
|
},
|
||||||
|
message: "should have required property 'enabled'",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = fromOpenApiValidationError({})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
// it tells the user that the property is required
|
||||||
|
expect.stringContaining('required') &&
|
||||||
|
// it tells the user the name of the missing property
|
||||||
|
expect.stringContaining(error.params.missingProperty),
|
||||||
|
path: 'enabled',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Gives useful error messages for type errors', () => {
|
||||||
|
const error = {
|
||||||
|
keyword: 'type',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body.parameters',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/parameters/type',
|
||||||
|
params: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
message: 'should be object',
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameterValue = [];
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
parameters: parameterValue,
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
// it provides the message
|
||||||
|
expect.stringContaining(error.message) &&
|
||||||
|
// it tells the user what they provided
|
||||||
|
expect.stringContaining(JSON.stringify(parameterValue)),
|
||||||
|
path: 'parameters',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Gives useful pattern error messages', () => {
|
||||||
|
const error = {
|
||||||
|
instancePath: '',
|
||||||
|
keyword: 'pattern',
|
||||||
|
dataPath: '.body.description',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/description/pattern',
|
||||||
|
params: {
|
||||||
|
pattern: '^this is',
|
||||||
|
},
|
||||||
|
message: 'should match pattern "^this is"',
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDescription = 'A pattern that does not match.';
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
description: requestDescription,
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
expect.stringContaining(error.params.pattern) &&
|
||||||
|
expect.stringContaining('description') &&
|
||||||
|
expect.stringContaining(requestDescription),
|
||||||
|
path: 'description',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Gives useful min/maxlength error messages', () => {
|
||||||
|
const error = {
|
||||||
|
instancePath: '',
|
||||||
|
keyword: 'maxLength',
|
||||||
|
dataPath: '.body.description',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/description/maxLength',
|
||||||
|
params: {
|
||||||
|
limit: 5,
|
||||||
|
},
|
||||||
|
message: 'should NOT be longer than 5 characters',
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDescription = 'Longer than the max length';
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
description: requestDescription,
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
// it tells the user what the limit is
|
||||||
|
expect.stringContaining(error.params.limit.toString()) &&
|
||||||
|
// it tells the user which property it pertains to
|
||||||
|
expect.stringContaining('description') &&
|
||||||
|
// it tells the user what they provided
|
||||||
|
expect.stringContaining(requestDescription),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles numerical min/max errors', () => {
|
||||||
|
const error = {
|
||||||
|
keyword: 'maximum',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body.newprop',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/newprop/maximum',
|
||||||
|
params: {
|
||||||
|
comparison: '<=',
|
||||||
|
limit: 5,
|
||||||
|
exclusive: false,
|
||||||
|
},
|
||||||
|
message: 'should be <= 5',
|
||||||
|
};
|
||||||
|
|
||||||
|
const propertyValue = 6;
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
newprop: propertyValue,
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
// it tells the user what the limit is
|
||||||
|
expect.stringContaining(error.params.limit.toString()) &&
|
||||||
|
// it tells the user what kind of comparison it performed
|
||||||
|
expect.stringContaining(error.params.comparison) &&
|
||||||
|
// it tells the user which property it pertains to
|
||||||
|
expect.stringContaining('newprop') &&
|
||||||
|
// it tells the user what they provided
|
||||||
|
expect.stringContaining(propertyValue.toString()),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles multiple errors', () => {
|
||||||
|
const errors: [ErrorObject, ...ErrorObject[]] = [
|
||||||
|
{
|
||||||
|
keyword: 'maximum',
|
||||||
|
instancePath: '',
|
||||||
|
// @ts-expect-error
|
||||||
|
dataPath: '.body.newprop',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/newprop/maximum',
|
||||||
|
params: {
|
||||||
|
comparison: '<=',
|
||||||
|
limit: 5,
|
||||||
|
exclusive: false,
|
||||||
|
},
|
||||||
|
message: 'should be <= 5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyword: 'required',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/required',
|
||||||
|
params: {
|
||||||
|
missingProperty: 'enabled',
|
||||||
|
},
|
||||||
|
message: "should have required property 'enabled'",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// create an error and serialize it as it would be shown to the end user.
|
||||||
|
const serializedUnleashError: ApiErrorSchema =
|
||||||
|
fromOpenApiValidationErrors({ newprop: 7 }, errors).toJSON();
|
||||||
|
|
||||||
|
expect(serializedUnleashError).toMatchObject({
|
||||||
|
name: 'BadDataError',
|
||||||
|
message: expect.stringContaining('`details`'),
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
description: expect.stringContaining('newprop'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: expect.stringContaining('enabled'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disallowed additional properties', () => {
|
||||||
|
it('gives useful messages for base-level properties', () => {
|
||||||
|
const openApiError = {
|
||||||
|
keyword: 'additionalProperties',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/additionalProperties',
|
||||||
|
params: { additionalProperty: 'bogus' },
|
||||||
|
message: 'should NOT have additional properties',
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = fromOpenApiValidationError({ bogus: 5 })(
|
||||||
|
openApiError,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
description:
|
||||||
|
expect.stringContaining(
|
||||||
|
openApiError.params.additionalProperty,
|
||||||
|
) &&
|
||||||
|
expect.stringMatching('/\broot\b/i') &&
|
||||||
|
expect.stringMatching(/\badditional properties\b/i),
|
||||||
|
path: 'bogus',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gives useful messages for nested properties', () => {
|
||||||
|
const request2 = {
|
||||||
|
nestedObject: {
|
||||||
|
nested2: { extraPropertyName: 'illegal property' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const openApiError = {
|
||||||
|
keyword: 'additionalProperties',
|
||||||
|
instancePath: '',
|
||||||
|
dataPath: '.body.nestedObject.nested2',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/nestedObject/properties/nested2/additionalProperties',
|
||||||
|
params: { additionalProperty: 'extraPropertyName' },
|
||||||
|
message: 'should NOT have additional properties',
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = fromOpenApiValidationError(request2)(openApiError);
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
description:
|
||||||
|
expect.stringContaining('nestedObject.nested2') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
openApiError.params.additionalProperty,
|
||||||
|
) &&
|
||||||
|
expect.stringMatching(/\badditional properties\b/i),
|
||||||
|
path: 'nestedObject.nested2.extraPropertyName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles deeply nested properties gracefully', () => {
|
||||||
|
const error = {
|
||||||
|
keyword: 'type',
|
||||||
|
dataPath: '.body.nestedObject.a.b',
|
||||||
|
schemaPath:
|
||||||
|
'#/components/schemas/addonCreateUpdateSchema/properties/nestedObject/properties/a/properties/b/type',
|
||||||
|
params: { type: 'string' },
|
||||||
|
message: 'should be string',
|
||||||
|
instancePath: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
nestedObject: { a: { b: [] } },
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
expect.stringMatching(/\bnestedObject.a.b\b/) &&
|
||||||
|
expect.stringContaining('[]'),
|
||||||
|
path: 'nestedObject.a.b',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handles deeply nested properties on referenced schemas', () => {
|
||||||
|
const error = {
|
||||||
|
keyword: 'type',
|
||||||
|
dataPath: '.body.nestedObject.a.b',
|
||||||
|
schemaPath: '#/components/schemas/parametersSchema/type',
|
||||||
|
params: { type: 'object' },
|
||||||
|
message: 'should be object',
|
||||||
|
instancePath: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const illegalValue = 'illegal string';
|
||||||
|
const result = fromOpenApiValidationError({
|
||||||
|
nestedObject: { a: { b: illegalValue } },
|
||||||
|
})(error);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
description:
|
||||||
|
expect.stringMatching(/\bnestedObject.a.b\b/) &&
|
||||||
|
expect.stringContaining(illegalValue),
|
||||||
|
path: 'nestedObject.a.b',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error serialization special cases', () => {
|
||||||
|
it('OwaspValidationErrors: adds `validationErrors` to `details`', () => {
|
||||||
|
const results = owasp.test('123');
|
||||||
|
const error = new OwaspValidationError(results);
|
||||||
|
const json = fromLegacyError(error).toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
message: results.errors[0],
|
||||||
|
validationErrors: results.errors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error serialization special cases', () => {
|
||||||
|
it('AuthenticationRequired: adds `path` and `type`', () => {
|
||||||
|
const type = 'password';
|
||||||
|
const path = '/api/login';
|
||||||
|
const error = new AuthenticationRequired({
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
message: 'this is a message',
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({ path, type });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NoAccessError: adds `permission`', () => {
|
||||||
|
const permission = 'x';
|
||||||
|
const error = new NoAccessError(permission);
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json.permission).toBe(permission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BadDataError: adds `details` with error details', () => {
|
||||||
|
const description = 'You did **this** wrong';
|
||||||
|
const error = new BadDataError(description).toJSON();
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
message: description,
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('OwaspValidationErrors: adds `validationErrors` to `details`', () => {
|
||||||
|
const results = owasp.test('123');
|
||||||
|
const error = new OwaspValidationError(results);
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({
|
||||||
|
message: results.errors[0],
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
message: results.errors[0],
|
||||||
|
validationErrors: results.errors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('IncompatibleProjectError: adds `validationErrors: []` to the `details` list', () => {
|
||||||
|
const targetProject = '8927CCCA-AD39-46E2-9D83-8E50D9AACE75';
|
||||||
|
const error = new IncompatibleProjectError(targetProject);
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
validationErrors: [],
|
||||||
|
message: expect.stringContaining(targetProject),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PasswordUndefinedError: adds `validationErrors: []` to the `details` list', () => {
|
||||||
|
const error = new PasswordUndefinedError();
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
validationErrors: [],
|
||||||
|
message: json.message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ProjectWithoutOwnerError: adds `validationErrors: []` to the `details` list', () => {
|
||||||
|
const error = new ProjectWithoutOwnerError();
|
||||||
|
const json = error.toJSON();
|
||||||
|
|
||||||
|
expect(json).toMatchObject({
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
validationErrors: [],
|
||||||
|
message: json.message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Stack traces', () => {
|
||||||
|
it('captures stack traces regardless of whether `Error.captureStackTrace` is called explicitly or not', () => {
|
||||||
|
const e = new PasswordUndefinedError();
|
||||||
|
|
||||||
|
expect(e.stack).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
213
src/lib/error/unleash-error.ts
Normal file
213
src/lib/error/unleash-error.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
const UnleashApiErrorTypes = [
|
||||||
|
'ContentTypeError',
|
||||||
|
'DisabledError',
|
||||||
|
'FeatureHasTagError',
|
||||||
|
'IncompatibleProjectError',
|
||||||
|
'InvalidOperationError',
|
||||||
|
'InvalidTokenError',
|
||||||
|
'MinimumOneEnvironmentError',
|
||||||
|
'NameExistsError',
|
||||||
|
'NoAccessError',
|
||||||
|
'NotFoundError',
|
||||||
|
'NotImplementedError',
|
||||||
|
'OperationDeniedError',
|
||||||
|
'PasswordMismatch',
|
||||||
|
'PasswordUndefinedError',
|
||||||
|
'ProjectWithoutOwnerError',
|
||||||
|
'RoleInUseError',
|
||||||
|
'UnknownError',
|
||||||
|
'UsedTokenError',
|
||||||
|
'BadDataError',
|
||||||
|
'ValidationError',
|
||||||
|
'AuthenticationRequired',
|
||||||
|
'UnauthorizedError',
|
||||||
|
'NoAccessError',
|
||||||
|
'InvalidTokenError',
|
||||||
|
'OwaspValidationError',
|
||||||
|
|
||||||
|
// server errors; not the end user's fault
|
||||||
|
'InternalError',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type UnleashApiErrorName = typeof UnleashApiErrorTypes[number];
|
||||||
|
|
||||||
|
const statusCode = (errorName: string): number => {
|
||||||
|
switch (errorName) {
|
||||||
|
case 'ContentTypeError':
|
||||||
|
return 415;
|
||||||
|
case 'ValidationError':
|
||||||
|
return 400;
|
||||||
|
case 'BadDataError':
|
||||||
|
return 400;
|
||||||
|
case 'OwaspValidationError':
|
||||||
|
return 400;
|
||||||
|
case 'PasswordUndefinedError':
|
||||||
|
return 400;
|
||||||
|
case 'MinimumOneEnvironmentError':
|
||||||
|
return 400;
|
||||||
|
case 'InvalidTokenError':
|
||||||
|
return 401;
|
||||||
|
case 'NoAccessError':
|
||||||
|
return 403;
|
||||||
|
case 'UsedTokenError':
|
||||||
|
return 403;
|
||||||
|
case 'InvalidOperationError':
|
||||||
|
return 403;
|
||||||
|
case 'IncompatibleProjectError':
|
||||||
|
return 403;
|
||||||
|
case 'OperationDeniedError':
|
||||||
|
return 403;
|
||||||
|
case 'NotFoundError':
|
||||||
|
return 404;
|
||||||
|
case 'NameExistsError':
|
||||||
|
return 409;
|
||||||
|
case 'FeatureHasTagError':
|
||||||
|
return 409;
|
||||||
|
case 'RoleInUseError':
|
||||||
|
return 400;
|
||||||
|
case 'ProjectWithoutOwnerError':
|
||||||
|
return 409;
|
||||||
|
case 'UnknownError':
|
||||||
|
return 500;
|
||||||
|
case 'InternalError':
|
||||||
|
return 500;
|
||||||
|
case 'PasswordMismatch':
|
||||||
|
return 401;
|
||||||
|
case 'UnauthorizedError':
|
||||||
|
return 401;
|
||||||
|
case 'DisabledError':
|
||||||
|
return 422;
|
||||||
|
case 'NotImplementedError':
|
||||||
|
return 405;
|
||||||
|
case 'NoAccessError':
|
||||||
|
return 403;
|
||||||
|
case 'AuthenticationRequired':
|
||||||
|
return 401;
|
||||||
|
default:
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class UnleashError extends Error {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
additionalParameters: object;
|
||||||
|
|
||||||
|
constructor(message: string, name?: string) {
|
||||||
|
super();
|
||||||
|
this.id = uuidV4();
|
||||||
|
this.name = name || this.constructor.name;
|
||||||
|
super.message = message;
|
||||||
|
|
||||||
|
this.statusCode = statusCode(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
help(): string {
|
||||||
|
return `Get help for id ${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): ApiErrorSchema {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name,
|
||||||
|
message: this.message,
|
||||||
|
details: [{ message: this.message, description: this.message }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `${this.name}: ${this.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenericUnleashError extends UnleashError {
|
||||||
|
constructor({
|
||||||
|
name,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
name: UnleashApiErrorName;
|
||||||
|
message: string;
|
||||||
|
}) {
|
||||||
|
super(message, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiErrorSchema = {
|
||||||
|
$id: '#/components/schemas/apiError',
|
||||||
|
type: 'object',
|
||||||
|
required: ['id', 'name', 'message'],
|
||||||
|
description:
|
||||||
|
'An Unleash API error. Contains information about what went wrong.',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'The kind of error that occurred. Meant for machine consumption.',
|
||||||
|
example: 'ValidationError',
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'A unique identifier for this error instance. Can be used to search logs etc.',
|
||||||
|
example: '0b84c7fd-5278-4087-832d-0b502c7929b3',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'A human-readable explanation of what went wrong.',
|
||||||
|
example:
|
||||||
|
"We couldn't find an addon provider with the name that you are trying to add ('bogus-addon')",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const fromLegacyError = (e: Error): UnleashError => {
|
||||||
|
if (e instanceof UnleashError) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
const name = UnleashApiErrorTypes.includes(e.name as UnleashApiErrorName)
|
||||||
|
? (e.name as UnleashApiErrorName)
|
||||||
|
: 'UnknownError';
|
||||||
|
|
||||||
|
if (name === 'NoAccessError') {
|
||||||
|
return new GenericUnleashError({
|
||||||
|
name: 'NoAccessError',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'ValidationError' || name === 'BadDataError') {
|
||||||
|
return new GenericUnleashError({
|
||||||
|
name: 'BadDataError',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'OwaspValidationError') {
|
||||||
|
return new GenericUnleashError({
|
||||||
|
name: 'OwaspValidationError',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'AuthenticationRequired') {
|
||||||
|
return new GenericUnleashError({
|
||||||
|
name: 'AuthenticationRequired',
|
||||||
|
message: `You must be authenticated to view this content. Please log in.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenericUnleashError({
|
||||||
|
name: name as UnleashApiErrorName,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiErrorSchema = FromSchema<typeof apiErrorSchema>;
|
@ -1,22 +1,8 @@
|
|||||||
class UsedTokenError extends Error {
|
import { UnleashError } from './unleash-error';
|
||||||
constructor(usedAt: Date) {
|
|
||||||
super();
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
this.message = `Token was already used at ${usedAt}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): any {
|
class UsedTokenError extends UnleashError {
|
||||||
const obj = {
|
constructor(usedAt: Date) {
|
||||||
isJoi: true,
|
super(`Token was already used at ${usedAt}`);
|
||||||
name: this.constructor.name,
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
message: this.message,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return obj;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,6 @@ import { isValidField } from './import-context-validation';
|
|||||||
import { IImportTogglesStore } from './import-toggles-store-type';
|
import { IImportTogglesStore } from './import-toggles-store-type';
|
||||||
import { ImportPermissionsService } from './import-permissions-service';
|
import { ImportPermissionsService } from './import-permissions-service';
|
||||||
import { ImportValidationMessages } from './import-validation-messages';
|
import { ImportValidationMessages } from './import-validation-messages';
|
||||||
import { UnleashError } from '../../error/api-error';
|
|
||||||
|
|
||||||
export default class ExportImportService {
|
export default class ExportImportService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -378,12 +377,10 @@ export default class ExportImportService {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (firstError !== undefined) {
|
if (firstError !== undefined) {
|
||||||
throw new UnleashError({
|
throw new BadDataError(
|
||||||
name: 'BadDataError',
|
'Some of the context fields you are trying to import are not supported.',
|
||||||
message:
|
[firstError, ...remainingErrors],
|
||||||
'Some of the context fields you are trying to import are not supported.',
|
);
|
||||||
details: [firstError, ...remainingErrors],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,12 +463,10 @@ export default class ExportImportService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (firstError !== undefined) {
|
if (firstError !== undefined) {
|
||||||
throw new UnleashError({
|
throw new BadDataError(
|
||||||
name: 'BadDataError',
|
'Some of the strategies you are trying to import are not supported.',
|
||||||
message:
|
[firstError, ...remainingErrors],
|
||||||
'Some of the strategies you are trying to import are not supported.',
|
);
|
||||||
details: [firstError, ...remainingErrors],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { IAuthRequest } from '../routes/unleash-types';
|
import { IAuthRequest } from '../routes/unleash-types';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { LogProvider } from '../logger';
|
import { LogProvider } from '../logger';
|
||||||
import { UnleashError } from '../error/api-error';
|
import { AuthenticationRequired } from '../server-impl';
|
||||||
|
import UnauthorizedError from '../error/unauthorized-error';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
const authorizationMiddleware = (
|
const authorizationMiddleware = (
|
||||||
@ -20,19 +21,16 @@ const authorizationMiddleware = (
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
if (req.header('authorization')) {
|
if (req.header('authorization')) {
|
||||||
// API clients should get 401 without body
|
// API clients should get 401 with a basic body
|
||||||
return res.status(401).json(
|
const error = new UnauthorizedError(
|
||||||
new UnleashError({
|
'You must log in to use Unleash.',
|
||||||
name: 'PasswordMismatchError',
|
|
||||||
message: 'You must log in to use Unleash.',
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
return res.status(error.statusCode).json(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = `${baseUriPath}/auth/simple/login`;
|
const path = `${baseUriPath}/auth/simple/login`;
|
||||||
const error = new UnleashError({
|
const error = new AuthenticationRequired({
|
||||||
name: 'AuthenticationRequired',
|
message: `You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at ${path}`,
|
||||||
message: `You must log in to use Unleash. Your request had no authorization header, so we could not authorize you. Try logging in at ${baseUriPath}/auth/simple/login.`,
|
|
||||||
type: 'password',
|
type: 'password',
|
||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { UnleashError } from '../error/api-error';
|
|
||||||
import { is } from 'type-is';
|
import { is } from 'type-is';
|
||||||
|
import ContentTypeError from '../error/content-type-error';
|
||||||
|
|
||||||
const DEFAULT_ACCEPTED_CONTENT_TYPE = 'application/json';
|
const DEFAULT_ACCEPTED_CONTENT_TYPE = 'application/json';
|
||||||
|
|
||||||
@ -22,14 +22,10 @@ export default function requireContentType(
|
|||||||
if (is(contentType, acceptedContentTypes)) {
|
if (is(contentType, acceptedContentTypes)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
const error = new UnleashError({
|
const error = new ContentTypeError(
|
||||||
name: 'ContentTypeError',
|
acceptedContentTypes as [string, ...string[]],
|
||||||
message: `We do not accept the content-type you provided (${
|
contentType,
|
||||||
contentType || "you didn't provide one"
|
);
|
||||||
}). Try using one of the content-types we do accept instead (${acceptedContentTypes.join(
|
|
||||||
', ',
|
|
||||||
)}) and make sure the body is in the corresponding format.`,
|
|
||||||
});
|
|
||||||
res.status(error.statusCode).json(error).end();
|
res.status(error.statusCode).json(error).end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
import { Context } from 'unleash-client';
|
import { Context } from 'unleash-client';
|
||||||
import { enrichContextWithIp } from '../../proxy';
|
import { enrichContextWithIp } from '../../proxy';
|
||||||
import { corsOriginMiddleware } from '../../middleware';
|
import { corsOriginMiddleware } from '../../middleware';
|
||||||
import { UnleashError } from '../../error/api-error';
|
import NotImplementedError from '../../error/not-implemented-error';
|
||||||
|
|
||||||
interface ApiUserRequest<
|
interface ApiUserRequest<
|
||||||
PARAM = any,
|
PARAM = any,
|
||||||
@ -136,10 +136,9 @@ export default class ProxyController extends Controller {
|
|||||||
req: ApiUserRequest,
|
req: ApiUserRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
) {
|
) {
|
||||||
const error = new UnleashError({
|
const error = new NotImplementedError(
|
||||||
name: 'NotImplementedError',
|
'The frontend API does not support this endpoint.',
|
||||||
message: 'The frontend API does not support this endpoint.',
|
);
|
||||||
});
|
|
||||||
res.status(error.statusCode).json(error);
|
res.status(error.statusCode).json(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import joi from 'joi';
|
import joi from 'joi';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { fromLegacyError, UnleashError } from '../error/api-error';
|
import { fromLegacyError, UnleashError } from '../error/unleash-error';
|
||||||
|
|
||||||
export const customJoi = joi.extend((j) => ({
|
export const customJoi = joi.extend((j) => ({
|
||||||
type: 'isUrlFriendly',
|
type: 'isUrlFriendly',
|
||||||
@ -29,6 +29,14 @@ export const handleErrors: (
|
|||||||
const finalError =
|
const finalError =
|
||||||
error instanceof UnleashError ? error : fromLegacyError(error);
|
error instanceof UnleashError ? error : fromLegacyError(error);
|
||||||
|
|
||||||
|
if (!(error instanceof UnleashError)) {
|
||||||
|
logger.warn(
|
||||||
|
`I encountered an error that wasn't an instance of the \`UnleashError\` type. This probably means that we had an unexpected crash. The original error and what it was mapped to are:`,
|
||||||
|
error,
|
||||||
|
finalError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.warn(finalError.id, finalError.message);
|
logger.warn(finalError.id, finalError.message);
|
||||||
|
|
||||||
if (['InternalError', 'UnknownError'].includes(finalError.name)) {
|
if (['InternalError', 'UnknownError'].includes(finalError.name)) {
|
||||||
|
@ -11,7 +11,7 @@ import { ApiOperation } from '../openapi/util/api-operation';
|
|||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { validateSchema } from '../openapi/validate';
|
import { validateSchema } from '../openapi/validate';
|
||||||
import { IFlagResolver } from '../types';
|
import { IFlagResolver } from '../types';
|
||||||
import { fromOpenApiValidationErrors } from '../error/api-error';
|
import { fromOpenApiValidationErrors } from '../error/bad-data-error';
|
||||||
|
|
||||||
export class OpenApiService {
|
export class OpenApiService {
|
||||||
private readonly config: IUnleashConfig;
|
private readonly config: IUnleashConfig;
|
||||||
|
@ -27,7 +27,7 @@ import DisabledError from '../error/disabled-error';
|
|||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
import { isDefined } from '../util/isDefined';
|
import { isDefined } from '../util/isDefined';
|
||||||
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
import { TokenUserSchema } from '../openapi/spec/token-user-schema';
|
||||||
import { UnleashError } from '../error/api-error';
|
import PasswordMismatch from '../error/password-mismatch';
|
||||||
|
|
||||||
const systemUser = new User({ id: -1, username: 'system' });
|
const systemUser = new User({ id: -1, username: 'system' });
|
||||||
|
|
||||||
@ -304,10 +304,9 @@ class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnleashError({
|
throw new PasswordMismatch(
|
||||||
name: 'PasswordMismatchError',
|
`The combination of password and username you provided is invalid. If you have forgotten your password, visit ${this.baseUriPath}/forgotten-password or get in touch with your instance administrator.`,
|
||||||
message: `The combination of password and username you provided is invalid. If you have forgotten your password, visit ${this.baseUriPath}/forgotten-password or get in touch with your instance administrator.`,
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ApiErrorSchema, UnleashError } from '../error/unleash-error';
|
||||||
|
|
||||||
interface IBaseOptions {
|
interface IBaseOptions {
|
||||||
type: string;
|
type: string;
|
||||||
path: string;
|
path: string;
|
||||||
@ -9,13 +11,11 @@ interface IOptions extends IBaseOptions {
|
|||||||
options?: IBaseOptions[];
|
options?: IBaseOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationRequired {
|
class AuthenticationRequired extends UnleashError {
|
||||||
private type: string;
|
private type: string;
|
||||||
|
|
||||||
private path: string;
|
private path: string;
|
||||||
|
|
||||||
private message: string;
|
|
||||||
|
|
||||||
private defaultHidden: boolean;
|
private defaultHidden: boolean;
|
||||||
|
|
||||||
private options?: IBaseOptions[];
|
private options?: IBaseOptions[];
|
||||||
@ -27,12 +27,20 @@ class AuthenticationRequired {
|
|||||||
options,
|
options,
|
||||||
defaultHidden = false,
|
defaultHidden = false,
|
||||||
}: IOptions) {
|
}: IOptions) {
|
||||||
|
super(message);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.message = message;
|
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.defaultHidden = defaultHidden;
|
this.defaultHidden = defaultHidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON(): ApiErrorSchema {
|
||||||
|
return {
|
||||||
|
...super.toJSON(),
|
||||||
|
path: this.path,
|
||||||
|
type: this.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuthenticationRequired;
|
export default AuthenticationRequired;
|
||||||
|
0
src/lib/util/get-prop-from-string.ts
Normal file
0
src/lib/util/get-prop-from-string.ts
Normal file
@ -14,7 +14,7 @@ test('semver validation should throw with bad format', () => {
|
|||||||
try {
|
try {
|
||||||
validateSemver(badSemver);
|
validateSemver(badSemver);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toContain(
|
||||||
`the provided value is not a valid semver format. The value provided was: ${badSemver}`,
|
`the provided value is not a valid semver format. The value provided was: ${badSemver}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ test('semver valdiation should pass with correct format', () => {
|
|||||||
try {
|
try {
|
||||||
validateSemver(validSemver);
|
validateSemver(validSemver);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toContain(
|
||||||
`the provided value is not a valid semver format. The value provided was: ${validSemver}`,
|
`the provided value is not a valid semver format. The value provided was: ${validSemver}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ test('semver validation should fail partial semver', () => {
|
|||||||
try {
|
try {
|
||||||
validateSemver(partial);
|
validateSemver(partial);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toContain(
|
||||||
`the provided value is not a valid semver format. The value provided was: ${partial}`,
|
`the provided value is not a valid semver format. The value provided was: ${partial}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ test('semver validation should fail with leading v', () => {
|
|||||||
try {
|
try {
|
||||||
validateSemver(leadingV);
|
validateSemver(leadingV);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toBe(
|
expect(e.message).toContain(
|
||||||
`the provided value is not a valid semver format. The value provided was: ${leadingV}`,
|
`the provided value is not a valid semver format. The value provided was: ${leadingV}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ test('should fail validation if value does not exist in single legal value', ()
|
|||||||
try {
|
try {
|
||||||
validateLegalValues(legalValues, value);
|
validateLegalValues(legalValues, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(
|
expect(error.message).toContain(
|
||||||
`${value} is not specified as a legal value on this context field`,
|
`${value} is not specified as a legal value on this context field`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ test('should pass validation if value exists in single legal value', () => {
|
|||||||
try {
|
try {
|
||||||
validateLegalValues(legalValues, value);
|
validateLegalValues(legalValues, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(
|
expect(error.message).toContain(
|
||||||
`${value} is not specified as a legal value on this context field`,
|
`${value} is not specified as a legal value on this context field`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ test('should fail validation if one of the values does not exist in multiple leg
|
|||||||
try {
|
try {
|
||||||
validateLegalValues(legalValues, values);
|
validateLegalValues(legalValues, values);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(
|
expect(error.message).toContain(
|
||||||
`input values are not specified as a legal value on this context field`,
|
`input values are not specified as a legal value on this context field`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ test('should pass validation if all of the values exists in legal values', () =>
|
|||||||
try {
|
try {
|
||||||
validateLegalValues(legalValues, values);
|
validateLegalValues(legalValues, values);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toBe(
|
expect(error.message).toContain(
|
||||||
`input values are not specified as a legal value on this context field`,
|
`input values are not specified as a legal value on this context field`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user