Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: schema generation can fail #875

Merged
merged 3 commits into from
Dec 2, 2019
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

# Unreleased

## Fixed

Correctly handle the possibility of a body/headers generation failure [#875](https://github.com/stoplightio/prism/pull/875)

# 3.2.1 (2019-11-21)

## Fixed
Expand Down
24 changes: 12 additions & 12 deletions packages/core/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ export function factory<Resource, Input, Output, Config extends IPrismConfig>(
const produceOutput = isProxyConfig(config)
? components.forward(input, config.upstream.href)(components.logger.child({ name: 'PROXY' }))
: TaskEither.fromEither(
components.mock({
resource,
input: {
validations,
data: input,
},
config: config.mock,
})(components.logger.child({ name: 'NEGOTIATOR' }))
);
components.mock({
resource,
input: {
validations,
data: input,
},
config: config.mock,
})(components.logger.child({ name: 'NEGOTIATOR' }))
);

return pipe(
produceOutput,
Expand Down Expand Up @@ -102,9 +102,9 @@ export function factory<Resource, Input, Output, Config extends IPrismConfig>(
TaskEither.map(({ output, resource, validations: inputValidations }) => {
const outputValidations = config.validateResponse
? pipe(
Either.swap(components.validateOutput({ resource, element: output })),
Either.getOrElse<Output, IPrismDiagnostic[]>(() => [])
)
Either.swap(components.validateOutput({ resource, element: output })),
Either.getOrElse<Output, IPrismDiagnostic[]>(() => [])
)
: [];

return {
Expand Down
33 changes: 19 additions & 14 deletions packages/http/src/mocker/__tests__/HttpMocker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger, IPrismInput } from '@stoplight/prism-core';
import { IHttpOperation, INodeExample, DiagnosticSeverity } from '@stoplight/types';
import { right } from 'fp-ts/lib/ReaderEither';
import * as Either from 'fp-ts/lib/Either';
import { flatMap } from 'lodash';
import mock from '../../mocker';
import * as JSONSchemaGenerator from '../../mocker/generator/JSONSchema';
Expand Down Expand Up @@ -166,7 +167,7 @@ describe('mocker', () => {
input: mockInput,
})(logger);

assertRight(response, result => {
assertRight(response, () => {
expect(runCallback).toHaveBeenCalledTimes(2);
expect(runCallback).toHaveBeenNthCalledWith(
1,
Expand All @@ -193,11 +194,11 @@ describe('mocker', () => {
properties: {
param1: { type: 'string' },
param2: { type: 'string' },
}
}
}
]
}
},
},
},
],
},
},
callbacks: [
{
Expand Down Expand Up @@ -236,12 +237,16 @@ describe('mocker', () => {
})(logger);

assertRight(response, () => {
expect(runCallback).toHaveBeenCalledWith(expect.objectContaining({ request: expect.objectContaining({
body: {
param1: 'test1',
param2: 'test2',
}
}) }));
expect(runCallback).toHaveBeenCalledWith(
expect.objectContaining({
request: expect.objectContaining({
body: {
param1: 'test1',
param2: 'test2',
},
}),
})
);
});
});
});
Expand Down Expand Up @@ -291,7 +296,7 @@ describe('mocker', () => {
})
);

jest.spyOn(JSONSchemaGenerator, 'generate').mockReturnValue('example value chelsea');
jest.spyOn(JSONSchemaGenerator, 'generate').mockReturnValue(Either.right('example value chelsea'));

