diff --git a/.eslintrc b/.eslintrc index 0296584..a0de1db 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,7 @@ { - "extends": ["eslint:recommended", "eslint-config-hapi"], + "extends": ["eslint:recommended", "@hapi/eslint-config-hapi"], "parserOptions": { - "ecmaVersion": 2017, + "ecmaVersion": 2018, "sourceType": "module" }, "env": { diff --git a/.npmrc b/.npmrc index 9cf9495..cafe685 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -package-lock=false \ No newline at end of file +package-lock=true diff --git a/.vscode/launch.json b/.vscode/launch.json index ed31437..56d524e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,4 +15,4 @@ "console": "internalConsole" } ] -} \ No newline at end of file +} diff --git a/lib/api-dto-mapper.js b/lib/api-dto-mapper.js new file mode 100644 index 0000000..15748d8 --- /dev/null +++ b/lib/api-dto-mapper.js @@ -0,0 +1,48 @@ +'use strict'; + +const Oas2Strategy = require('./mapping-strategies/oas2'); +const Oas3Strategy = require('./mapping-strategies/oas3'); + +class ApiDto { + constructor(spec, baseDir, mappingStrategy) { + + this.baseDir = baseDir; + this.spec = spec; + this._mappingStrategy = mappingStrategy; + } + + get basePath() { + + return this._mappingStrategy.getBasePath.call(this); + } + + get customAuthSchemes() { + + return this._mappingStrategy.getCustomAuthSchemes.call(this); + } + + get customAuthStrategies() { + + return this._mappingStrategy.getCustomAuthStrategies.call(this); + } + + get operations() { + + return this._mappingStrategy.getOperations.call(this); + } + + getOperation(method, path) { + + return this.operations.find((op) => op.method === method && op.path === path); + } +} + +const toDto = (spec, baseDir) => { + + const mappingStrategy = spec.swagger ? Oas2Strategy : Oas3Strategy; + return new ApiDto(spec, baseDir, mappingStrategy); +}; + +module.exports = { + toDto +}; diff --git a/lib/caller.js b/lib/caller.js index 8eae627..db38126 100644 --- a/lib/caller.js +++ b/lib/caller.js @@ -11,10 +11,13 @@ * @see https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi */ const caller = function (depth) { + const pst = Error.prepareStackTrace; Error.prepareStackTrace = function (_, frames) { + const stack = frames.map((frame) => { + return frame.getFileName(); }); diff --git a/lib/index.js b/lib/index.js index 2f67b06..5e4e127 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,6 +8,7 @@ const Path = require('path'); const Parser = require('swagger-parser'); const Utils = require('./utils'); const Routes = require('./routes'); +const ApiDtoMapper = require('./api-dto-mapper'); const Yaml = require('js-yaml'); const Fs = require('fs'); const Util = require('util'); @@ -32,13 +33,16 @@ const optionsSchema = Joi.object({ }).required(); const stripVendorExtensions = function (obj) { + if (Util.isArray(obj)) { const clean = []; for (const value of obj) { clean.push(stripVendorExtensions(value)); } + return clean; } + if (Util.isObject(obj)) { const clean = {}; for (const [key, value] of Object.entries(obj)) { @@ -46,12 +50,15 @@ const stripVendorExtensions = function (obj) { clean[key] = stripVendorExtensions(value); } } + return clean; } + return obj; }; const requireApi = function (path) { + let document; if (path.match(/\.ya?ml?/)) { @@ -65,69 +72,51 @@ const requireApi = function (path) { return document; }; -const register = async function (server, options, next) { - - const validation = optionsSchema.validate(options); +const exposePluginApi = (server, spec) => { - Hoek.assert(!validation.error, validation.error); - - const { api, cors, vhost, handlers, extensions, outputvalidation } = validation.value; - let { docs, docspath } = validation.value; - const spec = await Parser.validate(api); - - spec.basePath = Utils.unsuffix(Utils.prefix(spec.basePath || '/', '/'), '/'); - - //Expose plugin api server.expose({ getApi() { + return spec; }, setHost: function setHost(host) { + spec.host = host; } }); +}; - let basedir; - let apiDocument; +const registerCustomAuth = async (server, apiDto) => { - if (Util.isString(api)) { - apiDocument = requireApi(api); - basedir = Path.dirname(Path.resolve(api)); - } - else { - apiDocument = api; - basedir = CALLER_DIR; - } + await Promise.all( + apiDto.customAuthSchemes.map(async ({ scheme, path }) => { + + await server.register({ + plugin: require(path), + options: { + name: scheme + } + }); + }) + ); - if (spec['x-hapi-auth-schemes']) { - for (const [name, path] of Object.entries(spec['x-hapi-auth-schemes'])) { - const scheme = require(Path.resolve(Path.join(basedir, path))); + await Promise.all( + apiDto.customAuthStrategies.map(async ({ strategy, config }) => { await server.register({ - plugin: scheme, + plugin: require(config.path), options: { - name + name: strategy, + scheme: config.scheme || config.type, + lookup: config.name, + where: config.in } }); - } - } - if (spec.securityDefinitions) { - for (const [name, security] of Object.entries(spec.securityDefinitions)) { - if (security['x-hapi-auth-strategy']) { - const strategy = require(Path.resolve(Path.join(basedir, security['x-hapi-auth-strategy']))); - - await server.register({ - plugin: strategy, - options: { - name, - scheme: security.type, - lookup: security.name, - where: security.in - } - }); - } - } - } + }) + ); +}; + +const registerDocsPath = (server, docs, docspath, api, basePath, cors, vhost) => { if (docspath !== '/api-docs' && docs.path === '/api-docs') { server.log(['warn'], 'docspath is deprecated. Use docs instead.'); @@ -138,12 +127,13 @@ const register = async function (server, options, next) { } let apiPath = docs.path; - if (docs.prefixBasePath){ + if (docs.prefixBasePath) { docs.path = Utils.prefix(docs.path, '/'); docs.path = Utils.unsuffix(docs.path, '/'); - apiPath = spec.basePath + docs.path; + apiPath = basePath + docs.path; } + let apiDocument = Util.isString(api) ? requireApi(api) : api; if (docs.stripExtensions) { apiDocument = stripVendorExtensions(apiDocument); } @@ -154,6 +144,7 @@ const register = async function (server, options, next) { path: apiPath, config: { handler(request, h) { + return apiDocument; }, cors, @@ -164,9 +155,27 @@ const register = async function (server, options, next) { }, vhost }); +}; + +const register = async function (server, options, next) { + + const validation = optionsSchema.validate(options); + + Hoek.assert(!validation.error, validation.error); + + const { api, cors, vhost, handlers, extensions, outputvalidation } = validation.value; + const { docs, docspath } = validation.value; + + const basedir = Util.isString(api) ? Path.dirname(Path.resolve(api)) : CALLER_DIR; + const spec = await Parser.validate(api); + const apiDto = ApiDtoMapper.toDto(spec, basedir); + + exposePluginApi(server, spec); - const routes = await Routes.create(server, { api: spec, basedir, cors, vhost, handlers, extensions, outputvalidation }); + await registerCustomAuth(server, apiDto); + registerDocsPath(server, docs, docspath, api, apiDto.basePath, cors, vhost); + const routes = await Routes.create({ api: apiDto, cors, vhost, handlers, extensions, outputvalidation }); for (const route of routes) { server.route(route); } diff --git a/lib/mapping-strategies/oas2.js b/lib/mapping-strategies/oas2.js new file mode 100644 index 0000000..d641af2 --- /dev/null +++ b/lib/mapping-strategies/oas2.js @@ -0,0 +1,77 @@ +'use strict'; + +const Path = require('path'); +const Utils = require('../../lib/utils'); + +const getBasePath = function () { + + return Utils.unsuffix(Utils.prefix(this.spec.basePath || '/', '/'), '/'); +}; + +const getCustomAuthSchemes = function () { + + const schemes = this.spec['x-hapi-auth-schemes'] || {}; + + return Object.entries(schemes) + .map(([scheme, pathToScheme]) => ({ scheme, path: Path.join(this.baseDir, pathToScheme) }) + ); +}; + +const getCustomAuthStrategies = function () { + + const strategies = this.spec.securityDefinitions || {}; + + return Object.entries(strategies) + .filter(([, config]) => config['x-hapi-auth-strategy']) + .map(([strategy, { 'x-hapi-auth-strategy': pathToStrategy, ...rest }]) => + ({ + strategy, + config: { + ...rest, + path: Path.join(this.baseDir, pathToStrategy) + } + }) + ); +}; + +const mapSecurity = function (security = []) { + + return security.flatMap((auth) => + + Object.entries(auth).map(([strategy, scopes]) => ({ strategy, scopes })) + ); +}; + +const getOperations = function () { + + const globalSecurity = this.spec.security; + const paths = this.spec.paths || {}; + + return Object.entries(paths).flatMap(([path, operations]) => + + Object.entries(operations) + .filter(([method]) => Utils.isHttpMethod(method)) + .map(([method, operation]) => ({ + path, + method, + description: operation.description, + operationId: operation.operationId, + tags: operation.tags, + security: mapSecurity(operation.security || globalSecurity), + mediaTypes: { + request: operation.consumes || this.spec.consumes + }, + parameters: [...(operations.parameters || []), ...(operation.parameters || [])], + handler: operations['x-hapi-handler'] && Path.join(this.baseDir, operations['x-hapi-handler']), + responses: operation.responses, + customOptions: operation['x-hapi-options'] + })) + ); +}; + +module.exports = { + getBasePath, + getCustomAuthSchemes, + getCustomAuthStrategies, + getOperations +}; diff --git a/lib/mapping-strategies/oas3.js b/lib/mapping-strategies/oas3.js new file mode 100644 index 0000000..b0116bd --- /dev/null +++ b/lib/mapping-strategies/oas3.js @@ -0,0 +1,118 @@ +'use strict'; + +const Path = require('path'); +const URL = require('url'); +const Utils = require('../../lib/utils'); + +const getBasePath = function () { + + const server = this.spec.servers && this.spec.servers.length === 1 && this.spec.servers[0] || {}; + const url = URL.parse(server.url || ''); + return Utils.unsuffix(Utils.prefix(url.pathname, '/'), '/'); +}; + +const getCustomAuthSchemes = function () { + + const schemes = this.spec['x-hapi-auth-schemes'] || {}; + + return Object.entries(schemes) + .map(([scheme, pathToScheme]) => ({ scheme, path: Path.join(this.baseDir, pathToScheme) }) + ); +}; + +const getCustomAuthStrategies = function () { + + const strategies = this.spec.components && this.spec.components.securitySchemes || {}; + + return Object.entries(strategies) + .filter(([, config]) => config['x-hapi-auth-strategy']) + .map(([strategy, { 'x-hapi-auth-strategy': pathToStrategy, ...rest }]) => + ({ + strategy, + config: { + ...rest, + path: Path.join(this.baseDir, pathToStrategy) + } + }) + ); +}; + +const mapSecurity = function (security = []) { + + return security.flatMap((auth) => + + Object.entries(auth).map(([strategy, scopes]) => ({ strategy, scopes })) + ); +}; + +const getRequestMediaTypes = function (operation) { + + return operation.requestBody && + operation.requestBody.content && + Object.keys(operation.requestBody.content); +}; + +const mapRequestBodyToParameter = function (operation) { + + const mediaTypes = getRequestMediaTypes(operation); + const schema = mediaTypes && + mediaTypes.length && + operation.requestBody.content[mediaTypes[0]].schema; + + return schema ? + [{ name: 'payload', in: 'body', required: operation.requestBody.required, schema }] : + []; +}; + +const mapResponses = function (responses = {}) { + + return Object.entries(responses).reduce((acc, [statusCode, response]) => { + + const mappedResponse = { description: response.description }; + if (response.content) { + const contentType = Object.keys(response.content)[0]; + mappedResponse.schema = response.content[contentType].schema; + } + + acc[statusCode] = mappedResponse; + return acc; + }, {}); +}; + +const getOperations = function () { + + const globalSecurity = this.spec.security; + const paths = this.spec.paths || {}; + + return Object.entries(paths).flatMap(([path, operations]) => + + Object.entries(operations) + .filter(([method]) => Utils.isHttpMethod(method)) + .map(([method, operation]) => ({ + path, + method, + description: operation.description, + operationId: operation.operationId, + tags: operation.tags, + security: mapSecurity(operation.security || globalSecurity), + mediaTypes: { + request: getRequestMediaTypes(operation) + }, + parameters: [ + ...(operations.parameters || []), + ...(operation.parameters || []), + ...mapRequestBodyToParameter(operation) + ], + handler: operations['x-hapi-handler'] && Path.join(this.baseDir, operations['x-hapi-handler']), + responses: mapResponses(operation.responses), + customOptions: operation['x-hapi-options'] + })) + ); +}; + +module.exports = { + getBasePath, + getCustomAuthSchemes, + getCustomAuthStrategies, + getOperations +}; diff --git a/lib/routes.js b/lib/routes.js index 8299061..ee097cc 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -7,118 +7,109 @@ const Utils = require('./utils'); const Path = require('path'); const Props = require('dot-prop'); -const create = async function (server, { api, basedir, cors, vhost, handlers, extensions, outputvalidation }) { +const create = async function ({ api, cors, vhost, handlers, extensions, outputvalidation }) { + const routes = []; - const validator = Validators.create({ api }); + const validator = Validators.create(); if (typeof handlers === 'string') { handlers = await ObjectFiles.merge(handlers, extensions); } + //Support x-hapi-handler when no handlers set. if (!handlers) { - for (const [path, operations] of Object.entries(api.paths)) { - if (operations['x-hapi-handler']) { - const pathnames = path.split('/').slice(1).join('.'); + api.operations.filter((operation) => operation.handler) + .forEach((operation) => { + + const pathnames = operation.path.split('/').slice(1).join('.'); if (!handlers) { handlers = {}; } - const xhandler = require(Path.resolve(Path.join(basedir, operations['x-hapi-handler']))); + const xhandler = require(Path.resolve(operation.handler)); Props.set(handlers, pathnames, xhandler); - } - } + }); } - for (const [path, operations] of Object.entries(api.paths)) { - const pathnames = Utils.unsuffix(path, '/').split('/').slice(1).join('.'); + api.operations.forEach((operation) => { - for (const [method, operation] of Object.entries(operations)) { - const pathsearch = `${pathnames}.${method}`; - const handler = Hoek.reach(handlers, pathsearch); - const xoptions = operation['x-hapi-options'] || {}; + const pathnames = Utils.unsuffix(operation.path, '/').split('/').slice(1).join('.'); + const pathsearch = `${pathnames}.${operation.method}`; + const handler = Hoek.reach(handlers, pathsearch); + const xoptions = operation.customOptions || {}; - if (!handler) { - continue; - } - - const customTags = operation.tags || []; - const options = Object.assign({ - cors, - id: operation.operationId, - // hapi does not support empty descriptions - description: operation.description !== '' ? operation.description : undefined, - tags: ['api', ...customTags] - }, xoptions); - - options.handler = handler; + if (!handler) { + return; + } - if (Utils.canCarry(method)) { - options.payload = options.payload ? Hoek.applyToDefaults({ allow: operation.consumes || api.consumes }, options.payload) : { allow: operation.consumes || api.consumes }; - } + const customTags = operation.tags || []; + const options = { + cors, + id: operation.operationId, + // hapi does not support empty descriptions + description: operation.description !== '' ? operation.description : undefined, + tags: ['api', ...customTags], + handler, + ...xoptions + }; + + if (Utils.canCarry(operation.method)) { + options.payload = options.payload ? + Hoek.applyToDefaults({ allow: operation.mediaTypes.request }, options.payload) : + { allow: operation.mediaTypes.request }; + } - if (Array.isArray(handler)) { - options.pre = []; + if (Array.isArray(handler)) { + options.pre = []; - for (let i = 0; i < handler.length - 1; ++i) { - options.pre.push({ - assign: handler[i].name || 'p' + (i + 1), - method: handler[i] - }); - } - options.handler = handler[handler.length - 1]; + for (let i = 0; i < handler.length - 1; ++i) { + options.pre.push({ + assign: handler[i].name || 'p' + (i + 1), + method: handler[i] + }); } - const skipValidation = options.payload && options.payload.parse === false; - - if (operation.parameters && !skipValidation) { - const allowUnknownProperties = xoptions.validate && xoptions.validate.options && xoptions.validate.options.allowUnknown === true; - const v = validator.makeAll(operation.parameters, operation.consumes || api.consumes, allowUnknownProperties); - options.validate = v.validate; - options.ext = { - onPreAuth: { method: v.routeExt } - }; - } + options.handler = handler[handler.length - 1]; + } - if (outputvalidation && operation.responses) { - options.response = {}; - options.response.status = validator.makeResponseValidator(operation.responses); - } + const skipValidation = options.payload && options.payload.parse === false; - if (operation.security === undefined && api.security) { - operation.security = api.security; - } + if (operation.parameters && !skipValidation) { + const allowUnknownProperties = xoptions.validate && xoptions.validate.options && xoptions.validate.options.allowUnknown === true; + const v = validator.makeAll(operation.parameters, operation.mediaTypes.request, allowUnknownProperties); + options.validate = v.validate; + options.ext = { + onPreAuth: { method: v.routeExt } + }; + } - if (operation.security && operation.security.length) { - for (const secdef of operation.security) { - const securitySchemes = Object.keys(secdef); + if (outputvalidation && operation.responses) { + options.response = {}; + options.response.status = validator.makeResponseValidator(operation.responses); + } - for (const securityDefinitionName of securitySchemes) { - const securityDefinition = api.securityDefinitions[securityDefinitionName]; + operation.security.forEach((secdef) => { - Hoek.assert(securityDefinition, 'Security scheme not defined.'); + options.auth = options.auth || { access: {}, mode: 'required' }; + options.auth.access.scope = options.auth.access.scope || []; + options.auth.access.scope.push(...secdef.scopes); + options.auth.strategies = options.auth.strategies || []; + options.auth.strategies.push(secdef.strategy); - options.auth = options.auth || { access: {}, mode: 'required' }; - options.auth.access.scope = options.auth.access.scope || []; - options.auth.access.scope.push(...secdef[securityDefinitionName]); - options.auth.strategies = options.auth.strategies || []; - options.auth.strategies.push(securityDefinitionName); - } - } - if (options.auth.access.scope.length === 0) { - options.auth.access.scope = false; - } + if (options.auth.access.scope.length === 0) { + options.auth.access.scope = false; } - - routes.push({ - method, - path: api.basePath + path, - options, - vhost - }); - } - } + }); + + routes.push({ + method: operation.method, + path: api.basePath + operation.path, + options, + vhost + }); + }); return routes; }; diff --git a/lib/utils.js b/lib/utils.js index 65535fc..7b00a4e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -15,6 +15,7 @@ const utils = { ], canCarry: function (method) { + switch (method) { case 'post': case 'put': @@ -27,10 +28,12 @@ const utils = { }, isHttpMethod: function (method) { + return (typeof method === 'string') && !!~utils.verbs.indexOf(method.toLowerCase()); }, endsWith: function (haystack, needle) { + if (!haystack || !needle) { return false; } @@ -43,6 +46,7 @@ const utils = { }, prefix: function (str, pre) { + str = str || ''; if (str.indexOf(pre) === 0) { return str; @@ -53,6 +57,7 @@ const utils = { }, unprefix: function (str, pre) { + str = str || ''; if (str.indexOf(pre) === 0) { str = str.substr(pre.length); @@ -63,6 +68,7 @@ const utils = { }, suffix: function (str, suff) { + str = str || ''; if (this.endsWith(str, suff)) { return str; @@ -73,6 +79,7 @@ const utils = { }, unsuffix: function (str, suff) { + str = str || ''; if (this.endsWith(str, suff)) { str = str.substr(0, str.length - suff.length); diff --git a/lib/validators.js b/lib/validators.js index e885fc3..f37eaeb 100644 --- a/lib/validators.js +++ b/lib/validators.js @@ -20,6 +20,7 @@ const extensions = [ ]; const refineType = function (type, format) { + if (type === 'integer') { type = 'number'; } @@ -41,6 +42,7 @@ const enjoi = Enjoi.defaults({ extensions, refineType }); const create = function (options = {}) { const makeValidator = function (parameter, consumes, allowUnknownProperties = false) { + const coerce = coercion(parameter, consumes); let schema; @@ -52,32 +54,14 @@ const create = function (options = {}) { } } else { - const template = { - required: parameter.required, - enum: parameter.enum, - type: parameter.type, - schema: parameter.schema, - items: parameter.items, - properties: parameter.properties, - pattern: parameter.pattern, - format: parameter.format, - allowEmptyValue: parameter.allowEmptyValue, - collectionFormat: parameter.collectionFormat, - default: parameter.default, - maximum: parameter.maximum, - minimum: parameter.minimum, - maxLength: parameter.maxLength, - minLength: parameter.minLength, - maxItems: parameter.maxItems, - minItems: parameter.minItems, - uniqueItems: parameter.uniqueItems, - multipleOf: parameter.multipleOf - }; + schema = enjoi.schema(parameter.schema || parameter); + } - schema = enjoi.schema(template); + if (schema.type === 'object') { + schema = schema.unknown(allowUnknownProperties); } - if (parameter.type === 'array') { + if (schema.type === 'array') { schema = schema.single(true); } @@ -93,13 +77,16 @@ const create = function (options = {}) { parameter, schema, routeExt: function (request, h) { + const p = parameter.in === 'query' ? 'query' : 'params'; if (request[p][parameter.name] !== undefined) { request[p][parameter.name] = coerce && request[p][parameter.name] && coerce(request[p][parameter.name]); } + return h.continue; }, validate: function (value) { + const data = coerce && value && coerce(value); const result = schema.validate(data); @@ -108,6 +95,7 @@ const create = function (options = {}) { result.error.message = result.error.message.replace('value', parameter.name); result.error.details.forEach((detail) => { + detail.message = detail.message.replace('value', parameter.name); detail.path = [parameter.name]; }); @@ -121,6 +109,7 @@ const create = function (options = {}) { }; const makeResponseValidator = function (responses) { + const schemas = {}; for (const [code, response] of Object.entries(responses)) { @@ -130,20 +119,24 @@ const create = function (options = {}) { if (response.schema.type === 'array') { schema.single(true); } + schemas[code] = schema; } } } + return schemas; }; const makeAll = function (parameters = [], consumes, allowUnknownProperties = false) { + const routeExt = []; const validate = {}; const formValidators = {}; let headers = {}; const formValidator = function (value) { + const result = this.validate(value); if (result.error) { @@ -187,6 +180,7 @@ const create = function (options = {}) { if (!validate.payload && Object.keys(formValidators).length > 0) { validate.payload = async function (value) { + const results = {}; for (const [param, data] of Object.entries(value)) { @@ -218,6 +212,7 @@ const create = function (options = {}) { }; const pathsep = function (format) { + switch (format) { case 'csv': return ','; @@ -233,26 +228,38 @@ const pathsep = function (format) { }; const coercion = function (parameter, consumes) { + let fn; + let schemaToCoerce = parameter; + + if (!parameter.type && parameter.schema.type) { + schemaToCoerce = parameter.schema; + } - switch (parameter.type) { + switch (schemaToCoerce.type) { case 'array': fn = function (data) { + if (Array.isArray(data)) { return data; } + const sep = pathsep(parameter.collectionFormat || 'csv'); return data.split(sep); }; + break; case 'integer': case 'number': fn = function (data) { - if (parameter.format === 'int64') { + + if (schemaToCoerce.format === 'int64') { return data; } + return Number(data); }; + break; case 'string': //TODO: handle date, date-time, binary, byte formats. @@ -260,17 +267,21 @@ const coercion = function (parameter, consumes) { break; case 'boolean': fn = function (data) { + return (data === 'true') || (data === '1') || (data === true); }; + break; case 'file': { fn = function (data) { + return { value: data, consumes, in: parameter.in }; }; + break; } } diff --git a/package.json b/package.json index 78227c8..b0d249b 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,14 @@ "@hapi/hapi": "^19.1.1", "eslint": "^6.8.0", "nyc": "^15.0.0", + "onchange": "^7.0.2", "tape": "^5.0.0" }, "scripts": { "test": "tape test/*.js", "cover": "nyc npm test", - "lint": "eslint lib" + "lint": "eslint lib", + "watch": "onchange **/*.js* -- npm test" }, "license": "Apache-2.0", "main": "./lib/index", diff --git a/test.js b/test.js new file mode 100644 index 0000000..226b104 --- /dev/null +++ b/test.js @@ -0,0 +1,15 @@ +class Api { + constructor(authStrategies, routes) { + this.authStrategies = authStrategies; + this.routes = routes; + } +} + +class ApiRoute { + constructor(method, path, vhost, routeOptions) { + this.method = method; + this.path = path; + this.vhost = vhost; + this.routeOptions = routeOptions; + } +} diff --git a/test/fixtures/defs/form.json b/test/fixtures/defs/oas2/form.json similarity index 100% rename from test/fixtures/defs/form.json rename to test/fixtures/defs/oas2/form.json diff --git a/test/fixtures/defs/form_xoptions.json b/test/fixtures/defs/oas2/form_xoptions.json similarity index 99% rename from test/fixtures/defs/form_xoptions.json rename to test/fixtures/defs/oas2/form_xoptions.json index dc58e0b..b4caea5 100644 --- a/test/fixtures/defs/form_xoptions.json +++ b/test/fixtures/defs/oas2/form_xoptions.json @@ -58,4 +58,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/fixtures/defs/pets.json b/test/fixtures/defs/oas2/pets.json similarity index 100% rename from test/fixtures/defs/pets.json rename to test/fixtures/defs/oas2/pets.json diff --git a/test/fixtures/defs/pets.yaml b/test/fixtures/defs/oas2/pets.yaml similarity index 100% rename from test/fixtures/defs/pets.yaml rename to test/fixtures/defs/oas2/pets.yaml diff --git a/test/fixtures/defs/pets_authed.json b/test/fixtures/defs/oas2/pets_authed.json similarity index 100% rename from test/fixtures/defs/pets_authed.json rename to test/fixtures/defs/oas2/pets_authed.json diff --git a/test/fixtures/defs/pets_root_authed.json b/test/fixtures/defs/oas2/pets_root_authed.json similarity index 100% rename from test/fixtures/defs/pets_root_authed.json rename to test/fixtures/defs/oas2/pets_root_authed.json diff --git a/test/fixtures/defs/pets_xauthed.json b/test/fixtures/defs/oas2/pets_xauthed.json similarity index 98% rename from test/fixtures/defs/pets_xauthed.json rename to test/fixtures/defs/oas2/pets_xauthed.json index f018d96..8cb05a1 100644 --- a/test/fixtures/defs/pets_xauthed.json +++ b/test/fixtures/defs/oas2/pets_xauthed.json @@ -241,14 +241,14 @@ } }, "x-hapi-auth-schemes": { - "apiKey": "../lib/xauth-scheme.js" + "apiKey": "../../lib/xauth-scheme.js" }, "securityDefinitions": { "api_key": { - "x-hapi-auth-strategy": "../lib/xauth-strategy.js", + "x-hapi-auth-strategy": "../../lib/xauth-strategy.js", "type": "apiKey", "name": "authorization", "in": "header" } } -} \ No newline at end of file +} diff --git a/test/fixtures/defs/pets_xhandlers.json b/test/fixtures/defs/oas2/pets_xhandlers.json similarity index 98% rename from test/fixtures/defs/pets_xhandlers.json rename to test/fixtures/defs/oas2/pets_xhandlers.json index 8db6c2a..342ceb9 100644 --- a/test/fixtures/defs/pets_xhandlers.json +++ b/test/fixtures/defs/oas2/pets_xhandlers.json @@ -28,7 +28,7 @@ ], "paths": { "/pets": { - "x-hapi-handler": "../handlers/pets.js", + "x-hapi-handler": "../../handlers/pets.js", "get": { "description": "Returns all pets from the system that the user has access to", "operationId": "findPets", @@ -111,7 +111,7 @@ } }, "/pets/{id}": { - "x-hapi-handler": "../handlers/pets/{id}.js", + "x-hapi-handler": "../../handlers/pets/{id}.js", "get": { "description": "Returns a user based on a single ID, if the user does not have access to the pet", "operationId": "findPetById", @@ -228,4 +228,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/fixtures/defs/oas3/form.json b/test/fixtures/defs/oas3/form.json new file mode 100644 index 0000000..0e80c32 --- /dev/null +++ b/test/fixtures/defs/oas3/form.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Upload Test", + "description": "Form data validation" + }, + "servers": [ + { + "url": "http://example.com/v1/forms" + } + ], + "paths": { + "/upload": { + "post": { + "operationId": "uploadFile", + "description": "uploads a file", + "requestBody": { + "content": { + "application/x-www-form-unlencoded": { + "schema": { + "type": "object", + "required": ["upload"], + "properties": { + "upload": { + "type": "string", + "format": "binary" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "default response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "upload": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + } + } + } +} diff --git a/test/fixtures/defs/oas3/form_xoptions.json b/test/fixtures/defs/oas3/form_xoptions.json new file mode 100644 index 0000000..e384a79 --- /dev/null +++ b/test/fixtures/defs/oas3/form_xoptions.json @@ -0,0 +1,61 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Upload Test", + "description": "Form data validation" + }, + "servers": [ + { + "url": "http://example.com/v1/forms" + } + ], + "paths": { + "/upload": { + "post": { + "operationId": "uploadFile", + "description": "uploads a file", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["upload"], + "properties": { + "upload": { + "type": "string", + "format": "binary" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "x-hapi-options": { + "isInternal": true + }, + "responses": { + "200": { + "description": "default response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "upload": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + } + } + } + } +} diff --git a/test/fixtures/defs/oas3/pets.json b/test/fixtures/defs/oas3/pets.json new file mode 100644 index 0000000..067d2df --- /dev/null +++ b/test/fixtures/defs/oas3/pets.json @@ -0,0 +1,315 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + }, + "x-meta" : "test" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "paths": { + "/pets": { + "get": { + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "x-meta": "test" + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/html": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/newPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/test/fixtures/defs/oas3/pets.yaml b/test/fixtures/defs/oas3/pets.yaml new file mode 100644 index 0000000..d8852b6 --- /dev/null +++ b/test/fixtures/defs/oas3/pets.yaml @@ -0,0 +1,202 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore (Simple) + description: >- + A sample API that uses a petstore as an example to demonstrate features in + the swagger-2.0 specification + termsOfService: 'http://helloreverb.com/terms/' + contact: + name: Swagger API team + email: foo@example.com + url: 'http://swagger.io' + license: + name: MIT + url: 'http://opensource.org/licenses/MIT' +servers: + - url: http://petstore.swagger.io/v1/petstore +paths: + /pets: + get: + description: Returns all pets from the system that the user has access to + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/pet' + text/xml: + schema: + type: array + items: + $ref: '#/components/schemas/pet' + text/html: + schema: + type: array + items: + $ref: '#/components/schemas/pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/errorModel' + application/xml: + schema: + $ref: '#/components/schemas/errorModel' + text/xml: + schema: + $ref: '#/components/schemas/errorModel' + text/html: + schema: + $ref: '#/components/schemas/errorModel' + post: + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/newPet' + responses: + '200': + description: pet response + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/errorModel' + '/pets/{id}': + get: + description: >- + Returns a user based on a single ID, if the user does not have access to + the pet + operationId: findPetById + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: pet response + content: + application/json: + schema: + $ref: '#/components/schemas/pet' + application/xml: + schema: + $ref: '#/components/schemas/pet' + text/xml: + schema: + $ref: '#/components/schemas/pet' + text/html: + schema: + $ref: '#/components/schemas/pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/errorModel' + application/xml: + schema: + $ref: '#/components/schemas/errorModel' + text/xml: + schema: + $ref: '#/components/schemas/errorModel' + text/html: + schema: + $ref: '#/components/schemas/errorModel' + delete: + description: deletes a single pet based on the ID supplied + operationId: deletePet + parameters: + - name: id + in: path + description: ID of pet to delete + required: true + schema: + type: integer + format: int64 + responses: + '204': + description: pet deleted + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/errorModel' +components: + schemas: + pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + newPet: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + errorModel: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/test/fixtures/defs/oas3/pets_authed.json b/test/fixtures/defs/oas3/pets_authed.json new file mode 100644 index 0000000..7a36d67 --- /dev/null +++ b/test/fixtures/defs/oas3/pets_authed.json @@ -0,0 +1,346 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "paths": { + "/pets": { + "get": { + "security": [ + { + "api_key": [ + "api1:read" + ] + }, + { + "api_key2": [ + "api2:read" + ] + } + ], + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "custom-header", + "in": "header", + "description": "A custom header", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/html": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/newPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "authorization", + "in": "header" + }, + "api_key2": { + "type": "apiKey", + "name": "authorization", + "in": "header" + } + } + } +} diff --git a/test/fixtures/defs/oas3/pets_root_authed.json b/test/fixtures/defs/oas3/pets_root_authed.json new file mode 100644 index 0000000..d242445 --- /dev/null +++ b/test/fixtures/defs/oas3/pets_root_authed.json @@ -0,0 +1,119 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "security": [ + { + "api_key": [ + "api1:read" + ] + }, + { + "api_key2": [ + "api2:read" + ] + } + ], + "paths": { + "/pets": { + "get": { + "parameters": [ + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "api_key": { + "type": "apiKey", + "name": "authorization", + "in": "header" + }, + "api_key2": { + "type": "apiKey", + "name": "authorization", + "in": "header" + } + } + } +} diff --git a/test/fixtures/defs/oas3/pets_xauthed.json b/test/fixtures/defs/oas3/pets_xauthed.json new file mode 100644 index 0000000..40f9555 --- /dev/null +++ b/test/fixtures/defs/oas3/pets_xauthed.json @@ -0,0 +1,340 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the openapi-3.0.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "paths": { + "/pets": { + "get": { + "security": [ + { + "api_key": [ + "read" + ] + } + ], + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "custom-header", + "in": "header", + "description": "A custom header", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/html": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/newPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "api_key": { + "x-hapi-auth-strategy": "../../lib/xauth-strategy.js", + "type": "apiKey", + "name": "authorization", + "in": "header" + } + } + }, + "x-hapi-auth-schemes": { + "apiKey": "../../lib/xauth-scheme.js" + } +} diff --git a/test/fixtures/defs/oas3/pets_xauthed_bearer.json b/test/fixtures/defs/oas3/pets_xauthed_bearer.json new file mode 100644 index 0000000..5214076 --- /dev/null +++ b/test/fixtures/defs/oas3/pets_xauthed_bearer.json @@ -0,0 +1,337 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the openapi-3.0.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "paths": { + "/pets": { + "get": { + "security": [ + { + "http_bearer": [] + } + ], + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "custom-header", + "in": "header", + "description": "A custom header", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/html": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/newPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "http_bearer": { + "x-hapi-auth-strategy": "../../lib/xauth-strategy.js", + "type": "http", + "scheme": "bearer" + } + } + }, + "x-hapi-auth-schemes": { + "bearer": "../../lib/xauth-scheme.js" + } +} diff --git a/test/fixtures/defs/oas3/pets_xhandlers.json b/test/fixtures/defs/oas3/pets_xhandlers.json new file mode 100644 index 0000000..0021354 --- /dev/null +++ b/test/fixtures/defs/oas3/pets_xhandlers.json @@ -0,0 +1,315 @@ +{ + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1/petstore" + } + ], + "paths": { + "/pets": { + "x-hapi-handler": "../../handlers/pets.js", + "get": { + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + }, + "text/html": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pet" + } + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/newPet" + } + } + } + }, + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + }, + "/pets/{id}": { + "x-hapi-handler": "../../handlers/pets/{id}.js", + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/pet" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorModel" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} diff --git a/test/fixtures/lib/xauth-strategy.js b/test/fixtures/lib/xauth-strategy.js index 814d6e3..44817c3 100644 --- a/test/fixtures/lib/xauth-strategy.js +++ b/test/fixtures/lib/xauth-strategy.js @@ -2,7 +2,7 @@ const Boom = require('@hapi/boom'); -const register = function (server, { name, scheme, where, lookup }) { +const register = function (server, { name, scheme, where, lookup = 'authorization' }) { server.auth.strategy(name, scheme, { validate: async function (request) { const token = request.headers[lookup]; diff --git a/test/test-api-dto-mapper.js b/test/test-api-dto-mapper.js new file mode 100644 index 0000000..3276cb8 --- /dev/null +++ b/test/test-api-dto-mapper.js @@ -0,0 +1,802 @@ +'use strict'; + +const Test = require('tape'); +const Path = require('path'); +const Mapper = require('../lib/api-dto-mapper'); + +Test('api dto mapper', (t) => { + + const baseOas2Doc = { + swagger: '2.0', + info: { + title: 'oas2', + version: '1', + }, + }; + + const baseOas3Doc = { + openapi: '3.0.0', + info: { + title: 'oas3', + version: '1', + }, + }; + + t.test('maps relative basePath', async (t) => { + t.plan(2); + + const oas2 = { + ...baseOas2Doc, + basePath: '/basePath', + }; + const oas3 = { + ...baseOas3Doc, + servers: [{ url: '/basePath' }], + }; + + for (const api of [oas2, oas3]) { + const { basePath } = Mapper.toDto(api); + + t.equal(basePath, '/basePath', `${api.info.title} basePath was mapped`); + } + }); + + t.test('defaults basePath to empty string', async (t) => { + t.plan(2); + + for (const api of [baseOas2Doc, baseOas3Doc]) { + const { basePath } = Mapper.toDto(api); + + t.equal(basePath, '', `${api.info.title} basePath was mapped`); + } + }); + + t.test('maps "/" basePath to empty string', async (t) => { + t.plan(2); + + const oas2 = { + ...baseOas2Doc, + basePath: '/', + }; + const oas3 = { + ...baseOas3Doc, + servers: ['/'], + }; + + for (const api of [oas2, oas3]) { + const { basePath } = Mapper.toDto(api); + + t.equal(basePath, '', `${api.info.title} basePath was mapped`); + } + }); + + t.test('trims "/" from basePath', async (t) => { + t.plan(2); + + const oas2 = { + ...baseOas2Doc, + basePath: '/basePath/', + }; + const oas3 = { + ...baseOas3Doc, + servers: [{ url: '/basePath/' }], + } + + for (const api of [oas2, oas3]) { + const { basePath } = Mapper.toDto(api); + + t.equal(basePath, '/basePath', `${api.info.title} basePath was trimmed`); + } + }); + + t.test('prefixes basePath with "/"', async (t) => { + t.plan(2); + + const oas2 = { + ...baseOas2Doc, + basePath: 'basePath', + }; + const oas3 = { + ...baseOas3Doc, + servers: [{ url: 'basePath' }], + }; + + for (const api of [oas2, oas3]) { + const { basePath } = Mapper.toDto(api); + + t.equal(basePath, '/basePath', `${api.info.title} basePath was prefixed`); + } + }); + + t.test('maps OAS3 basePath from full url', async (t) => { + t.plan(1); + + const oas3 = { + ...baseOas3Doc, + servers: [{ url: 'http://example.com/basePath' }], + }; + + const { basePath } = Mapper.toDto(oas3); + + t.equal(basePath, '/basePath', 'oas3 basePath was mapped'); + }); + + t.test('defaults OAS3 basePath to "/" if multiple servers', async (t) => { + t.plan(1); + + const oas3 = { + ...baseOas3Doc, + servers: [{ url: '/server1' }, { url: '/server2' }], + }; + + const { basePath } = Mapper.toDto(oas3); + + t.equal(basePath, '', 'oas3 basePath was mapped'); + }); + + t.test('maps empty array when no authentication schemes', async (t) => { + t.plan(4); + + for (const api of [baseOas2Doc, baseOas3Doc]) { + const { customAuthSchemes } = Mapper.toDto(api, 'baseDir'); + + t.ok(Array.isArray(customAuthSchemes), `${api.info.title} customAuthSchemes is an array`); + t.notOk(customAuthSchemes.length, `${api.info.title} customAuthSchemes is empty`); + } + }); + + t.test('maps custom authentication schemes', async (t) => { + t.plan(2); + + const baseDir = 'baseDir'; + const authSchemes = { + apiKey: 'pathToApiKeyScheme', + oauth2: 'pathToOAuth2Scheme', + }; + const oas2 = { ...baseOas2Doc, 'x-hapi-auth-schemes': authSchemes }; + const oas3 = { ...baseOas3Doc, 'x-hapi-auth-schemes': authSchemes }; + + const expectedAuthSchemes = [ + { scheme: 'apiKey', path: Path.join(baseDir, 'pathToApiKeyScheme') }, + { scheme: 'oauth2', path: Path.join(baseDir, 'pathToOAuth2Scheme') }, + ]; + + for (const api of [oas2, oas3]) { + const { customAuthSchemes } = Mapper.toDto(api, baseDir); + + t.deepEqual(customAuthSchemes, expectedAuthSchemes, `${api.info.title} auth schemes were mapped`); + } + }); + + t.test('maps empty object when no authentication strategies', async (t) => { + t.plan(4); + + for (const api of [baseOas2Doc, baseOas3Doc]) { + const { customAuthStrategies } = Mapper.toDto(api); + + t.ok(Array.isArray(customAuthStrategies), `${api.info.title} customAuthStrategies is an array`); + t.notOk(customAuthStrategies.length, `${api.info.title} is empty`); + } + }); + + t.test('maps custom authentication strategies', async (t) => { + t.plan(2); + + const baseDir = 'baseDir'; + const authStrategies = { + api_key1: { + 'x-hapi-auth-strategy': 'path_to_api_key1_strategy', + type: 'apiKey', + name: 'authorization', + in: 'header', + }, + api_key2: { + 'x-hapi-auth-strategy': 'path_to_api_key2_strategy', + type: 'apiKey', + name: 'api_key_query', + in: 'query', + }, + }; + const oas2 = { ...baseOas2Doc, securityDefinitions: authStrategies }; + const oas3 = { ...baseOas3Doc, components: { securitySchemes: authStrategies } }; + const expectedAuthStrategies = [ + { + strategy: 'api_key1', + config: { + path: Path.join(baseDir, 'path_to_api_key1_strategy'), + type: 'apiKey', + name: 'authorization', + in: 'header', + }, + }, + { + strategy: 'api_key2', + config: { + path: Path.join(baseDir, 'path_to_api_key2_strategy'), + type: 'apiKey', + name: 'api_key_query', + in: 'query', + }, + }, + ]; + + for (const api of [oas2, oas3]) { + const { customAuthStrategies } = Mapper.toDto(api, baseDir); + + t.deepEqual(customAuthStrategies, expectedAuthStrategies, `${api.info.title} customAuthStrategies were mapped`); + } + }); + + t.test('operation mapping', async (t) => { + + t.test('maps multiple paths', async (t) => { + t.plan(2); + + const paths = { + '/testPath1': { + get: {}, + }, + '/testPath2': { + post: {}, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedOperations = [ + { + path: '/testPath1', + method: 'get', + }, + { + path: '/testPath2', + method: 'post', + }, + ]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + // for this test we only care about the path & method to see that the operation was mapped + const actual = operations.map(({ path, method }) => ({ path, method })); + + t.deepEqual(actual, expectedOperations, `${api.info.title} operations were mapped`); + } + }); + + t.test('maps multiple operations from same path', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: {}, + post: {}, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedOperations = [ + { + path: '/testPath', + method: 'get', + }, + { + path: '/testPath', + method: 'post', + }, + ]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + // for this test we only care about the path & method to see that the operation was mapped + const actual = operations.map(({ path, method }) => ({ path, method })); + + t.deepEqual(actual, expectedOperations, `${api.info.title} operations were mapped`); + } + }); + + t.test('maps tags for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + tags: ['tag1', 'tag2'], + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for(const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].tags, ['tag1', 'tag2'], `${api.info.title} tags were mapped`); + } + }); + + t.test('maps description for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + description: 'test operation description', + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.equal(operations[0].description, 'test operation description', `${api.info.title} description was mapped`); + } + }); + + t.test('maps operationId for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { operationId: 'testOperationId' }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.equal(operations[0].operationId, 'testOperationId', `${api.info.title} operationId was mapped`); + } + }); + + t.test('maps x-hapi-options for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + 'x-hapi-options': { + isInternal: true, + }, + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].customOptions, { isInternal: true }, `${api.info.title} x-hapi-options were mapped`); + } + }); + + t.test('maps security for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + security: [ + { api_key: [] }, + ], + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedSecurity = [{ strategy: 'api_key', scopes: [] }]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].security, expectedSecurity, `${api.info.title} security was mapped`); + } + }); + + t.test('maps multiple security array items for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + security: [ + { api_key1: ['api1_read'] }, + { api_key2: ['api2_read','api2_write'] }, + ], + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedSecurity = [ + { strategy: 'api_key1', scopes: ['api1_read'] }, + { strategy: 'api_key2', scopes: ['api2_read','api2_write'] }, + ]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].security, expectedSecurity, `${api.info.title} security was mapped`); + } + }); + + t.test('maps multiple security types in single array item for operation', async (t) => { + t.plan(2); + + const paths = { + '/testPath': { + get: { + security: [{ + api_key1: ['api1_read'], + api_key2: ['api2_read','api2_write'], + }], + }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedSecurity = [ + { strategy: 'api_key1', scopes: ['api1_read'] }, + { strategy: 'api_key2', scopes: ['api2_read','api2_write'] }, + ]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].security, expectedSecurity, `${api.info.title} security was mapped`); + } + }); + + t.test('maps global security to operation', async (t) => { + t.plan(2); + + const security = [{ api_key: [] }]; + const paths = { '/testPath': { get: {} } }; + const oas2 = { ...baseOas2Doc, security, paths }; + const oas3 = { ...baseOas3Doc, security, paths }; + const expectedSecurity = [{ strategy: 'api_key', scopes: [] }]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].security, expectedSecurity, `${api.info.title} security was mapped`); + } + }); + + t.test('operation security overrides global securiity', async (t) => { + t.plan(2); + + const security = [{ api_key1: ['api1_read'] }]; + const paths = { + '/testPath': { + get: { + security: [{ + api_key2: ['api2_read', 'api2_write'], + }], + }, + }, + }; + const oas2 = { ...baseOas2Doc, security, paths }; + const oas3 = { ...baseOas3Doc, security, paths }; + const expectedSecurity = [ + { strategy: 'api_key2', scopes: ['api2_read','api2_write'] }, + ]; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].security, expectedSecurity, `${api.info.title} security was mapped`); + } + }); + + t.test('maps x-hapi-handler', async (t) => { + t.plan(2); + + const baseDir = 'baseDir'; + const paths = { + '/testPath': { + 'x-hapi-handler': 'pathToHandler', + get: {}, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api, baseDir); + + t.equal(operations[0].handler, Path.join(baseDir, 'pathToHandler'), `${api.info.title} handler was mapped`); + } + }); + + t.test('maps x-hapi-handler to multiple operations', async (t) => { + t.plan(4); + + const baseDir = 'baseDir'; + const paths = { + '/testPath': { + 'x-hapi-handler': 'pathToHandler', + get: {}, + post: {}, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api, baseDir); + + const getOperation = operations.find(op => op.method === 'get'); + const postOperation = operations.find(op => op.method === 'post'); + + t.equal(getOperation.handler, Path.join(baseDir, 'pathToHandler'), `${api.info.title} get handler was mapped`); + t.equal(postOperation.handler, Path.join(baseDir, 'pathToHandler'), `${api.info.title} post handler was mapped`); + } + }); + + t.test('maps request media type for operation', async (t) => { + t.plan(2); + + const oas2 = { + ...baseOas2Doc, + paths: { + '/testPath': { + post: { consumes: ['requestMediaType'] }, + }, + }, + }; + const oas3 = { + ...baseOas3Doc, + paths: { + '/testPath': { + post: { requestBody: { content: { 'requestMediaType': {} } } }, + }, + }, + }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].mediaTypes.request, ['requestMediaType'], `${api.info.title} request media type was mapped`); + } + }); + + t.test('maps global request media type to operation', async (t) => { + t.plan(1); + + const consumes = ['requestMediaType']; + const paths = { + '/testPath': { + get: { }, + }, + }; + // only oas2 here - oas3 does not support global request media types + const api = { ...baseOas2Doc, consumes, paths }; + + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].mediaTypes.request, ['requestMediaType'], 'request media type was mapped'); + }); + + t.test('operation overrides global request media type', async (t) => { + t.plan(1); + + const consumes = ['globalRequestMediaType']; + const paths = { + '/testPath': { + get: { consumes: ['operationRequestMediaType'] }, + }, + }; + const api = { ...baseOas2Doc, consumes, paths }; + + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].mediaTypes.request, ['operationRequestMediaType'], 'request media type was mapped'); + }); + + t.test('maps parameters for operation', async (t) => { + t.plan(2); + + const parameters = [ + { + in: 'header', + name: 'testParameter', + type: 'integer', + }, + ]; + const paths = { + '/testPath': { + get: { parameters }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].parameters, parameters, `${api.info.title} parameters were mapped`); + } + }); + + t.test('maps parameters for path', async (t) => { + t.plan(2); + + const parameters = [ + { + in: 'header', + name: 'testParameter', + type: 'integer', + }, + ]; + const paths = { + '/testPath': { + parameters, + get: { }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].parameters, parameters, `${api.info.title} parameters were mapped`); + } + }); + + t.test('combines path and operation parameters', async (t) => { + t.plan(2); + + const pathParameters = [ + { + in: 'header', + name: 'testParameter1', + type: 'integer', + }, + ]; + const operationParameters = [ + { + in: 'header', + name: 'testParameter2', + type: 'integer', + }, + ]; + const paths = { + '/testPath': { + parameters: pathParameters, + get: { parameters: operationParameters }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].parameters, [...pathParameters, ...operationParameters], `${api.info.title} parameters were mapped`); + } + }); + + t.test('maps oas3 requestBody to parameters', async (t) => { + t.plan(1); + + const paths = { + '/testPath': { + post: { + requestBody: { + required: true, + content: { + 'application/json': { + schema: { type: 'object' }, + }, + }, + }, + }, + }, + }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedParameter = { name: 'payload', in: 'body', required: true, schema: { type: 'object' } }; + + const { operations } = Mapper.toDto(oas3); + + t.deepEqual(operations[0].parameters, [expectedParameter], 'oas3 requestBody was mapped to parameters'); + }); + + t.test('maps responses for operation', async (t) => { + t.plan(2); + + const responses = { + '200': { description: 'OK' }, + '404': { description: 'Not Found' }, + }; + const paths = { + '/testPath': { + get: { responses }, + }, + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const { operations } = Mapper.toDto(api); + + t.deepEqual(operations[0].responses, responses, `${api.info.title} responses were mapped`); + } + }); + + t.test('maps oas3 response content to response schema', async (t) => { + t.plan(1); + + const responses = { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { type: 'object' }, + }, + }, + }, + }; + const paths = { + '/testPath': { + get: { responses }, + }, + }; + const oas3 = { ...baseOas3Doc, paths }; + const expectedResponses = { + '200': { + description: 'OK', + schema: { type: 'object' }, + }, + }; + + const { operations } = Mapper.toDto(oas3); + + t.deepEqual(operations[0].responses, expectedResponses, 'oas3 response body mapped to schema'); + }); + + t.test('get operation by method and path', async (t) => { + t.plan(8); + + const paths = { + '/testPath1': { + get: { + description: 'GET testPath1', + response: { '200': { description: 'OK' } } + }, + post: { + description: 'POST testPath1', + response: { '200': { description: 'OK' } } + }, + }, + '/testPath2': { + get: { + description: 'GET testPath2', + response: { '200': { description: 'OK' } } + }, + post: { + description: 'POST testPath2', + response: { '200': { description: 'OK' } } + }, + } + }; + const oas2 = { ...baseOas2Doc, paths }; + const oas3 = { ...baseOas3Doc, paths }; + + for (const api of [oas2, oas3]) { + const dto = Mapper.toDto(api); + + t.equal(dto.getOperation('get', '/testPath1').description, 'GET testPath1', `${api.info.title} retrieved GET /testPath1`); + t.equal(dto.getOperation('post', '/testPath1').description, 'POST testPath1', `${api.info.title} retrieved POST /testPath1`); + t.equal(dto.getOperation('get', '/testPath2').description, 'GET testPath2', `${api.info.title} retrieved GET /testPath2`); + t.equal(dto.getOperation('post', '/testPath2').description, 'POST testPath2', `${api.info.title} retrieved POST /testPath2`); + } + }); + }); +}); diff --git a/test/test-auth.js b/test/test-auth.js index 7a21322..2bf3f23 100644 --- a/test/test-auth.js +++ b/test/test-auth.js @@ -6,161 +6,205 @@ const OpenAPI = require('../lib'); const Hapi = require('@hapi/hapi'); const StubAuthTokenScheme = require('./fixtures/lib/stub-auth-token-scheme'); -Test('authentication', function (t) { +Test('authentication', (t) => { const buildValidateFunc = function (allowedToken) { - return async function (token) { + return function (token) { if (token === allowedToken) { - return { credentials: { scope: [ 'api1:read' ] }, artifacts: { }}; + return { credentials: { scope: ['api1:read'] }, artifacts: { } }; } return {}; - } + }; }; - t.test('token authentication', async function (t) { - t.plan(2); + for (const schemaVersion of ['oas2', 'oas3']) { - const server = new Hapi.Server(); + t.test('token authentication', async (t) => { + t.plan(2); - try { - await server.register({ plugin: StubAuthTokenScheme }); + const server = new Hapi.Server(); - server.auth.strategy('api_key', 'stub-auth-token', { - validateFunc: buildValidateFunc('12345') - }); + try { + await server.register({ plugin: StubAuthTokenScheme }); - server.auth.strategy('api_key2', 'stub-auth-token', { - validateFunc: buildValidateFunc('98765') - }); + server.auth.strategy('api_key', 'stub-auth-token', { + validateFunc: buildValidateFunc('12345') + }); - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets_authed.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); + server.auth.strategy('api_key2', 'stub-auth-token', { + validateFunc: buildValidateFunc('98765') + }); - let response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets' - }); + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets_authed.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - t.strictEqual(response.statusCode, 401, `${response.request.path} unauthenticated.`); + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); - response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets', - headers: { - authorization: '12345', - 'custom-header': 'Hello' - } - }); + t.strictEqual(response.statusCode, 401, `${schemaVersion} ${response.request.path} unauthenticated.`); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK when authorized and authenticated.`); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('unauthorized', async function (t) { - t.plan(1); - - const server = new Hapi.Server(); - - try { - await server.register({ plugin: StubAuthTokenScheme }); - - server.auth.strategy('api_key', 'stub-auth-token', { - validateFunc: async function (token) { - return { credentials: { scope: [ 'api3:read' ] }, artifacts: { }}; - } - }); - - server.auth.strategy('api_key2', 'stub-auth-token', { - validateFunc: () => ({ isValid: true }) - }); + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets', + headers: { + authorization: '12345', + 'custom-header': 'Hello' + } + }); - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets_authed.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); - - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets', - headers: { - authorization: '12345' - } - }); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK when authorized and authenticated.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('unauthorized', async (t) => { + t.plan(1); + + const server = new Hapi.Server(); + + try { + await server.register({ plugin: StubAuthTokenScheme }); + + server.auth.strategy('api_key', 'stub-auth-token', { + validateFunc: function (token) { + return { credentials: { scope: ['api3:read'] }, artifacts: { } }; + } + }); + + server.auth.strategy('api_key2', 'stub-auth-token', { + validateFunc: () => ({ isValid: true }) + }); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets_authed.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); + + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets', + headers: { + authorization: '12345' + } + }); + + t.strictEqual(response.statusCode, 403, `${schemaVersion} ${response.request.path} unauthorized.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('with root auth', async (t) => { + t.plan(1); + + const server = new Hapi.Server(); + + try { + await server.register({ plugin: StubAuthTokenScheme }); + + server.auth.strategy('api_key', 'stub-auth-token', { + validateFunc: function (token) { + return { credentials: { scope: ['api3:read'] }, artifacts: { } }; + } + }); + + server.auth.strategy('api_key2', 'stub-auth-token', { + validateFunc: () => ({ isValid: true }) + }); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets_root_authed.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); + + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets', + headers: { + authorization: '12345' + } + }); + + t.strictEqual(response.statusCode, 403, `${schemaVersion} ${response.request.path} unauthorized.`); + } + catch (error) { + t.fail(error.message); + } + }); + } +}); - t.strictEqual(response.statusCode, 403, `${response.request.path} unauthorized.`); - } - catch (error) { - t.fail(error.message); - } - }); +Test('authentication with x-auth', (t) => { - t.test('with root auth', async function (t) { - t.plan(1); + for (const schemaVersion of ['oas2', 'oas3']) { - const server = new Hapi.Server(); + t.test('authenticated', async (t) => { + t.plan(2); - try { - await server.register({ plugin: StubAuthTokenScheme }); + const server = new Hapi.Server(); - server.auth.strategy('api_key', 'stub-auth-token', { - validateFunc: async function (token) { - return { credentials: { scope: [ 'api3:read' ] }, artifacts: { }}; - } - }); + try { - server.auth.strategy('api_key2', 'stub-auth-token', { - validateFunc: () => ({ isValid: true }) - }); + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets_xauthed.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets_root_authed.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets', - headers: { - authorization: '12345' - } - }); + t.strictEqual(response.statusCode, 401, `${schemaVersion} ${response.request.path} unauthenticated.`); - t.strictEqual(response.statusCode, 403, `${response.request.path} unauthorized.`); - } - catch (error) { - t.fail(error.message); - } - }); -}); + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets', + headers: { + authorization: '12345' + } + }); -Test('authentication with x-auth', function (t) { + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK when authorized and authenticated.`); + } + catch (error) { + t.fail(error.message); + } + }); + } - t.test('authenticated', async function (t) { + t.test('oas3 bearer authentication', async (t) => { t.plan(2); const server = new Hapi.Server(); try { + await server.register({ plugin: OpenAPI, options: { - api: Path.join(__dirname, './fixtures/defs/pets_xauthed.json'), + api: Path.join(__dirname, `./fixtures/defs/oas3/pets_xauthed_bearer.json`), handlers: Path.join(__dirname, './fixtures/handlers') } }); @@ -170,7 +214,7 @@ Test('authentication with x-auth', function (t) { url: '/v1/petstore/pets' }); - t.strictEqual(response.statusCode, 401, `${response.request.path} unauthenticated.`); + t.strictEqual(response.statusCode, 401, `oas3 ${response.request.path} unauthenticated.`); response = await server.inject({ method: 'GET', @@ -180,11 +224,10 @@ Test('authentication with x-auth', function (t) { } }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK when authorized and authenticated.`); + t.strictEqual(response.statusCode, 200, `oas3 ${response.request.path} OK when authorized and authenticated.`); } catch (error) { t.fail(error.message); } }); - }); diff --git a/test/test-forms.js b/test/test-forms.js index d8b8fa6..f9ff25b 100644 --- a/test/test-forms.js +++ b/test/test-forms.js @@ -8,116 +8,119 @@ const Hapi = require('@hapi/hapi'); Test('form data', function (t) { - t.test('upload', async function (t) { - t.plan(1); - - try { - const server = new Hapi.Server(); - - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/form.json'), - handlers: { - upload: { - post: function (req, h) { - return { - upload: req.payload.toString() - }; + // TODO: figure out the content-type issue for oas3. It may have to do with not using a 'form' type + for (const schemaVersion of ['oas2']) { + t.test('upload', async function (t) { + t.plan(1); + + try { + const server = new Hapi.Server(); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/form.json`), + handlers: { + upload: { + post: function (req, h) { + return { + upload: req.payload.toString() + }; + } } - } + }, + outputvalidation: true + } + }); + + const response = await server.inject({ + method: 'POST', + url: '/v1/forms/upload', + headers: { + 'content-type': 'application/x-www-form-urlencoded' }, - outputvalidation: true - } - }); - - const response = await server.inject({ - method: 'POST', - url: '/v1/forms/upload', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - payload: 'name=thing&upload=data' - }); - - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } - - }); - - t.test('bad content type', async function (t) { - t.plan(1); - - try { - const server = new Hapi.Server(); - - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/form.json'), - handlers: { - upload: { - post: function (req, h) { - return ''; + payload: 'name=thing&upload=data' + }); + + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } + + }); + + t.test('bad content type', async function (t) { + t.plan(1); + + try { + const server = new Hapi.Server(); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/form.json`), + handlers: { + upload: { + post: function (req, h) { + return ''; + } } } } - } - }); - - const response = await server.inject({ - method: 'POST', - url: '/v1/forms/upload', - payload: 'name=thing&upload=data' - }); - - t.strictEqual(response.statusCode, 415, `${response.request.path} unsupported media type.`); - } - catch (error) { - t.fail(error.message); - } - - }); - - - t.test('invalid payload', async function (t) { - t.plan(1); - - try { - const server = new Hapi.Server(); - - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/form.json'), - handlers: { - upload: { - post: function (req, h) { - return; + }); + + const response = await server.inject({ + method: 'POST', + url: '/v1/forms/upload', + payload: 'name=thing&upload=data' + }); + + t.strictEqual(response.statusCode, 415, `${schemaVersion} ${response.request.path} unsupported media type.`); + } + catch (error) { + t.fail(error.message); + } + + }); + + + t.test('invalid payload', async function (t) { + t.plan(1); + + try { + const server = new Hapi.Server(); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/form.json`), + handlers: { + upload: { + post: function (req, h) { + return; + } } } } - } - }); - - const response = await server.inject({ - method: 'POST', - url: '/v1/forms/upload', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - payload: 'name=thing&upload=' - }); - - t.strictEqual(response.statusCode, 400, `${response.request.path} validation error.`); - } - catch (error) { - t.fail(error.message); - } - - }); + }); + + const response = await server.inject({ + method: 'POST', + url: '/v1/forms/upload', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + payload: 'name=thing&upload=' + }); + + t.strictEqual(response.statusCode, 400, `${schemaVersion} ${response.request.path} validation error.`); + } + catch (error) { + t.fail(error.message); + } + + }); + } }); diff --git a/test/test-hapi-openapi.js b/test/test-hapi-openapi.js index 4a606af..c934420 100644 --- a/test/test-hapi-openapi.js +++ b/test/test-hapi-openapi.js @@ -7,828 +7,844 @@ const Hapi = require('@hapi/hapi'); Test('test plugin', function (t) { - t.test('register', async function (t) { - t.plan(3); + for (const schemaVersion of ['oas2', 'oas3']) { - const server = new Hapi.Server(); + t.test('register', async function (t) { + t.plan(3); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); - t.ok(server.plugins.openapi.getApi, 'server.plugins.openapi.api exists.'); - t.ok(server.plugins.openapi.setHost, 'server.plugins.openapi.setHost exists.'); - - server.plugins.openapi.setHost('api.paypal.com'); - - t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', 'server.plugins.openapi.setHost set host.'); - } - catch (error) { - t.fail(error.message); - } - - }); - - t.test('register with cors options', async function (t) { - t.plan(3); - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - cors: { - origin: ["*"], - maxAge: 86400, - headers: ["Accept", "Authorization", "Content-Type", "If-None-Match"], - exposedHeaders: ["x-count", "link"] + const server = new Hapi.Server(); + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers') } - } - }); - t.ok(server.plugins.openapi.getApi, 'server.plugins.openapi.api exists.'); - t.ok(server.plugins.openapi.setHost, 'server.plugins.openapi.setHost exists.'); + }); + t.ok(server.plugins.openapi.getApi, `${schemaVersion} server.plugins.openapi.api exists.`); + t.ok(server.plugins.openapi.setHost, `${schemaVersion} server.plugins.openapi.setHost exists.`); - server.plugins.openapi.setHost('api.paypal.com'); + server.plugins.openapi.setHost('api.paypal.com'); - t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', 'server.plugins.openapi.setHost set host.'); - } - catch (error) { - t.fail(error.message); - } + t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', `${schemaVersion} server.plugins.openapi.setHost set host.`); + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('register with boolean cors', async function (t) { - t.plan(3); + t.test('register with cors options', async function (t) { + t.plan(3); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - cors: true - } - }); - t.ok(server.plugins.openapi.getApi, 'server.plugins.openapi.api exists.'); - t.ok(server.plugins.openapi.setHost, 'server.plugins.openapi.setHost exists.'); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + cors: { + origin: ["*"], + maxAge: 86400, + headers: ["Accept", "Authorization", "Content-Type", "If-None-Match"], + exposedHeaders: ["x-count", "link"] + } + } + }); + t.ok(server.plugins.openapi.getApi, `${schemaVersion} server.plugins.openapi.api exists.`); + t.ok(server.plugins.openapi.setHost, `${schemaVersion} server.plugins.openapi.setHost exists.`); - server.plugins.openapi.setHost('api.paypal.com'); + server.plugins.openapi.setHost('api.paypal.com'); - t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', 'server.plugins.openapi.setHost set host.'); - } - catch (error) { - t.fail(error.message); - } + t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', `${schemaVersion} server.plugins.openapi.setHost set host.`); + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('register with object api', async function (t) { - t.plan(3); + t.test('register with boolean cors', async function (t) { + t.plan(3); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Test Object API', - version: '1.0.0' - }, - host: 'example.com', - consumes: [ - 'application/json' - ], - produces: [ - 'application/json' - ], - paths: { - '/test': { - get: { - operationId: 'testGet', - responses: { - 200: { - description: 'default response' + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + cors: true + } + }); + t.ok(server.plugins.openapi.getApi, `${schemaVersion} server.plugins.openapi.api exists.`); + t.ok(server.plugins.openapi.setHost, `${schemaVersion} server.plugins.openapi.setHost exists.`); + + server.plugins.openapi.setHost('api.paypal.com'); + + t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', `${schemaVersion} server.plugins.openapi.setHost set host.`); + } + catch (error) { + t.fail(error.message); + } + + }); + + t.test('register with object api', async function (t) { + t.plan(3); + + const server = new Hapi.Server(); + + const schemas = {}; + schemas.oas2 = { + swagger: '2.0', + info: { + title: 'Test Object API', + version: '1.0.0' + }, + host: 'example.com', + consumes: [ + 'application/json' + ], + produces: [ + 'application/json' + ], + paths: { + '/test': { + get: { + operationId: 'testGet', + responses: { + 200: { + description: 'default response' + } } } } } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get(request, h) { - return; + schemas.oas3 = { + openapi: '3.0.0', + info: { + title: 'Test Object API', + version: '1.0.0' + }, + servers: [ + { + url: 'example.com' + } + ], + paths: { + '/test': { + get: { + operationId: 'testGet', + responses: { + 200: { + description: 'default response' + } } } } } - }); + } - t.ok(server.plugins.openapi.getApi, 'server.plugins.openapi.api exists.'); - t.ok(server.plugins.openapi.setHost, 'server.plugins.openapi.setHost exists.'); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: schemas[schemaVersion], + handlers: { + test: { + get(request, h) { + return; + } + } + } + } + }); - server.plugins.openapi.setHost('api.paypal.com'); + t.ok(server.plugins.openapi.getApi, `${schemaVersion} server.plugins.openapi.api exists.`); + t.ok(server.plugins.openapi.setHost, `${schemaVersion} server.plugins.openapi.setHost exists.`); - t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', 'server.plugins.openapi.setHost set host.'); - } - catch (error) { - console.log(error.stack) - t.fail(error.message); - } + server.plugins.openapi.setHost('api.paypal.com'); - }); + t.strictEqual(server.plugins.openapi.getApi().host, 'api.paypal.com', `${schemaVersion} server.plugins.openapi.setHost set host.`); + } + catch (error) { + console.log(error.stack) + t.fail(error.message); + } - t.test('register with optional query parameters does not change "request.orig"', async function (t) { - t.plan(1); + }); - const server = new Hapi.Server(); + t.test('register with optional query parameters does not change "request.orig"', async function (t) { + t.plan(1); - const api = { - swagger: '2.0', - info: { - title: 'Test Optional Query Params', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - parameters: [ - { - name: 'optionalParameter', - in: 'query', - required: false, - type: 'string', - }, - ], - responses: { - 200: { - description: 'OK' - } + const server = new Hapi.Server(); + + const schemas = { + oas2: { + version: { swagger: '2.0' }, + parameter: { + name: 'optionalParameter', + in: 'query', + required: false, + type: 'string', + } + }, + oas3: { + version: { openapi: '3.0.0' }, + parameter: { + name: 'optionalParameter', + in: 'query', + required: false, + schema: { + type: 'string' } } } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get(request, h) { - return request.orig; + const api = { + ...schemas[schemaVersion].version, + info: { + title: 'Test Optional Query Params', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + parameters: [schemas[schemaVersion].parameter], + responses: { + 200: { + description: 'OK' + } } } } } - }); + }; - const { result } = await server.inject({ - method: 'GET', - url: '/test' - }); - t.ok(Object.entries(result.query).length === 0, 'request.orig was not modified'); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('api docs', async function (t) { - t.plan(3); - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + get(request, h) { + return request.orig; + } + } + } + } + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/api-docs' - }); + const { result } = await server.inject({ + method: 'GET', + url: '/test' + }); + t.ok(Object.entries(result.query).length === 0, `${schemaVersion} request.orig was not modified`); + } + catch (error) { + t.fail(error.message); + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.test('api docs', async function (t) { + t.plan(3); - const body = JSON.parse(response.payload); + const server = new Hapi.Server(); - t.equal(body.info['x-meta'], undefined, 'stripped x-'); - t.equal(body.paths['/pets'].get.parameters[0]['x-meta'], undefined, 'stripped x- from array.'); - } - catch (error) { - t.fail(error.message); - } - }); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - t.test('api docs strip vendor extensions false', async function (t) { - t.plan(3); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/api-docs' + }); - const server = new Hapi.Server(); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - docs: { - stripExtensions: false + const body = JSON.parse(response.payload); + + t.equal(body.info['x-meta'], undefined, `${schemaVersion} stripped x-`); + t.equal(body.paths['/pets'].get.parameters[0]['x-meta'], undefined, `${schemaVersion} stripped x- from array.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('api docs strip vendor extensions false', async function (t) { + t.plan(3); + + const server = new Hapi.Server(); + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + docs: { + stripExtensions: false + } } - } - }); + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/api-docs' - }); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/api-docs' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - const body = JSON.parse(response.payload); + const body = JSON.parse(response.payload); - t.equal(body.info['x-meta'], 'test'); - t.equal(body.paths['/pets'].get.parameters[0]['x-meta'], 'test'); - } - catch (error) { - t.fail(error.message); - } - }); + t.equal(body.info['x-meta'], 'test'); + t.equal(body.paths['/pets'].get.parameters[0]['x-meta'], 'test'); + } + catch (error) { + t.fail(error.message); + } + }); - t.test('api docs auth false', async function (t) { - t.plan(1); + t.test('api docs auth false', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - docs: { - auth: false + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + docs: { + auth: false + } } - } - }); + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/api-docs' - }); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/api-docs' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('api docs change path', async function (t) { - t.plan(1); - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - docs: { - path: '/spec' + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('api docs change path', async function (t) { + t.plan(1); + + const server = new Hapi.Server(); + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + docs: { + path: '/spec' + } } - } - }); + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/spec' - }); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/spec' + }); + + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('api docs change path (with no basepath prefix)', async function (t) { + t.plan(1); + + const server = new Hapi.Server(); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('api docs change path (with no basepath prefix)', async function (t) { - t.plan(1); - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - docs: { - path: '/spec', - prefixBasePath: false + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + docs: { + path: '/spec', + prefixBasePath: false + } } - } - }); + }); - const response = await server.inject({ - method: 'GET', - url: '/spec' - }); + const response = await server.inject({ + method: 'GET', + url: '/spec' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('api docs change path old way', async function (t) { - t.plan(1); - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - docspath: '/spec' - } - }); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/spec' - }); + t.test('api docs change path old way', async function (t) { + t.plan(1); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } - }); + const server = new Hapi.Server(); - t.test('minimal api spec support', async function (t) { - t.plan(1); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + docspath: '/spec' + } + }); - const server = new Hapi.Server(); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/spec' + }); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - responses: { - 200: { - description: 'default response' + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } + }); + + t.test('minimal api spec support', async function (t) { + t.plan(1); + + const server = new Hapi.Server(); + + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + const api = { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + responses: { + 200: { + description: 'default response' + } } } } } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get(request, h) { - return 'test'; + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } - } - }); + }); - let response = await server.inject({ - method: 'GET', - url: '/test' - }); + let response = await server.inject({ + method: 'GET', + url: '/test' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('trailing slashes', async function (t) { - t.plan(1); + t.test('trailing slashes', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test/': { - get: { - responses: { - 200: { - description: 'default response' + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + const api = { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test/': { + get: { + responses: { + 200: { + description: 'default response' + } } } } } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get(request, h) { - return 'test'; + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } - } - }); + }); - let response = await server.inject({ - method: 'GET', - url: '/test/' - }); + let response = await server.inject({ + method: 'GET', + url: '/test/' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('routes', async function (t) { - t.plan(5); + t.test('routes', async function (t) { + t.plan(5); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - let response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets' - }); + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'POST', - url: '/v1/petstore/pets', - payload: { - id: '0', - name: 'Cat' - } - }); + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + id: '0', + name: 'Cat' + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'POST', - url: '/v1/petstore/pets', - payload: { - name: 123 - } - }); + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + name: 123 + } + }); - t.strictEqual(response.statusCode, 400, `${response.request.path} payload bad.`); + t.strictEqual(response.statusCode, 400, `${schemaVersion} ${response.request.path} payload bad.`); - response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets/0' - }); + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'DELETE', - url: '/v1/petstore/pets/0' - }); + response = await server.inject({ + method: 'DELETE', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('routes with output validation', async function (t) { - t.plan(5); + t.test('routes with output validation', async function (t) { + t.plan(5); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers'), - outputvalidation: true - } - }); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers'), + outputvalidation: true + } + }); - let response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets' - }); + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'POST', - url: '/v1/petstore/pets', - payload: { - id: '0', - name: 'Cat' - } - }); + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + id: '0', + name: 'Cat' + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'POST', - url: '/v1/petstore/pets', - payload: { - name: 123 - } - }); + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + name: 123 + } + }); - t.strictEqual(response.statusCode, 400, `${response.request.path} payload bad.`); + t.strictEqual(response.statusCode, 400, `${schemaVersion} ${response.request.path} payload bad.`); - response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets/0' - }); + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'DELETE', - url: '/v1/petstore/pets/0' - }); + response = await server.inject({ + method: 'DELETE', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('output validation fails', async function (t) { - t.plan(1); + t.test('output validation fails', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: { - pets: { - '{id}': { - get(req, h) { - return 'bad response type'; + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: { + pets: { + '{id}': { + get(req, h) { + return 'bad response type'; + } } } - } - }, - outputvalidation: true - } - }); + }, + outputvalidation: true + } + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets/0' - }); + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets/0' + }); + + t.strictEqual(response.statusCode, 500, `${schemaVersion} ${response.request.path} failed.`); + + } + catch (error) { + t.fail(error.message); + } - t.strictEqual(response.statusCode, 500, `${response.request.path} failed.`); + }); - } - catch (error) { - t.fail(error.message); - } + t.test('routes x-handler', async function (t) { + t.plan(4); - }); + const server = new Hapi.Server(); - t.test('routes x-handler', async function (t) { - t.plan(4); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets_xhandlers.json`) + } + }); - const server = new Hapi.Server(); + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets_xhandlers.json') - } - }); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - let response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets' - }); + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + id: '0', + name: 'Cat' + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'POST', - url: '/v1/petstore/pets', - payload: { - id: '0', - name: 'Cat' - } - }); + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets/0' - }); + response = await server.inject({ + method: 'DELETE', + url: '/v1/petstore/pets/0' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - response = await server.inject({ - method: 'DELETE', - url: '/v1/petstore/pets/0' - }); + } + catch (error) { + t.fail(error.message); + } - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + }); - } - catch (error) { - t.fail(error.message); - } + t.test('query validation', async function (t) { - }); + const server = new Hapi.Server(); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.json`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - t.test('query validation', async function (t) { + const queryStringToStatusCode = { + 'limit=2': 200, + 'tags=some_tag&tags=some_other_tag': 200, + 'tags=single_tag': 200, + 'limit=2&tags=some_tag&tags=some_other_tag': 200, + 'limit=a_string': 400 + } - const server = new Hapi.Server(); + for (const queryString in queryStringToStatusCode) { + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets?' + queryString + }); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.json'), - handlers: Path.join(__dirname, './fixtures/handlers') + t.strictEqual(response.statusCode, queryStringToStatusCode[queryString], `${schemaVersion} ${queryString}`); } - }); - const queryStringToStatusCode = { - 'limit=2': 200, - 'tags=some_tag&tags=some_other_tag': 200, - 'tags=single_tag': 200, - 'limit=2&tags=some_tag&tags=some_other_tag': 200, - 'limit=a_string': 400 + t.end(); + } + catch (error) { + t.fail(error.message); } + }); - for (const queryString in queryStringToStatusCode) { - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets?' + queryString - }); + t.test('query validation with arrays', async function (t) { - t.strictEqual(response.statusCode, queryStringToStatusCode[queryString], queryString); - } + const server = new Hapi.Server(); - t.end(); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('query validation with arrays', async function (t) { - - const server = new Hapi.Server(); - - try { - await server.register({ - plugin: OpenAPI, - options: { - api: { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' + const schemas = { + oas2: { + version: { swagger: '2.0' }, + parameter: { + name: 'tags', + in: 'query', + required: false, + type: 'array', + items: { + type: 'string' }, - paths: { - '/test': { - get: { - description: '', - parameters: [ - { - name: 'tags', - in: 'query', - required: false, - type: 'array', - items: { - type: 'string' - }, - collectionFormat: 'csv' - } - ], - responses: { - 200: { - description: 'default response' - } - } - } - } - } - }, - handlers: { - test: { - get(request, h) { - t.ok(request.query.tags, 'query exists.'); - t.equal(request.query.tags.length, 2, 'two array elements.'); - t.equal(request.query.tags[0], 'some_tag', 'values correct.'); - return 'test'; + collectionFormat: 'csv' + } + }, + oas3: { + version: { openapi: '3.0.0' }, + parameter: { + name: 'tags', + in: 'query', + required: false, + schema: { + type: 'array', + items: { + type: 'string' } - } + }, + explode: false } } - }); - - const response = await server.inject({ - method: 'GET', - url: '/test?tags=some_tag,some_other_tag' - }); - - t.strictEqual(response.statusCode, 200, 'csv format supported.'); - - t.end(); - } - catch (error) { - t.fail(error.message); - } - }); - - t.test('parse description from api definition', async function (t) { - t.test('do not break with empty descriptions', async function (t) { - t.plan(1); - - const server = new Hapi.Server(); + }; try { await server.register({ plugin: OpenAPI, options: { api: { - swagger: '2.0', + ...schemas[schemaVersion].version, info: { title: 'Minimal', version: '1.0.0' @@ -837,6 +853,7 @@ Test('test plugin', function (t) { '/test': { get: { description: '', + parameters: [schemas[schemaVersion].parameter], responses: { 200: { description: 'default response' @@ -849,6 +866,9 @@ Test('test plugin', function (t) { handlers: { test: { get(request, h) { + t.ok(request.query.tags, `${schemaVersion} query exists.`); + t.equal(request.query.tags.length, 2, `${schemaVersion} two array elements.`); + t.equal(request.query.tags[0], 'some_tag', `${schemaVersion} values correct.`); return 'test'; } } @@ -856,77 +876,130 @@ Test('test plugin', function (t) { } }); - t.pass(); - } catch (error) { + const response = await server.inject({ + method: 'GET', + url: '/test?tags=some_tag,some_other_tag' + }); + + t.strictEqual(response.statusCode, 200, `${schemaVersion} csv format supported.`); + + t.end(); + } + catch (error) { t.fail(error.message); } }); - t.test('create the right description for the route', async function (t) { - t.plan(1); + t.test('parse description from api definition', async function (t) { + t.test('does not break with empty descriptions', async function (t) { + t.plan(1); + + const server = new Hapi.Server(); + + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + description: '', + responses: { + 200: { + description: 'default response' + } + } + } + } + } + }, + handlers: { + test: { + get(request, h) { + return 'test'; + } + } + } + } + }); - const server = new Hapi.Server(); + t.pass(); + } catch (error) { + t.fail(error.message); + } + }); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - description: 'A simple description for the route', - responses: { - 200: { - description: 'default response' + t.test('create the right description for the route', async function (t) { + t.plan(1); + + const server = new Hapi.Server(); + + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + description: 'A simple description for the route', + responses: { + 200: { + description: 'default response' + } } } } } - } - }, - handlers: { - test: { - get(request, h) { - return 'test'; + }, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } - } - }); + }); - const response = await server.inject({ method: 'GET', url: '/test' }); - t.strictEqual(response.request.route.settings.description, 'A simple description for the route'); - } catch (error) { - t.fail(error.message); - } + const response = await server.inject({ method: 'GET', url: '/test' }); + t.strictEqual(response.request.route.settings.description, 'A simple description for the route'); + } catch (error) { + t.fail(error.message); + } + }); }); - }); - t.test('hapi payload options (assert via parse:false)', async function (t) { - t.plan(1); + t.test('hapi payload options (assert via parse:false)', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - post: { - 'x-hapi-options': { - payload: { - parse: false - } - }, + const schemas = { + oas2: { + version: { swagger: '2.0' }, + body: { parameters: [ { name: 'thing', @@ -940,70 +1013,96 @@ Test('test plugin', function (t) { } } } - ], - responses: { - 200: { - description: 'default response' + ] + } + }, + oas3: { + version: { openapi: '3.0.0' }, + body: { + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { + type: 'string' + } + } + } + } } } - } + }, } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - post() { - return 'test'; + const api = { + ...schemas[schemaVersion].version, + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + post: { + 'x-hapi-options': { + payload: { + parse: false + } + }, + ...schemas[schemaVersion].body, + responses: { + 200: { + description: 'default response' + } } } } } - }); + }; - let response = await server.inject({ - method: 'POST', - url: '/test', - payload: { - id: 1 //won't fail because parse is false - } - }); + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + post() { + return 'test'; + } + } + } + } + }); + + let response = await server.inject({ + method: 'POST', + url: '/test', + payload: { + id: 1 //won't fail because parse is false + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('hapi allowUnknown request payload properties', async function (t) { - t.plan(1); + t.test('hapi allowUnknown request payload properties', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - post: { - 'x-hapi-options': { - validate: { - options: { - allowUnknown: true - } - } - }, + const schemas = { + oas2: { + version: { swagger: '2.0' }, + body: { parameters: [ { name: 'thing', @@ -1017,64 +1116,99 @@ Test('test plugin', function (t) { } } } - ], - responses: { - 200: { - description: 'default response' - } + ] + } + }, + oas3: { + version: { openapi: '3.0.0' }, + body: { + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { + type: 'string' + } + } + } + } + } + } + }, + } + }; + + const api = { + ...schemas[schemaVersion].version, + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + post: { + 'x-hapi-options': { + validate: { + options: { + allowUnknown: true + } + } + }, + ...schemas[schemaVersion].body, + responses: { + 200: { + description: 'default response' + } + } } } } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - post() { - return 'test'; + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + post() { + return 'test'; + } } } } - } - }); + }); - let response = await server.inject({ - method: 'POST', - url: '/test', - payload: { - id: 'string-id', - excessive: 42 - } - }); + let response = await server.inject({ + method: 'POST', + url: '/test', + payload: { + id: 'string-id', + excessive: 42 + } + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('hapi array parameters', async function (t) { - t.plan(1); + t.test('hapi array parameters', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - post: { + const schemas = { + oas2: { + version: { swagger: '2.0' }, + body: { parameters: [ { name: 'body', @@ -1095,348 +1229,411 @@ Test('test plugin', function (t) { } } ], - responses: { - 200: { - description: 'default response' + } + }, + oas3: { + version: { openapi: '3.0.0' }, + body: { + requestBody: { + content: { + 'application/json': { + schema: { + type: "array", + items: { + type: "object", + properties: { + name: { + type: "string" + }, + breed: { + type: "string" + } + } + } + } + } } } - } + }, } - } - }; + }; - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - post() { - return 'test'; + const api = { + ...schemas[schemaVersion].version, + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + post: { + ...schemas[schemaVersion].body, + responses: { + 200: { + description: 'default response' + } } } } } - }); + }; - let response = await server.inject({ - method: 'POST', - url: '/test', - payload: [ - { - name: 'Fido', - breed: 'Pointer' - }, - { - name: 'Frodo', - breed: 'Beagle' + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + post() { + return 'test'; + } + } + } } - ] - }); + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + let response = await server.inject({ + method: 'POST', + url: '/test', + payload: [ + { + name: 'Fido', + breed: 'Pointer' + }, + { + name: 'Frodo', + breed: 'Beagle' + } + ] + }); - } - catch (error) { - t.fail(error.message); - } + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); - }); + } + catch (error) { + t.fail(error.message); + } - t.test('hapi operation tags', async function (t) { - t.plan(1); + }); - const server = new Hapi.Server(); + t.test('hapi operation tags', async function (t) { + t.plan(1); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - tags: [ - 'sample1', - 'sample2' - ], - responses: { - 200: { - description: 'default response' + const server = new Hapi.Server(); + + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + const api = { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + tags: [ + 'sample1', + 'sample2' + ], + responses: { + 200: { + description: 'default response' + } } } } } - } - }; - const expectedTags = ['api', 'sample1', 'sample2'] - - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get() { - return 'test'; + }; + const expectedTags = ['api', 'sample1', 'sample2'] + + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + get() { + return 'test'; + } } } } - } - }); + }); - let response = await server.inject({ - method: 'GET', - url: '/test' - }); - const responsteTags = response.request.route.settings.tags + let response = await server.inject({ + method: 'GET', + url: '/test' + }); + const responsteTags = response.request.route.settings.tags - t.deepEqual(responsteTags, expectedTags, 'additional tags successfully configured'); + t.deepEqual(responsteTags, expectedTags, `${schemaVersion} additional tags successfully configured`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('hapi operation tags omitted', async function (t) { - t.plan(1); + t.test('hapi operation tags omitted', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - responses: { - 200: { - description: 'default response' + const versions = { + oas2: { swagger: '2.0' }, + oas3: { openapi: '3.0.0' } + }; + + const api = { + ...versions[schemaVersion], + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + responses: { + 200: { + description: 'default response' + } } } } } - } - }; - const expectedDefaultTags = ['api'] - - try { - await server.register({ - plugin: OpenAPI, - options: { - api, - handlers: { - test: { - get() { - return 'test'; + }; + const expectedDefaultTags = ['api'] + + try { + await server.register({ + plugin: OpenAPI, + options: { + api, + handlers: { + test: { + get() { + return 'test'; + } } } } - } - }); - - let response = await server.inject({ - method: 'GET', - url: '/test' - }); - const responsteTags = response.request.route.settings.tags - - t.deepEqual(responsteTags, expectedDefaultTags, 'returned default tags'); + }); - } - catch (error) { - t.fail(error.message); - } + let response = await server.inject({ + method: 'GET', + url: '/test' + }); + const responsteTags = response.request.route.settings.tags - }); + t.deepEqual(responsteTags, expectedDefaultTags, `${schemaVersion} returned default tags`); + } + catch (error) { + t.fail(error.message); + } + }); + } }); Test('multi-register', function (t) { - const api1 = { - swagger: '2.0', - info: { - title: 'API 1', - version: '1.0.0' - }, - basePath: '/api1', - paths: { - '/test': { - get: { - responses: { - 200: { - description: 'default response' + for (const schemaVersion of ['oas2', 'oas3']) { + const schemas = { + oas2: { + version: { swagger: '2.0' }, + server: path => ({ basePath: path }) + }, + oas3: { + version: { openapi: '3.0.0' }, + server: path => ({ servers: [{ url: path }]}) + } + }; + + const api1 = { + ...schemas[schemaVersion].version, + info: { + title: 'API 1', + version: '1.0.0' + }, + ...schemas[schemaVersion].server('/api1'), + paths: { + '/test': { + get: { + responses: { + 200: { + description: 'default response' + } } } } } - } - }; - - const api2 = { - swagger: '2.0', - info: { - title: 'API 2', - version: '1.0.0' - }, - basePath: '/api2', - paths: { - '/test': { - get: { - responses: { - 200: { - description: 'default response' + }; + + const api2 = { + ...schemas[schemaVersion].version, + info: { + title: 'API 2', + version: '1.0.0' + }, + ...schemas[schemaVersion].server('/api2'), + paths: { + '/test': { + get: { + responses: { + 200: { + description: 'default response' + } } } } } - } - }; + }; - t.test('support register multiple', async function (t) { - t.plan(2); + t.test('support register multiple', async function (t) { + t.plan(2); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register([ - { - plugin: OpenAPI, - options: { - api: api1, - handlers: { - test: { - get(request, h) { - return 'test'; + try { + await server.register([ + { + plugin: OpenAPI, + options: { + api: api1, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } - } - }, - { - plugin: OpenAPI, - options: { - api: api2, - handlers: { - test: { - get(request, h) { - return 'test'; + }, + { + plugin: OpenAPI, + options: { + api: api2, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } } - } - ]); + ]); - let response = await server.inject({ - method: 'GET', - url: '/api1/test' - }); + let response = await server.inject({ + method: 'GET', + url: '/api1/test' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - response = await server.inject({ - method: 'GET', - url: '/api2/test' - }); + response = await server.inject({ + method: 'GET', + url: '/api2/test' + }); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + } + catch (error) { + t.fail(error.message); + } - }); + }); - t.test('support fail on conflicts', async function (t) { - t.plan(1); + t.test('support fail on conflicts', async function (t) { + t.plan(1); - const server = new Hapi.Server(); + const server = new Hapi.Server(); - try { - await server.register([ - { - plugin: OpenAPI, - options: { - api: api1, - docs: { - path: 'docs1' - }, - handlers: { - test: { - get(request, h) { - return 'test'; + try { + await server.register([ + { + plugin: OpenAPI, + options: { + api: api1, + docs: { + path: 'docs1' + }, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } - } - }, - { - plugin: OpenAPI, - options: { - api: api1, - docs: { - path: 'docs2' - }, - handlers: { - test: { - get(request, h) { - return 'test'; + }, + { + plugin: OpenAPI, + options: { + api: api1, + docs: { + path: 'docs2' + }, + handlers: { + test: { + get(request, h) { + return 'test'; + } } } } } - } - ]); + ]); - t.fail('should have errored'); - } - catch (error) { - t.pass('expected failure'); - } - - }); + t.fail(`${schemaVersion} should have errored`); + } + catch (error) { + t.pass(`${schemaVersion} expected failure`); + } + }); + } }); Test('yaml support', function (t) { - t.test('register', async function (t) { - t.plan(3); - const server = new Hapi.Server(); + for (const schemaVersion of ['oas2', 'oas3']) { + t.test('register', async function (t) { + t.plan(3); - try { - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/pets.yaml'), - handlers: Path.join(__dirname, './fixtures/handlers') - } - }); + const server = new Hapi.Server(); - t.ok(server.plugins.openapi.getApi, 'server.plugins.openapi.getApi exists.'); - t.ok(server.plugins.openapi.setHost, 'server.plugins.openapi.setHost exists.'); + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/pets.yaml`), + handlers: Path.join(__dirname, './fixtures/handlers') + } + }); - const response = await server.inject({ - method: 'GET', - url: '/v1/petstore/pets' - }); + t.ok(server.plugins.openapi.getApi, `${schemaVersion} server.plugins.openapi.getApi exists.`); + t.ok(server.plugins.openapi.setHost, `${schemaVersion} server.plugins.openapi.setHost exists.`); - t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); - } - catch (error) { - t.fail(error.message); - } + const response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); + + t.strictEqual(response.statusCode, 200, `${schemaVersion} ${response.request.path} OK.`); + } + catch (error) { + t.fail(error.message); + } - }); + }); + } }); diff --git a/test/test-validators.js b/test/test-validators.js index 581b5d8..deb066c 100644 --- a/test/test-validators.js +++ b/test/test-validators.js @@ -1,143 +1,222 @@ const Test = require('tape'); const Validators = require('../lib/validators'); +const { toDto } = require('../lib/api-dto-mapper'); Test('validator special types', function(t) { - const api = { - swagger: '2.0', - info: { - title: 'Minimal', - version: '1.0.0' - }, - paths: { - '/test': { - get: { - description: '', - parameters: [ - { - name: 'dateTime', - in: 'query', - required: false, - type: 'string', - format: 'date-time' - } - ], - responses: { - 200: { - description: 'default response' - } - } + + const validator = Validators.create(); + + const schemas = {}; + schemas.oas2 = { + swagger: '2.0', + info: { + title: 'Minimal', + version: '1.0.0' }, - post: { - description: '', - parameters: [ - { - name: 'payload', - in: 'body', - required: true, - schema: { - type: 'object', - required: ['requiredProperty'], - properties: { - requiredProperty: { - type: 'string' - } + paths: { + '/test': { + get: { + description: '', + parameters: [ + { + name: 'dateTime', + in: 'query', + required: false, + type: 'string', + format: 'date-time' + } + ], + responses: { + 200: { + description: 'default response' + } + } + }, + post: { + description: '', + parameters: [ + { + name: 'payload', + in: 'body', + required: true, + schema: { + type: 'object', + required: ['requiredProperty'], + properties: { + requiredProperty: { + type: 'string' + } + } + } + } + ] + } + }, + '/test/{foo*}': { + get: { + description: '', + parameters: [ + { + name: 'foo*', + in: 'path', + required: true, + type: 'string' + } + ], + responses: { + 200: { + description: 'default response' + } + } } - } } - ] } - }, - '/test/{foo*}': { - get: { - description: '', - parameters: [ - { - name: 'foo*', - in: 'path', - required: true, - type: 'string' - } - ], - responses: { - 200: { - description: 'default response' + }; + + schemas.oas3 = { + openapi: '3.0.0', + info: { + title: 'Minimal', + version: '1.0.0' + }, + paths: { + '/test': { + get: { + description: '', + parameters: [ + { + name: 'dateTime', + in: 'query', + required: false, + schema: { + type: 'string', + format: 'date-time' + } + } + ], + responses: { + 200: { + description: 'default response' + } + } + }, + post: { + description: '', + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: ['requiredProperty'], + properties: { + requiredProperty: { + type: 'string' + } + } + } + } + } + } + } + }, + '/test/{foo*}': { + get: { + description: '', + parameters: [ + { + name: 'foo*', + in: 'path', + required: true, + schema: { + type: 'string' + } + } + ], + responses: { + 200: { + description: 'default response' + } + } + } } - } } - } - } - }; + }; - const validator = Validators.create(api); + for (const schemaVersion of ['oas2', 'oas3']) { - t.test('valid date-time', async function(t) { - t.plan(1); + const api = toDto(schemas[schemaVersion]); - const { validate } = validator.makeValidator( - api.paths['/test'].get.parameters[0] - ); + t.test('valid date-time', async function(t) { + t.plan(1); - try { - validate('1995-09-07T10:40:52Z'); - t.pass('valid date-time'); - } catch (error) { - t.fail(error.message); - } - }); + const { validate } = validator.makeValidator( + api.getOperation('get', '/test').parameters[0] + ); + + try { + validate('1995-09-07T10:40:52Z'); + t.pass(`${schemaVersion} valid date-time`); + } catch (error) { + t.fail(error.message); + } + }); - t.test('invalid date-time', async function(t) { - t.plan(1); + t.test('invalid date-time', async function(t) { + t.plan(1); - const { validate } = validator.makeValidator( - api.paths['/test'].get.parameters[0] - ); + const { validate } = validator.makeValidator( + api.getOperation('get', '/test').parameters[0] + ); - const timestamp = Date.now(); + const timestamp = Date.now(); - try { - validate(timestamp); - t.fail(`${timestamp} should be invalid.`); - } catch (error) { - t.pass(`${timestamp} is invalid.`); - } - }); + try { + validate(timestamp); + t.fail(`${schemaVersion} ${timestamp} should be invalid.`); + } catch (error) { + t.pass(`${schemaVersion} ${timestamp} is invalid.`); + } + }); + + t.test('validate multi-segment paths', async function(t) { + t.plan(1); - t.test('validate multi-segment paths', async function(t) { - t.plan(1); + const v = validator.makeAll(api.getOperation('get', '/test/{foo*}').parameters); + + const keys = Object.keys(v.validate.params.describe().keys); + + if (keys.length === 1 && keys[0] === 'foo') { + return t.pass(`${schemaVersion} ${keys.join(', ')} are valid.`); + } + t.fail(`${schemaVersion} ${keys.join(', ')} are invalid.`); + }); - const v = validator.makeAll(api.paths['/test/{foo*}'].get.parameters); - const keys = Object.keys(v.validate.params.describe().keys); + t.test('validate missing body parameter', async function(t) { + t.plan(1); - if (keys.length === 1 && keys[0] === 'foo') { - return t.pass(`${keys.join(', ')} are valid.`); + const { validate } = validator.makeValidator(api.getOperation('post', '/test').parameters[0]); + + try { + validate(); + t.fail(`${schemaVersion} "undefined" should be invalid`); + } catch (error) { + t.equal(error.message, '"payload" is required', `${schemaVersion} received expected payload error message`); + } + }); + + t.test('validate empty object with required property', async function(t) { + t.plan(1); + + const { validate } = validator.makeValidator(api.getOperation('post', '/test').parameters[0]); + + try { + validate({}); + t.fail(`${schemaVersion} "undefined" should be invalid`); + } catch (error) { + t.match(error.message, /"requiredProperty" is required/, `${schemaVersion} received expected property error message`); + } + }); } - t.fail(`${keys.join(', ')} are invalid.`); - }); - - t.test('validate missing body parameter', async function(t) { - t.plan(1); - - const { validate } = validator.makeValidator(api.paths['/test'].post.parameters[0]); - - try { - validate(); - t.fail('"undefined" should be invalid'); - } catch (error) { - t.equal(error.message, '"payload" is required', "received expected payload error message"); - } - }); - - t.test('validate empty object with required property', async function(t) { - t.plan(1); - - const { validate } = validator.makeValidator(api.paths['/test'].post.parameters[0]); - - try { - validate({}); - t.fail('"undefined" should be invalid'); - } catch (error) { - t.match(error.message, /"requiredProperty" is required/, "received expected property error message"); - } - }) }); diff --git a/test/test_xoptions.js b/test/test_xoptions.js index 2872bb5..de3dc1a 100644 --- a/test/test_xoptions.js +++ b/test/test_xoptions.js @@ -8,43 +8,45 @@ const Hapi = require('@hapi/hapi'); Test('x-hapi-options', function (t) { - t.test('overrides', async function (t) { - t.plan(1); - - try { - const server = new Hapi.Server(); - - await server.register({ - plugin: OpenAPI, - options: { - api: Path.join(__dirname, './fixtures/defs/form_xoptions.json'), - handlers: { - upload: { - post: function (req, h) { - return { - upload: req.payload.toString() - }; + for (const schemaVersion of ['oas2', 'oas3']) { + t.test('overrides', async function (t) { + t.plan(1); + + try { + const server = new Hapi.Server(); + + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, `./fixtures/defs/${schemaVersion}/form_xoptions.json`), + handlers: { + upload: { + post: function (req, h) { + return { + upload: req.payload.toString() + }; + } } } } - } - }); - - const response = await server.inject({ - method: 'POST', - url: '/v1/forms/upload', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - payload: 'name=thing&upload=data' - }); - - t.strictEqual(response.statusCode, 404, `${response.request.path} not found due to isInternal.`); - } - catch (error) { - t.fail(error.message); - } - - }); + }); + + const response = await server.inject({ + method: 'POST', + url: '/v1/forms/upload', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + payload: 'name=thing&upload=data' + }); + + t.strictEqual(response.statusCode, 404, `${schemaVersion} ${response.request.path} not found due to isInternal.`); + } + catch (error) { + t.fail(error.message); + } + + }); + } });