1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00
unleash.unleash/src/lib/error/unleash-error.test.ts
Nuno Góis ab4ff29407
fix: hide password login when it's disabled (#3851)
https://linear.app/unleash/issue/2-1085/bug-password-based-login-still-shows-on-the-login-page-even-if

Fixes a regression introduced with the changes related with #3633 where
we still show the password login even though it's disabled.

---------

Co-authored-by: Thomas Heartman <thomas@getunleash.io>
2023-05-24 09:39:24 +01:00

481 lines
16 KiB
TypeScript

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('AuthenticationRequired adds `options` if they are present', () => {
const config = {
type: 'password',
path: `base-path/auth/simple/login`,
message: 'You must sign in order to use Unleash',
defaultHidden: true,
options: [
{
type: 'google',
message: 'Sign in with Google',
path: `base-path/auth/google/login`,
},
],
};
const json = new AuthenticationRequired(config).toJSON();
expect(json).toMatchObject(config);
});
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();
});
});