From 750c11e2d96213e7d03f06086a3c2e95d70c4837 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 12:01:42 -0400 Subject: [PATCH 01/12] feat: make generator simple --- packages/cli/src/commands/mock.ts | 47 ++++++++++++------- .../src/mocker/generator/IExampleGenerator.ts | 4 -- .../generator/JSONSchemaExampleGenerator.ts | 19 ++------ 3 files changed, 32 insertions(+), 38 deletions(-) delete mode 100644 packages/http/src/mocker/generator/IExampleGenerator.ts diff --git a/packages/cli/src/commands/mock.ts b/packages/cli/src/commands/mock.ts index 294132d3a..d198555fc 100644 --- a/packages/cli/src/commands/mock.ts +++ b/packages/cli/src/commands/mock.ts @@ -1,4 +1,5 @@ import { Command } from '@oclif/command'; +import * as cluster from 'cluster'; import * as signale from 'signale'; import { ARGS, FLAGS } from '../const/options'; import { createServer } from '../util/createServer'; @@ -11,33 +12,43 @@ 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); - if (true || dynamic) { - signale.star('Dynamic example generation enabled.'); - } - - const server = createServer(spec, { mock: { dynamic: true || dynamic } }); - try { - const address = await server.listen(port); + if (cluster.isMaster) { + signaleInteractiveInstance.await('Starting Prism…'); - if (server.prism.resources.length === 0) { - signaleInteractiveInstance.fatal('No operations found in the current file.'); - this.exit(1); + if (true || dynamic) { + signale.star('Dynamic example generation enabled.'); } - signaleInteractiveInstance.success(`Prism is listening on ${address}`); - - server.prism.resources.forEach(resource => { - signale.note(`${resource.method.toUpperCase().padEnd(10)} ${address}${resource.path}`); + cluster.setupMaster({ + silent: true, }); - } catch (e) { - signaleInteractiveInstance.fatal(e.message); + + const worker = cluster.fork(); + + if (worker.process.stdout) worker.process.stdout.pipe(process.stdout); + } else { + const server = createServer(spec, { mock: { dynamic: true || dynamic } }); + try { + const address = await server.listen(port); + + if (server.prism.resources.length === 0) { + signaleInteractiveInstance.fatal('No operations found in the current file.'); + this.exit(1); + } + + signaleInteractiveInstance.success(`Prism is listening on ${address}`); + + server.prism.resources.forEach(resource => { + signale.note(`${resource.method.toUpperCase().padEnd(10)} ${address}${resource.path}`); + }); + } catch (e) { + signaleInteractiveInstance.fatal(e.message); + } } } } diff --git a/packages/http/src/mocker/generator/IExampleGenerator.ts b/packages/http/src/mocker/generator/IExampleGenerator.ts deleted file mode 100644 index 073136d43..000000000 --- a/packages/http/src/mocker/generator/IExampleGenerator.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @todo S is Schema interface (not defined yet) -export interface IExampleGenerator { - generate(schema: S, mediaType: string): Promise; -} diff --git a/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts b/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts index c00770001..e11d2d2fd 100644 --- a/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts +++ b/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts @@ -1,7 +1,5 @@ // @ts-ignore import * as jsf from '@stoplight/json-schema-faker'; -import { cloneDeep } from 'lodash'; -import { IExampleGenerator } from './IExampleGenerator'; jsf.option({ failOnInvalidTypes: false, @@ -16,19 +14,8 @@ jsf.option({ maxLength: 100, }); -export class JSONSchemaExampleGenerator implements IExampleGenerator { - public async generate(schema: unknown, mediaType: string): Promise { - const example = await jsf.resolve(cloneDeep(schema)); - return this.transform(mediaType, example); - } - - private transform(mediaType: string, input: object): string { - switch (mediaType) { - case 'application/json': - return JSON.stringify(input); - - default: - throw new Error(`Unknown media type '${mediaType}'`); - } +export class JSONSchemaExampleGenerator { + public async generate(source: unknown): Promise { + return jsf.resolve(source); } } From 8e0fcaf2d0b567492aee22c8941a911c76380810 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 12:59:17 -0400 Subject: [PATCH 02/12] chore: undo stupid cluster stuff --- packages/cli/src/commands/mock.ts | 45 ++++++++++++------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/cli/src/commands/mock.ts b/packages/cli/src/commands/mock.ts index d198555fc..07bc3a897 100644 --- a/packages/cli/src/commands/mock.ts +++ b/packages/cli/src/commands/mock.ts @@ -1,5 +1,4 @@ import { Command } from '@oclif/command'; -import * as cluster from 'cluster'; import * as signale from 'signale'; import { ARGS, FLAGS } from '../const/options'; import { createServer } from '../util/createServer'; @@ -17,38 +16,28 @@ export default class Server extends Command { args: { spec }, } = this.parse(Server); - if (cluster.isMaster) { - signaleInteractiveInstance.await('Starting Prism…'); + signaleInteractiveInstance.await('Starting Prism…'); - if (true || dynamic) { - signale.star('Dynamic example generation enabled.'); - } - - cluster.setupMaster({ - silent: true, - }); - - const worker = cluster.fork(); + if (true || dynamic) { + signale.star('Dynamic example generation enabled.'); + } - if (worker.process.stdout) worker.process.stdout.pipe(process.stdout); - } else { - const server = createServer(spec, { mock: { dynamic: true || dynamic } }); - try { - const address = await server.listen(port); + const server = createServer(spec, { mock: { dynamic: true || dynamic } }); + try { + const address = await server.listen(port); - if (server.prism.resources.length === 0) { - signaleInteractiveInstance.fatal('No operations found in the current file.'); - this.exit(1); - } + if (server.prism.resources.length === 0) { + signaleInteractiveInstance.fatal('No operations found in the current file.'); + this.exit(1); + } - signaleInteractiveInstance.success(`Prism is listening on ${address}`); + signaleInteractiveInstance.success(`Prism is listening on ${address}`); - server.prism.resources.forEach(resource => { - signale.note(`${resource.method.toUpperCase().padEnd(10)} ${address}${resource.path}`); - }); - } catch (e) { - signaleInteractiveInstance.fatal(e.message); - } + server.prism.resources.forEach(resource => { + signale.note(`${resource.method.toUpperCase().padEnd(10)} ${address}${resource.path}`); + }); + } catch (e) { + signaleInteractiveInstance.fatal(e.message); } } } From 0a8117bbd2de5e26270c755aa2b3e53cd3e7b930 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 13:31:14 -0400 Subject: [PATCH 03/12] refactor: lower down generator responsibilities --- .../src/__tests__/server.oas.spec.ts | 6 +-- packages/http-server/src/server.ts | 2 +- .../http-prism-instance.spec.ts.snap | 1 + .../src/__tests__/http-prism-instance.spec.ts | 20 +++++--- packages/http/src/index.ts | 5 +- packages/http/src/mocker/HttpMocker.ts | 17 +++---- .../src/mocker/__tests__/HttpMocker.spec.ts | 37 ++++++++------ .../__snapshots__/HttpMocker.spec.ts.snap | 10 ---- .../__snapshots__/functional.spec.ts.snap | 22 ++++++-- .../src/mocker/__tests__/functional.spec.ts | 10 ++-- ...chemaExampleGenerator.ts => JSONSchema.ts} | 7 ++- .../generator/__tests__/JSONSchema.spec.ts | 34 +++++++++++++ .../JSONSchemaExampleGenerator.spec.ts | 50 ------------------- .../JSONSchemaExampleGenerator.spec.ts.snap | 3 -- 14 files changed, 110 insertions(+), 114 deletions(-) rename packages/http/src/mocker/generator/{JSONSchemaExampleGenerator.ts => JSONSchema.ts} (70%) create mode 100644 packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts delete mode 100644 packages/http/src/mocker/generator/__tests__/JSONSchemaExampleGenerator.spec.ts delete mode 100644 packages/http/src/mocker/generator/__tests__/__snapshots__/JSONSchemaExampleGenerator.spec.ts.snap diff --git a/packages/http-server/src/__tests__/server.oas.spec.ts b/packages/http-server/src/__tests__/server.oas.spec.ts index 0fe60d834..af16650ec 100644 --- a/packages/http-server/src/__tests__/server.oas.spec.ts +++ b/packages/http-server/src/__tests__/server.oas.spec.ts @@ -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': file === 'petstore.oas3.json' ? 'string' : {}, }; for (const headerName of Object.keys(expectedValues)) { diff --git a/packages/http-server/src/server.ts b/packages/http-server/src/server.ts index d83008ceb..db72fc1d4 100644 --- a/packages/http-server/src/server.ts +++ b/packages/http-server/src/server.ts @@ -71,7 +71,7 @@ const replyHandler = ( reply.headers(output.headers); } - reply.serializer((payload: unknown) => payload).send(output.body); + reply.send(output.body); } else { reply.code(500).send('Unable to find any decent response for the current request.'); } diff --git a/packages/http/src/__tests__/__snapshots__/http-prism-instance.spec.ts.snap b/packages/http/src/__tests__/__snapshots__/http-prism-instance.spec.ts.snap index f675e8e5b..31c644d9b 100644 --- a/packages/http/src/__tests__/__snapshots__/http-prism-instance.spec.ts.snap +++ b/packages/http/src/__tests__/__snapshots__/http-prism-instance.spec.ts.snap @@ -15,6 +15,7 @@ Object { }, }, "output": Object { + "body": Anything, "headers": Object { "Content-type": "application/json", }, diff --git a/packages/http/src/__tests__/http-prism-instance.spec.ts b/packages/http/src/__tests__/http-prism-instance.spec.ts index a66b52757..5abb7d36f 100644 --- a/packages/http/src/__tests__/http-prism-instance.spec.ts +++ b/packages/http/src/__tests__/http-prism-instance.spec.ts @@ -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'; @@ -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', () => { @@ -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); }); }); diff --git a/packages/http/src/index.ts b/packages/http/src/index.ts index 284fad8ee..c11b4ad0e 100644 --- a/packages/http/src/index.ts +++ b/packages/http/src/index.ts @@ -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, @@ -31,7 +30,7 @@ const createInstance = ( router, forwarder, validator, - mocker: new HttpMocker(new JSONSchemaExampleGenerator()), + mocker: new HttpMocker(generate), }, )(config, overrides); }; diff --git a/packages/http/src/mocker/HttpMocker.ts b/packages/http/src/mocker/HttpMocker.ts index 2e0385823..e912b1712 100644 --- a/packages/http/src/mocker/HttpMocker.ts +++ b/packages/http/src/mocker/HttpMocker.ts @@ -5,12 +5,13 @@ import * as caseless from 'caseless'; import { fromPairs, keyBy, mapValues, toPairs } from 'lodash'; import { IHttpConfig, IHttpOperationConfig, IHttpRequest, IHttpResponse, ProblemJsonError } from '../types'; import { UNPROCESSABLE_ENTITY } from './errors'; -import { IExampleGenerator } from './generator/IExampleGenerator'; import helpers from './negotiator/NegotiatorHelpers'; import { IHttpNegotiationResult } from './negotiator/types'; +type GeneratorSignature = (f: unknown) => Promise; + export class HttpMocker implements IMocker { - constructor(private _exampleGenerator: IExampleGenerator) {} + constructor(private _exampleGenerator: GeneratorSignature) {} public async mock({ resource, @@ -71,7 +72,7 @@ function isINodeExample(nodeExample: INodeExample | INodeExternalExample | undef return !!nodeExample && 'value' in nodeExample; } -function computeMockedHeaders(headers: IHttpHeaderParam[], ex: IExampleGenerator): Promise> { +function computeMockedHeaders(headers: IHttpHeaderParam[], ex: GeneratorSignature): Promise> { const headerWithPromiseValues = mapValues(keyBy(headers, h => h.name), async header => { if (header.content) { if (header.content.examples.length > 0) { @@ -81,7 +82,7 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: IExampleGenerator } } if (header.content.schema) { - return ex.generate(header.content.schema, 'application/json'); + return ex(header.content.schema); } } return 'string'; @@ -92,14 +93,12 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: IExampleGenerator async function computeBody( negotiationResult: Pick, - ex: IExampleGenerator, + ex: GeneratorSignature, ) { 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; } diff --git a/packages/http/src/mocker/__tests__/HttpMocker.spec.ts b/packages/http/src/mocker/__tests__/HttpMocker.spec.ts index 24d74ff93..6f42bc91c 100644 --- a/packages/http/src/mocker/__tests__/HttpMocker.spec.ts +++ b/packages/http/src/mocker/__tests__/HttpMocker.spec.ts @@ -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()); @@ -120,7 +119,7 @@ describe('HttpMocker', () => { ).resolves.toMatchSnapshot(); }); - it('returns dynamic example', () => { + it('returns dynamic example', async () => { jest.spyOn(helpers, 'negotiateOptionsForValidRequest').mockReturnValue({ code: '202', mediaType: 'test', @@ -128,14 +127,15 @@ describe('HttpMocker', () => { 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), + }); }); }); @@ -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({ @@ -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(); }); @@ -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 => @@ -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), + }); }); }); }); diff --git a/packages/http/src/mocker/__tests__/__snapshots__/HttpMocker.spec.ts.snap b/packages/http/src/mocker/__tests__/__snapshots__/HttpMocker.spec.ts.snap index b614587bc..9b7e7a04b 100644 --- a/packages/http/src/mocker/__tests__/__snapshots__/HttpMocker.spec.ts.snap +++ b/packages/http/src/mocker/__tests__/__snapshots__/HttpMocker.spec.ts.snap @@ -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", diff --git a/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap b/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap index fe431ad82..4c21bff22 100644 --- a/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap +++ b/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap @@ -2,7 +2,11 @@ exports[`http mocker request is invalid returns 422 and static error response 1`] = ` Object { - "body": "[{\\"message\\":\\"error\\"}]", + "body": Array [ + Object { + "message": "error", + }, + ], "headers": Object { "Content-type": "application/json", }, @@ -12,7 +16,13 @@ Object { 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\\"}]", + "body": Array [ + Object { + "completed": true, + "id": 1, + "name": "make prism", + }, + ], "headers": Object { "Content-type": "application/json", }, @@ -43,7 +53,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", }, diff --git a/packages/http/src/mocker/__tests__/functional.spec.ts b/packages/http/src/mocker/__tests__/functional.spec.ts index f37974dd0..d7d683d96 100644 --- a/packages/http/src/mocker/__tests__/functional.spec.ts +++ b/packages/http/src/mocker/__tests__/functional.spec.ts @@ -2,11 +2,11 @@ import { ISchema } from '@stoplight/types'; import * as Ajv from 'ajv'; import { httpOperations, httpRequests } from '../../__tests__/fixtures'; -import { JSONSchemaExampleGenerator } from '../generator/JSONSchemaExampleGenerator'; +import { generate } from '../generator/JSONSchema'; import { HttpMocker } from '../index'; describe('http mocker', () => { - const mocker = new HttpMocker(new JSONSchemaExampleGenerator()); + const mocker = new HttpMocker(generate); describe('request is valid', () => { describe('given only enforced content type', () => { @@ -229,7 +229,7 @@ describe('http mocker', () => { 'Content-type': 'application/json', 'x-todos-publish': expect.any(String), }); - expect(validate(JSON.parse(response.body))).toBe(true); + expect(validate(response.body)).toBeTruthy(); }); }); }); @@ -255,9 +255,9 @@ describe('http mocker', () => { }); const ajv = new Ajv(); - const validate = ajv.compile(httpOperations[1].responses[1].contents[0].schema as ISchema); + const validate = ajv.compile(httpOperations[1].responses[1].contents[0].schema!); - expect(validate(JSON.parse(response.body))).toBe(true); + expect(validate(response.body)).toBeTruthy(); }); }); }); diff --git a/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts b/packages/http/src/mocker/generator/JSONSchema.ts similarity index 70% rename from packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts rename to packages/http/src/mocker/generator/JSONSchema.ts index e11d2d2fd..34bee96e1 100644 --- a/packages/http/src/mocker/generator/JSONSchemaExampleGenerator.ts +++ b/packages/http/src/mocker/generator/JSONSchema.ts @@ -1,5 +1,6 @@ // @ts-ignore import * as jsf from '@stoplight/json-schema-faker'; +import { cloneDeep } from 'lodash'; jsf.option({ failOnInvalidTypes: false, @@ -14,8 +15,6 @@ jsf.option({ maxLength: 100, }); -export class JSONSchemaExampleGenerator { - public async generate(source: unknown): Promise { - return jsf.resolve(source); - } +export async function generate(source: unknown): Promise { + return jsf.resolve(cloneDeep(source)); } diff --git a/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts new file mode 100644 index 000000000..661d111ed --- /dev/null +++ b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts @@ -0,0 +1,34 @@ +import { generate } from '../JSONSchema'; + +describe('JSONSchemaExampleGenerator', () => { + describe('generate()', () => { + it('generates dynamic example from schema', async () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1 }, + email: { type: 'string', format: 'email' }, + }, + required: ['name', 'email'], + }; + + const instance = await generate(schema); + expect(instance).toHaveProperty('name'); + expect(instance).toHaveProperty('email'); + }); + + it('operates on sealed schema objects', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name'], + }; + + Object.defineProperty(schema.properties, 'name', { writable: false }); + + return expect(generate(schema)).resolves.toBeTruthy(); + }); + }); +}); diff --git a/packages/http/src/mocker/generator/__tests__/JSONSchemaExampleGenerator.spec.ts b/packages/http/src/mocker/generator/__tests__/JSONSchemaExampleGenerator.spec.ts deleted file mode 100644 index 32cf2a5f4..000000000 --- a/packages/http/src/mocker/generator/__tests__/JSONSchemaExampleGenerator.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { JSONSchemaExampleGenerator } from '../JSONSchemaExampleGenerator'; - -describe('JSONSchemaExampleGenerator', () => { - let jsonSchemaExampleGenerator: JSONSchemaExampleGenerator; - - beforeEach(() => { - jsonSchemaExampleGenerator = new JSONSchemaExampleGenerator(); - }); - - describe('generate()', () => { - it('generates dynamic example from schema', async () => { - const schema = { - type: 'object', - properties: { - name: { type: 'string', minLength: 1 }, - email: { type: 'string', format: 'email' }, - }, - required: ['name', 'email'], - }; - - const example = await jsonSchemaExampleGenerator.generate(schema, 'application/json'); - const instance = JSON.parse(example); - - expect(instance.name).toMatch(/^.+$/); - - // naive email check, should be enough - expect(instance.email).toMatch(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}/); - }); - - it('fails when media type is unknown', () => { - return expect( - jsonSchemaExampleGenerator.generate({}, 'non-existing/media-type'), - ).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('operates on sealed schema objects', () => { - const schema = { - type: 'object', - properties: { - name: { type: 'string' }, - }, - required: ['name'], - }; - - Object.defineProperty(schema.properties, 'name', { writable: false }); - - return expect(jsonSchemaExampleGenerator.generate(schema, 'application/json')).resolves.toBeTruthy(); - }); - }); -}); diff --git a/packages/http/src/mocker/generator/__tests__/__snapshots__/JSONSchemaExampleGenerator.spec.ts.snap b/packages/http/src/mocker/generator/__tests__/__snapshots__/JSONSchemaExampleGenerator.spec.ts.snap deleted file mode 100644 index df626be35..000000000 --- a/packages/http/src/mocker/generator/__tests__/__snapshots__/JSONSchemaExampleGenerator.spec.ts.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`JSONSchemaExampleGenerator generate() fails when media type is unknown 1`] = `"Unknown media type 'non-existing/media-type'"`; From 77ab99fa9baa02f51d7befff37972a7aef8ff92c Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 13:37:00 -0400 Subject: [PATCH 04/12] refactor: PayloadGenerator and put it in types --- packages/http/src/mocker/HttpMocker.ts | 17 +++++++++++------ packages/http/src/types.ts | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/http/src/mocker/HttpMocker.ts b/packages/http/src/mocker/HttpMocker.ts index e912b1712..e97e12a47 100644 --- a/packages/http/src/mocker/HttpMocker.ts +++ b/packages/http/src/mocker/HttpMocker.ts @@ -3,15 +3,20 @@ import { Dictionary, IHttpHeaderParam, IHttpOperation, INodeExample, INodeExtern import * as caseless from 'caseless'; import { fromPairs, keyBy, mapValues, toPairs } from 'lodash'; -import { IHttpConfig, IHttpOperationConfig, IHttpRequest, IHttpResponse, ProblemJsonError } from '../types'; +import { + IHttpConfig, + IHttpOperationConfig, + IHttpRequest, + IHttpResponse, + PayloadGenerator, + ProblemJsonError, +} from '../types'; import { UNPROCESSABLE_ENTITY } from './errors'; import helpers from './negotiator/NegotiatorHelpers'; import { IHttpNegotiationResult } from './negotiator/types'; -type GeneratorSignature = (f: unknown) => Promise; - export class HttpMocker implements IMocker { - constructor(private _exampleGenerator: GeneratorSignature) {} + constructor(private _exampleGenerator: PayloadGenerator) {} public async mock({ resource, @@ -72,7 +77,7 @@ function isINodeExample(nodeExample: INodeExample | INodeExternalExample | undef return !!nodeExample && 'value' in nodeExample; } -function computeMockedHeaders(headers: IHttpHeaderParam[], ex: GeneratorSignature): Promise> { +function computeMockedHeaders(headers: IHttpHeaderParam[], ex: PayloadGenerator): Promise> { const headerWithPromiseValues = mapValues(keyBy(headers, h => h.name), async header => { if (header.content) { if (header.content.examples.length > 0) { @@ -93,7 +98,7 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: GeneratorSignatur async function computeBody( negotiationResult: Pick, - ex: GeneratorSignature, + ex: PayloadGenerator, ) { if (isINodeExample(negotiationResult.bodyExample) && negotiationResult.bodyExample.value !== undefined) { return negotiationResult.bodyExample.value; diff --git a/packages/http/src/types.ts b/packages/http/src/types.ts index 426ffc6ed..d01a2b05a 100644 --- a/packages/http/src/types.ts +++ b/packages/http/src/types.ts @@ -104,3 +104,5 @@ export class ProblemJsonError extends Error { Error.captureStackTrace(this, ProblemJsonError); } } + +export type PayloadGenerator = (f: unknown) => Promise; From 1270970ecd4ff44a1ccfb4efde14762ce20f19d9 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 14:29:35 -0400 Subject: [PATCH 05/12] chore: throw exception so that it's an application/problem-json --- packages/http-server/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-server/src/server.ts b/packages/http-server/src/server.ts index db72fc1d4..b265a3443 100644 --- a/packages/http-server/src/server.ts +++ b/packages/http-server/src/server.ts @@ -73,7 +73,7 @@ const replyHandler = ( 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; From 7f41cf40e7dd6a8a6d88b4f377773d614d3ca6b0 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 14:44:38 -0400 Subject: [PATCH 06/12] test: prefer assertions to snapshots --- packages/http/src/__tests__/fixtures/index.ts | 20 +++++-------- .../__snapshots__/functional.spec.ts.snap | 30 ------------------- .../src/mocker/__tests__/functional.spec.ts | 10 +++++-- 3 files changed, 16 insertions(+), 44 deletions(-) diff --git a/packages/http/src/__tests__/fixtures/index.ts b/packages/http/src/__tests__/fixtures/index.ts index 7a9248571..6d1ad9bf3 100644 --- a/packages/http/src/__tests__/fixtures/index.ts +++ b/packages/http/src/__tests__/fixtures/index.ts @@ -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', @@ -143,11 +141,9 @@ export const httpOperations: IHttpOperation[] = [ examples: [ { key: 'application/json', - value: [ - { - message: 'error', - }, - ], + value: { + message: 'error', + }, }, ], encodings: [], diff --git a/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap b/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap index 4c21bff22..e041b5393 100644 --- a/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap +++ b/packages/http/src/mocker/__tests__/__snapshots__/functional.spec.ts.snap @@ -1,35 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`http mocker request is invalid returns 422 and static error response 1`] = ` -Object { - "body": Array [ - Object { - "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": Array [ - Object { - "completed": true, - "id": 1, - "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": "Shoppingfalse", diff --git a/packages/http/src/mocker/__tests__/functional.spec.ts b/packages/http/src/mocker/__tests__/functional.spec.ts index d7d683d96..3aeb7a255 100644 --- a/packages/http/src/mocker/__tests__/functional.spec.ts +++ b/packages/http/src/mocker/__tests__/functional.spec.ts @@ -166,7 +166,12 @@ describe('http mocker', () => { input: httpRequests[0], }); - expect(response).toMatchSnapshot(); + expect(response.statusCode).toBe(200); + expect(response.body).toMatchObject({ + completed: true, + id: 1, + name: 'make prism', + }); }); test('return lowest 2xx response and the first example matching the media type', async () => { @@ -241,7 +246,8 @@ describe('http mocker', () => { input: httpRequests[1], }); - expect(response).toMatchSnapshot(); + expect(response.statusCode).toBe(422); + expect(response.body).toMatchObject({ message: 'error' }); }); test('returns 422 and dynamic error response', async () => { From a060f1793db6ebdf73706c229eff84bbcc0b8fd6 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 14:54:19 -0400 Subject: [PATCH 07/12] docs: fix path to the CLI script --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cd98dbc5..64b6e5277 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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//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. From e83a3f64a283814df9fd0f1fec9e900a60057346 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 14:55:15 -0400 Subject: [PATCH 08/12] docs: put the new simplified command --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64b6e5277..25b89905f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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! From 7fca363758dc2e3d5187ffd8eec967670588d9b5 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 15:11:13 -0400 Subject: [PATCH 09/12] chore: do not publish on alpha we're doing stuff anyway there --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c0d06c7e..c6c99bb57 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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/ From 5b0c159048e9df842c793cf855888ef501afa76a Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Sat, 25 May 2019 15:41:15 -0400 Subject: [PATCH 10/12] test: rename test describe title --- packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts index 661d111ed..6ae699498 100644 --- a/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts +++ b/packages/http/src/mocker/generator/__tests__/JSONSchema.spec.ts @@ -1,6 +1,6 @@ import { generate } from '../JSONSchema'; -describe('JSONSchemaExampleGenerator', () => { +describe('JSONSchema generator', () => { describe('generate()', () => { it('generates dynamic example from schema', async () => { const schema = { From 9a8f91ddd470e16b750cf79ea3d210a8d0c44f10 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Mon, 27 May 2019 11:53:02 +0200 Subject: [PATCH 11/12] refactor: return empty string if the generated object is empty --- packages/http-server/src/__tests__/server.oas.spec.ts | 2 +- packages/http/src/mocker/HttpMocker.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/http-server/src/__tests__/server.oas.spec.ts b/packages/http-server/src/__tests__/server.oas.spec.ts index af16650ec..f141d1e67 100644 --- a/packages/http-server/src/__tests__/server.oas.spec.ts +++ b/packages/http-server/src/__tests__/server.oas.spec.ts @@ -220,7 +220,7 @@ describe.each([['petstore.oas2.json'], ['petstore.oas3.json']])('server %s', fil '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': 'string', }; for (const headerName of Object.keys(expectedValues)) { diff --git a/packages/http/src/mocker/HttpMocker.ts b/packages/http/src/mocker/HttpMocker.ts index e97e12a47..c0c3b7291 100644 --- a/packages/http/src/mocker/HttpMocker.ts +++ b/packages/http/src/mocker/HttpMocker.ts @@ -2,7 +2,7 @@ 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 { fromPairs, isEmpty, isObject, keyBy, mapValues, toPairs } from 'lodash'; import { IHttpConfig, IHttpOperationConfig, @@ -87,7 +87,8 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: PayloadGenerator) } } if (header.content.schema) { - return ex(header.content.schema); + const example = await ex(header.content.schema); + if (!(isObject(example) && isEmpty(example))) return example; } } return 'string'; From 9a80dcb5da3334f7c42ff1de60dd9dbe7576a932 Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Mon, 27 May 2019 15:14:16 +0200 Subject: [PATCH 12/12] chore: empty string --- packages/http-server/src/__tests__/server.oas.spec.ts | 2 +- packages/http/src/mocker/HttpMocker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/http-server/src/__tests__/server.oas.spec.ts b/packages/http-server/src/__tests__/server.oas.spec.ts index f141d1e67..ac11f7cef 100644 --- a/packages/http-server/src/__tests__/server.oas.spec.ts +++ b/packages/http-server/src/__tests__/server.oas.spec.ts @@ -220,7 +220,7 @@ describe.each([['petstore.oas2.json'], ['petstore.oas3.json']])('server %s', fil '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': 'string', + 'x-strange-header': '', }; for (const headerName of Object.keys(expectedValues)) { diff --git a/packages/http/src/mocker/HttpMocker.ts b/packages/http/src/mocker/HttpMocker.ts index c0c3b7291..28defbcb5 100644 --- a/packages/http/src/mocker/HttpMocker.ts +++ b/packages/http/src/mocker/HttpMocker.ts @@ -91,7 +91,7 @@ function computeMockedHeaders(headers: IHttpHeaderParam[], ex: PayloadGenerator) if (!(isObject(example) && isEmpty(example))) return example; } } - return 'string'; + return ''; }); return resolvePromiseInProps(headerWithPromiseValues);