Skip to content

Commit

Permalink
add support for ExactType schema in buildRouteValidationwithExcess an…
Browse files Browse the repository at this point in the history
…d add unit tests
  • Loading branch information
neptunian committed Jan 26, 2024
1 parent 02e6017 commit 64802ab
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@ export const sizeRT = rt.union([
]);
export const assetDateRT = rt.union([dateRt, datemathStringRt]);

export const servicesFiltersRT = rt.exact(
rt.type({
['host.name']: rt.string,
})
);
export const servicesFiltersRT = rt.strict({
['host.name']: rt.string,
});

export type ServicesFilter = rt.TypeOf<typeof servicesFiltersRT>;

Expand Down
133 changes: 133 additions & 0 deletions x-pack/plugins/infra/server/utils/route_validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import type { RouteValidationResultFactory } from '@kbn/core/server';

import { buildRouteValidationWithExcess } from './route_validation';

describe('buildRouteValidationwithExcess', () => {
const schema = rt.type({
ids: rt.array(rt.string),
});
type Schema = rt.TypeOf<typeof schema>;

const deepSchema = rt.type({
topLevel: rt.type({
secondLevel: rt.type({
thirdLevel: rt.string,
}),
}),
});
type DeepSchema = rt.TypeOf<typeof deepSchema>;

// t.strict({ name: A }) is an alias of t.exact(t.type({ name: A })))
const strictSchema = rt.strict({
requiredField: rt.string,
});
type StrictSchema = rt.TypeOf<typeof strictSchema>;
const validationResult: RouteValidationResultFactory = {
ok: jest.fn().mockImplementation((validatedInput) => validatedInput),
badRequest: jest.fn().mockImplementation((e) => e),
};

beforeEach(() => {
jest.clearAllMocks();
});
beforeEach(() => {
jest.clearAllMocks();
});

test('return validation error', () => {
const input: Omit<Schema, 'ids'> & { id: string } = { id: 'someId' };
const result = buildRouteValidationWithExcess(schema)(input, validationResult);

expect(result).toEqual('Invalid value {"id":"someId"}, excess properties: ["id"]');
});

test('return validation error with intersection', () => {
const schemaI = rt.intersection([
rt.type({
ids: rt.array(rt.string),
}),
rt.partial({
valid: rt.array(rt.string),
}),
]);
type SchemaI = rt.TypeOf<typeof schemaI>;
const input: Omit<SchemaI, 'ids'> & { id: string } = { id: 'someId', valid: ['yes'] };
const result = buildRouteValidationWithExcess(schemaI)(input, validationResult);

expect(result).toEqual(
'Invalid value {"id":"someId","valid":["yes"]}, excess properties: ["id"]'
);
});

test('return NO validation error with a partial intersection', () => {
const schemaI = rt.intersection([
rt.type({
id: rt.array(rt.string),
}),
rt.partial({
valid: rt.array(rt.string),
}),
]);
const input = { id: ['someId'] };
const result = buildRouteValidationWithExcess(schemaI)(input, validationResult);

expect(result).toEqual({ id: ['someId'] });
});

test('return validated input', () => {
const input: Schema = { ids: ['someId'] };
const result = buildRouteValidationWithExcess(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 = buildRouteValidationWithExcess(schema)(input, validationResult);
expect(result).toEqual(
'Invalid value {"ids":["someId"],"somethingExtra":"hello"}, excess properties: ["somethingExtra"]'
);
});

test('return validation input for a deep 3rd level object', () => {
const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } };
const result = buildRouteValidationWithExcess(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 = buildRouteValidationWithExcess(deepSchema)(input, validationResult);
expect(result).toEqual(
'Invalid value {"topLevel":{"secondLevel":{"thirdLevel":"hello","somethingElse":"extraKey"}}}, excess properties: ["somethingElse"]'
);
});
test('return validation error for excess properties with ExactType', () => {
// Create an input object with a required field and an excess property
const input: StrictSchema & { excessProp: string } = {
requiredField: 'value',
excessProp: 'extra',
};
const result = buildRouteValidationWithExcess(strictSchema)(input, validationResult);

// Expect a validation error indicating the presence of an excess property
expect(result).toEqual(
'Invalid value {"requiredField":"value","excessProp":"extra"}, excess properties: ["excessProp"]'
);
});
});
20 changes: 12 additions & 8 deletions x-pack/plugins/infra/server/utils/route_validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ type RequestValidationResult<T> =

export const buildRouteValidationWithExcess =
<
T extends rt.InterfaceType<rt.Props> | GenericIntersectionC | rt.PartialType<rt.Props>,
T extends
| rt.InterfaceType<rt.Props>
| GenericIntersectionC
| rt.PartialType<rt.Props>
| rt.ExactC<any>,
A = rt.TypeOf<T>
>(
schema: T
Expand All @@ -48,7 +52,11 @@ export type GenericIntersectionC =
| rt.IntersectionC<[any, any, any, any, any]>;

export const excess = <
C extends rt.InterfaceType<rt.Props> | GenericIntersectionC | rt.PartialType<rt.Props>
C extends
| rt.InterfaceType<rt.Props>
| GenericIntersectionC
| rt.PartialType<rt.Props>
| rt.ExactC<any>
>(
codec: C
): C => {
Expand Down Expand Up @@ -129,13 +137,9 @@ const getProps = (
case 'RefinementType':
case 'ReadonlyType':
return getProps(codec.type);
case 'InterfaceType':
case 'ExactType':
// Handle ExactType by extracting props from the underlying InterfaceType
if (codec._tag === 'ExactType') {
return getProps(codec.type);
}
case 'StrictType':
return getProps(codec.type);
case 'InterfaceType':
case 'PartialType':
return codec.props;
case 'IntersectionType': {
Expand Down

0 comments on commit 64802ab

Please sign in to comment.