open-api-mocker
Advanced tools
Comparing version 1.7.0 to 1.7.1
@@ -9,2 +9,10 @@ # Changelog | ||
## [1.7.1] - 2021-06-22 | ||
### Fixed | ||
- Response header `x-powered-by` has now the correct value | ||
- Schema Object `pattern` property can be passed as a string (#35) | ||
- Refactor to improve code quality and maintainability (#37) | ||
- Schemas with `oneOf`, `anyOf` and `allOf` now work properly and don't get wrong default values injected (#41) | ||
- Dependencies updated to fix vulnerabilities | ||
## [1.7.0] - 2021-04-20 | ||
@@ -11,0 +19,0 @@ ### Added |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const ComponentsStruct = require('./structs'); | ||
const extractExtensions = require('../utils/extract-extensions'); | ||
@@ -41,4 +42,3 @@ class Parser { | ||
const extensionProps = Object.entries(otherProps) | ||
.filter(([propName]) => propName.substr(0, 2) === 'x-'); | ||
const extensionProps = extractExtensions(otherProps); | ||
@@ -45,0 +45,0 @@ return new Components({ |
@@ -8,2 +8,16 @@ 'use strict'; | ||
const optionalListOf = elementsStruct => struct.optional(struct.list([elementsStruct])); | ||
const optionalUnionOf = validStructs => struct.optional(struct.union(validStructs)); | ||
const referenceUnion = otherStruct => struct.union([otherStruct, ReferenceStruct]); | ||
const getDefaultType = ({ allOf, oneOf, anyOf }) => (allOf || oneOf || anyOf ? undefined : 'object'); | ||
const getDefaultTypeFlags = ({ type, allOf, oneOf, anyOf }) => { | ||
const finalType = type || getDefaultType({ allOf, oneOf, anyOf }); | ||
return finalType ? false : undefined; | ||
}; | ||
const SchemaStruct = struct.intersection([ | ||
@@ -20,3 +34,3 @@ 'object', | ||
minLength: 'number?', | ||
pattern: 'regexp?', | ||
pattern: 'string|regexp?', | ||
maxItems: 'number?', | ||
@@ -28,38 +42,17 @@ minItems: 'number?', | ||
required: struct.union(['boolean?', struct.optional(['string'])]), | ||
enum: struct.optional(struct.list([struct.union(['string', 'number', 'boolean'])])), | ||
enum: optionalListOf(struct.union(['string', 'number', 'boolean'])), | ||
type: 'string?', | ||
allOf: struct.lazy(() => struct.optional(struct.list([struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
])]))), | ||
oneOf: struct.lazy(() => struct.optional(struct.list([struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
])]))), | ||
anyOf: struct.lazy(() => struct.optional(struct.list([struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
])]))), | ||
not: struct.lazy(() => struct.optional(struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
]))), | ||
items: struct.lazy(() => struct.optional(struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
]))), | ||
properties: struct.lazy(() => struct.optional(struct.dict(['string', struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
])]))), | ||
additionalProperties: struct.optional(struct.union([ | ||
allOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), | ||
oneOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), | ||
anyOf: struct.lazy(() => optionalListOf(referenceUnion(SchemaStruct))), | ||
not: struct.lazy(() => struct.optional(referenceUnion(SchemaStruct))), | ||
items: struct.lazy(() => struct.optional(referenceUnion(SchemaStruct))), | ||
properties: struct.lazy(() => struct.optional(struct.dict(['string', referenceUnion(SchemaStruct)]))), | ||
additionalProperties: optionalUnionOf([ | ||
'boolean', | ||
struct.lazy(() => struct.union([ | ||
SchemaStruct, | ||
ReferenceStruct | ||
])) | ||
])), | ||
struct.lazy(() => referenceUnion(SchemaStruct)) | ||
]), | ||
description: 'string?', | ||
format: 'string?', | ||
default: struct.optional(struct.union(['string', 'number', 'boolean', 'object', 'array'])), | ||
default: optionalUnionOf(['string', 'number', 'boolean', 'object', 'array']), | ||
nullable: 'boolean?', | ||
@@ -69,10 +62,10 @@ readOnly: 'boolean?', | ||
externalDocs: ExternalDocsStruct, | ||
example: struct.optional(struct.union(['string', 'number', 'boolean', 'object', 'array'])), | ||
example: optionalUnionOf(['string', 'number', 'boolean', 'object', 'array']), | ||
deprecated: 'boolean?' | ||
}, { | ||
type: 'object', | ||
nullable: false, | ||
readOnly: false, | ||
writeOnly: false, | ||
deprecated: false | ||
type: getDefaultType, | ||
nullable: getDefaultTypeFlags, | ||
readOnly: getDefaultTypeFlags, | ||
writeOnly: getDefaultTypeFlags, | ||
deprecated: getDefaultTypeFlags | ||
}) | ||
@@ -79,0 +72,0 @@ ]); |
'use strict'; | ||
const ParserError = require('../errors/parser-error'); | ||
const ExternalDocumentation = require('./external-documentation'); | ||
const ExternalDocumentationsStruct = require('./structs'); | ||
const extractExtensions = require('../utils/extract-extensions'); | ||
const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); | ||
@@ -23,7 +24,3 @@ class Parser { | ||
} catch(e) { | ||
const path = e.path | ||
.reduce((acum, pathPart) => `${acum}.${pathPart}`, 'externalDocs'); | ||
throw new ParserError(e.message, path); | ||
throw enhanceStructValidationError(e, 'externalDocs'); | ||
} | ||
@@ -34,4 +31,3 @@ } | ||
const extensionProps = Object.entries(otherProps) | ||
.filter(([propName]) => propName.substr(0, 2) === 'x-'); | ||
const extensionProps = extractExtensions(otherProps); | ||
@@ -38,0 +34,0 @@ return new ExternalDocumentation({ |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const InfoStruct = require('./structs'); | ||
const extractExtensions = require('../utils/extract-extensions'); | ||
@@ -36,4 +37,3 @@ class Parser { | ||
const extensionProps = Object.entries(otherProps) | ||
.filter(([propName]) => propName.substr(0, 2) === 'x-'); | ||
const extensionProps = extractExtensions(otherProps); | ||
@@ -40,0 +40,0 @@ return new Info({ |
@@ -11,4 +11,3 @@ 'use strict'; | ||
const colors = require('colors'); | ||
const parsePreferHeader = require('parse-prefer-header'); | ||
const memoize = require('micro-memoize'); | ||
const handleRequest = require('./request-handler'); | ||
@@ -70,64 +69,29 @@ const openApiMockSymbol = Symbol('openApiMock'); | ||
this.paths.map(path => { | ||
this.setRoutes(app); | ||
logger.debug(`Processing schema path ${path.httpMethod.toUpperCase()} ${path.uri}`); | ||
app.all('*', this._notFoundHandler.bind(this)); | ||
const expressHttpMethod = path.httpMethod.toLowerCase(); | ||
return this.startServer(app); | ||
} | ||
const uris = this._normalizeExpressPath(path.uri); | ||
setRoutes(app) { | ||
this.paths.map(path => this.setRoute(app, path)); | ||
} | ||
// Create a function that is memoized using the URL, query, the Prefer header and the body. | ||
// eslint-disable-next-line no-unused-vars | ||
const getResponse = (url, query, preferHeader, body) => { | ||
const { example: preferredExampleName, statuscode: preferredStatusCode } = parsePreferHeader(preferHeader) || {}; | ||
setRoute(app, path) { | ||
if(preferredStatusCode) | ||
logger.debug(`Searching requested response with status code ${preferredStatusCode}`); | ||
else | ||
logger.debug('Searching first response'); | ||
return path.getResponse(preferredStatusCode, preferredExampleName); | ||
}; | ||
logger.debug(`Processing schema path ${path.httpMethod.toUpperCase()} ${path.uri}`); | ||
const getResponseMemo = memoize(getResponse, { | ||
maxSize: 10 | ||
}); | ||
const expressHttpMethod = path.httpMethod.toLowerCase(); | ||
app[expressHttpMethod](uris, (req, res) => { | ||
const uris = this._normalizeExpressPath(path.uri); | ||
this._checkContentType(req); | ||
app[expressHttpMethod](uris, handleRequest(path)); | ||
const { | ||
query, | ||
params, | ||
headers, | ||
cookies, | ||
body: requestBody | ||
} = req; | ||
const failedValidations = path.validateRequestParameters({ | ||
query, | ||
path: params, | ||
headers, | ||
cookies, | ||
requestBody | ||
}); | ||
if(failedValidations.length) | ||
return this.sendResponse(req, res, { errors: failedValidations }, 400); | ||
const preferHeader = req.header('prefer') || ''; | ||
const { statusCode, headers: responseHeaders, body } = | ||
getResponseMemo(req.path, JSON.stringify(req.query), preferHeader, JSON.stringify(requestBody)); | ||
return this.sendResponse(req, res, body, statusCode, responseHeaders); | ||
}); | ||
return uris.map(uri => { | ||
return logger.info(`Handling route ${path.httpMethod.toUpperCase()} ${uri}`); | ||
}); | ||
return uris.map(uri => { | ||
return logger.info(`Handling route ${path.httpMethod.toUpperCase()} ${uri}`); | ||
}); | ||
} | ||
app.all('*', this._notFoundHandler.bind(this)); | ||
startServer(app) { | ||
return new Promise((resolve, reject) => { | ||
@@ -221,3 +185,3 @@ this.server = app.listen(this.port); | ||
.set(headers) | ||
.set('x-powered-by', 'jormaechea/open-api-mock') | ||
.set('x-powered-by', 'jormaechea/open-api-mocker') | ||
.json(body); | ||
@@ -224,0 +188,0 @@ } |
'use strict'; | ||
const ParserError = require('../errors/parser-error'); | ||
const Path = require('./path'); | ||
@@ -8,2 +7,3 @@ const PathsStruct = require('./structs'); | ||
const { knownHttpMethods } = require('../utils/http-methods'); | ||
const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); | ||
@@ -31,7 +31,3 @@ class Parser { | ||
} catch(e) { | ||
const path = e.path | ||
.reduce((acum, pathPart) => `${acum}.${pathPart}`, 'paths'); | ||
throw new ParserError(e.message, path); | ||
throw enhanceStructValidationError(e, 'paths'); | ||
} | ||
@@ -38,0 +34,0 @@ } |
@@ -59,9 +59,3 @@ 'use strict'; | ||
if(!content) { | ||
// Cannot validate the body if it's content is not defined | ||
logger.warn('Missing content for request body'); | ||
return []; | ||
} | ||
if(!content['application/json']) { | ||
if(!content || !content['application/json'] || !content['application/json'].schema) { | ||
// Cannot validate the body if it's application/json content is not defined | ||
@@ -72,8 +66,2 @@ logger.warn('Missing application/json content for request body'); | ||
if(!content['application/json'].schema) { | ||
// Cannot validate the body if it's application/json content schema is not defined | ||
logger.warn('Missing application/json content schema for request body'); | ||
return []; | ||
} | ||
// Validate the body | ||
@@ -80,0 +68,0 @@ const { schema } = content['application/json']; |
@@ -10,11 +10,9 @@ 'use strict'; | ||
class ResponseGenerator { | ||
static generate(schemaResponse, preferredExampleName) { | ||
if(schemaResponse.example) | ||
return schemaResponse.example; | ||
if(schemaResponse.examples && Object.values(schemaResponse.examples).length) { | ||
if(preferredExampleName && schemaResponse.examples[preferredExampleName] && schemaResponse.examples[preferredExampleName].value) | ||
return schemaResponse.examples[preferredExampleName].value; | ||
if(Object.values(schemaResponse.examples)[0].value) | ||
return Object.values(schemaResponse.examples)[0].value; | ||
if(schemaResponse.example || (schemaResponse.examples && Object.values(schemaResponse.examples).length)) { | ||
const bestExample = this.getBestExample(schemaResponse, preferredExampleName); | ||
if(bestExample !== undefined) | ||
return bestExample; | ||
} | ||
@@ -25,11 +23,18 @@ | ||
if(schemaResponse.schema) | ||
return this.generateBySchema(schemaResponse.schema); | ||
if(schemaResponse.schema || schemaResponse.type) | ||
return this.generateBySchema(schemaResponse.schema || schemaResponse); | ||
if(schemaResponse.type) | ||
return this.generateBySchema(schemaResponse); | ||
throw new Error(`Could not generate response: invalid schema: ${JSON.stringify(schemaResponse)}`); | ||
} | ||
static getBestExample(schemaResponse, preferredExampleName) { | ||
if(schemaResponse.example) | ||
return schemaResponse.example; | ||
if(preferredExampleName && schemaResponse.examples[preferredExampleName] && schemaResponse.examples[preferredExampleName].value) | ||
return schemaResponse.examples[preferredExampleName].value; | ||
if(Object.values(schemaResponse.examples)[0].value) | ||
return Object.values(schemaResponse.examples)[0].value; | ||
} | ||
static generateByEnum(enumOptions) { | ||
@@ -40,2 +45,3 @@ return enumOptions[0]; | ||
static generateBySchema(schemaResponse) { | ||
if(schemaResponse.example) | ||
@@ -52,8 +58,5 @@ return schemaResponse.example; | ||
if(schemaResponse.oneOf) | ||
return this.generate(schemaResponse.oneOf[0]); | ||
if(schemaResponse.oneOf || schemaResponse.anyOf) | ||
return this.generate((schemaResponse.oneOf || schemaResponse.anyOf)[0]); | ||
if(schemaResponse.anyOf) | ||
return this.generate(schemaResponse.anyOf[0]); | ||
const fakerExtension = schemaResponse['x-faker']; | ||
@@ -70,2 +73,6 @@ if(fakerExtension) { | ||
return this.generateByType(schemaResponse); | ||
} | ||
static generateByType(schemaResponse) { | ||
switch(schemaResponse.type) { | ||
@@ -96,4 +103,5 @@ case 'array': | ||
static generateByFaker(fakerString) { | ||
// Check if faker string is a template string | ||
if(fakerString.includes('{{') && fakerString.includes('}}')) | ||
if(fakerString.match(/\{\{.+\}\}/)) | ||
return faker.fake(fakerString); | ||
@@ -104,24 +112,11 @@ | ||
); | ||
if(!fakerRegex) { | ||
throw new Error( | ||
'Faker extension method is not in the right format. Expecting <namespace>.<method>(<args>) format.' | ||
); | ||
} | ||
if(!fakerRegex) | ||
throw new Error('Faker extension method is not in the right format. Expecting <namespace>.<method> or <namespace>.<method>(<json-args>) format.'); | ||
const { namespace, method, argsString } = fakerRegex.groups; | ||
if(!(namespace in faker)) { | ||
throw new Error( | ||
`Invalid faker namespace used. Must be one of ${Object.keys(faker).join( | ||
',' | ||
)}` | ||
); | ||
} | ||
if(!(method in faker[namespace])) { | ||
throw new Error( | ||
`Method '${method}' not found in faker namespace '${namespace}'. Must be one of ${Object.keys( | ||
faker[namespace] | ||
).join(',')}` | ||
); | ||
} | ||
if(!faker[namespace] || !faker[namespace][method]) | ||
throw new Error(`Faker method '${namespace}.${method}' not found`); | ||
const args = argsString ? JSON.parse(`[${argsString}]`) : []; | ||
@@ -128,0 +123,0 @@ |
'use strict'; | ||
const ParserError = require('../errors/parser-error'); | ||
const SecurityRequirement = require('./security-requirement'); | ||
const SecurityRequirementsStruct = require('./structs'); | ||
const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); | ||
@@ -27,7 +27,3 @@ class Parser { | ||
} catch(e) { | ||
const path = e.path | ||
.reduce((acum, pathPart) => `${acum}.${pathPart}`, 'security'); | ||
throw new ParserError(e.message, path); | ||
throw enhanceStructValidationError(e, 'security'); | ||
} | ||
@@ -34,0 +30,0 @@ } |
'use strict'; | ||
const ParserError = require('../errors/parser-error'); | ||
const Server = require('./server'); | ||
const ServersStruct = require('./structs'); | ||
const extractExtensions = require('../utils/extract-extensions'); | ||
const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); | ||
@@ -31,7 +32,3 @@ class Parser { | ||
} catch(e) { | ||
const path = e.path | ||
.reduce((acum, pathPart) => `${acum}.${pathPart}`, 'servers'); | ||
throw new ParserError(e.message, path); | ||
throw enhanceStructValidationError(e, 'servers'); | ||
} | ||
@@ -42,4 +39,3 @@ } | ||
const extensionProps = Object.entries(otherProps) | ||
.filter(([propName]) => propName.substr(0, 2) === 'x-'); | ||
const extensionProps = extractExtensions(otherProps); | ||
@@ -46,0 +42,0 @@ return new Server({ |
'use strict'; | ||
const ParserError = require('../errors/parser-error'); | ||
const Tag = require('./tag'); | ||
const TagsStruct = require('./structs'); | ||
const extractExtensions = require('../utils/extract-extensions'); | ||
const enhanceStructValidationError = require('../utils/enhance-struct-validation-error'); | ||
@@ -27,7 +28,3 @@ class Parser { | ||
} catch(e) { | ||
const path = e.path | ||
.reduce((acum, pathPart) => `${acum}.${pathPart}`, 'tags'); | ||
throw new ParserError(e.message, path); | ||
throw enhanceStructValidationError(e, 'tags'); | ||
} | ||
@@ -38,4 +35,3 @@ } | ||
const extensionProps = Object.entries(otherProps) | ||
.filter(([propName]) => propName.substr(0, 2) === 'x-'); | ||
const extensionProps = extractExtensions(otherProps); | ||
@@ -42,0 +38,0 @@ return new Tag({ |
{ | ||
"name": "open-api-mocker", | ||
"version": "1.7.0", | ||
"version": "1.7.1", | ||
"description": "A mock server based in Open API Specification", | ||
@@ -5,0 +5,0 @@ "main": "lib/open-api-mocker.js", |
58949
58
1577