const mockResult = mock({
config: { dynamic: true },
Expand All @@ -309,7 +314,7 @@ describe('mocker', () => {
const generatedExample = { hello: 'world' };

beforeAll(() => {
jest.spyOn(JSONSchemaGenerator, 'generate').mockReturnValue(generatedExample);
jest.spyOn(JSONSchemaGenerator, 'generate').mockReturnValue(Either.right(generatedExample));
jest.spyOn(JSONSchemaGenerator, 'generateStatic');
});

Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/mocker/generator/HttpParamGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function generate(param: IHttpParam | IHttpContent): Option.Option<unknow
pipe(
Option.fromNullable(param.schema),
Option.map(improveSchema),
Option.map(generateDynamicExample)
Option.chain(schema => Option.fromEither(generateDynamicExample(schema)))
)
)
);
Expand Down
9 changes: 5 additions & 4 deletions packages/http/src/mocker/generator/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { JSONSchema } from '../../types';
import * as jsf from 'json-schema-faker';
// @ts-ignore
import * as sampler from 'openapi-sampler';
import { Either, tryCatch, toError } from 'fp-ts/lib/Either';

jsf.extend('faker', () => faker);

Expand All @@ -20,10 +21,10 @@ jsf.option({
maxLength: 100,
});

export function generate(source: JSONSchema): unknown {
return jsf.generate(cloneDeep(source));
export function generate(source: JSONSchema): Either<Error, unknown> {
return tryCatch(() => jsf.generate(cloneDeep(source)), toError);
}

export function generateStatic(source: JSONSchema): unknown {
return sampler.sample(source);
export function generateStatic(source: JSONSchema): Either<Error, unknown> {
return tryCatch(() => sampler.sample(source), toError);
}
52 changes: 28 additions & 24 deletions packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { get } from 'lodash';
import { JSONSchema } from '../../../types';
import { generate } from '../JSONSchema';
import { assertRight } from '@stoplight/prism-core/src/__tests__/utils';

describe('JSONSchema generator', () => {
const ipRegExp = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
const emailRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const uuidRegExp = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/;

describe('generate()', () => {
Expand All @@ -18,12 +19,13 @@ describe('JSONSchema generator', () => {
};

it('will have a string property not matching anything in particular', () => {
const instance = generate(schema);
expect(instance).toHaveProperty('name');
const name = get(instance, 'name');
assertRight(generate(schema), instance => {
expect(instance).toHaveProperty('name');
const name = get(instance, 'name');

expect(ipRegExp.test(name)).toBeFalsy();
expect(emailRegExp.test(name)).toBeFalsy();
expect(ipRegExp.test(name)).toBeFalsy();
expect(emailRegExp.test(name)).toBeFalsy();
});
});
});

Expand All @@ -37,12 +39,13 @@ describe('JSONSchema generator', () => {
};

it('will have a string property matching the email regex', () => {
const instance = generate(schema);
expect(instance).toHaveProperty('email');
const email = get(instance, 'email');
assertRight(generate(schema), instance => {
expect(instance).toHaveProperty('email');
const email = get(instance, 'email');

expect(ipRegExp.test(email)).toBeFalsy();
expect(emailRegExp.test(email)).toBeTruthy();
expect(ipRegExp.test(email)).toBeFalsy();
expect(emailRegExp.test(email)).toBeTruthy();
});
});
});

Expand All @@ -56,17 +59,17 @@ describe('JSONSchema generator', () => {
};

it('will have a string property matching uuid regex', () => {
const instance = generate(schema);
const id = get(instance, 'id');

expect(uuidRegExp.test(id)).toBeTruthy();
assertRight(generate(schema), instance => {
const id = get(instance, 'id');
expect(uuidRegExp.test(id)).toBeTruthy();
});
});

it('will not be presented in the form of UUID as a URN', () => {
const instance = generate(schema);
const id = get(instance, 'id');

expect(uuidRegExp.test(id)).not.toContainEqual('urn:uuid');
assertRight(generate(schema), instance => {
const id = get(instance, 'id');
expect(uuidRegExp.test(id)).not.toContainEqual('urn:uuid');
});
});
});

Expand All @@ -80,12 +83,13 @@ describe('JSONSchema generator', () => {
};

it('will have a string property matching the ip regex', () => {
const instance = generate(schema);
expect(instance).toHaveProperty('ip');
const ip = get(instance, 'ip');
assertRight(generate(schema), instance => {
Copy link
Contributor

@karol-maciaszek karol-maciaszek Dec 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since generate returns Either we should have a test of a failure. Maybe a circular object will do the job?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ekhm :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahi, I didn't see this. Will add.

expect(instance).toHaveProperty('ip');
const ip = get(instance, 'ip');

expect(ipRegExp.test(ip)).toBeTruthy();
expect(emailRegExp.test(ip)).toBeFalsy();
expect(ipRegExp.test(ip)).toBeTruthy();
expect(emailRegExp.test(ip)).toBeFalsy();
});
});
});

Expand Down
84 changes: 50 additions & 34 deletions packages/http/src/mocker/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { IPrismComponents, IPrismInput } from '@stoplight/prism-core';
import {
DiagnosticSeverity,
Dictionary,
IHttpHeaderParam,
IHttpOperation,
INodeExample,
Expand All @@ -10,12 +9,13 @@ import {

import * as caseless from 'caseless';
import * as Either from 'fp-ts/lib/Either';
import * as Record from 'fp-ts/lib/Record';
import { pipe } from 'fp-ts/lib/pipeable';
import * as Reader from 'fp-ts/lib/Reader';
import * as Option from 'fp-ts/lib/Option';
import * as ReaderEither from 'fp-ts/lib/ReaderEither';
import { map } from 'fp-ts/lib/Array';
import { isEmpty, isObject, keyBy, mapValues, groupBy, get } from 'lodash';
import { isNumber, isString, keyBy, mapValues, groupBy, get } from 'lodash';
import { Logger } from 'pino';
import * as typeIs from 'type-is';
import {
Expand All @@ -39,6 +39,10 @@ import {
findContentByMediaTypeOrFirst,
splitUriParams,
} from '../validator/validators/body';
import { sequenceT } from 'fp-ts/lib/Apply';

const eitherRecordSequence = Record.sequence(Either.either);
const eitherSequence = sequenceT(Either.either);

const mock: IPrismComponents<IHttpOperation, IHttpRequest, IHttpResponse, IMockHttpConfig>['mock'] = ({
resource,
Expand Down Expand Up @@ -207,60 +211,72 @@ function assembleResponse(
return logger =>
pipe(
result,
Either.map(negotiationResult => {
const mockedBody = computeBody(negotiationResult, payloadGenerator);
const mockedHeaders = computeMockedHeaders(negotiationResult.headers || [], payloadGenerator);

const response: IHttpResponse = {
statusCode: parseInt(negotiationResult.code),
headers: {
...mockedHeaders,
...(negotiationResult.mediaType && { 'Content-type': negotiationResult.mediaType }),
},
body: mockedBody,
};
Either.chain(negotiationResult =>
pipe(
eitherSequence(
computeBody(negotiationResult, payloadGenerator),
computeMockedHeaders(negotiationResult.headers || [], payloadGenerator)
),
Either.map(([mockedBody, mockedHeaders]) => {
const response: IHttpResponse = {
statusCode: parseInt(negotiationResult.code),
headers: {
...mockedHeaders,
...(negotiationResult.mediaType && { 'Content-type': negotiationResult.mediaType }),
},
body: mockedBody,
};

logger.success(`Responding with the requested status code ${response.statusCode}`);
logger.success(`Responding with the requested status code ${response.statusCode}`);

return response;
})
return response;
})
)
)
);
}

function isINodeExample(nodeExample: ContentExample | undefined): nodeExample is INodeExample {
return !!nodeExample && 'value' in nodeExample;
}

function computeMockedHeaders(headers: IHttpHeaderParam[], payloadGenerator: PayloadGenerator): Dictionary<string> {
return mapValues(
keyBy(headers, h => h.name),
header => {
if (header.schema) {
if (header.examples && header.examples.length > 0) {
const example = header.examples[0];
if (isINodeExample(example)) {
return example.value;
function computeMockedHeaders(headers: IHttpHeaderParam[], payloadGenerator: PayloadGenerator) {
return eitherRecordSequence(
mapValues(
keyBy(headers, h => h.name),
header => {
if (header.schema) {
if (header.examples && header.examples.length > 0) {
const example = header.examples[0];
if (isINodeExample(example)) {
return Either.right(example.value);
}
} else {
return pipe(
payloadGenerator(header.schema),
Either.map(example => {
if (isNumber(example) || isString(example)) return example;
return null;
})
);
}
} else {
const example = payloadGenerator(header.schema);
if (!(isObject(example) && isEmpty(example))) return example;
}
return Either.right(null);
}
return null;
}
)
);
}

function computeBody(
negotiationResult: Pick<IHttpNegotiationResult, 'schema' | 'mediaType' | 'bodyExample'>,
payloadGenerator: PayloadGenerator
) {
): Either.Either<Error, unknown> {
if (isINodeExample(negotiationResult.bodyExample) && negotiationResult.bodyExample.value !== undefined) {
return negotiationResult.bodyExample.value;
return Either.right(negotiationResult.bodyExample.value);
} else if (negotiationResult.schema) {
return payloadGenerator(negotiationResult.schema);
}
return undefined;
return Either.right(undefined);
}

export default mock;
Loading