Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:

- run: yarn test

- run: yarn workspaces foreach --no-private npm publish --provenance --access public
- run: npx lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
136 changes: 136 additions & 0 deletions packages/http/src/validator/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,142 @@ describe('HttpValidator', () => {
});
});

describe('falsey primitive body values', () => {
afterAll(() => {
// Re-setup the mocks that were defined in the outer beforeAll
jest.spyOn(validators, 'validateQuery').mockReturnValue(E.left([mockError]));
jest.spyOn(validators, 'validateBody').mockReturnValue(E.left([mockError]));
jest.spyOn(validators, 'validateHeaders').mockReturnValue(E.left([mockError]));
jest.spyOn(validators, 'validatePath').mockReturnValue(E.left([mockError]));
});

beforeEach(() => jest.restoreAllMocks());

it('accepts 0 as a valid integer body', () => {
const result = validator.validateInput({
resource: {
method: 'post',
path: '/',
id: '1',
request: {
body: {
id: faker.random.word(),
required: true,
contents: [
{
id: faker.random.word(),
mediaType: 'application/json',
schema: { type: 'integer' },
},
],
},
},
responses: [{ id: faker.random.word(), code: '200' }],
},
element: {
method: 'post',
url: { path: '/', query: {} },
body: 0,
headers: { 'content-type': 'application/json', 'content-length': '1' },
},
});
assertRight(result);
});

it('accepts false as a valid boolean body', () => {
const result = validator.validateInput({
resource: {
method: 'post',
path: '/',
id: '1',
request: {
body: {
id: faker.random.word(),
required: true,
contents: [
{
id: faker.random.word(),
mediaType: 'application/json',
schema: { type: 'boolean' },
},
],
},
},
responses: [{ id: faker.random.word(), code: '200' }],
},
element: {
method: 'post',
url: { path: '/', query: {} },
body: false,
headers: { 'content-type': 'application/json', 'content-length': '5' },
},
});
assertRight(result);
});

it('accepts empty string as a valid string body', () => {
const result = validator.validateInput({
resource: {
method: 'post',
path: '/',
id: '1',
request: {
body: {
id: faker.random.word(),
required: true,
contents: [
{
id: faker.random.word(),
mediaType: 'application/json',
schema: { type: 'string' },
},
],
},
},
responses: [{ id: faker.random.word(), code: '200' }],
},
element: {
method: 'post',
url: { path: '/', query: {} },
body: '',
headers: { 'content-type': 'application/json', 'content-length': '2' },
},
});
assertRight(result);
});

it('accepts null as a valid null body', () => {
const result = validator.validateInput({
resource: {
method: 'post',
path: '/',
id: '1',
request: {
body: {
id: faker.random.word(),
required: true,
contents: [
{
id: faker.random.word(),
mediaType: 'application/json',
schema: { type: 'null' },
},
],
},
},
responses: [{ id: faker.random.word(), code: '200' }],
},
element: {
method: 'post',
url: { path: '/', query: {} },
body: null,
headers: { 'content-type': 'application/json', 'content-length': '4' },
},
});
assertRight(result);
});
});

describe('headers validation in enabled', () => {
describe('request is not set', () => {
it('does not validate headers', validate(undefined, undefined, 0));
Expand Down
27 changes: 19 additions & 8 deletions packages/http/src/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ import { wildcardMediaTypeMatch } from './utils/wildcardMediaTypeMatch';

export { validateSecurity } from './validators/security';

const checkRequiredBodyIsProvided = (requestBody: O.Option<IHttpOperationRequestBody>, body: unknown) =>
const checkRequiredBodyIsProvided = (
requestBody: O.Option<IHttpOperationRequestBody>,
body: unknown,
bodyIsProvided: boolean
) =>
pipe(
requestBody,
E.fromPredicate<O.Option<IHttpOperationRequestBody>, NonEmptyArray<IPrismDiagnostic>>(
requestBody => O.isNone(requestBody) || !(!!requestBody.value.required && !body),
requestBody => O.isNone(requestBody) || !(!!requestBody.value.required && !bodyIsProvided),
() => [{ code: 'required', message: 'Body parameter is required', severity: DiagnosticSeverity.Error }]
),
E.map(requestBody => [requestBody, O.fromNullable(body)] as const)
E.map(requestBody => [requestBody, bodyIsProvided ? O.some(body) : O.none] as const)
);

const isMediaTypeSupportedInContents = (mediaType?: string, contents?: IMediaTypeContent[]): boolean =>
Expand Down Expand Up @@ -73,17 +77,23 @@ const validateInputBody = (
bundle: unknown,
body: unknown,
headers: IHttpNameValue
) =>
pipe(
checkRequiredBodyIsProvided(requestBody, body),
E.map(b => [...b, caseless(headers || {})] as const),
) => {
const headersCaseless = caseless(headers || {});
const contentLength = parseInt(headersCaseless.get('content-length')) || 0;
// A body is considered "provided" if:
// - body is not undefined, AND
// - body is not null OR content-length > 0 (null with content-length > 0 means JSON null value)
const bodyIsProvided = body !== undefined && (body !== null || contentLength > 0);

return pipe(
checkRequiredBodyIsProvided(requestBody, body, bodyIsProvided),
E.map(b => [...b, headersCaseless] as const),
E.chain(([requestBody, body, headers]) => {
const contentTypeHeader = headers.get('content-type');
const [multipartBoundary, mediaType] = contentTypeHeader
? parseMIMEHeader(contentTypeHeader)
: [undefined, undefined];

const contentLength = parseInt(headers.get('content-length')) || 0;
if (contentLength === 0) {
// generously allow this content type if there isn't a body actually provided
return E.right([requestBody, body, mediaType, multipartBoundary] as const);
Expand Down Expand Up @@ -114,6 +124,7 @@ const validateInputBody = (
validateInputIfBodySpecIsProvided(body, requestBody, mediaType, multipartBoundary, bundle)
)
);
};

export const validateInput: ValidatorFn<IHttpOperation, IHttpRequest> = ({ resource, element }) => {
const { request } = resource;
Expand Down