bug: parameter allowEmptyValue + required interactions (via #5142)

* add failing tests
* standardize parameter keying
* validateParam test migrations
* migrate test cases to new pattern
* disambiguate name/in ordering in `body.body` test cases
* `name+in`=> `{in}.{name}`
* consider allowEmptyValue parameter inclusion in runtime validation
* use config object for all validateParam options
* drop isXml flag from validateParams
This commit is contained in:
kyle
2019-01-29 16:46:03 -06:00
committed by GitHub
parent be3500c299
commit abf34961e9
9 changed files with 719 additions and 383 deletions

View File

@@ -5,7 +5,7 @@ import serializeError from "serialize-error"
import isString from "lodash/isString" import isString from "lodash/isString"
import debounce from "lodash/debounce" import debounce from "lodash/debounce"
import set from "lodash/set" import set from "lodash/set"
import { isJSONObject } from "core/utils" import { isJSONObject, paramToValue } from "core/utils"
// Actions conform to FSA (flux-standard-actions) // Actions conform to FSA (flux-standard-actions)
// {type: string,payload: Any|Error, meta: obj, error: bool} // {type: string,payload: Any|Error, meta: obj, error: bool}
@@ -345,19 +345,19 @@ export const executeRequest = (req) =>
// ensure that explicitly-included params are in the request // ensure that explicitly-included params are in the request
if(op && op.parameters && op.parameters.length) { if (operation && operation.get("parameters")) {
op.parameters operation.get("parameters")
.filter(param => param && param.allowEmptyValue === true) .filter(param => param && param.get("allowEmptyValue") === true)
.forEach(param => { .forEach(param => {
if (specSelectors.parameterInclusionSettingFor([pathName, method], param.name, param.in)) { if (specSelectors.parameterInclusionSettingFor([pathName, method], param.get("name"), param.get("in"))) {
req.parameters = req.parameters || {} req.parameters = req.parameters || {}
const paramValue = req.parameters[param.name] const paramValue = paramToValue(param, req.parameters)
// if the value is falsy or an empty Immutable iterable... // if the value is falsy or an empty Immutable iterable...
if(!paramValue || (paramValue && paramValue.size === 0)) { if(!paramValue || (paramValue && paramValue.size === 0)) {
// set it to empty string, so Swagger Client will treat it as // set it to empty string, so Swagger Client will treat it as
// present but empty. // present but empty.
req.parameters[param.name] = "" req.parameters[param.get("name")] = ""
} }
} }
}) })

View File

@@ -1,10 +1,12 @@
import { fromJS, List } from "immutable" import { fromJS, List } from "immutable"
import { fromJSOrdered, validateParam } from "core/utils" import { fromJSOrdered, validateParam, paramToValue } from "core/utils"
import win from "../../window" import win from "../../window"
// selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper // selector-in-reducer is suboptimal, but `operationWithMeta` is more of a helper
import { import {
operationWithMeta specJsonWithResolvedSubtrees,
parameterValues,
parameterInclusionSettingFor,
} from "./selectors" } from "./selectors"
import { import {
@@ -25,6 +27,7 @@ import {
CLEAR_VALIDATE_PARAMS, CLEAR_VALIDATE_PARAMS,
SET_SCHEME SET_SCHEME
} from "./actions" } from "./actions"
import { paramToIdentifier } from "../../utils"
export default { export default {
@@ -54,14 +57,7 @@ export default {
[UPDATE_PARAM]: ( state, {payload} ) => { [UPDATE_PARAM]: ( state, {payload} ) => {
let { path: pathMethod, paramName, paramIn, param, value, isXml } = payload let { path: pathMethod, paramName, paramIn, param, value, isXml } = payload
let paramKey let paramKey = param ? paramToIdentifier(param) : `${paramIn}.${paramName}`
// `hashCode` is an Immutable.js Map method
if(param && param.hashCode && !paramIn && !paramName) {
paramKey = `${param.get("name")}.${param.get("in")}.hash-${param.hashCode()}`
} else {
paramKey = `${paramName}.${paramIn}`
}
const valueKey = isXml ? "value_xml" : "value" const valueKey = isXml ? "value_xml" : "value"
@@ -79,7 +75,7 @@ export default {
return state return state
} }
const paramKey = `${paramName}.${paramIn}` const paramKey = `${paramIn}.${paramName}`
return state.setIn( return state.setIn(
["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey], ["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey],
@@ -88,15 +84,18 @@ export default {
}, },
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => { [VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {
let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) ) const op = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod])
let isXml = /xml/i.test(meta.get("consumes_value")) const paramValues = parameterValues(state, pathMethod).toJS()
const op = operationWithMeta(state, ...pathMethod)
return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => { return state.updateIn(["meta", "paths", ...pathMethod, "parameters"], fromJS({}), paramMeta => {
return op.get("parameters", List()).reduce((res, param) => { return op.get("parameters", List()).reduce((res, param) => {
const errors = validateParam(param, isXml, isOAS3) const value = paramToValue(param, paramValues)
return res.setIn([`${param.get("name")}.${param.get("in")}`, "errors"], fromJS(errors)) const isEmptyValueIncluded = parameterInclusionSettingFor(state, pathMethod, param.get("name"), param.get("in"))
const errors = validateParam(param, value, {
bypassRequiredCheck: isEmptyValueIncluded,
isOAS3,
})
return res.setIn([paramToIdentifier(param), "errors"], fromJS(errors))
}, paramMeta) }, paramMeta)
}) })
}, },

View File

@@ -1,6 +1,7 @@
import { createSelector } from "reselect" import { createSelector } from "reselect"
import { sorters } from "core/utils" import { sorters } from "core/utils"
import { fromJS, Set, Map, OrderedMap, List } from "immutable" import { fromJS, Set, Map, OrderedMap, List } from "immutable"
import { paramToIdentifier } from "../../utils"
const DEFAULT_TAG = "default" const DEFAULT_TAG = "default"
@@ -302,11 +303,11 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], OrderedMap()) const metaParams = state.getIn(["meta", "paths", ...pathMethod, "parameters"], OrderedMap())
const mergedParams = opParams.map((currentParam) => { const mergedParams = opParams.map((currentParam) => {
const nameInKeyedMeta = metaParams.get(`${param.get("name")}.${param.get("in")}`) const inNameKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}`)
const hashKeyedMeta = metaParams.get(`${param.get("name")}.${param.get("in")}.hash-${param.hashCode()}`) const hashKeyedMeta = metaParams.get(`${param.get("in")}.${param.get("name")}.hash-${param.hashCode()}`)
return OrderedMap().merge( return OrderedMap().merge(
currentParam, currentParam,
nameInKeyedMeta, inNameKeyedMeta,
hashKeyedMeta hashKeyedMeta
) )
}) })
@@ -315,7 +316,7 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
} }
export const parameterInclusionSettingFor = (state, pathMethod, paramName, paramIn) => { export const parameterInclusionSettingFor = (state, pathMethod, paramName, paramIn) => {
const paramKey = `${paramName}.${paramIn}` const paramKey = `${paramIn}.${paramName}`
return state.getIn(["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey], false) return state.getIn(["meta", "paths", ...pathMethod, "parameter_inclusions", paramKey], false)
} }
@@ -364,7 +365,7 @@ export function parameterValues(state, pathMethod, isXml) {
let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List()) let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
return paramValues.reduce( (hash, p) => { return paramValues.reduce( (hash, p) => {
let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value") let value = isXml && p.get("in") === "body" ? p.get("value_xml") : p.get("value")
return hash.set(`${p.get("in")}.${p.get("name")}`, value) return hash.set(paramToIdentifier(p, { allowHashes: false }), value)
}, fromJS({})) }, fromJS({}))
} }

View File

@@ -484,9 +484,8 @@ export const validatePattern = (val, rxPattern) => {
} }
// validation of parameters before execute // validation of parameters before execute
export const validateParam = (param, isXml, isOAS3 = false) => { export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
let errors = [] let errors = []
let value = isXml && param.get("in") === "body" ? param.get("value_xml") : param.get("value")
let required = param.get("required") let required = param.get("required")
let paramDetails = isOAS3 ? param.get("schema") : param let paramDetails = isOAS3 ? param.get("schema") : param
@@ -501,7 +500,6 @@ export const validateParam = (param, isXml, isOAS3 = false) => {
let minLength = paramDetails.get("minLength") let minLength = paramDetails.get("minLength")
let pattern = paramDetails.get("pattern") let pattern = paramDetails.get("pattern")
/* /*
If the parameter is required OR the parameter has a value (meaning optional, but filled in) If the parameter is required OR the parameter has a value (meaning optional, but filled in)
then we should do our validation routine. then we should do our validation routine.
@@ -540,7 +538,7 @@ export const validateParam = (param, isXml, isOAS3 = false) => {
const passedAnyCheck = allChecks.some(v => !!v) const passedAnyCheck = allChecks.some(v => !!v)
if ( required && !passedAnyCheck ) { if (required && !passedAnyCheck && !bypassRequiredCheck ) {
errors.push("Required field is not provided") errors.push("Required field is not provided")
return errors return errors
} }
@@ -805,3 +803,43 @@ export function numberToString(thing) {
return thing return thing
} }
export function paramToIdentifier(param, { returnAll = false, allowHashes = true } = {}) {
if(!Im.Map.isMap(param)) {
throw new Error("paramToIdentifier: received a non-Im.Map parameter as input")
}
const paramName = param.get("name")
const paramIn = param.get("in")
let generatedIdentifiers = []
// Generate identifiers in order of most to least specificity
if (param && param.hashCode && paramIn && paramName && allowHashes) {
generatedIdentifiers.push(`${paramIn}.${paramName}.hash-${param.hashCode()}`)
}
if(paramIn && paramName) {
generatedIdentifiers.push(`${paramIn}.${paramName}`)
}
generatedIdentifiers.push(paramName)
// Return the most preferred identifier, or all if requested
return returnAll ? generatedIdentifiers : (generatedIdentifiers[0] || "")
}
export function paramToValue(param, paramValues) {
const allIdentifiers = paramToIdentifier(param, { returnAll: true })
// Map identifiers to values in the provided value hash, filter undefined values,
// and return the first value found
const values = allIdentifiers
.map(id => {
return paramValues[id]
})
.filter(value => value !== undefined)
return values[0]
}

View File

@@ -130,7 +130,7 @@ describe("spec plugin - reducer", function(){
}) })
}) })
describe("SPEC_UPDATE_PARAM", function() { describe("SPEC_UPDATE_PARAM", function() {
it("should store parameter values by name+in", () => { it("should store parameter values by {in}.{name}", () => {
const updateParam = reducer["spec_update_param"] const updateParam = reducer["spec_update_param"]
const path = "/pet/post" const path = "/pet/post"
@@ -140,14 +140,14 @@ describe("spec plugin - reducer", function(){
const result = updateParam(state, { const result = updateParam(state, {
payload: { payload: {
path: [path, method], path: [path, method],
paramName: "body", paramName: "myBody",
paramIn: "body", paramIn: "body",
value: `{ "a": 123 }`, value: `{ "a": 123 }`,
isXml: false isXml: false
} }
}) })
const response = result.getIn(["meta", "paths", path, method, "parameters", "body.body", "value"]) const response = result.getIn(["meta", "paths", path, method, "parameters", "body.myBody", "value"])
expect(response).toEqual(`{ "a": 123 }`) expect(response).toEqual(`{ "a": 123 }`)
}) })
it("should store parameter values by identity", () => { it("should store parameter values by identity", () => {
@@ -157,7 +157,7 @@ describe("spec plugin - reducer", function(){
const method = "POST" const method = "POST"
const param = fromJS({ const param = fromJS({
name: "body", name: "myBody",
in: "body", in: "body",
schema: { schema: {
type: "string" type: "string"
@@ -174,12 +174,12 @@ describe("spec plugin - reducer", function(){
} }
}) })
const value = result.getIn(["meta", "paths", path, method, "parameters", `body.body.hash-${param.hashCode()}`, "value"]) const value = result.getIn(["meta", "paths", path, method, "parameters", `body.myBody.hash-${param.hashCode()}`, "value"])
expect(value).toEqual(`{ "a": 123 }`) expect(value).toEqual(`{ "a": 123 }`)
}) })
}) })
describe("SPEC_UPDATE_EMPTY_PARAM_INCLUSION", function() { describe("SPEC_UPDATE_EMPTY_PARAM_INCLUSION", function() {
it("should store parameter values by name+in", () => { it("should store parameter values by {in}.{name}", () => {
const updateParam = reducer["spec_update_empty_param_inclusion"] const updateParam = reducer["spec_update_empty_param_inclusion"]
const path = "/pet/post" const path = "/pet/post"
@@ -196,7 +196,7 @@ describe("spec plugin - reducer", function(){
} }
}) })
const response = result.getIn(["meta", "paths", path, method, "parameter_inclusions", "param.query"]) const response = result.getIn(["meta", "paths", path, method, "parameter_inclusions", "query.param"])
expect(response).toEqual(true) expect(response).toEqual(true)
}) })
}) })

View File

@@ -497,7 +497,7 @@ describe("spec plugin - selectors", function(){
}) })
describe("operationWithMeta", function() { describe("operationWithMeta", function() {
it("should support merging in name+in keyed param metadata", function () { it("should support merging in {in}.{name} keyed param metadata", function () {
const state = fromJS({ const state = fromJS({
json: { json: {
paths: { paths: {
@@ -505,7 +505,7 @@ describe("spec plugin - selectors", function(){
"get": { "get": {
parameters: [ parameters: [
{ {
name: "body", name: "myBody",
in: "body" in: "body"
} }
] ]
@@ -518,7 +518,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
"body.body": { "body.myBody": {
value: "abc123" value: "abc123"
} }
} }
@@ -533,7 +533,7 @@ describe("spec plugin - selectors", function(){
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
parameters: [ parameters: [
{ {
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
} }
@@ -542,7 +542,7 @@ describe("spec plugin - selectors", function(){
}) })
it("should support merging in hash-keyed param metadata", function () { it("should support merging in hash-keyed param metadata", function () {
const bodyParam = fromJS({ const bodyParam = fromJS({
name: "body", name: "myBody",
in: "body" in: "body"
}) })
@@ -563,7 +563,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
[`body.body.hash-${bodyParam.hashCode()}`]: { [`body.myBody.hash-${bodyParam.hashCode()}`]: {
value: "abc123" value: "abc123"
} }
} }
@@ -578,7 +578,7 @@ describe("spec plugin - selectors", function(){
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
parameters: [ parameters: [
{ {
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
} }
@@ -587,7 +587,7 @@ describe("spec plugin - selectors", function(){
}) })
}) })
describe("parameterWithMeta", function() { describe("parameterWithMeta", function() {
it("should support merging in name+in keyed param metadata", function () { it("should support merging in {in}.{name} keyed param metadata", function () {
const state = fromJS({ const state = fromJS({
json: { json: {
paths: { paths: {
@@ -595,7 +595,7 @@ describe("spec plugin - selectors", function(){
"get": { "get": {
parameters: [ parameters: [
{ {
name: "body", name: "myBody",
in: "body" in: "body"
} }
] ]
@@ -608,7 +608,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
"body.body": { "body.myBody": {
value: "abc123" value: "abc123"
} }
} }
@@ -618,17 +618,17 @@ describe("spec plugin - selectors", function(){
} }
}) })
const result = parameterWithMeta(state, ["/", "get"], "body", "body") const result = parameterWithMeta(state, ["/", "get"], "myBody", "body")
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
}) })
}) })
it("should give best-effort when encountering hash-keyed param metadata", function () { it("should give best-effort when encountering hash-keyed param metadata", function () {
const bodyParam = fromJS({ const bodyParam = fromJS({
name: "body", name: "myBody",
in: "body" in: "body"
}) })
@@ -649,7 +649,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
[`body.body.hash-${bodyParam.hashCode()}`]: { [`body.myBody.hash-${bodyParam.hashCode()}`]: {
value: "abc123" value: "abc123"
} }
} }
@@ -659,10 +659,10 @@ describe("spec plugin - selectors", function(){
} }
}) })
const result = parameterWithMeta(state, ["/", "get"], "body", "body") const result = parameterWithMeta(state, ["/", "get"], "myBody", "body")
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
}) })
@@ -670,9 +670,9 @@ describe("spec plugin - selectors", function(){
}) })
describe("parameterWithMetaByIdentity", function() { describe("parameterWithMetaByIdentity", function() {
it("should support merging in name+in keyed param metadata", function () { it("should support merging in {in}.{name} keyed param metadata", function () {
const bodyParam = fromJS({ const bodyParam = fromJS({
name: "body", name: "myBody",
in: "body" in: "body"
}) })
@@ -691,7 +691,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
"body.body": { "body.myBody": {
value: "abc123" value: "abc123"
} }
} }
@@ -704,14 +704,14 @@ describe("spec plugin - selectors", function(){
const result = parameterWithMetaByIdentity(state, ["/", "get"], bodyParam) const result = parameterWithMetaByIdentity(state, ["/", "get"], bodyParam)
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
}) })
}) })
it("should support merging in hash-keyed param metadata", function () { it("should support merging in hash-keyed param metadata", function () {
const bodyParam = fromJS({ const bodyParam = fromJS({
name: "body", name: "myBody",
in: "body" in: "body"
}) })
@@ -732,7 +732,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
parameters: { parameters: {
[`body.body.hash-${bodyParam.hashCode()}`]: { [`body.myBody.hash-${bodyParam.hashCode()}`]: {
value: "abc123" value: "abc123"
} }
} }
@@ -745,14 +745,14 @@ describe("spec plugin - selectors", function(){
const result = parameterWithMetaByIdentity(state, ["/", "get"], bodyParam) const result = parameterWithMetaByIdentity(state, ["/", "get"], bodyParam)
expect(result.toJS()).toEqual({ expect(result.toJS()).toEqual({
name: "body", name: "myBody",
in: "body", in: "body",
value: "abc123" value: "abc123"
}) })
}) })
}) })
describe("parameterInclusionSettingFor", function() { describe("parameterInclusionSettingFor", function() {
it("should support getting name+in param inclusion settings", function () { it("should support getting {in}.{name} param inclusion settings", function () {
const param = fromJS({ const param = fromJS({
name: "param", name: "param",
in: "query", in: "query",
@@ -776,7 +776,7 @@ describe("spec plugin - selectors", function(){
"/": { "/": {
"get": { "get": {
"parameter_inclusions": { "parameter_inclusions": {
[`param.query`]: true [`query.param`]: true
} }
} }
} }

View File

@@ -25,7 +25,9 @@ import {
sanitizeUrl, sanitizeUrl,
extractFileNameFromContentDispositionHeader, extractFileNameFromContentDispositionHeader,
deeplyStripKey, deeplyStripKey,
getSampleSchema getSampleSchema,
paramToIdentifier,
paramToValue,
} from "core/utils" } from "core/utils"
import win from "core/window" import win from "core/window"
@@ -342,30 +344,33 @@ describe("utils", function() {
describe("validateParam", function() { describe("validateParam", function() {
let param = null let param = null
let value = null
let result = null let result = null
const assertValidateParam = (param, expectedError) => { const assertValidateParam = (param, value, expectedError) => {
// Swagger 2.0 version // Swagger 2.0 version
result = validateParam( fromJS(param), false ) result = validateParam( fromJS(param), fromJS(value))
expect( result ).toEqual( expectedError ) expect( result ).toEqual( expectedError )
// OAS3 version, using `schema` sub-object // OAS3 version, using `schema` sub-object
let oas3Param = { let oas3Param = {
value: param.value,
required: param.required, required: param.required,
schema: { schema: {
...param, ...param,
value: undefined,
required: undefined required: undefined
} }
} }
result = validateParam( fromJS(oas3Param), false, true ) result = validateParam( fromJS(oas3Param), fromJS(value), {
isOAS3: true
})
expect( result ).toEqual( expectedError ) expect( result ).toEqual( expectedError )
} }
const assertValidateOas3Param = (param, expectedError) => { const assertValidateOas3Param = (param, value, expectedError) => {
// for cases where you _only_ want to try OAS3 // for cases where you _only_ want to try OAS3
result = validateParam( fromJS(param), false, true ) result = validateParam(fromJS(param), value, {
isOAS3: true
})
expect( result ).toEqual( expectedError ) expect( result ).toEqual( expectedError )
} }
@@ -373,10 +378,12 @@ describe("utils", function() {
// This should "skip" validation because there is no `schema` property // This should "skip" validation because there is no `schema` property
// and we are telling `validateParam` this is an OAS3 spec // and we are telling `validateParam` this is an OAS3 spec
param = fromJS({ param = fromJS({
value: "",
required: true required: true
}) })
result = validateParam( param, false, true ) value = ""
result = validateParam( param, value, {
isOAS3: true
} )
expect( result ).toEqual( [] ) expect( result ).toEqual( [] )
}) })
@@ -386,34 +393,34 @@ describe("utils", function() {
required: true, required: true,
schema: { schema: {
type: "object" type: "object"
}, }
value: { }
value = {
abc: 123 abc: 123
} }
} assertValidateOas3Param(param, value, [])
assertValidateOas3Param(param, [])
// valid object-as-string // valid object-as-string
param = { param = {
required: true, required: true,
schema: { schema: {
type: "object" type: "object"
}, }
value: JSON.stringify({ }
value = JSON.stringify({
abc: 123 abc: 123
}) })
} assertValidateOas3Param(param, value, [])
assertValidateOas3Param(param, [])
// invalid object-as-string // invalid object-as-string
param = { param = {
required: true, required: true,
schema: { schema: {
type: "object" type: "object"
},
value: "{{}"
} }
assertValidateOas3Param(param, ["Parameter string value must be valid JSON"]) }
value = "{{}"
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
// missing when required // missing when required
param = { param = {
@@ -422,7 +429,8 @@ describe("utils", function() {
type: "object" type: "object"
}, },
} }
assertValidateOas3Param(param, ["Required field is not provided"]) value = undefined
assertValidateOas3Param(param, value, ["Required field is not provided"])
}) })
it("validates optional OAS3 objects", function() { it("validates optional OAS3 objects", function() {
@@ -430,32 +438,32 @@ describe("utils", function() {
param = { param = {
schema: { schema: {
type: "object" type: "object"
}, }
value: { }
value = {
abc: 123 abc: 123
} }
} assertValidateOas3Param(param, value, [])
assertValidateOas3Param(param, [])
// valid object-as-string // valid object-as-string
param = { param = {
schema: { schema: {
type: "object" type: "object"
}, }
value: JSON.stringify({ }
value = JSON.stringify({
abc: 123 abc: 123
}) })
} assertValidateOas3Param(param, value, [])
assertValidateOas3Param(param, [])
// invalid object-as-string // invalid object-as-string
param = { param = {
schema: { schema: {
type: "object" type: "object"
},
value: "{{}"
} }
assertValidateOas3Param(param, ["Parameter string value must be valid JSON"]) }
value = "{{}"
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
// missing when not required // missing when not required
param = { param = {
@@ -463,35 +471,36 @@ describe("utils", function() {
type: "object" type: "object"
}, },
} }
assertValidateOas3Param(param, []) value = undefined
assertValidateOas3Param(param, value, [])
}) })
it("validates required strings", function() { it("validates required strings", function() {
// invalid string // invalid string
param = { param = {
required: true, required: true,
type: "string", type: "string"
value: ""
} }
assertValidateParam(param, ["Required field is not provided"]) value = ""
assertValidateParam(param, value, ["Required field is not provided"])
// valid string // valid string
param = { param = {
required: true, required: true,
type: "string", type: "string"
value: "test string"
} }
assertValidateParam(param, []) value = "test string"
assertValidateParam(param, value, [])
// valid string with min and max length // valid string with min and max length
param = { param = {
required: true, required: true,
type: "string", type: "string",
value: "test string",
maxLength: 50, maxLength: 50,
minLength: 1 minLength: 1
} }
assertValidateParam(param, []) value = "test string"
assertValidateParam(param, value, [])
}) })
it("validates required strings with min and max length", function() { it("validates required strings with min and max length", function() {
@@ -499,380 +508,381 @@ describe("utils", function() {
param = { param = {
required: true, required: true,
type: "string", type: "string",
value: "test string",
maxLength: 5 maxLength: 5
} }
assertValidateParam(param, ["Value must be less than MaxLength"]) value = "test string"
assertValidateParam(param, value, ["Value must be less than MaxLength"])
// invalid string with max length 0 // invalid string with max length 0
param = { param = {
required: true, required: true,
type: "string", type: "string",
value: "test string",
maxLength: 0 maxLength: 0
} }
assertValidateParam(param, ["Value must be less than MaxLength"]) value = "test string"
assertValidateParam(param, value, ["Value must be less than MaxLength"])
// invalid string with min length // invalid string with min length
param = { param = {
required: true, required: true,
type: "string", type: "string",
value: "test string",
minLength: 50 minLength: 50
} }
assertValidateParam(param, ["Value must be greater than MinLength"]) value = "test string"
assertValidateParam(param, value, ["Value must be greater than MinLength"])
}) })
it("validates optional strings", function() { it("validates optional strings", function() {
// valid (empty) string // valid (empty) string
param = { param = {
required: false, required: false,
type: "string", type: "string"
value: ""
} }
assertValidateParam(param, []) value = ""
assertValidateParam(param, value, [])
// valid string // valid string
param = { param = {
required: false, required: false,
type: "string", type: "string"
value: "test"
} }
assertValidateParam(param, []) value = "test"
assertValidateParam(param, value, [])
}) })
it("validates required files", function() { it("validates required files", function() {
// invalid file // invalid file
param = { param = {
required: true, required: true,
type: "file", type: "file"
value: undefined
} }
assertValidateParam(param, ["Required field is not provided"]) value = undefined
assertValidateParam(param, value, ["Required field is not provided"])
// valid file // valid file
param = { param = {
required: true, required: true,
type: "file", type: "file"
value: new win.File()
} }
assertValidateParam(param, []) value = new win.File()
assertValidateParam(param, value, [])
}) })
it("validates optional files", function() { it("validates optional files", function() {
// invalid file // invalid file
param = { param = {
required: false, required: false,
type: "file", type: "file"
value: "not a file"
} }
assertValidateParam(param, ["Value must be a file"]) value = "not a file"
assertValidateParam(param, value, ["Value must be a file"])
// valid (empty) file // valid (empty) file
param = { param = {
required: false, required: false,
type: "file", type: "file"
value: undefined
} }
assertValidateParam(param, []) value = undefined
assertValidateParam(param, value, [])
// valid file // valid file
param = { param = {
required: false, required: false,
type: "file", type: "file"
value: new win.File()
} }
assertValidateParam(param, []) value = new win.File()
assertValidateParam(param, value, [])
}) })
it("validates required arrays", function() { it("validates required arrays", function() {
// invalid (empty) array // invalid (empty) array
param = { param = {
required: true, required: true,
type: "array", type: "array"
value: []
} }
assertValidateParam(param, ["Required field is not provided"]) value = []
assertValidateParam(param, value, ["Required field is not provided"])
// invalid (not an array) // invalid (not an array)
param = { param = {
required: true, required: true,
type: "array", type: "array"
value: undefined
} }
assertValidateParam(param, ["Required field is not provided"]) value = undefined
assertValidateParam(param, value, ["Required field is not provided"])
// invalid array, items do not match correct type // invalid array, items do not match correct type
param = { param = {
required: true, required: true,
type: "array", type: "array",
value: [1],
items: { items: {
type: "string" type: "string"
} }
} }
assertValidateParam(param, [{index: 0, error: "Value must be a string"}]) value = [1]
assertValidateParam(param, value, [{index: 0, error: "Value must be a string"}])
// valid array, with no 'type' for items // valid array, with no 'type' for items
param = { param = {
required: true, required: true,
type: "array", type: "array"
value: ["1"]
} }
assertValidateParam(param, []) value = [1]
assertValidateParam(param, value, [])
// valid array, items match type // valid array, items match type
param = { param = {
required: true, required: true,
type: "array", type: "array",
value: ["1"],
items: { items: {
type: "string" type: "string"
} }
} }
assertValidateParam(param, []) value = ["1"]
assertValidateParam(param, value, [])
}) })
it("validates optional arrays", function() { it("validates optional arrays", function() {
// valid, empty array // valid, empty array
param = { param = {
required: false, required: false,
type: "array", type: "array"
value: []
} }
assertValidateParam(param, []) value = []
assertValidateParam(param, value, [])
// invalid, items do not match correct type // invalid, items do not match correct type
param = { param = {
required: false, required: false,
type: "array", type: "array",
value: ["number"],
items: { items: {
type: "number" type: "number"
} }
} }
assertValidateParam(param, [{index: 0, error: "Value must be a number"}]) value = ["number"]
assertValidateParam(param, value, [{index: 0, error: "Value must be a number"}])
// valid // valid
param = { param = {
required: false, required: false,
type: "array", type: "array",
value: ["test"],
items: { items: {
type: "string" type: "string"
} }
} }
assertValidateParam(param, []) value = ["test"]
assertValidateParam(param, value, [])
}) })
it("validates required booleans", function() { it("validates required booleans", function() {
// invalid boolean value // invalid boolean value
param = { param = {
required: true, required: true,
type: "boolean", type: "boolean"
value: undefined
} }
assertValidateParam(param, ["Required field is not provided"]) value = undefined
assertValidateParam(param, value, ["Required field is not provided"])
// invalid boolean value (not a boolean) // invalid boolean value (not a boolean)
param = { param = {
required: true, required: true,
type: "boolean", type: "boolean"
value: "test string"
} }
assertValidateParam(param, ["Value must be a boolean"]) value = "test string"
assertValidateParam(param, value, ["Value must be a boolean"])
// valid boolean value // valid boolean value
param = { param = {
required: true, required: true,
type: "boolean", type: "boolean"
value: "true"
} }
assertValidateParam(param, []) value = "true"
assertValidateParam(param, value, [])
// valid boolean value // valid boolean value
param = { param = {
required: true, required: true,
type: "boolean", type: "boolean"
value: false
} }
assertValidateParam(param, []) value = false
assertValidateParam(param, value, [])
}) })
it("validates optional booleans", function() { it("validates optional booleans", function() {
// valid (empty) boolean value // valid (empty) boolean value
param = { param = {
required: false, required: false,
type: "boolean", type: "boolean"
value: undefined
} }
assertValidateParam(param, []) value = undefined
assertValidateParam(param, value, [])
// invalid boolean value (not a boolean) // invalid boolean value (not a boolean)
param = { param = {
required: false, required: false,
type: "boolean", type: "boolean"
value: "test string"
} }
assertValidateParam(param, ["Value must be a boolean"]) value = "test string"
assertValidateParam(param, value, ["Value must be a boolean"])
// valid boolean value // valid boolean value
param = { param = {
required: false, required: false,
type: "boolean", type: "boolean"
value: "true"
} }
assertValidateParam(param, []) value = "true"
assertValidateParam(param, value, [])
// valid boolean value // valid boolean value
param = { param = {
required: false, required: false,
type: "boolean", type: "boolean"
value: false
} }
assertValidateParam(param, []) value = false
assertValidateParam(param, value, [])
}) })
it("validates required numbers", function() { it("validates required numbers", function() {
// invalid number, string instead of a number // invalid number, string instead of a number
param = { param = {
required: true, required: true,
type: "number", type: "number"
value: "test"
} }
assertValidateParam(param, ["Value must be a number"]) value = "test"
assertValidateParam(param, value, ["Value must be a number"])
// invalid number, undefined value // invalid number, undefined value
param = { param = {
required: true, required: true,
type: "number", type: "number"
value: undefined
} }
assertValidateParam(param, ["Required field is not provided"]) value = undefined
assertValidateParam(param, value, ["Required field is not provided"])
// valid number with min and max // valid number with min and max
param = { param = {
required: true, required: true,
type: "number", type: "number",
value: 10,
minimum: 5, minimum: 5,
maximum: 99 maximum: 99
} }
assertValidateParam(param, []) value = 10
assertValidateParam(param, value, [])
// valid negative number with min and max // valid negative number with min and max
param = { param = {
required: true, required: true,
type: "number", type: "number",
value: -10,
minimum: -50, minimum: -50,
maximum: -5 maximum: -5
} }
assertValidateParam(param, []) value = -10
assertValidateParam(param, value, [])
// invalid number with maximum:0 // invalid number with maximum:0
param = { param = {
required: true, required: true,
type: "number", type: "number",
value: 1,
maximum: 0 maximum: 0
} }
assertValidateParam(param, ["Value must be less than Maximum"]) value = 1
assertValidateParam(param, value, ["Value must be less than Maximum"])
// invalid number with minimum:0 // invalid number with minimum:0
param = { param = {
required: true, required: true,
type: "number", type: "number",
value: -10,
minimum: 0 minimum: 0
} }
assertValidateParam(param, ["Value must be greater than Minimum"]) value = -10
assertValidateParam(param, value, ["Value must be greater than Minimum"])
}) })
it("validates optional numbers", function() { it("validates optional numbers", function() {
// invalid number, string instead of a number // invalid number, string instead of a number
param = { param = {
required: false, required: false,
type: "number", type: "number"
value: "test"
} }
assertValidateParam(param, ["Value must be a number"]) value = "test"
assertValidateParam(param, value, ["Value must be a number"])
// valid (empty) number // valid (empty) number
param = { param = {
required: false, required: false,
type: "number", type: "number"
value: undefined
} }
assertValidateParam(param, []) value = undefined
assertValidateParam(param, value, [])
// valid number // valid number
param = { param = {
required: false, required: false,
type: "number", type: "number"
value: 10
} }
assertValidateParam(param, []) value = 10
assertValidateParam(param, value, [])
}) })
it("validates required integers", function() { it("validates required integers", function() {
// invalid integer, string instead of an integer // invalid integer, string instead of an integer
param = { param = {
required: true, required: true,
type: "integer", type: "integer"
value: "test"
} }
assertValidateParam(param, ["Value must be an integer"]) value = "test"
assertValidateParam(param, value, ["Value must be an integer"])
// invalid integer, undefined value // invalid integer, undefined value
param = { param = {
required: true, required: true,
type: "integer", type: "integer"
value: undefined
} }
assertValidateParam(param, ["Required field is not provided"]) value = undefined
assertValidateParam(param, value, ["Required field is not provided"])
// valid integer, but 0 is falsy in JS // valid integer, but 0 is falsy in JS
param = { param = {
required: true, required: true,
type: "integer", type: "integer"
value: 0
} }
assertValidateParam(param, []) value = 0
assertValidateParam(param, value, [])
// valid integer // valid integer
param = { param = {
required: true, required: true,
type: "integer", type: "integer"
value: 10
} }
assertValidateParam(param, []) value = 10
assertValidateParam(param, value, [])
}) })
it("validates optional integers", function() { it("validates optional integers", function() {
// invalid integer, string instead of an integer // invalid integer, string instead of an integer
param = { param = {
required: false, required: false,
type: "integer", type: "integer"
value: "test"
} }
assertValidateParam(param, ["Value must be an integer"]) value = "test"
assertValidateParam(param, value, ["Value must be an integer"])
// valid (empty) integer // valid (empty) integer
param = { param = {
required: false, required: false,
type: "integer", type: "integer"
value: undefined
} }
assertValidateParam(param, []) value = undefined
assertValidateParam(param, value, [])
// integers // integers
param = { param = {
required: false, required: false,
type: "integer", type: "integer"
value: 10
} }
assertValidateParam(param, []) value = 10
assertValidateParam(param, value, [])
}) })
}) })
@@ -1209,6 +1219,7 @@ describe("utils", function() {
expect(sanitizeUrl({})).toEqual("") expect(sanitizeUrl({})).toEqual("")
}) })
}) })
describe("getSampleSchema", function() { describe("getSampleSchema", function() {
const oriDate = Date const oriDate = Date
@@ -1235,4 +1246,144 @@ describe("utils", function() {
expect(res).toEqual(new Date().toISOString()) expect(res).toEqual(new Date().toISOString())
}) })
}) })
describe("paramToIdentifier", function() {
it("should convert an Immutable parameter map to an identifier", () => {
const param = fromJS({
name: "id",
in: "query"
})
const res = paramToIdentifier(param)
expect(res).toEqual("query.id.hash-606199662")
})
it("should convert an Immutable parameter map to a set of identifiers", () => {
const param = fromJS({
name: "id",
in: "query"
})
const res = paramToIdentifier(param, { returnAll: true })
expect(res).toEqual([
"query.id.hash-606199662",
"query.id",
"id"
])
})
it("should convert an unhashable Immutable parameter map to an identifier", () => {
const param = fromJS({
name: "id",
in: "query"
})
param.hashCode = null
const res = paramToIdentifier(param)
expect(res).toEqual("query.id")
})
it("should convert an unhashable Immutable parameter map to a set of identifiers", () => {
const param = fromJS({
name: "id",
in: "query"
})
param.hashCode = null
const res = paramToIdentifier(param, { returnAll: true })
expect(res).toEqual([
"query.id",
"id"
])
})
it("should convert an Immutable parameter map lacking an `in` value to an identifier", () => {
const param = fromJS({
name: "id"
})
const res = paramToIdentifier(param)
expect(res).toEqual("id")
})
it("should convert an Immutable parameter map lacking an `in` value to an identifier", () => {
const param = fromJS({
name: "id"
})
const res = paramToIdentifier(param, { returnAll: true })
expect(res).toEqual(["id"])
})
it("should throw gracefully when given a non-Immutable parameter input", () => {
const param = {
name: "id"
}
let error = null
let res = null
try {
const res = paramToIdentifier(param)
} catch(e) {
error = e
}
expect(error).toBeA(Error)
expect(error.message).toInclude("received a non-Im.Map parameter as input")
expect(res).toEqual(null)
})
})
describe("paramToValue", function() {
it("should identify a hash-keyed value", () => {
const param = fromJS({
name: "id",
in: "query"
})
const paramValues = {
"query.id.hash-606199662": "asdf"
}
const res = paramToValue(param, paramValues)
expect(res).toEqual("asdf")
})
it("should identify a in+name value", () => {
const param = fromJS({
name: "id",
in: "query"
})
const paramValues = {
"query.id": "asdf"
}
const res = paramToValue(param, paramValues)
expect(res).toEqual("asdf")
})
it("should identify a name value", () => {
const param = fromJS({
name: "id",
in: "query"
})
const paramValues = {
"id": "asdf"
}
const res = paramToValue(param, paramValues)
expect(res).toEqual("asdf")
})
})
}) })

View File

@@ -0,0 +1,26 @@
openapi: "3.0.0"
paths:
/aev:
get:
parameters:
- name: param
in: query
allowEmptyValue: true
schema:
type: string
responses:
200:
description: ok
/aev/and/required:
get:
parameters:
- name: param
in: query
allowEmptyValue: true
required: true
schema:
type: string
responses:
200:
description: ok

View File

@@ -0,0 +1,121 @@
describe("#5129: parameter required + allowEmptyValue interactions", () => {
describe("allowEmptyValue parameter", () => {
const opId = "#operations-default-get_aev"
it("should omit the parameter by default", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev")
})
it("should include a value", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=text]`)
.type("asdf")
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev?param=asdf")
})
it("should include an empty value when empty value box is checked", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=checkbox]`)
.check()
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev?param=")
})
it("should include a value when empty value box is checked and then input is provided", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=checkbox]`)
.check()
.get(`.parameters-col_description input[type=text]`)
.type("1234")
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev?param=1234")
})
})
describe("allowEmptyValue + required parameter", () => {
const opId = "#operations-default-get_aev_and_required"
it("should refuse to execute by default", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(".btn.execute")
.click()
.wait(1000)
.get(".request-url pre")
.should("not.exist")
})
it("should include a value", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=text]`)
.type("asdf")
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev/and/required?param=asdf")
})
it("should include an empty value when empty value box is checked", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=checkbox]`)
.check()
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev/and/required?param=")
})
it("should include a value when empty value box is checked and then input is provided", () => {
cy
.visit("/?url=/documents/bugs/5129.yaml")
.get(opId)
.click()
.get(".btn.try-out__btn")
.click()
.get(`.parameters-col_description input[type=checkbox]`)
.check()
.get(`.parameters-col_description input[type=text]`)
.type("1234")
.get(".btn.execute")
.click()
.get(".request-url pre")
.should("have.text", "http://localhost:3230/aev/and/required?param=1234")
})
})
})