[SIEM] Adds recursive exact key checks for validation and formatter
## Summary * Adds exact check to cause failures whenever a schema is using t.exact({}) and that schema has extra keys at any level of the schema hierarchy. * Fixes the exact check to work with unknown in a safe way and only do recursive checks for keys only when the unknown is an Object. * Changes the output to use a format error mechanism which pushes all the errors onto one line to be consistent with the response errors. We can change this as a team to whatever we want I just put it to a comma separator as that is what the responses type checks were using downstream. * Moves the downstream code up higher to be used within SIEM for timeline and detection engine. * Adds tests in TDD/red light/green light fashion where I fail the extra key checks first and then pass them second. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
This commit is contained in:
parent
2af0c6d47e
commit
2caed9b7be
|
@ -9,8 +9,9 @@ import { fold } from 'fp-ts/lib/Either';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import * as t from 'io-ts';
|
||||
|
||||
import { formatErrors } from '../../../../utils/build_validation/format_errors';
|
||||
import { exactCheck } from '../../../../utils/build_validation/exact_check';
|
||||
import { PartialAlert, FindResult } from '../../../../../../alerting/server';
|
||||
import { formatErrors } from '../schemas/response/utils';
|
||||
import {
|
||||
isAlertType,
|
||||
IRuleSavedAttributesSavedObjectAttributes,
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
import { OutputRuleAlertRest } from '../../types';
|
||||
import { createBulkErrorObject, BulkError } from '../utils';
|
||||
import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema';
|
||||
import { exactCheck } from '../schemas/response/exact_check';
|
||||
import { transformFindAlerts, transform, transformAlertToRule } from './utils';
|
||||
import { findRulesSchema } from '../schemas/response/find_rules_schema';
|
||||
import { RuleActions } from '../../rule_actions/types';
|
||||
|
|
|
@ -4,32 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { RulesSchema } from '../rules_schema';
|
||||
import { RulesBulkSchema } from '../rules_bulk_schema';
|
||||
import { ErrorSchema } from '../error_schema';
|
||||
import { FindRulesSchema } from '../find_rules_schema';
|
||||
import { formatErrors } from '../utils';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
interface Message<T> {
|
||||
errors: t.Errors;
|
||||
schema: T | {};
|
||||
}
|
||||
|
||||
const onLeft = <T>(errors: t.Errors): Message<T> => {
|
||||
return { schema: {}, errors };
|
||||
};
|
||||
|
||||
const onRight = <T>(schema: T): Message<T> => {
|
||||
return {
|
||||
schema,
|
||||
errors: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const foldLeftRight = fold(onLeft, onRight);
|
||||
|
||||
export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z';
|
||||
|
||||
|
@ -127,18 +105,3 @@ export const getFindResponseSingle = (): FindRulesSchema => ({
|
|||
total: 1,
|
||||
data: [getBaseResponsePayload()],
|
||||
});
|
||||
|
||||
/**
|
||||
* Convenience utility to keep the error message handling within tests to be
|
||||
* very concise.
|
||||
* @param validation The validation to get the errors from
|
||||
*/
|
||||
export const getPaths = <A>(validation: t.Validation<A>): string[] => {
|
||||
return pipe(
|
||||
validation,
|
||||
fold(
|
||||
errors => formatErrors(errors),
|
||||
() => ['no errors']
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,17 +15,13 @@ import {
|
|||
addQueryFields,
|
||||
addMlFields,
|
||||
} from './check_type_dependents';
|
||||
import {
|
||||
foldLeftRight,
|
||||
getBaseResponsePayload,
|
||||
getPaths,
|
||||
getMlRuleResponsePayload,
|
||||
} from './__mocks__/utils';
|
||||
import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { exactCheck } from './exact_check';
|
||||
import { RulesSchema } from './rules_schema';
|
||||
import { TypeAndTimelineOnly } from './type_timeline_only_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
|
||||
describe('check_type_dependents', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils';
|
||||
import { getErrorPayload } from './__mocks__/utils';
|
||||
import { errorSchema, ErrorSchema } from './error_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('error_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -5,17 +5,13 @@
|
|||
*/
|
||||
|
||||
import { findRulesSchema, FindRulesSchema } from './find_rules_schema';
|
||||
import { exactCheck } from './exact_check';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import {
|
||||
foldLeftRight,
|
||||
getFindResponseSingle,
|
||||
getBaseResponsePayload,
|
||||
getPaths,
|
||||
} from './__mocks__/utils';
|
||||
import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { RulesSchema } from './rules_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
|
||||
describe('find_rules_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from './__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { left, Either } from 'fp-ts/lib/Either';
|
||||
import { ImportRulesSchema, importRulesSchema } from './import_rules_schema';
|
||||
import { ErrorSchema } from './error_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
import { Errors } from 'io-ts';
|
||||
|
||||
describe('import_rules_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -79,13 +80,31 @@ describe('import_rules_schema', () => {
|
|||
});
|
||||
|
||||
test('it should NOT validate a success that is not a boolean', () => {
|
||||
type UnsafeCastForTest = Either<
|
||||
Errors,
|
||||
{
|
||||
success: string;
|
||||
success_count: number;
|
||||
errors: Array<
|
||||
{
|
||||
id?: string | undefined;
|
||||
rule_id?: string | undefined;
|
||||
} & {
|
||||
error: {
|
||||
status_code: number;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
}
|
||||
>;
|
||||
const payload: Omit<ImportRulesSchema, 'success'> & { success: string } = {
|
||||
success: 'hello',
|
||||
success_count: 0,
|
||||
errors: [],
|
||||
};
|
||||
const decoded = importRulesSchema.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
|
||||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "hello" supplied to "success"']);
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from './__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('prepackaged_rules_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -4,15 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from './__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import {
|
||||
PrePackagedRulesStatusSchema,
|
||||
prePackagedRulesStatusSchema,
|
||||
} from './prepackaged_rules_status_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('prepackaged_rules_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -7,17 +7,13 @@
|
|||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import {
|
||||
foldLeftRight,
|
||||
getBaseResponsePayload,
|
||||
getErrorPayload,
|
||||
getPaths,
|
||||
} from './__mocks__/utils';
|
||||
import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils';
|
||||
import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema';
|
||||
import { RulesSchema } from './rules_schema';
|
||||
import { ErrorSchema } from './error_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('prepackaged_rule_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { rulesSchema, RulesSchema, removeList } from './rules_schema';
|
||||
import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils';
|
||||
import { getBaseResponsePayload } from './__mocks__/utils';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z';
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
import { left } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { exactCheck } from './exact_check';
|
||||
import { foldLeftRight, getPaths } from './__mocks__/utils';
|
||||
import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { exactCheck } from '../../../../../utils/build_validation/exact_check';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('prepackaged_rule_schema', () => {
|
||||
beforeAll(() => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { IsoDateString } from './iso_date_string';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('ios_date_string', () => {
|
||||
test('it should validate a iso string', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { ListsDefaultArray } from './lists_default_array';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('lists_default_array', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('positive_integer_greater_than_zero', () => {
|
||||
test('it should validate a positive number', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { PositiveInteger } from './positive_integer';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('positive_integer_greater_than_zero', () => {
|
||||
test('it should validate a positive number', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { ReferencesDefaultArray } from './references_default_array';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('references_default_array', () => {
|
||||
test('it should validate an empty array', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { RiskScore } from './risk_score';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('risk_score', () => {
|
||||
test('it should validate a positive number', () => {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { UUID } from './uuid';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { foldLeftRight, getPaths } from '../response/__mocks__/utils';
|
||||
import { left } from 'fp-ts/lib/Either';
|
||||
import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils';
|
||||
|
||||
describe('uuid', () => {
|
||||
test('it should validate a uuid', () => {
|
||||
|
|
|
@ -85,7 +85,7 @@ describe('export timelines', () => {
|
|||
const result = server.validate(request);
|
||||
|
||||
expect(result.badRequest.mock.calls[0][0]).toEqual(
|
||||
'Invalid value undefined supplied to : { ids: Array<string> }/ids: Array<string>'
|
||||
'Invalid value "undefined" supplied to "ids"'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -98,11 +98,7 @@ describe('export timelines', () => {
|
|||
const result = server.validate(request);
|
||||
|
||||
expect(result.badRequest.mock.calls[1][0]).toEqual(
|
||||
[
|
||||
'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/file_name: string',
|
||||
'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/0: "true"',
|
||||
'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/1: "false"',
|
||||
].join('\n')
|
||||
'Invalid value "undefined" supplied to "file_name",Invalid value "undefined" supplied to "exclude_export_details",Invalid value "undefined" supplied to "exclude_export_details"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -332,10 +332,7 @@ describe('import timelines', () => {
|
|||
const result = server.validate(request);
|
||||
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
[
|
||||
'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/0: ReadableRt',
|
||||
'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/1: { hapi: { filename: string } }',
|
||||
].join('\n')
|
||||
'Invalid value "undefined" supplied to "file",Invalid value "undefined" supplied to "file"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { formatErrors } from '../format_errors';
|
||||
|
||||
interface Message<T> {
|
||||
errors: t.Errors;
|
||||
schema: T | {};
|
||||
}
|
||||
|
||||
const onLeft = <T>(errors: t.Errors): Message<T> => {
|
||||
return { schema: {}, errors };
|
||||
};
|
||||
|
||||
const onRight = <T>(schema: T): Message<T> => {
|
||||
return {
|
||||
schema,
|
||||
errors: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const foldLeftRight = fold(onLeft, onRight);
|
||||
|
||||
/**
|
||||
* Convenience utility to keep the error message handling within tests to be
|
||||
* very concise.
|
||||
* @param validation The validation to get the errors from
|
||||
*/
|
||||
export const getPaths = <A>(validation: t.Validation<A>): string[] => {
|
||||
return pipe(
|
||||
validation,
|
||||
fold(
|
||||
errors => formatErrors(errors),
|
||||
() => ['no errors']
|
||||
)
|
||||
);
|
||||
};
|
|
@ -5,22 +5,13 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { left, right } from 'fp-ts/lib/Either';
|
||||
import { left, right, Either } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { foldLeftRight, getPaths } from './__mocks__/utils';
|
||||
import { exactCheck, findDifferencesRecursive } from './exact_check';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
|
||||
describe('exact_check', () => {
|
||||
beforeAll(() => {
|
||||
setFeatureFlagsForTestsOnly();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unSetFeatureFlagsForTestsOnly();
|
||||
});
|
||||
|
||||
test('it returns an error if given extra object properties', () => {
|
||||
const someType = t.exact(
|
||||
t.type({
|
||||
|
@ -36,14 +27,22 @@ describe('exact_check', () => {
|
|||
});
|
||||
|
||||
test('it returns an error if the data type is not as expected', () => {
|
||||
type UnsafeCastForTest = Either<
|
||||
t.Errors,
|
||||
{
|
||||
a: number;
|
||||
}
|
||||
>;
|
||||
|
||||
const someType = t.exact(
|
||||
t.type({
|
||||
a: t.string,
|
||||
})
|
||||
);
|
||||
|
||||
const payload = { a: 1 };
|
||||
const decoded = someType.decode(payload);
|
||||
const checked = exactCheck(payload, decoded);
|
||||
const checked = exactCheck(payload, decoded as UnsafeCastForTest);
|
||||
const message = pipe(checked, foldLeftRight);
|
||||
expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']);
|
||||
expect(message.schema).toEqual({});
|
|
@ -25,10 +25,7 @@ import { isObject, get } from 'lodash/fp';
|
|||
* @param decoded The decoded either which has either an existing error or the
|
||||
* decoded object which could have additional keys stripped from it.
|
||||
*/
|
||||
export const exactCheck = <T>(
|
||||
original: object,
|
||||
decoded: Either<t.Errors, T>
|
||||
): Either<t.Errors, T> => {
|
||||
export const exactCheck = <T>(original: T, decoded: Either<t.Errors, T>): Either<t.Errors, T> => {
|
||||
const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors);
|
||||
const onRight = (decodedValue: T): Either<t.Errors, T> => {
|
||||
const differences = findDifferencesRecursive(original, decodedValue);
|
||||
|
@ -47,7 +44,7 @@ export const exactCheck = <T>(
|
|||
return pipe(decoded, fold(onLeft, onRight));
|
||||
};
|
||||
|
||||
export const findDifferencesRecursive = <T>(original: object, decodedValue: T): string[] => {
|
||||
export const findDifferencesRecursive = <T>(original: T, decodedValue: T): string[] => {
|
||||
if (decodedValue == null) {
|
||||
try {
|
||||
// It is null and painful when the original contains an object or an array
|
||||
|
@ -56,29 +53,33 @@ export const findDifferencesRecursive = <T>(original: object, decodedValue: T):
|
|||
} catch (err) {
|
||||
return ['circular reference'];
|
||||
}
|
||||
} else if (typeof original !== 'object' || original == null) {
|
||||
// We are not an object or null so do not report differences
|
||||
return [];
|
||||
} else {
|
||||
const decodedKeys = Object.keys(decodedValue);
|
||||
const differences = Object.keys(original).flatMap(originalKey => {
|
||||
const foundKey = decodedKeys.some(key => key === originalKey);
|
||||
const topLevelKey = foundKey ? [] : [originalKey];
|
||||
// I use lodash to cheat and get an any (not going to lie ;-))
|
||||
const valueObjectOrArrayOriginal = get(originalKey, original);
|
||||
const valueObjectOrArrayDecoded = get(originalKey, decodedValue);
|
||||
if (isObject(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded),
|
||||
];
|
||||
} else if (Array.isArray(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...valueObjectOrArrayOriginal.flatMap((arrayElement, index) =>
|
||||
findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded))
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return topLevelKey;
|
||||
}
|
||||
});
|
||||
return differences;
|
||||
}
|
||||
const decodedKeys = Object.keys(decodedValue);
|
||||
const differences = Object.keys(original).flatMap(originalKey => {
|
||||
const foundKey = decodedKeys.some(key => key === originalKey);
|
||||
const topLevelKey = foundKey ? [] : [originalKey];
|
||||
// I use lodash to cheat and get an any (not going to lie ;-))
|
||||
const valueObjectOrArrayOriginal = get(originalKey, original);
|
||||
const valueObjectOrArrayDecoded = get(originalKey, decodedValue);
|
||||
if (isObject(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded),
|
||||
];
|
||||
} else if (Array.isArray(valueObjectOrArrayOriginal)) {
|
||||
return [
|
||||
...topLevelKey,
|
||||
...valueObjectOrArrayOriginal.flatMap((arrayElement, index) =>
|
||||
findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded))
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return topLevelKey;
|
||||
}
|
||||
});
|
||||
return differences;
|
||||
};
|
|
@ -5,18 +5,9 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { formatErrors } from './utils';
|
||||
import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags';
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
describe('utils', () => {
|
||||
beforeAll(() => {
|
||||
setFeatureFlagsForTestsOnly();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unSetFeatureFlagsForTestsOnly();
|
||||
});
|
||||
|
||||
test('returns an empty error message string if there are no errors', () => {
|
||||
const errors: t.Errors = [];
|
||||
const output = formatErrors(errors);
|
|
@ -9,9 +9,33 @@ import * as rt from 'io-ts';
|
|||
import { RouteValidationResultFactory } from '../../../../../../src/core/server/http';
|
||||
|
||||
describe('buildRouteValidation', () => {
|
||||
const schema = rt.type({
|
||||
ids: rt.array(rt.string),
|
||||
});
|
||||
const schema = rt.exact(
|
||||
rt.type({
|
||||
ids: rt.array(rt.string),
|
||||
})
|
||||
);
|
||||
type Schema = rt.TypeOf<typeof schema>;
|
||||
|
||||
/**
|
||||
* If your schema is using exact all the way down then the validation will
|
||||
* catch any additional keys that should not be present within the validation
|
||||
* when the route_validation uses the exact check.
|
||||
*/
|
||||
const deepSchema = rt.exact(
|
||||
rt.type({
|
||||
topLevel: rt.exact(
|
||||
rt.type({
|
||||
secondLevel: rt.exact(
|
||||
rt.type({
|
||||
thirdLevel: rt.string,
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
type DeepSchema = rt.TypeOf<typeof deepSchema>;
|
||||
|
||||
const validationResult: RouteValidationResultFactory = {
|
||||
ok: jest.fn().mockImplementation(validatedInput => validatedInput),
|
||||
badRequest: jest.fn().mockImplementation(e => e),
|
||||
|
@ -22,18 +46,41 @@ describe('buildRouteValidation', () => {
|
|||
});
|
||||
|
||||
test('return validation error', () => {
|
||||
const input = { id: 'someId' };
|
||||
const input: Omit<Schema, 'ids'> & { id: string } = { id: 'someId' };
|
||||
const result = buildRouteValidation(schema)(input, validationResult);
|
||||
|
||||
expect(result).toEqual(
|
||||
'Invalid value undefined supplied to : { ids: Array<string> }/ids: Array<string>'
|
||||
);
|
||||
expect(result).toEqual('Invalid value "undefined" supplied to "ids"');
|
||||
});
|
||||
|
||||
test('return validated input', () => {
|
||||
const input = { ids: ['someId'] };
|
||||
const input: Schema = { ids: ['someId'] };
|
||||
const result = buildRouteValidation(schema)(input, validationResult);
|
||||
|
||||
expect(result).toEqual(input);
|
||||
});
|
||||
|
||||
test('returns validation error if given extra keys on input for an array', () => {
|
||||
const input: Schema & { somethingExtra: string } = {
|
||||
ids: ['someId'],
|
||||
somethingExtra: 'hello',
|
||||
};
|
||||
const result = buildRouteValidation(schema)(input, validationResult);
|
||||
expect(result).toEqual('invalid keys "somethingExtra"');
|
||||
});
|
||||
|
||||
test('return validation input for a deep 3rd level object', () => {
|
||||
const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } };
|
||||
const result = buildRouteValidation(deepSchema)(input, validationResult);
|
||||
expect(result).toEqual(input);
|
||||
});
|
||||
|
||||
test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => {
|
||||
const input: DeepSchema & {
|
||||
topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } };
|
||||
} = {
|
||||
topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } },
|
||||
};
|
||||
const result = buildRouteValidation(deepSchema)(input, validationResult);
|
||||
expect(result).toEqual('invalid keys "somethingElse"');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import * as rt from 'io-ts';
|
||||
import { failure } from 'io-ts/lib/PathReporter';
|
||||
import {
|
||||
RouteValidationFunction,
|
||||
RouteValidationResultFactory,
|
||||
RouteValidationError,
|
||||
} from '../../../../../../src/core/server';
|
||||
import { exactCheck } from './exact_check';
|
||||
import { formatErrors } from './format_errors';
|
||||
|
||||
type RequestValidationResult<T> =
|
||||
| {
|
||||
|
@ -32,8 +33,9 @@ export const buildRouteValidation = <T extends rt.Mixed, A = rt.TypeOf<T>>(
|
|||
) =>
|
||||
pipe(
|
||||
schema.decode(inputValue),
|
||||
decoded => exactCheck(inputValue, decoded),
|
||||
fold<rt.Errors, A, RequestValidationResult<A>>(
|
||||
(errors: rt.Errors) => validationResult.badRequest(failure(errors).join('\n')),
|
||||
(errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()),
|
||||
(validatedInput: A) => validationResult.ok(validatedInput)
|
||||
)
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue