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

feat: STOP-243 - create prism instance with full spec #2501

Merged
merged 8 commits into from
Mar 22, 2024
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
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 () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was checking a 413, so I updated the title to match.

the rest of the changes in this file are formatting changes

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