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

refactor: Payload Generator improvements SO-232 #322

Merged
merged 14 commits into from
May 27, 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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
- run:
name: Publish
command: yarn lerna publish from-git --github-release --yes --dist-tag alpha
command: yarn lerna publish from-git --github-release --yes
- run:
name: Create CLI binaries
command: npx pkg --out-path ./cli-binaries ./packages/cli/
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Yarn is a package manager for your code, similar to npm. While you can use npm t
2. Fork the [https://github.com/stoplightio/prism](https://github.com/stoplightio/prism) repo.
3. Git clone your fork (i.e. `git clone https://github.com/<your-username>/prism.git`) to your machine.
4. Run `yarn` to install dependencies and setup the project.
5. Because [oclif](https://oclif.io) does not respect the provided `tsconfig`, you can't use the bin directly, but we provide a script for that: `cd packages && yarn cli mock openapi.yaml`
5. Because [oclif](https://oclif.io) does not respect the provided `tsconfig`, you can't use the bin directly, but we provide a script for that: `cd packages/cli && yarn cli mock openapi.yaml`.
6. Run `git checkout -b [name_of_your_new_branch]` to create a new branch for your work. To help build nicer changelogs, we have a convention for branch names. Please start your branch with either `feature/{branch-name}`, `chore/{branch-name}`, or `fix/{branch-name}`. For example, if I was adding a CLI, I would make my branch name: `feature/add-cli`.
7. Make changes, write code and tests, etc. The fun stuff!
8. Run `yarn test` to test your changes.
Expand Down Expand Up @@ -72,7 +72,7 @@ The best way to debug a Prism behavior is probably to attach your debugger to th
```bash
cd packages/cli

node --inspect-brk -r tsconfig-paths/register bin/run mock file.oas.yml
yarn cli:debug mock file.oas.yml
```

The application will wait for a debugger to be attached and break on the first line; from there, you can put your breakpoint here and there and help us debug the software!
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export default class Server extends Command {
public async run() {
const signaleInteractiveInstance = new signale.Signale({ interactive: true });

signaleInteractiveInstance.await('Starting Prism…');

const {
flags: { port, dynamic },
args: { spec },
} = this.parse(Server);

signaleInteractiveInstance.await('Starting Prism…');

if (true || dynamic) {
signale.star('Dynamic example generation enabled.');
}
Expand Down
6 changes: 3 additions & 3 deletions packages/http-server/src/__tests__/server.oas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ describe.each([['petstore.oas2.json'], ['petstore.oas3.json']])('server %s', fil
// accorging to the schema

const expectedValues = {
'x-rate-limit': file === 'petstore.oas3.json' ? 1000 : expect.any(String),
'x-stats': file === 'petstore.oas3.json' ? 1500 : expect.any(String),
'x-rate-limit': file === 'petstore.oas3.json' ? 1000 : expect.any(Number),
'x-stats': file === 'petstore.oas3.json' ? 1500 : expect.any(Number),
'x-expires-after': expect.any(String),
'x-strange-header': file === 'petstore.oas3.json' ? 'string' : '{}',
'x-strange-header': '',
};

for (const headerName of Object.keys(expectedValues)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/http-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ const replyHandler = <LoaderInput>(
reply.headers(output.headers);
}

reply.serializer((payload: unknown) => payload).send(output.body);
XVincentX marked this conversation as resolved.
Show resolved Hide resolved
reply.send(output.body);
} else {
reply.code(500).send('Unable to find any decent response for the current request.');
throw new Error('Unable to find any decent response for the current request.');
}
} catch (e) {
const status = 'status' in e ? e.status : 500;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Object {
},
},
"output": Object {
"body": Anything,
"headers": Object {
"Content-type": "application/json",
},
Expand Down
20 changes: 8 additions & 12 deletions packages/http/src/__tests__/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ export const httpOperations: IHttpOperation[] = [
examples: [
{
key: 'application/json',
value: [
{
id: 1,
completed: true,
name: 'make prism',
},
],
value: {
id: 1,
completed: true,
name: 'make prism',
},
},
{
key: 'bear',
Expand Down Expand Up @@ -143,11 +141,9 @@ export const httpOperations: IHttpOperation[] = [
examples: [
{
key: 'application/json',
value: [
{
message: 'error',
},
],
value: {
message: 'error',
},
},
],
encodings: [],
Expand Down
20 changes: 13 additions & 7 deletions packages/http/src/__tests__/http-prism-instance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { IPrism } from '@stoplight/prism-core';
import { IHttpOperation } from '@stoplight/types/http-spec';
import { omit } from 'lodash';
import { relative, resolve } from 'path';
import { createInstance, IHttpConfig, IHttpRequest, IHttpResponse, ProblemJsonError } from '../';
import { forwarder } from '../forwarder';
Expand Down Expand Up @@ -47,15 +46,22 @@ describe('Http Prism Instance function tests', () => {
},
},
});
const parsedBody = JSON.parse(response!.output!.body);

const parsedBody = response!.output!.body;

expect(parsedBody.length).toBeGreaterThan(0);

parsedBody.forEach((element: any) => {
expect(typeof element.name).toEqual('string');
expect(Array.isArray(element.photoUrls)).toBeTruthy();
expect(typeof element.name).toBe('string');
expect(element.photoUrls).toBeInstanceOf(Array);
expect(element.photoUrls.length).toBeGreaterThan(0);
});
// because body is generated randomly
expect(omit(response, 'output.body')).toMatchSnapshot();

expect(response).toMatchSnapshot({
output: {
body: expect.anything(),
},
});
});

test('given route with invalid param should return a validation error', () => {
Expand Down Expand Up @@ -155,6 +161,6 @@ describe('Http Prism Instance function tests', () => {
});

expect(response.output).toBeDefined();
expect(typeof response.output!.body).toBe('string');
expect(response.output!.body).toBeInstanceOf(Array);
});
});
5 changes: 2 additions & 3 deletions packages/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { factory, FilesystemLoader, PartialPrismConfig } from '@stoplight/prism-core';
import { IHttpOperation } from '@stoplight/types';

import { forwarder } from './forwarder';
import { HttpMocker } from './mocker';
import { JSONSchemaExampleGenerator } from './mocker/generator/JSONSchemaExampleGenerator';
import { generate } from './mocker/generator/JSONSchema';
import { router } from './router';
import {
IHttpConfig,
Expand Down Expand Up @@ -31,7 +30,7 @@ const createInstance = <LoaderInput>(
router,
forwarder,
validator,
mocker: new HttpMocker(new JSONSchemaExampleGenerator()),
mocker: new HttpMocker(generate),
},
)(config, overrides);
};
Expand Down
29 changes: 17 additions & 12 deletions packages/http/src/mocker/HttpMocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { IMocker, IMockerOpts } from '@stoplight/prism-core';
import { Dictionary, IHttpHeaderParam, IHttpOperation, INodeExample, INodeExternalExample } from '@stoplight/types';

import * as caseless from 'caseless';
import { fromPairs, keyBy, mapValues, toPairs } from 'lodash';
import { IHttpConfig, IHttpOperationConfig, IHttpRequest, IHttpResponse, ProblemJsonError } from '../types';
import { fromPairs, isEmpty, isObject, keyBy, mapValues, toPairs } from 'lodash';
import {
IHttpConfig,
IHttpOperationConfig,
IHttpRequest,
IHttpResponse,
PayloadGenerator,
ProblemJsonError,
} from '../types';
import { UNPROCESSABLE_ENTITY } from './errors';
import { IExampleGenerator } from './generator/IExampleGenerator';
import helpers from './negotiator/NegotiatorHelpers';
import { IHttpNegotiationResult } from './negotiator/types';

export class HttpMocker implements IMocker<IHttpOperation, IHttpRequest, IHttpConfig, IHttpResponse> {
constructor(private _exampleGenerator: IExampleGenerator) {}
constructor(private _exampleGenerator: PayloadGenerator) {}

public async mock({
resource,
Expand Down Expand Up @@ -71,7 +77,7 @@ function isINodeExample(nodeExample: INodeExample | INodeExternalExample | undef
return !!nodeExample && 'value' in nodeExample;
}

function computeMockedHeaders(headers: IHttpHeaderParam[], ex: IExampleGenerator): Promise<Dictionary<string>> {
function computeMockedHeaders(headers: IHttpHeaderParam[], ex: PayloadGenerator): Promise<Dictionary<string>> {
const headerWithPromiseValues = mapValues(keyBy(headers, h => h.name), async header => {
if (header.content) {
if (header.content.examples.length > 0) {
Expand All @@ -81,25 +87,24 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: IExampleGenerator
}
}
if (header.content.schema) {
return ex.generate(header.content.schema, 'application/json');
const example = await ex(header.content.schema);
if (!(isObject(example) && isEmpty(example))) return example;
}
}
return 'string';
return '';
});

return resolvePromiseInProps(headerWithPromiseValues);
}

async function computeBody(
negotiationResult: Pick<IHttpNegotiationResult, 'schema' | 'mediaType' | 'bodyExample'>,
ex: IExampleGenerator,
ex: PayloadGenerator,
) {
if (isINodeExample(negotiationResult.bodyExample) && negotiationResult.bodyExample.value !== undefined) {
return typeof negotiationResult.bodyExample.value === 'string'
? negotiationResult.bodyExample.value
: JSON.stringify(negotiationResult.bodyExample.value);
return negotiationResult.bodyExample.value;
} else if (negotiationResult.schema) {
return ex.generate(negotiationResult.schema, negotiationResult.mediaType);
return ex(negotiationResult.schema);
}
return undefined;
}
Expand Down
37 changes: 21 additions & 16 deletions packages/http/src/mocker/__tests__/HttpMocker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { IHttpOperation, INodeExample } from '@stoplight/types';
import { flatMap } from 'lodash';
import { HttpMocker } from '../../mocker';
import { JSONSchemaExampleGenerator } from '../../mocker/generator/JSONSchemaExampleGenerator';
import * as JSONSchemaGenerator from '../../mocker/generator/JSONSchema';
import helpers from '../negotiator/NegotiatorHelpers';

describe('HttpMocker', () => {
const mockExampleGenerator = new JSONSchemaExampleGenerator();
const httpMocker = new HttpMocker(mockExampleGenerator);
const httpMocker = new HttpMocker(JSONSchemaGenerator.generate);

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

Expand Down Expand Up @@ -120,22 +119,23 @@ describe('HttpMocker', () => {
).resolves.toMatchSnapshot();
});

it('returns dynamic example', () => {
it('returns dynamic example', async () => {
jest.spyOn(helpers, 'negotiateOptionsForValidRequest').mockReturnValue({
code: '202',
mediaType: 'test',
schema: mockResource.responses![0].contents![0].schema,
headers: [],
});

jest.spyOn(mockExampleGenerator, 'generate').mockResolvedValue('example value');
const response = await httpMocker.mock({
resource: mockResource,
input: mockInput,
});

return expect(
httpMocker.mock({
resource: mockResource,
input: mockInput,
}),
).resolves.toMatchSnapshot();
return expect(response.body).toMatchObject({
name: expect.any(String),
surname: expect.any(String),
});
});
});

Expand Down Expand Up @@ -167,7 +167,7 @@ describe('HttpMocker', () => {
schema: { type: 'string' },
});

jest.spyOn(mockExampleGenerator, 'generate').mockResolvedValue('example value chelsea');
jest.spyOn(JSONSchemaGenerator, 'generate').mockResolvedValue('example value chelsea');

return expect(
httpMocker.mock({
Expand All @@ -181,10 +181,12 @@ describe('HttpMocker', () => {
describe('when an example is defined', () => {
describe('and dynamic flag is true', () => {
describe('should generate a dynamic response', () => {
const generatedExample = JSON.stringify({ hello: 'world' });
const generatedExample = { hello: 'world' };

beforeAll(() => {
jest.spyOn(mockExampleGenerator, 'generate').mockResolvedValue(generatedExample);
jest.spyOn(JSONSchemaGenerator, 'generate').mockResolvedValue(generatedExample);
});

afterAll(() => {
jest.restoreAllMocks();
});
Expand All @@ -196,7 +198,7 @@ describe('HttpMocker', () => {
config: { mock: { dynamic: true } },
});

expect(mockExampleGenerator.generate).toHaveBeenCalled();
expect(JSONSchemaGenerator.generate).not.toHaveBeenCalled();
expect(response.body).toBeDefined();

const allExamples = flatMap(mockResource.responses, res =>
Expand All @@ -206,7 +208,10 @@ describe('HttpMocker', () => {
});

allExamples.forEach(example => expect(response.body).not.toEqual(example));
expect(response.body).toBe(generatedExample);
expect(response.body).toMatchObject({
name: expect.any(String),
surname: expect.any(String),
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ Object {
}
`;

exports[`HttpMocker mock() with valid negotiator response returns dynamic example 1`] = `
Object {
"body": "example value",
"headers": Object {
"Content-type": "test",
},
"statusCode": 202,
}
`;

exports[`HttpMocker mock() with valid negotiator response returns static example 1`] = `
Object {
"body": "hello",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`http mocker request is invalid returns 422 and static error response 1`] = `
Object {
"body": "[{\\"message\\":\\"error\\"}]",
"headers": Object {
"Content-type": "application/json",
},
"statusCode": 422,
}
`;

exports[`http mocker request is valid HttpOperation contains example return lowest 2xx code and match response example to media type accepted by request 1`] = `
Object {
"body": "[{\\"id\\":1,\\"completed\\":true,\\"name\\":\\"make prism\\"}]",
"headers": Object {
"Content-type": "application/json",
},
"statusCode": 200,
}
`;

exports[`http mocker request is valid HttpOperation contains example return lowest 2xx response and the first example matching the media type 1`] = `
Object {
"body": "<todo><name>Shopping</name><completed>false</completed></todo>",
Expand All @@ -43,7 +23,13 @@ Object {

exports[`http mocker request is valid given enforced example key should return application/json, 200 response 1`] = `
Object {
"body": "[{\\"id\\":2,\\"completed\\":false,\\"name\\":\\"make bears\\"}]",
"body": Array [
Object {
"completed": false,
"id": 2,
"name": "make bears",
},
],
"headers": Object {
"Content-type": "application/json",
},
Expand Down
Loading