Skip to content

Commit

Permalink
feat: STOP-243 - create prism instance with full spec (stoplightio#2501)
Browse files Browse the repository at this point in the history
Co-authored-by: Ed Vinyard <ed@stoplight.io>
  • Loading branch information
brendarearden and EdVinyard authored Mar 22, 2024
1 parent 752bc8b commit ed41dca
Show file tree
Hide file tree
Showing 17 changed files with 434 additions and 158 deletions.
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
},
"bugs": "https://github.com/stoplightio/prism/issues",
"dependencies": {
"@stoplight/http-spec": "^7.0.2",
"@stoplight/json": "^3.18.1",
"@stoplight/json-schema-ref-parser": "9.2.7",
"@stoplight/prism-core": "^5.6.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/__tests__/commands.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as operationUtils from '../../operations';
import * as operationUtils from '@stoplight/prism-http';
import * as yargs from 'yargs';
import { createMultiProcessPrism, createSingleProcessPrism } from '../../util/createServer';
import mockCommand from '../mock';
Expand Down
57 changes: 2 additions & 55 deletions packages/cli/src/operations.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,2 @@
import { transformOas3Operations } from '@stoplight/http-spec/oas3/operation';
import { transformOas2Operations } from '@stoplight/http-spec/oas2/operation';
import { transformPostmanCollectionOperations } from '@stoplight/http-spec/postman/operation';
import * as $RefParser from '@stoplight/json-schema-ref-parser';
import { HTTPResolverOptions } from '@stoplight/json-schema-ref-parser';
import { bundleTarget, decycle } from '@stoplight/json';
import { IHttpOperation } from '@stoplight/types';
import { get } from 'lodash';
import * as os from 'os';
import type { Spec } from 'swagger-schema-official';
import type { OpenAPIObject } from 'openapi3-ts';
import type { CollectionDefinition } from 'postman-collection';

export async function getHttpOperationsFromSpec(specFilePathOrObject: string | object): Promise<IHttpOperation[]> {
const prismVersion = require('../package.json').version;
const httpResolverOpts: HTTPResolverOptions = {
headers: {
'User-Agent': `PrismMockServer/${prismVersion} (${os.type()} ${os.arch()} ${os.release()})`,
},
};
const result = decycle(
await new $RefParser().dereference(specFilePathOrObject, { resolve: { http: httpResolverOpts } })
);

let operations: IHttpOperation[] = [];
if (isOpenAPI2(result)) operations = transformOas2Operations(result);
else if (isOpenAPI3(result)) operations = transformOas3Operations(result);
else if (isPostmanCollection(result)) operations = transformPostmanCollectionOperations(result);
else throw new Error('Unsupported document format');

operations.forEach((op, i, ops) => {
ops[i] = bundleTarget({
document: {
...result,
__target__: op,
},
path: '#/__target__',
cloneDocument: false,
});
});

return operations;
}

function isOpenAPI2(document: unknown): document is Spec {
return get(document, 'swagger') !== undefined;
}

function isOpenAPI3(document: unknown): document is OpenAPIObject {
return get(document, 'openapi') !== undefined;
}

function isPostmanCollection(document: unknown): document is CollectionDefinition {
return Array.isArray(get(document, 'item')) && get(document, 'info.name') !== undefined;
}
// add to keep this from being a breaking change.
export { getHttpOperationsFromSpec } from '@stoplight/prism-http';
4 changes: 2 additions & 2 deletions packages/cli/src/util/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import * as signale from 'signale';
import * as split from 'split2';
import { PassThrough, Readable } from 'stream';
import { LOG_COLOR_MAP } from '../const/options';
import { CreatePrism } from './runner';
import { getHttpOperationsFromSpec } from '@stoplight/prism-http';
import { createExamplePath } from './paths';
import { attachTagsToParamsValues, transformPathParamsValues } from './colorizer';
import { CreatePrism } from './runner';
import { getHttpOperationsFromSpec } from '../operations';
import { configureExtensionsUserProvided } from '../extensions';

type PrismLogDescriptor = pino.LogDescriptor & {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/util/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { IPrismHttpServer } from '@stoplight/prism-http-server/src/types';
import * as chokidar from 'chokidar';
import * as os from 'os';
import { CreateMockServerOptions } from './createServer';
import { getHttpOperationsFromSpec } from '../operations';
import { getHttpOperationsFromSpec } from '@stoplight/prism-http';

export type CreatePrism = (options: CreateMockServerOptions) => Promise<IPrismHttpServer | void>;

Expand Down
106 changes: 55 additions & 51 deletions packages/http-server/src/__tests__/body-params-validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ describe('body params validation', () => {
});

describe('and size bigger than 10MB', () => {
test('returns 422', async () => {
test('returns 413', async () => {
const response = await makeRequest('/json-body-required', {
method: 'POST',
headers: { 'content-type': 'application/json' },
Expand Down Expand Up @@ -761,31 +761,30 @@ describe('body params validation', () => {
},
user_profiles: {
type: 'array',
items:
{
type: 'object',
properties: {
foo: {
type: 'string'
},
data: {
type: 'boolean'
},
num: {
type: 'integer'
}
},
required: ['foo', 'data']
items: {
type: 'object',
properties: {
foo: {
type: 'string',
},
}
data: {
type: 'boolean',
},
num: {
type: 'integer',
},
},
required: ['foo', 'data'],
},
},
},
required: ['arrays', 'user_profiles'],
$schema: 'http://json-schema.org/draft-07/schema#',
},
examples: [],
encodings: [
{ property: 'arrays', style: HttpParamStyles.Form, allowReserved: true, explode: false },
{ property: 'user_profiles', style: HttpParamStyles.Form, allowReserved: true, explode: false }
{ property: 'user_profiles', style: HttpParamStyles.Form, allowReserved: true, explode: false },
],
},
],
Expand Down Expand Up @@ -844,45 +843,44 @@ describe('body params validation', () => {
type: 'object',
properties: {
status: {
type: 'string'
type: 'string',
},
lines: {
type: 'string'
type: 'string',
},
test_img_file: {
type: 'string'
type: 'string',
},
test_json_file: {
type: 'string'
type: 'string',
},
num: {
type: 'integer'
type: 'integer',
},
arrays: {
type: 'array',
items: { type: 'string' },
},
user_profiles: {
type: 'array',
items:
{
type: 'object',
properties: {
foo: {
type: 'integer'
}
},
required: ['foo']
items: {
type: 'object',
properties: {
foo: {
type: 'integer',
},
}
},
required: ['foo'],
},
},
},
required: ['status', 'arrays', 'user_profiles'],
$schema: 'http://json-schema.org/draft-07/schema#',
},
examples: [],
encodings: [
{ property: 'arrays', style: HttpParamStyles.Form, allowReserved: true, explode: false },
{ property: 'user_profiles', style: HttpParamStyles.Form, allowReserved: true, explode: false }
{ property: 'user_profiles', style: HttpParamStyles.Form, allowReserved: true, explode: false },
],
},
],
Expand Down Expand Up @@ -980,7 +978,8 @@ describe('body params validation', () => {
test('returns 200', async () => {
const params = new URLSearchParams({
arrays: 'a,b,c',
user_profiles: '{"foo":"value1","num ":1, "data":true}, {"foo":"value2","data":false, "test": " hello +"}',
user_profiles:
'{"foo":"value1","num ":1, "data":true}, {"foo":"value2","data":false, "test": " hello +"}',
});

const response = await makeRequest('/application-x-www-form-urlencoded-complex-request-body', {
Expand All @@ -996,7 +995,8 @@ describe('body params validation', () => {
const params = new URLSearchParams({
arrays: 'a,b,c',
// Note invalid JSON "foo:"value1"
user_profiles: '{"foo:"value1","num ":1, "data":true}, {"foo":"value2","data":false, "test": " hello +"}',
user_profiles:
'{"foo:"value1","num ":1, "data":true}, {"foo":"value2","data":false, "test": " hello +"}',
});

const response = await makeRequest('/application-x-www-form-urlencoded-complex-request-body', {
Expand All @@ -1007,10 +1007,10 @@ describe('body params validation', () => {

expect(response.status).toBe(415);
expect(response.json()).resolves.toMatchObject({
detail: "Cannot deserialize JSON object array in form data request body. Make sure the array is in JSON",
detail: 'Cannot deserialize JSON object array in form data request body. Make sure the array is in JSON',
status: 415,
title: "Invalid content type",
type: "https://stoplight.io/prism/errors#INVALID_CONTENT_TYPE",
title: 'Invalid content type',
type: 'https://stoplight.io/prism/errors#INVALID_CONTENT_TYPE',
});
});
});
Expand All @@ -1019,20 +1019,23 @@ describe('body params validation', () => {
let requestParams: Dictionary<any>;
beforeEach(() => {
const formData = new FormData();
formData.append("status", "--=\"");
formData.append("lines", "\r\n\r\n\s");
formData.append("test_img_file", "@test_img.png");
formData.append("test_json_file", "<test_json.json");
formData.append("num", "10");
formData.append('status', '--="');
formData.append('lines', '\r\n\r\ns');
formData.append('test_img_file', '@test_img.png');
formData.append('test_json_file', '<test_json.json');
formData.append('num', '10');
formData.append('arrays', 'a,b,c');
formData.append('user_profiles', '{"foo": 1, "foo, +bar":1}, {"foo":2, "{\\"test\\":x}": 2}, {"foo":3}, {"foo":4, "fizz buzz": 35}');
formData.append(
'user_profiles',
'{"foo": 1, "foo, +bar":1}, {"foo":2, "{\\"test\\":x}": 2}, {"foo":3}, {"foo":4, "fizz buzz": 35}'
);
requestParams = {
method: 'POST',
body: formData
body: formData,
};
});

describe('boundary string generated correctly', () =>{
describe('boundary string generated correctly', () => {
test('returns 200', async () => {
const response = await makeRequest('/multipart-form-data-body-required', requestParams);
expect(response.status).toBe(200);
Expand All @@ -1042,14 +1045,15 @@ describe('body params validation', () => {

describe('missing generated boundary string due to content-type manually specified in the header', () => {
test('returns 415 & error message', async () => {
requestParams['headers'] = { 'content-type':'multipart/form-data' };
requestParams['headers'] = { 'content-type': 'multipart/form-data' };
const response = await makeRequest('/multipart-form-data-body-required', requestParams);
expect(response.status).toBe(415);
expect(response.json()).resolves.toMatchObject({
detail: "Boundary parameter for multipart/form-data is not defined or generated in the request header. Try removing manually defined content-type from your request header if it exists.",
detail:
'Boundary parameter for multipart/form-data is not defined or generated in the request header. Try removing manually defined content-type from your request header if it exists.',
status: 415,
title: "Invalid content type",
type: "https://stoplight.io/prism/errors#INVALID_CONTENT_TYPE",
title: 'Invalid content type',
type: 'https://stoplight.io/prism/errors#INVALID_CONTENT_TYPE',
});
});
});
Expand Down
56 changes: 25 additions & 31 deletions packages/http-server/src/__tests__/server.oas.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createLogger } from '@stoplight/prism-core';
import { getHttpOperationsFromSpec } from '@stoplight/prism-cli/src/operations';
import { getHttpOperationsFromSpec } from '@stoplight/prism-http';
import { IHttpConfig, IHttpMockConfig } from '@stoplight/prism-http';
import { resolve } from 'path';
import { merge } from 'lodash';
Expand Down Expand Up @@ -162,42 +162,36 @@ describe('Ignore examples', () => {

describe('when running the server with ignoreExamples to true', () => {
describe('and there is no preference header sent', () => {
it('should return an example statically generated by Prism rather than json-schema-faker',
async () => {
const response = await makeRequest('/pets', { method: 'GET' });
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).toBe('doggie');
}
)
it('should return an example statically generated by Prism rather than json-schema-faker', async () => {
const response = await makeRequest('/pets', { method: 'GET' });
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).toBe('doggie');
});
});

describe('and I send a request with Prefer header selecting a specific example', () => {
it('should return an example statically generated by Prism rather than json-schema-faker',
async () => {
const response = await makeRequest('/pets', {
method: 'GET',
headers: { prefer: 'example=invalid_dog' }
});
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).toBe('doggie');
}
)
it('should return an example statically generated by Prism rather than json-schema-faker', async () => {
const response = await makeRequest('/pets', {
method: 'GET',
headers: { prefer: 'example=invalid_dog' },
});
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).toBe('doggie');
});
});

describe('and I send a request with dyanamic set to True', () => {
it('should return an example dynamically generated by json-schema-faker, ignoring the ignoreExamples flag',
async () => {
const response = await makeRequest('/pets', {
method: 'GET',
headers: { prefer: 'dynamic=true' }
});
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).not.toBe('doggie');
}
)
it('should return an example dynamically generated by json-schema-faker, ignoring the ignoreExamples flag', async () => {
const response = await makeRequest('/pets', {
method: 'GET',
headers: { prefer: 'dynamic=true' },
});
const payload = await response.json();
expect(hasPropertiesOfType(payload, schema)).toBe(true);
expect(payload.name).not.toBe('doggie');
});
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@stoplight/json-schema-merge-allof": "0.7.8",
"@stoplight/json-schema-sampler": "0.3.0",
"@stoplight/prism-core": "^5.6.0",
"@stoplight/http-spec": "^7.0.3",
"@stoplight/json-schema-ref-parser": "9.2.7",
"@stoplight/types": "^14.1.0",
"@stoplight/yaml": "^4.2.3",
"abstract-logging": "^2.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/http/src/__tests__/http-prism-instance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Scope as NockScope } from 'nock';
import * as nock from 'nock';
import { basename, resolve } from 'path';
import { createInstance, IHttpProxyConfig, IHttpRequest, IHttpResponse, ProblemJsonError } from '../';
import { getHttpOperationsFromSpec } from '@stoplight/prism-cli/src/operations';
import { getHttpOperationsFromSpec } from '../';
import { UNPROCESSABLE_ENTITY } from '../mocker/errors';
import { NO_PATH_MATCHED_ERROR, NO_SERVER_MATCHED_ERROR } from '../router/errors';
import { assertResolvesRight, assertResolvesLeft } from '@stoplight/prism-core/src/__tests__/utils';
Expand Down
Loading

0 comments on commit ed41dca

Please sign in to comment.