* Migrate `joi` to `17.4.0` and adapt the codebase (#99899) * bump joi to 17.4.0, start adapting stuff * remove custom validation rule, adapt instead * fix error handling * fix error handling again * fix strings type & validation * fix buffers and arrays * fix bytes * fix bytes_size type * update conditional_type error messages in tests * fix duration and map types * first attempt to fix union type error messages * revert conditional type assertions back to master state * fix object type * fix record type * fix stream types * rename test files to match sources * fix union type tests * temporary adapt feature/home usages of Joi * fix lint * adapt test assertion * fix http config schema validation * fix @kbn/test Config class * fix config again * fix reporting schema tests * fix security solution schema * adapt url tests * remove useless comment * remove space * typo * review comments # Conflicts: # src/core/server/http/__snapshots__/http_config.test.ts.snap * allow '0' value for server.host * fix config def * update snapshots
This commit is contained in:
parent
3629e76251
commit
3e1e047bc4
28 changed files with 473 additions and 468 deletions
|
@ -256,7 +256,7 @@
|
|||
"io-ts": "^2.0.5",
|
||||
"ipaddr.js": "2.0.0",
|
||||
"isbinaryfile": "4.0.2",
|
||||
"joi": "^13.5.2",
|
||||
"joi": "^17.4.0",
|
||||
"jquery": "^3.5.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"js-search": "^1.4.3",
|
||||
|
@ -543,7 +543,7 @@
|
|||
"@types/jest": "^26.0.22",
|
||||
"@types/jest-specific-snapshot": "^0.5.5",
|
||||
"@types/jest-when": "^2.7.2",
|
||||
"@types/joi": "^13.4.2",
|
||||
"@types/joi": "^17.2.3",
|
||||
"@types/jquery": "^3.3.31",
|
||||
"@types/js-search": "^1.4.0",
|
||||
"@types/js-yaml": "^3.11.1",
|
||||
|
|
|
@ -7,75 +7,30 @@
|
|||
*/
|
||||
|
||||
import Joi from 'joi';
|
||||
import {
|
||||
AnySchema,
|
||||
JoiRoot,
|
||||
Reference,
|
||||
Rules,
|
||||
SchemaLike,
|
||||
State,
|
||||
ValidationErrorItem,
|
||||
ValidationOptions,
|
||||
} from 'joi';
|
||||
import type { JoiRoot, CustomHelpers } from 'joi';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { isDuration } from 'moment';
|
||||
import { Stream } from 'stream';
|
||||
import { ByteSizeValue, ensureByteSizeValue } from '../byte_size_value';
|
||||
import { ensureDuration } from '../duration';
|
||||
|
||||
export { AnySchema, Reference, SchemaLike, ValidationErrorItem };
|
||||
|
||||
function isMap<K, V>(o: any): o is Map<K, V> {
|
||||
return o instanceof Map;
|
||||
}
|
||||
|
||||
const anyCustomRule: Rules = {
|
||||
name: 'custom',
|
||||
params: {
|
||||
validator: Joi.func().maxArity(1).required(),
|
||||
},
|
||||
validate(params, value, state, options) {
|
||||
let validationResultMessage;
|
||||
try {
|
||||
validationResultMessage = params.validator(value);
|
||||
} catch (e) {
|
||||
validationResultMessage = e.message || e;
|
||||
}
|
||||
|
||||
if (typeof validationResultMessage === 'string') {
|
||||
return this.createError(
|
||||
'any.custom',
|
||||
{ value, message: validationResultMessage },
|
||||
state,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const internals = Joi.extend([
|
||||
export const internals: JoiRoot = Joi.extend(
|
||||
{
|
||||
name: 'any',
|
||||
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'boolean',
|
||||
|
||||
type: 'boolean',
|
||||
base: Joi.boolean(),
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
coerce(value, { error }: CustomHelpers) {
|
||||
// If value isn't defined, let Joi handle default value if it's defined.
|
||||
if (value === undefined) {
|
||||
return value;
|
||||
return {
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
// Allow strings 'true' and 'false' to be coerced to booleans (case-insensitive).
|
||||
|
||||
// From Joi docs on `Joi.boolean`:
|
||||
// > Generates a schema object that matches a boolean data type. Can also
|
||||
// > be called via bool(). If the validation convert option is on
|
||||
|
@ -87,135 +42,125 @@ export const internals = Joi.extend([
|
|||
}
|
||||
|
||||
if (typeof value !== 'boolean') {
|
||||
return this.createError('boolean.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('boolean.base')],
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
return {
|
||||
value,
|
||||
};
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'binary',
|
||||
|
||||
base: Joi.binary(),
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
// If value isn't defined, let Joi handle default value if it's defined.
|
||||
if (value !== undefined && !(typeof value === 'object' && Buffer.isBuffer(value))) {
|
||||
return this.createError('binary.base', { value }, state, options);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'stream',
|
||||
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
// If value isn't defined, let Joi handle default value if it's defined.
|
||||
type: 'stream',
|
||||
prepare(value, { error }) {
|
||||
if (value instanceof Stream) {
|
||||
return value as any;
|
||||
return { value };
|
||||
}
|
||||
|
||||
return this.createError('stream.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('stream.base')],
|
||||
};
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
|
||||
base: Joi.string(),
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
type: 'bytes',
|
||||
coerce(value: any, { error }) {
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
return ByteSizeValue.parse(value);
|
||||
return { value: ByteSizeValue.parse(value) };
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return new ByteSizeValue(value);
|
||||
return { value: new ByteSizeValue(value) };
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError('bytes.parse', { value, message: e.message }, state, options);
|
||||
return {
|
||||
errors: [error('bytes.parse', { message: e.message })],
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
return { value };
|
||||
},
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
validate(value, { error }) {
|
||||
// If value isn't defined, let Joi handle default value if it's defined.
|
||||
if (value instanceof ByteSizeValue) {
|
||||
return value as any;
|
||||
return { value };
|
||||
}
|
||||
|
||||
return this.createError('bytes.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('bytes.base')],
|
||||
};
|
||||
},
|
||||
rules: [
|
||||
anyCustomRule,
|
||||
rules: {
|
||||
min: {
|
||||
args: [
|
||||
{
|
||||
name: 'min',
|
||||
params: {
|
||||
limit: Joi.alternatives([Joi.number(), Joi.string()]).required(),
|
||||
},
|
||||
validate(params, value, state, options) {
|
||||
const limit = ensureByteSizeValue(params.limit);
|
||||
if (value.isLessThan(limit)) {
|
||||
return this.createError('bytes.min', { value, limit }, state, options);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max',
|
||||
params: {
|
||||
limit: Joi.alternatives([Joi.number(), Joi.string()]).required(),
|
||||
},
|
||||
validate(params, value, state, options) {
|
||||
const limit = ensureByteSizeValue(params.limit);
|
||||
if (value.isGreaterThan(limit)) {
|
||||
return this.createError('bytes.max', { value, limit }, state, options);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
name: 'limit',
|
||||
assert: Joi.alternatives([Joi.number(), Joi.string()]).required(),
|
||||
},
|
||||
],
|
||||
method(limit) {
|
||||
return this.$_addRule({ name: 'min', args: { limit } });
|
||||
},
|
||||
validate(value, { error }, args) {
|
||||
const limit = ensureByteSizeValue(args.limit);
|
||||
if (value.isLessThan(limit)) {
|
||||
return error('bytes.min', { value, limit });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
max: {
|
||||
args: [
|
||||
{
|
||||
name: 'limit',
|
||||
assert: Joi.alternatives([Joi.number(), Joi.string()]).required(),
|
||||
},
|
||||
],
|
||||
method(limit) {
|
||||
return this.$_addRule({ name: 'max', args: { limit } });
|
||||
},
|
||||
validate(value, { error }, args) {
|
||||
const limit = ensureByteSizeValue(args.limit);
|
||||
if (value.isGreaterThan(limit)) {
|
||||
return error('bytes.max', { value, limit });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
type: 'duration',
|
||||
coerce(value, { error }) {
|
||||
try {
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return ensureDuration(value);
|
||||
return { value: ensureDuration(value) };
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError('duration.parse', { value, message: e.message }, state, options);
|
||||
return {
|
||||
errors: [error('duration.parse', { message: e.message })],
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
return { value };
|
||||
},
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
validate(value, { error }) {
|
||||
if (!isDuration(value)) {
|
||||
return this.createError('duration.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('duration.base')],
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
return { value };
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
|
||||
type: 'number',
|
||||
base: Joi.number(),
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
coerce(value, { error }) {
|
||||
// If value isn't defined, let Joi handle default value if it's defined.
|
||||
if (value === undefined) {
|
||||
return value;
|
||||
return { value };
|
||||
}
|
||||
|
||||
// Do we want to allow strings that can be converted, e.g. "2"? (Joi does)
|
||||
|
@ -226,198 +171,197 @@ export const internals = Joi.extend([
|
|||
// > strings that can be converted to numbers)
|
||||
const coercedValue: any = typeof value === 'string' ? Number(value) : value;
|
||||
if (typeof coercedValue !== 'number' || isNaN(coercedValue)) {
|
||||
return this.createError('number.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('number.base')],
|
||||
};
|
||||
}
|
||||
|
||||
return value;
|
||||
return { value };
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
|
||||
type: 'object',
|
||||
base: Joi.object(),
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
coerce(value: any, { error, prefs }) {
|
||||
if (value === undefined || isPlainObject(value)) {
|
||||
return value;
|
||||
return { value };
|
||||
}
|
||||
|
||||
if (options.convert && typeof value === 'string') {
|
||||
if (prefs.convert && typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (isPlainObject(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return this.createError('object.base', { value: parsed }, state, options);
|
||||
return { value: parsed };
|
||||
} catch (e) {
|
||||
return this.createError('object.parse', { value }, state, options);
|
||||
return { errors: [error('object.parse')] };
|
||||
}
|
||||
}
|
||||
|
||||
return this.createError('object.base', { value }, state, options);
|
||||
return { errors: [error('object.base')] };
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
{
|
||||
name: 'map',
|
||||
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
type: 'array',
|
||||
base: Joi.array(),
|
||||
coerce(value: any, { error, prefs }) {
|
||||
if (value === undefined || Array.isArray(value)) {
|
||||
return { value };
|
||||
}
|
||||
if (prefs.convert && typeof value === 'string') {
|
||||
try {
|
||||
// ensuring that the parsed object is an array is done by the base's validation
|
||||
return { value: JSON.parse(value) };
|
||||
} catch (e) {
|
||||
return {
|
||||
errors: [error('array.parse')],
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
errors: [error('array.base')],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'map',
|
||||
coerce(value, { error, prefs }) {
|
||||
if (value === undefined) {
|
||||
return value;
|
||||
return { value };
|
||||
}
|
||||
if (isPlainObject(value)) {
|
||||
return new Map(Object.entries(value));
|
||||
return { value: new Map(Object.entries(value)) };
|
||||
}
|
||||
if (options.convert && typeof value === 'string') {
|
||||
if (prefs.convert && typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (isPlainObject(parsed)) {
|
||||
return new Map(Object.entries(parsed));
|
||||
return { value: new Map(Object.entries(parsed)) };
|
||||
}
|
||||
return this.createError('map.base', { value: parsed }, state, options);
|
||||
return {
|
||||
value: parsed,
|
||||
};
|
||||
} catch (e) {
|
||||
return this.createError('map.parse', { value }, state, options);
|
||||
return {
|
||||
errors: [error('map.parse')],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
return { value };
|
||||
},
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
validate(value, { error }) {
|
||||
if (!isMap(value)) {
|
||||
return this.createError('map.base', { value }, state, options);
|
||||
return {
|
||||
errors: [error('map.base')],
|
||||
};
|
||||
}
|
||||
|
||||
return value as any;
|
||||
return { value };
|
||||
},
|
||||
rules: [
|
||||
anyCustomRule,
|
||||
rules: {
|
||||
entries: {
|
||||
args: [
|
||||
{
|
||||
name: 'entries',
|
||||
params: {
|
||||
key: Joi.object().schema(),
|
||||
value: Joi.object().schema(),
|
||||
name: 'key',
|
||||
assert: Joi.object().schema(),
|
||||
},
|
||||
validate(params, value, state, options) {
|
||||
{
|
||||
name: 'value',
|
||||
assert: Joi.object().schema(),
|
||||
},
|
||||
],
|
||||
method(key, value) {
|
||||
return this.$_addRule({ name: 'entries', args: { key, value } });
|
||||
},
|
||||
validate(value, { error }, args, options) {
|
||||
const result = new Map();
|
||||
for (const [entryKey, entryValue] of value) {
|
||||
const { value: validatedEntryKey, error: keyError } = Joi.validate(
|
||||
entryKey,
|
||||
params.key,
|
||||
{ presence: 'required' }
|
||||
);
|
||||
|
||||
if (keyError) {
|
||||
return this.createError('map.key', { entryKey, reason: keyError }, state, options);
|
||||
let validatedEntryKey: any;
|
||||
try {
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
|
||||
} catch (e) {
|
||||
return error('map.key', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
const { value: validatedEntryValue, error: valueError } = Joi.validate(
|
||||
entryValue,
|
||||
params.value,
|
||||
{ presence: 'required' }
|
||||
);
|
||||
|
||||
if (valueError) {
|
||||
return this.createError(
|
||||
'map.value',
|
||||
{ entryKey, reason: valueError },
|
||||
state,
|
||||
options
|
||||
);
|
||||
let validatedEntryValue: any;
|
||||
try {
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
|
||||
} catch (e) {
|
||||
return error('map.value', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
result.set(validatedEntryKey, validatedEntryValue);
|
||||
}
|
||||
|
||||
return result as any;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'record',
|
||||
pre(value: any, state: State, options: ValidationOptions) {
|
||||
type: 'record',
|
||||
coerce(value, { error, prefs }) {
|
||||
if (value === undefined || isPlainObject(value)) {
|
||||
return value;
|
||||
return { value };
|
||||
}
|
||||
|
||||
if (options.convert && typeof value === 'string') {
|
||||
if (prefs.convert && typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (isPlainObject(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return this.createError('record.base', { value: parsed }, state, options);
|
||||
return { value: parsed };
|
||||
} catch (e) {
|
||||
return this.createError('record.parse', { value }, state, options);
|
||||
return {
|
||||
errors: [error('record.parse')],
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
errors: [error('record.base')],
|
||||
};
|
||||
},
|
||||
validate(value, { error }) {
|
||||
if (!isPlainObject(value)) {
|
||||
return {
|
||||
errors: [error('record.base')],
|
||||
};
|
||||
}
|
||||
|
||||
return this.createError('record.base', { value }, state, options);
|
||||
return { value };
|
||||
},
|
||||
rules: [
|
||||
anyCustomRule,
|
||||
rules: {
|
||||
entries: {
|
||||
args: [
|
||||
{
|
||||
name: 'entries',
|
||||
params: { key: Joi.object().schema(), value: Joi.object().schema() },
|
||||
validate(params, value, state, options) {
|
||||
name: 'key',
|
||||
assert: Joi.object().schema(),
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
assert: Joi.object().schema(),
|
||||
},
|
||||
],
|
||||
method(key, value) {
|
||||
return this.$_addRule({ name: 'entries', args: { key, value } });
|
||||
},
|
||||
validate(value, { error }, args) {
|
||||
const result = {} as Record<string, any>;
|
||||
for (const [entryKey, entryValue] of Object.entries(value)) {
|
||||
const { value: validatedEntryKey, error: keyError } = Joi.validate(
|
||||
entryKey,
|
||||
params.key,
|
||||
{ presence: 'required' }
|
||||
);
|
||||
|
||||
if (keyError) {
|
||||
return this.createError('record.key', { entryKey, reason: keyError }, state, options);
|
||||
let validatedEntryKey: any;
|
||||
try {
|
||||
validatedEntryKey = Joi.attempt(entryKey, args.key, { presence: 'required' });
|
||||
} catch (e) {
|
||||
return error('record.key', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
const { value: validatedEntryValue, error: valueError } = Joi.validate(
|
||||
entryValue,
|
||||
params.value,
|
||||
{ presence: 'required' }
|
||||
);
|
||||
|
||||
if (valueError) {
|
||||
return this.createError(
|
||||
'record.value',
|
||||
{ entryKey, reason: valueError },
|
||||
state,
|
||||
options
|
||||
);
|
||||
let validatedEntryValue: any;
|
||||
try {
|
||||
validatedEntryValue = Joi.attempt(entryValue, args.value, { presence: 'required' });
|
||||
} catch (e) {
|
||||
return error('record.value', { entryKey, reason: e });
|
||||
}
|
||||
|
||||
result[validatedEntryKey] = validatedEntryValue;
|
||||
}
|
||||
|
||||
return result as any;
|
||||
return result;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
|
||||
base: Joi.array(),
|
||||
coerce(value: any, state: State, options: ValidationOptions) {
|
||||
if (value === undefined || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (options.convert && typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
return this.createError('array.base', { value: parsed }, state, options);
|
||||
} catch (e) {
|
||||
return this.createError('array.parse', { value }, state, options);
|
||||
}
|
||||
}
|
||||
|
||||
return this.createError('array.base', { value }, state, options);
|
||||
},
|
||||
rules: [anyCustomRule],
|
||||
},
|
||||
]) as JoiRoot;
|
||||
);
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { internals, Reference as InternalReference } from '../internals';
|
||||
import type { Reference as InternalReference } from 'joi';
|
||||
import { internals } from '../internals';
|
||||
|
||||
export class Reference<T> {
|
||||
public static isReference<V>(value: V | Reference<V> | undefined): value is Reference<V> {
|
||||
|
|
|
@ -27,6 +27,10 @@ test('includes namespace in failure', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('coerces strings to buffer', () => {
|
||||
expect(schema.buffer().validate('abc')).toStrictEqual(Buffer.from('abc'));
|
||||
});
|
||||
|
||||
describe('#defaultValue', () => {
|
||||
test('returns default when undefined', () => {
|
||||
const value = Buffer.from('Hi!');
|
||||
|
@ -49,8 +53,4 @@ test('returns error when not a buffer', () => {
|
|||
expect(() => schema.buffer().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [Buffer] but got [Array]"`
|
||||
);
|
||||
|
||||
expect(() => schema.buffer().validate('abc')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [Buffer] but got [string]"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -348,15 +348,15 @@ test('works within `oneOf`', () => {
|
|||
expect(type.validate(['a', 'b'], { type: 'array' })).toEqual(['a', 'b']);
|
||||
|
||||
expect(() => type.validate(1, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [number]
|
||||
- [1]: expected value of type [array] but got [number]"
|
||||
`);
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [number]
|
||||
- [1]: expected value of type [array] but got [number]"
|
||||
`);
|
||||
expect(() => type.validate(true, { type: 'string' })).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [boolean]
|
||||
- [1]: expected value of type [array] but got [boolean]"
|
||||
`);
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [boolean]
|
||||
- [1]: expected value of type [array] but got [boolean]"
|
||||
`);
|
||||
});
|
||||
|
||||
describe('#validate', () => {
|
||||
|
|
|
@ -17,7 +17,7 @@ export class LiteralType<T> extends Type<T> {
|
|||
protected handleError(type: string, { value, valids: [expectedValue] }: Record<string, any>) {
|
||||
switch (type) {
|
||||
case 'any.required':
|
||||
case 'any.allowOnly':
|
||||
case 'any.only':
|
||||
return `expected value to equal [${expectedValue}]`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export class MaybeType<V> extends Type<V | undefined> {
|
|||
type
|
||||
.getSchema()
|
||||
.optional()
|
||||
.default(() => undefined, 'undefined')
|
||||
.default(() => undefined)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,14 +181,15 @@ test('called with wrong type', () => {
|
|||
|
||||
test('handles oneOf', () => {
|
||||
const type = schema.object({
|
||||
key: schema.oneOf([schema.string()]),
|
||||
key: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
|
||||
});
|
||||
|
||||
expect(type.validate({ key: 'foo' })).toEqual({ key: 'foo' });
|
||||
expect(() => type.validate({ key: 123 })).toThrowErrorMatchingInlineSnapshot(`
|
||||
"[key]: types that failed validation:
|
||||
- [key.0]: expected value of type [string] but got [number]"
|
||||
`);
|
||||
"[key]: types that failed validation:
|
||||
- [key.0]: expected value of type [string] but got [number]
|
||||
- [key.1]: expected value of type [array] but got [number]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handles references', () => {
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnySchema } from 'joi';
|
||||
import typeDetect from 'type-detect';
|
||||
import { AnySchema, internals } from '../internals';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions } from './type';
|
||||
import { ValidationError } from '../errors';
|
||||
|
||||
|
@ -185,7 +186,7 @@ export class ObjectType<P extends Props = any> extends Type<ObjectResultType<P>>
|
|||
return `expected a plain object value, but found [${typeDetect(value)}] instead.`;
|
||||
case 'object.parse':
|
||||
return `could not parse object value from json input`;
|
||||
case 'object.allowUnknown':
|
||||
case 'object.unknown':
|
||||
return `definition for this key is missing`;
|
||||
case 'object.child':
|
||||
return reason[0];
|
||||
|
|
|
@ -46,13 +46,7 @@ test('includes namespace in failure', () => {
|
|||
describe('#defaultValue', () => {
|
||||
test('returns default when undefined', () => {
|
||||
const value = new Stream();
|
||||
expect(schema.stream({ defaultValue: value }).validate(undefined)).toMatchInlineSnapshot(`
|
||||
Stream {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
"_maxListeners": undefined,
|
||||
}
|
||||
`);
|
||||
expect(schema.stream({ defaultValue: value }).validate(undefined)).toBeInstanceOf(Stream);
|
||||
});
|
||||
|
||||
test('returns value when specified', () => {
|
||||
|
|
|
@ -30,6 +30,20 @@ test('includes namespace in failure', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('returns error when not string', () => {
|
||||
expect(() => schema.string().validate(123)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [number]"`
|
||||
);
|
||||
|
||||
expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [Array]"`
|
||||
);
|
||||
|
||||
expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [RegExp]"`
|
||||
);
|
||||
});
|
||||
|
||||
describe('#minLength', () => {
|
||||
test('returns value when longer string', () => {
|
||||
expect(schema.string({ minLength: 2 }).validate('foo')).toBe('foo');
|
||||
|
@ -71,14 +85,13 @@ describe('#hostname', () => {
|
|||
expect(hostNameSchema.validate('www.example.com')).toBe('www.example.com');
|
||||
expect(hostNameSchema.validate('3domain.local')).toBe('3domain.local');
|
||||
expect(hostNameSchema.validate('hostname')).toBe('hostname');
|
||||
expect(hostNameSchema.validate('2387628')).toBe('2387628');
|
||||
expect(hostNameSchema.validate('::1')).toBe('::1');
|
||||
expect(hostNameSchema.validate('0:0:0:0:0:0:0:1')).toBe('0:0:0:0:0:0:0:1');
|
||||
expect(hostNameSchema.validate('xn----ascii-7gg5ei7b1i.xn--90a3a')).toBe(
|
||||
'xn----ascii-7gg5ei7b1i.xn--90a3a'
|
||||
);
|
||||
|
||||
const hostNameWithMaxAllowedLength = 'a'.repeat(255);
|
||||
const hostNameWithMaxAllowedLength = Array(4).fill('a'.repeat(63)).join('.');
|
||||
expect(hostNameSchema.validate(hostNameWithMaxAllowedLength)).toBe(
|
||||
hostNameWithMaxAllowedLength
|
||||
);
|
||||
|
@ -87,6 +100,12 @@ describe('#hostname', () => {
|
|||
test('returns error when value is not a valid hostname', () => {
|
||||
const hostNameSchema = schema.string({ hostname: true });
|
||||
|
||||
expect(() => hostNameSchema.validate('2387628')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"value must be a valid hostname (see RFC 1123)."`
|
||||
);
|
||||
expect(() =>
|
||||
hostNameSchema.validate(Array(4).fill('a'.repeat(64)).join('.'))
|
||||
).toThrowErrorMatchingInlineSnapshot(`"value must be a valid hostname (see RFC 1123)."`);
|
||||
expect(() => hostNameSchema.validate('host:name')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"value must be a valid hostname (see RFC 1123)."`
|
||||
);
|
||||
|
@ -99,16 +118,14 @@ describe('#hostname', () => {
|
|||
expect(() => hostNameSchema.validate('0:?:0:0:0:0:0:1')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"value must be a valid hostname (see RFC 1123)."`
|
||||
);
|
||||
|
||||
const tooLongHostName = 'a'.repeat(256);
|
||||
expect(() => hostNameSchema.validate(tooLongHostName)).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => hostNameSchema.validate('a'.repeat(256))).toThrowErrorMatchingInlineSnapshot(
|
||||
`"value must be a valid hostname (see RFC 1123)."`
|
||||
);
|
||||
});
|
||||
|
||||
test('returns error when empty string', () => {
|
||||
expect(() => schema.string({ hostname: true }).validate('')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"any.empty"`
|
||||
`"\\"value\\" is not allowed to be empty"`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -176,17 +193,3 @@ describe('#validate', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns error when not string', () => {
|
||||
expect(() => schema.string().validate(123)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [number]"`
|
||||
);
|
||||
|
||||
expect(() => schema.string().validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [Array]"`
|
||||
);
|
||||
|
||||
expect(() => schema.string().validate(/abc/)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [RegExp]"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import typeDetect from 'type-detect';
|
||||
import { internals } from '../internals';
|
||||
import { Type, TypeOptions } from './type';
|
||||
import { Type, TypeOptions, convertValidationFunction } from './type';
|
||||
|
||||
export type StringOptions = TypeOptions<string> & {
|
||||
minLength?: number;
|
||||
|
@ -25,26 +25,32 @@ export class StringType extends Type<string> {
|
|||
let schema =
|
||||
options.hostname === true
|
||||
? internals.string().hostname()
|
||||
: internals.any().custom((value) => {
|
||||
: internals.any().custom(
|
||||
convertValidationFunction((value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return `expected value of type [string] but got [${typeDetect(value)}]`;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (options.minLength !== undefined) {
|
||||
schema = schema.custom((value) => {
|
||||
schema = schema.custom(
|
||||
convertValidationFunction((value) => {
|
||||
if (value.length < options.minLength!) {
|
||||
return `value has length [${value.length}] but it must have a minimum length of [${options.minLength}].`;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (options.maxLength !== undefined) {
|
||||
schema = schema.custom((value) => {
|
||||
schema = schema.custom(
|
||||
convertValidationFunction((value) => {
|
||||
if (value.length > options.maxLength!) {
|
||||
return `value has length [${value.length}] but it must have a maximum length of [${options.maxLength}].`;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
super(schema, options);
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnySchema, CustomValidator, ErrorReport } from 'joi';
|
||||
import { SchemaTypeError, ValidationError } from '../errors';
|
||||
import { AnySchema, internals, ValidationErrorItem } from '../internals';
|
||||
import { Reference } from '../references';
|
||||
|
||||
export interface TypeOptions<T> {
|
||||
|
@ -15,6 +15,25 @@ export interface TypeOptions<T> {
|
|||
validate?: (value: T) => string | void;
|
||||
}
|
||||
|
||||
export const convertValidationFunction = <T = unknown>(
|
||||
validate: (value: T) => string | void
|
||||
): CustomValidator<T> => {
|
||||
return (value, { error }) => {
|
||||
let validationResultMessage;
|
||||
try {
|
||||
validationResultMessage = validate(value);
|
||||
} catch (e) {
|
||||
validationResultMessage = e.message || e;
|
||||
}
|
||||
|
||||
if (typeof validationResultMessage === 'string') {
|
||||
return error('any.custom', { message: validationResultMessage });
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
export abstract class Type<V> {
|
||||
// This is just to enable the `TypeOf` helper, and because TypeScript would
|
||||
// fail if it wasn't initialized we use a "trick" to which basically just
|
||||
|
@ -36,24 +55,23 @@ export abstract class Type<V> {
|
|||
|
||||
// If default value is a function, then we must provide description for it.
|
||||
if (typeof options.defaultValue === 'function') {
|
||||
schema = schema.default(options.defaultValue, 'Type default value');
|
||||
schema = schema.default(options.defaultValue);
|
||||
} else {
|
||||
schema = schema.default(
|
||||
Reference.isReference(options.defaultValue)
|
||||
? options.defaultValue.getSchema()
|
||||
: options.defaultValue
|
||||
: (options.defaultValue as any)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.validate) {
|
||||
schema = schema.custom(options.validate);
|
||||
schema = schema.custom(convertValidationFunction(options.validate));
|
||||
}
|
||||
|
||||
// Attach generic error handler only if it hasn't been attached yet since
|
||||
// only the last error handler is counted.
|
||||
const schemaFlags = (schema.describe().flags as Record<string, any>) || {};
|
||||
if (schemaFlags.error === undefined) {
|
||||
if (schema.$_getFlag('error') === undefined) {
|
||||
schema = schema.error(([error]) => this.onError(error));
|
||||
}
|
||||
|
||||
|
@ -61,7 +79,7 @@ export abstract class Type<V> {
|
|||
}
|
||||
|
||||
public validate(value: any, context: Record<string, any> = {}, namespace?: string): V {
|
||||
const { value: validatedValue, error } = internals.validate(value, this.internalSchema, {
|
||||
const { value: validatedValue, error } = this.internalSchema.validate(value, {
|
||||
context,
|
||||
presence: 'required',
|
||||
});
|
||||
|
@ -88,14 +106,19 @@ export abstract class Type<V> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
private onError(error: SchemaTypeError | ValidationErrorItem): SchemaTypeError {
|
||||
private onError(error: SchemaTypeError | ErrorReport): SchemaTypeError {
|
||||
if (error instanceof SchemaTypeError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
const { context = {}, type, path, message } = error;
|
||||
const { local, code, path, value } = error;
|
||||
const convertedPath = path.map((entry) => entry.toString());
|
||||
const context: Record<string, any> = {
|
||||
...local,
|
||||
value,
|
||||
};
|
||||
|
||||
const errorHandleResult = this.handleError(type, context, path);
|
||||
const errorHandleResult = this.handleError(code, context, convertedPath);
|
||||
if (errorHandleResult instanceof SchemaTypeError) {
|
||||
return errorHandleResult;
|
||||
}
|
||||
|
@ -103,15 +126,18 @@ export abstract class Type<V> {
|
|||
// If error handler just defines error message, then wrap it into proper
|
||||
// `SchemaTypeError` instance.
|
||||
if (typeof errorHandleResult === 'string') {
|
||||
return new SchemaTypeError(errorHandleResult, path);
|
||||
return new SchemaTypeError(errorHandleResult, convertedPath);
|
||||
}
|
||||
|
||||
// If error is produced by the custom validator, just extract source message
|
||||
// from context and wrap it into `SchemaTypeError` instance.
|
||||
if (type === 'any.custom') {
|
||||
return new SchemaTypeError(context.message, path);
|
||||
if (code === 'any.custom' && context.message) {
|
||||
return new SchemaTypeError(context.message, convertedPath);
|
||||
}
|
||||
|
||||
return new SchemaTypeError(message || type, path);
|
||||
// `message` is only initialized once `toString` has been called (...)
|
||||
// see https://github.com/sideway/joi/blob/master/lib/errors.js
|
||||
const message = error.toString();
|
||||
return new SchemaTypeError(message || code, convertedPath);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,22 +62,42 @@ test('handles object', () => {
|
|||
});
|
||||
|
||||
test('handles object with wrong type', () => {
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() })]);
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() }), schema.string()]);
|
||||
|
||||
expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0.age]: expected value of type [number] but got [string]"
|
||||
`);
|
||||
"types that failed validation:
|
||||
- [0.age]: expected value of type [number] but got [string]
|
||||
- [1]: expected value of type [string] but got [Object]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('use shorter error messages when defining only one type', () => {
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() })]);
|
||||
|
||||
expect(() => type.validate({ age: 'foo' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[age]: expected value of type [number] but got [string]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('includes namespace in failure', () => {
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() })]);
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() }), schema.string()]);
|
||||
|
||||
expect(() => type.validate({ age: 'foo' }, {}, 'foo-namespace'))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"[foo-namespace]: types that failed validation:
|
||||
- [foo-namespace.0.age]: expected value of type [number] but got [string]"
|
||||
`);
|
||||
"[foo-namespace]: types that failed validation:
|
||||
- [foo-namespace.0.age]: expected value of type [number] but got [string]
|
||||
- [foo-namespace.1]: expected value of type [string] but got [Object]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('includes namespace in failure in shorthand mode', () => {
|
||||
const type = schema.oneOf([schema.object({ age: schema.number() })]);
|
||||
|
||||
expect(() =>
|
||||
type.validate({ age: 'foo' }, {}, 'foo-namespace')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[foo-namespace.age]: expected value of type [number] but got [string]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('handles multiple objects with same key', () => {
|
||||
|
@ -106,33 +126,32 @@ test('handles maybe', () => {
|
|||
test('fails if not matching type', () => {
|
||||
const type = schema.oneOf([schema.string()]);
|
||||
|
||||
expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [boolean]"
|
||||
`);
|
||||
expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [number]"
|
||||
`);
|
||||
expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [boolean]"`
|
||||
);
|
||||
expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"expected value of type [string] but got [number]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('fails if not matching multiple types', () => {
|
||||
const type = schema.oneOf([schema.string(), schema.number()]);
|
||||
|
||||
expect(() => type.validate(false)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [boolean]
|
||||
- [1]: expected value of type [number] but got [boolean]"
|
||||
`);
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [string] but got [boolean]
|
||||
- [1]: expected value of type [number] but got [boolean]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('fails if not matching literal', () => {
|
||||
const type = schema.oneOf([schema.literal('foo')]);
|
||||
const type = schema.oneOf([schema.literal('foo'), schema.literal('dolly')]);
|
||||
|
||||
expect(() => type.validate('bar')).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: expected value to equal [foo]"
|
||||
`);
|
||||
"types that failed validation:
|
||||
- [0]: expected value to equal [foo]
|
||||
- [1]: expected value to equal [dolly]"
|
||||
`);
|
||||
});
|
||||
|
||||
test('fails if nested union type fail', () => {
|
||||
|
@ -142,12 +161,10 @@ test('fails if nested union type fail', () => {
|
|||
]);
|
||||
|
||||
expect(() => type.validate('aaa')).toThrowErrorMatchingInlineSnapshot(`
|
||||
"types that failed validation:
|
||||
- [0]: types that failed validation:
|
||||
"types that failed validation:
|
||||
- [0]: expected value of type [boolean] but got [string]
|
||||
- [1]: types that failed validation:
|
||||
- [0]: types that failed validation:
|
||||
- [1]: types that failed validation:
|
||||
- [0]: could not parse object value from json input
|
||||
- [1]: expected value of type [number] but got [string]"
|
||||
`);
|
||||
`);
|
||||
});
|
|
@ -13,20 +13,21 @@ import { Type, TypeOptions } from './type';
|
|||
|
||||
export class UnionType<RTS extends Array<Type<any>>, T> extends Type<T> {
|
||||
constructor(types: RTS, options?: TypeOptions<T>) {
|
||||
const schema = internals.alternatives(types.map((type) => type.getSchema()));
|
||||
const schema = internals.alternatives(types.map((type) => type.getSchema())).match('any');
|
||||
|
||||
super(schema, options);
|
||||
}
|
||||
|
||||
protected handleError(type: string, { reason, value }: Record<string, any>, path: string[]) {
|
||||
protected handleError(type: string, { value, details }: Record<string, any>, path: string[]) {
|
||||
switch (type) {
|
||||
case 'any.required':
|
||||
return `expected at least one defined value but got [${typeDetect(value)}]`;
|
||||
case 'alternatives.child':
|
||||
case 'alternatives.match':
|
||||
return new SchemaTypesError(
|
||||
'types that failed validation:',
|
||||
path,
|
||||
reason.map((e: SchemaTypeError, index: number) => {
|
||||
details.map((detail: AlternativeErrorDetail, index: number) => {
|
||||
const e = detail.context.error;
|
||||
const childPathWithIndex = e.path.slice();
|
||||
childPathWithIndex.splice(path.length, 0, index.toString());
|
||||
|
||||
|
@ -38,3 +39,9 @@ export class UnionType<RTS extends Array<Type<any>>, T> extends Type<T> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AlternativeErrorDetail {
|
||||
context: {
|
||||
error: SchemaTypeError;
|
||||
};
|
||||
}
|
||||
|
|
18
packages/kbn-config-schema/types/joi.d.ts
vendored
18
packages/kbn-config-schema/types/joi.d.ts
vendored
|
@ -12,6 +12,7 @@ import { ByteSizeValue } from '../src/byte_size_value';
|
|||
declare module 'joi' {
|
||||
interface BytesSchema extends AnySchema {
|
||||
min(limit: number | string | ByteSizeValue): this;
|
||||
|
||||
max(limit: number | string | ByteSizeValue): this;
|
||||
}
|
||||
|
||||
|
@ -23,6 +24,14 @@ declare module 'joi' {
|
|||
entries(key: AnySchema, value: AnySchema): this;
|
||||
}
|
||||
|
||||
interface ErrorReport {
|
||||
// missing from the typedef
|
||||
// see https://github.com/sideway/joi/blob/master/lib/errors.js
|
||||
local?: Record<string, any>;
|
||||
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export type JoiRoot = Joi.Root & {
|
||||
bytes: () => BytesSchema;
|
||||
duration: () => AnySchema;
|
||||
|
@ -30,13 +39,4 @@ declare module 'joi' {
|
|||
record: () => RecordSchema;
|
||||
stream: () => AnySchema;
|
||||
};
|
||||
|
||||
interface AnySchema {
|
||||
custom(validator: (value: any) => string | void): this;
|
||||
}
|
||||
|
||||
// Joi types don't include `schema` function even though it's supported.
|
||||
interface ObjectSchema {
|
||||
schema(): this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Config } from './config';
|
||||
|
||||
describe('Config', () => {
|
||||
it('recognizes keys under `object().pattern`', () => {
|
||||
const config = new Config({
|
||||
settings: {
|
||||
services: {
|
||||
foo: () => 42,
|
||||
},
|
||||
},
|
||||
primary: true,
|
||||
path: process.cwd(),
|
||||
});
|
||||
|
||||
expect(config.has('services.foo')).toEqual(true);
|
||||
expect(config.get('services.foo')()).toEqual(42);
|
||||
});
|
||||
});
|
|
@ -50,20 +50,20 @@ export class Config {
|
|||
values: Record<string, any>,
|
||||
childSchema: any
|
||||
): boolean {
|
||||
if (!childSchema._inner) {
|
||||
if (!childSchema.$_terms.keys && !childSchema.$_terms.patterns) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// normalize child and pattern checks so we can iterate the checks in a single loop
|
||||
const checks: Array<{ test: (k: string) => boolean; schema: Schema }> = [
|
||||
// match children first, they have priority
|
||||
...(childSchema._inner.children || []).map((child: { key: string; schema: Schema }) => ({
|
||||
...(childSchema.$_terms.keys || []).map((child: { key: string; schema: Schema }) => ({
|
||||
test: (k: string) => child.key === k,
|
||||
schema: child.schema,
|
||||
})),
|
||||
|
||||
// match patterns on any key that doesn't match an explicit child
|
||||
...(childSchema._inner.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({
|
||||
...(childSchema.$_terms.patterns || []).map((pattern: { regex: RegExp; rule: Schema }) => ({
|
||||
test: (k: string) => pattern.regex.test(k) && has(values, k),
|
||||
schema: pattern.rule,
|
||||
})),
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { dirname, resolve } from 'path';
|
||||
|
||||
import Joi from 'joi';
|
||||
import type { CustomHelpers } from 'joi';
|
||||
|
||||
// valid pattern for ID
|
||||
// enforced camel-case identifiers for consistency
|
||||
|
@ -54,15 +55,17 @@ const dockerServerSchema = () =>
|
|||
image: requiredWhenEnabled(Joi.string()),
|
||||
port: requiredWhenEnabled(Joi.number()),
|
||||
portInContainer: requiredWhenEnabled(Joi.number()),
|
||||
waitForLogLine: Joi.alternatives(Joi.object().type(RegExp), Joi.string()).optional(),
|
||||
waitForLogLine: Joi.alternatives(Joi.object().instance(RegExp), Joi.string()).optional(),
|
||||
waitFor: Joi.func().optional(),
|
||||
args: Joi.array().items(Joi.string()).optional(),
|
||||
})
|
||||
.default();
|
||||
|
||||
const defaultRelativeToConfigPath = (path: string) => {
|
||||
const makeDefault: any = (_: any, options: any) => resolve(dirname(options.context.path), path);
|
||||
makeDefault.description = `<config.js directory>/${path}`;
|
||||
const makeDefault = (parent: any, helpers: CustomHelpers) => {
|
||||
helpers.schema.description(`<config.js directory>/${path}`);
|
||||
return resolve(dirname(helpers.prefs.context!.path), path);
|
||||
};
|
||||
return makeDefault;
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('config.validate()', () => {
|
|||
// This is intentionally not editable in the raw CSP config.
|
||||
// Users should set `server.securityResponseHeaders.disableEmbedding` to control this config property.
|
||||
expect(() => config.schema.validate({ disableEmbedding: true })).toThrowError(
|
||||
'[disableEmbedding.0]: expected value to equal [false]'
|
||||
'[disableEmbedding]: expected value to equal [false]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -111,7 +111,11 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`throws if invalid hostname 1`] = `"[host]: value must be a valid hostname (see RFC 1123)."`;
|
||||
exports[`throws if invalid hostname 1`] = `
|
||||
"[host]: types that failed validation:
|
||||
- [host.0]: value must be a valid hostname (see RFC 1123).
|
||||
- [host.1]: expected value to equal [0]"
|
||||
`;
|
||||
|
||||
exports[`with TLS throws if TLS is enabled but \`redirectHttpFromPort\` is equal to \`port\` 1`] = `"Kibana does not accept http traffic to [port] when ssl is enabled (only https is allowed), so [ssl.redirectHttpFromPort] cannot be configured to the same value. Both are [1234]."`;
|
||||
|
||||
|
|
|
@ -36,6 +36,11 @@ test('accepts valid hostnames', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('accepts `0` as hostname', () => {
|
||||
const { host } = config.schema.validate({ host: '0' });
|
||||
expect(host).toEqual('0');
|
||||
});
|
||||
|
||||
test('throws if invalid hostname', () => {
|
||||
const httpSchema = config.schema;
|
||||
const obj = {
|
||||
|
|
|
@ -84,9 +84,8 @@ const configSchema = schema.object(
|
|||
}
|
||||
},
|
||||
}),
|
||||
host: schema.string({
|
||||
host: schema.oneOf([schema.string({ hostname: true }), schema.literal('0')], {
|
||||
defaultValue: 'localhost',
|
||||
hostname: true,
|
||||
}),
|
||||
maxPayload: schema.byteSize({
|
||||
defaultValue: '1048576b',
|
||||
|
@ -217,13 +216,7 @@ export class HttpConfig implements IHttpConfig {
|
|||
rawExternalUrlConfig: ExternalUrlConfig
|
||||
) {
|
||||
this.autoListen = rawHttpConfig.autoListen;
|
||||
// TODO: Consider dropping support for '0' in v8.0.0. This value is passed
|
||||
// to hapi, which validates it. Prior to hapi v20, '0' was considered a
|
||||
// valid host, however the validation logic internally in hapi was
|
||||
// re-written for v20 and hapi no longer considers '0' a valid host. For
|
||||
// details, see:
|
||||
// https://github.com/elastic/kibana/issues/86716#issuecomment-749623781
|
||||
this.host = rawHttpConfig.host === '0' ? '0.0.0.0' : rawHttpConfig.host;
|
||||
this.host = rawHttpConfig.host;
|
||||
this.port = rawHttpConfig.port;
|
||||
this.cors = rawHttpConfig.cors;
|
||||
const { securityResponseHeaders, disableEmbedding } = parseRawSecurityResponseHeadersConfig(
|
||||
|
|
|
@ -302,7 +302,7 @@ describe('Reporting Config Schema', () => {
|
|||
// kibanaServer
|
||||
const throwValidationErr = () => ConfigSchema.validate({ kibanaServer: { hostname: '0' } });
|
||||
expect(throwValidationErr).toThrowError(
|
||||
`[kibanaServer.hostname]: must not be "0" for the headless browser to correctly resolve the host`
|
||||
`[kibanaServer.hostname]: value must be a valid hostname (see RFC 1123).`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,16 +9,7 @@ import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
|
|||
import moment from 'moment';
|
||||
|
||||
const KibanaServerSchema = schema.object({
|
||||
hostname: schema.maybe(
|
||||
schema.string({
|
||||
validate(value) {
|
||||
if (value === '0') {
|
||||
return 'must not be "0" for the headless browser to correctly resolve the host';
|
||||
}
|
||||
},
|
||||
hostname: true,
|
||||
})
|
||||
),
|
||||
hostname: schema.maybe(schema.string({ hostname: true })),
|
||||
port: schema.maybe(schema.number()),
|
||||
protocol: schema.maybe(
|
||||
schema.string({
|
||||
|
|
|
@ -87,29 +87,7 @@ const MacEntrySchema = schema.object({
|
|||
...CommonEntrySchema,
|
||||
});
|
||||
|
||||
/*
|
||||
* Entry Schema depending on Os type using schema.conditional.
|
||||
* If OS === WINDOWS then use Windows schema,
|
||||
* else if OS === LINUX then use Linux schema,
|
||||
* else use Mac schema
|
||||
*/
|
||||
const EntrySchemaDependingOnOS = schema.conditional(
|
||||
schema.siblingRef('os'),
|
||||
OperatingSystem.WINDOWS,
|
||||
WindowsEntrySchema,
|
||||
schema.conditional(
|
||||
schema.siblingRef('os'),
|
||||
OperatingSystem.LINUX,
|
||||
LinuxEntrySchema,
|
||||
MacEntrySchema
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Entities array schema.
|
||||
* The validate function checks there is no duplicated entry inside the array
|
||||
*/
|
||||
const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, {
|
||||
const entriesSchemaOptions = {
|
||||
minSize: 1,
|
||||
validate(entries: ConditionEntry[]) {
|
||||
return (
|
||||
|
@ -118,7 +96,27 @@ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, {
|
|||
.join(', ') || undefined
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Entities array schema depending on Os type using schema.conditional.
|
||||
* If OS === WINDOWS then use Windows schema,
|
||||
* else if OS === LINUX then use Linux schema,
|
||||
* else use Mac schema
|
||||
*
|
||||
* The validate function checks there is no duplicated entry inside the array
|
||||
*/
|
||||
const EntriesSchema = schema.conditional(
|
||||
schema.siblingRef('os'),
|
||||
OperatingSystem.WINDOWS,
|
||||
schema.arrayOf(WindowsEntrySchema, entriesSchemaOptions),
|
||||
schema.conditional(
|
||||
schema.siblingRef('os'),
|
||||
OperatingSystem.LINUX,
|
||||
schema.arrayOf(LinuxEntrySchema, entriesSchemaOptions),
|
||||
schema.arrayOf(MacEntrySchema, entriesSchemaOptions)
|
||||
)
|
||||
);
|
||||
|
||||
const getTrustedAppForOsScheme = (forUpdateFlow: boolean = false) =>
|
||||
schema.object({
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -5200,10 +5200,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0"
|
||||
integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A==
|
||||
|
||||
"@types/joi@^13.4.2":
|
||||
version "13.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae"
|
||||
integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog==
|
||||
"@types/joi@^17.2.3":
|
||||
version "17.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-17.2.3.tgz#b7768ed9d84f1ebd393328b9f97c1cf3d2b94798"
|
||||
integrity sha512-dGjs/lhrWOa+eO0HwgxCSnDm5eMGCsXuvLglMghJq32F6q5LyyNuXb41DHzrg501CKNOSSAHmfB7FDGeUnDmzw==
|
||||
dependencies:
|
||||
joi "*"
|
||||
|
||||
"@types/jquery@*", "@types/jquery@^3.3.31":
|
||||
version "3.3.31"
|
||||
|
@ -15331,11 +15333,6 @@ hmac-drbg@^1.0.1:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoek@5.x.x:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.4.tgz#0f7fa270a1cafeb364a4b2ddfaa33f864e4157da"
|
||||
integrity sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==
|
||||
|
||||
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
|
@ -16701,13 +16698,6 @@ isbinaryfile@4.0.2:
|
|||
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488"
|
||||
integrity sha512-C3FSxJdNrEr2F4z6uFtNzECDM5hXk+46fxaa+cwBe5/XrWSmzdG8DDgyjfX6/NRdBB21q2JXuRAzPCUs+fclnQ==
|
||||
|
||||
isemail@3.x.x:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.4.tgz#76e2187ff7bee59d57522c6fd1c3f09a331933cf"
|
||||
integrity sha512-yE/W5osEWuAGSLVixV9pAexhkbZzglmuhO2CxdHu7IBh7uzuZogQ4bk0lE26HoZ6HD4ZYfKRKilkNuCnuJIBJw==
|
||||
dependencies:
|
||||
punycode "2.x.x"
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
|
@ -17433,14 +17423,16 @@ jju@~1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
|
||||
integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo=
|
||||
|
||||
joi@^13.5.2:
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-13.7.0.tgz#cfd85ebfe67e8a1900432400b4d03bbd93fb879f"
|
||||
integrity sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==
|
||||
joi@*, joi@^17.4.0:
|
||||
version "17.4.0"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.0.tgz#b5c2277c8519e016316e49ababd41a1908d9ef20"
|
||||
integrity sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==
|
||||
dependencies:
|
||||
hoek "5.x.x"
|
||||
isemail "3.x.x"
|
||||
topo "3.x.x"
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
"@sideway/address" "^4.1.0"
|
||||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
joi@^17.3.0:
|
||||
version "17.3.0"
|
||||
|
@ -22404,16 +22396,16 @@ punycode@1.3.2:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
|
||||
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
|
||||
|
||||
punycode@2.x.x, punycode@^2.1.0, punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
punycode@^1.2.4, punycode@^1.3.2:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||
|
||||
punycode@^2.1.0, punycode@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
pupa@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726"
|
||||
|
@ -27021,13 +27013,6 @@ toidentifier@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||
|
||||
topo@3.x.x:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.0.tgz#37e48c330efeac784538e0acd3e62ca5e231fe7a"
|
||||
integrity sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==
|
||||
dependencies:
|
||||
hoek "5.x.x"
|
||||
|
||||
topojson-client@3.1.0, topojson-client@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
|
||||
|
|
Loading…
Reference in a new issue