fix(validateParam): validate JSON parameter values + support Parameter.content (#5657)

* improve(getParameterSchema): ParameterSchemaDescriptor pattern

* chore: update usage of `getParameterSchema`

* consider `Parameter.content` media type when validating JSON values
This commit is contained in:
kyle
2019-10-11 11:20:23 -07:00
committed by GitHub
parent 71a17f801e
commit 75a0e5d5dc
5 changed files with 190 additions and 51 deletions

View File

@@ -41,7 +41,7 @@ export default class ParameterRow extends Component {
let enumValue let enumValue
if(isOAS3) { if(isOAS3) {
let schema = getParameterSchema(parameterWithMeta, { isOAS3 }) let { schema } = getParameterSchema(parameterWithMeta, { isOAS3 })
enumValue = schema.get("enum") enumValue = schema.get("enum")
} else { } else {
enumValue = parameterWithMeta ? parameterWithMeta.get("enum") : undefined enumValue = parameterWithMeta ? parameterWithMeta.get("enum") : undefined
@@ -98,7 +98,7 @@ export default class ParameterRow extends Component {
const paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map() const paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
const schema = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() }) const { schema } = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() })
const parameterMediaType = paramWithMeta const parameterMediaType = paramWithMeta
.get("content", Map()) .get("content", Map())
@@ -209,9 +209,10 @@ export default class ParameterRow extends Component {
const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer") const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer")
const Example = getComponent("Example") const Example = getComponent("Example")
let { schema } = getParameterSchema(param, { isOAS3 })
let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map() let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
let format = param.get("format") let format = param.get("format")
let schema = getParameterSchema(param, { isOAS3 })
let type = schema.get("type") let type = schema.get("type")
let isFormData = inType === "formData" let isFormData = inType === "formData"
let isFormDataSupported = "FormData" in win let isFormDataSupported = "FormData" in win

View File

@@ -501,12 +501,14 @@ export const validatePattern = (val, rxPattern) => {
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => { export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
let errors = [] let errors = []
let required = param.get("required")
let paramDetails = getParameterSchema(param, { isOAS3 }) let paramRequired = param.get("required")
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
if(!paramDetails) return errors if(!paramDetails) return errors
let required = paramDetails.get("required")
let maximum = paramDetails.get("maximum") let maximum = paramDetails.get("maximum")
let minimum = paramDetails.get("minimum") let minimum = paramDetails.get("minimum")
let type = paramDetails.get("type") let type = paramDetails.get("type")
@@ -520,7 +522,7 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
then we should do our validation routine. then we should do our validation routine.
Only bother validating the parameter if the type was specified. Only bother validating the parameter if the type was specified.
*/ */
if ( type && (required || value) ) { if ( type && (paramRequired || required || value) ) {
// These checks should evaluate to true if there is a parameter // These checks should evaluate to true if there is a parameter
let stringCheck = type === "string" && value let stringCheck = type === "string" && value
let arrayCheck = type === "array" && Array.isArray(value) && value.length let arrayCheck = type === "array" && Array.isArray(value) && value.length
@@ -533,17 +535,6 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
let objectCheck = type === "object" && typeof value === "object" && value !== null let objectCheck = type === "object" && typeof value === "object" && value !== null
let objectStringCheck = type === "object" && typeof value === "string" && value let objectStringCheck = type === "object" && typeof value === "string" && value
// if(type === "object" && typeof value === "string") {
// // Disabled because `validateParam` doesn't consider the MediaType of the
// // `Parameter.content` hint correctly.
// try {
// JSON.parse(value)
// } catch(e) {
// errors.push("Parameter string value must be valid JSON")
// return errors
// }
// }
const allChecks = [ const allChecks = [
stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck, stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,
booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck, booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck,
@@ -551,11 +542,25 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
const passedAnyCheck = allChecks.some(v => !!v) const passedAnyCheck = allChecks.some(v => !!v)
if (required && !passedAnyCheck && !bypassRequiredCheck ) { if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) {
errors.push("Required field is not provided") errors.push("Required field is not provided")
return errors return errors
} }
if (
type === "object" &&
typeof value === "string" &&
(parameterContentMediaType === null ||
parameterContentMediaType === "application/json")
) {
try {
JSON.parse(value)
} catch (e) {
errors.push("Parameter string value must be valid JSON")
return errors
}
}
if (pattern) { if (pattern) {
let err = validatePattern(value, pattern) let err = validatePattern(value, pattern)
if (err) errors.push(err) if (err) errors.push(err)

View File

@@ -23,6 +23,12 @@ const swagger2SchemaKeys = Im.Set.of(
"multipleOf" "multipleOf"
) )
/**
* @typedef {Object} ParameterSchemaDescriptor
* @property {Immutable.Map} schema - the parameter schema
* @property {string|null} parameterContentMediaType - the effective media type, for `content`-based OpenAPI 3.0 Parameters, or `null` otherwise
*/
/** /**
* Get the effective schema value for a parameter, or an empty Immutable.Map if * Get the effective schema value for a parameter, or an empty Immutable.Map if
* no suitable schema can be found. * no suitable schema can be found.
@@ -35,18 +41,29 @@ const swagger2SchemaKeys = Im.Set.of(
* @param {object} config * @param {object} config
* @param {boolean} config.isOAS3 Whether the parameter is from an OpenAPI 2.0 * @param {boolean} config.isOAS3 Whether the parameter is from an OpenAPI 2.0
* or OpenAPI 3.0 definition * or OpenAPI 3.0 definition
* @return {Immutable.Map} The desired schema * @return {ParameterSchemaDescriptor} Information about the parameter schema
*/ */
export default function getParameterSchema(parameter, { isOAS3 } = {}) { export default function getParameterSchema(parameter, { isOAS3 } = {}) {
// Return empty Map if `parameter` isn't a Map // Return empty Map if `parameter` isn't a Map
if (!Im.Map.isMap(parameter)) return Im.Map() if (!Im.Map.isMap(parameter)) {
return {
schema: Im.Map(),
parameterContentMediaType: null,
}
}
if (!isOAS3) { if (!isOAS3) {
// Swagger 2.0 // Swagger 2.0
if (parameter.get("in") === "body") { if (parameter.get("in") === "body") {
return parameter.get("schema", Im.Map()) return {
schema: parameter.get("schema", Im.Map()),
parameterContentMediaType: null,
}
} else { } else {
return parameter.filter((v, k) => swagger2SchemaKeys.includes(k)) return {
schema: parameter.filter((v, k) => swagger2SchemaKeys.includes(k)),
parameterContentMediaType: null,
}
} }
} }
@@ -57,11 +74,19 @@ export default function getParameterSchema(parameter, { isOAS3 } = {}) {
.get("content", Im.Map({})) .get("content", Im.Map({}))
.keySeq() .keySeq()
return parameter.getIn( const parameterContentMediaType = parameterContentMediaTypes.first()
["content", parameterContentMediaTypes.first(), "schema"],
Im.Map() return {
) schema: parameter.getIn(
["content", parameterContentMediaType, "schema"],
Im.Map()
),
parameterContentMediaType,
}
} }
return parameter.get("schema", Im.Map()) return {
schema: parameter.get("schema", Im.Map()),
parameterContentMediaType: null,
}
} }

View File

@@ -3,20 +3,22 @@
*/ */
import expect from "expect" import expect from "expect"
import Im, { fromJS } from "immutable" import { fromJS } from "immutable"
import getParameterSchema from "../../../../src/helpers/get-parameter-schema" import getParameterSchema from "../../../../src/helpers/get-parameter-schema"
describe("getParameterSchema", () => { describe("getParameterSchema", () => {
it("should return an empty Map when given no parameters", () => { it("should return an empty Map when given no parameters", () => {
const result = getParameterSchema() const result = getParameterSchema()
expect(result).toEqual(fromJS({})) expect(result.schema.toJS()).toEqual({})
expect(result.parameterContentMediaType).toEqual(null)
}) })
it("should return an empty Map when given an empty Map", () => { it("should return an empty Map when given an empty Map", () => {
const result = getParameterSchema(fromJS({})) const result = getParameterSchema(fromJS({}))
expect(result).toEqual(fromJS({})) expect(result.schema.toJS()).toEqual({})
expect(result.parameterContentMediaType).toEqual(null)
}) })
it("should return a schema for a Swagger 2.0 query parameter", () => { it("should return a schema for a Swagger 2.0 query parameter", () => {
@@ -34,12 +36,13 @@ describe("getParameterSchema", () => {
}) })
) )
expect(result.toJS()).toEqual({ expect(result.schema.toJS()).toEqual({
type: "array", type: "array",
items: { items: {
type: "string", type: "string",
}, },
}) })
expect(result.parameterContentMediaType).toEqual(null)
}) })
it("should return a schema for a Swagger 2.0 body parameter", () => { it("should return a schema for a Swagger 2.0 body parameter", () => {
@@ -58,12 +61,13 @@ describe("getParameterSchema", () => {
}) })
) )
expect(result.toJS()).toEqual({ expect(result.schema.toJS()).toEqual({
type: "array", type: "array",
items: { items: {
type: "string", type: "string",
}, },
}) })
expect(result.parameterContentMediaType).toEqual(null)
}) })
it("should return a schema for an OpenAPI 3.0 query parameter", () => { it("should return a schema for an OpenAPI 3.0 query parameter", () => {
@@ -87,12 +91,13 @@ describe("getParameterSchema", () => {
} }
) )
expect(result.toJS()).toEqual({ expect(result.schema.toJS()).toEqual({
type: "array", type: "array",
items: { items: {
type: "string", type: "string",
}, },
}) })
expect(result.parameterContentMediaType).toEqual(null)
}) })
it("should return a schema for an OpenAPI 3.0 query parameter with `content`", () => { it("should return a schema for an OpenAPI 3.0 query parameter with `content`", () => {
@@ -126,7 +131,7 @@ describe("getParameterSchema", () => {
} }
) )
expect(result.toJS()).toEqual({ expect(result.schema.toJS()).toEqual({
type: "object", type: "object",
required: ["lat", "long"], required: ["lat", "long"],
properties: { properties: {
@@ -138,5 +143,6 @@ describe("getParameterSchema", () => {
}, },
}, },
}) })
expect(result.parameterContentMediaType).toEqual(`application/json`)
}) })
}) })

View File

@@ -414,15 +414,15 @@ describe("utils", function() {
}) })
assertValidateOas3Param(param, value, []) assertValidateOas3Param(param, value, [])
// // invalid object-as-string // invalid object-as-string
// param = { param = {
// required: true, required: true,
// schema: { schema: {
// type: "object" type: "object"
// } }
// } }
// value = "{{}" value = "{{}"
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"]) assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
// missing when required // missing when required
param = { param = {
@@ -458,14 +458,14 @@ describe("utils", function() {
}) })
assertValidateOas3Param(param, value, []) assertValidateOas3Param(param, value, [])
// // invalid object-as-string // invalid object-as-string
// param = { param = {
// schema: { schema: {
// type: "object" type: "object"
// } }
// } }
// value = "{{}" value = "{{}"
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"]) assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
// missing when not required // missing when not required
param = { param = {
@@ -505,6 +505,108 @@ describe("utils", function() {
assertValidateParam(param, value, []) assertValidateParam(param, value, [])
}) })
it("handles OAS3 `Parameter.content`", function() {
// invalid string
param = {
content: {
"text/plain": {
schema: {
required: true,
type: "string"
}
}
}
}
value = ""
assertValidateOas3Param(param, value, ["Required field is not provided"])
// valid string
param = {
content: {
"text/plain": {
schema: {
required: true,
type: "string"
}
}
}
}
value = "test string"
assertValidateOas3Param(param, value, [])
// invalid (empty) JSON string
param = {
content: {
"application/json": {
schema: {
required: true,
type: "object"
}
}
}
}
value = ""
assertValidateOas3Param(param, value, ["Required field is not provided"])
// invalid (malformed) JSON string
param = {
content: {
"application/json": {
schema: {
required: true,
type: "object"
}
}
}
}
value = "{{}"
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
// valid (empty object) JSON string
param = {
content: {
"application/json": {
schema: {
required: true,
type: "object"
}
}
}
}
value = "{}"
assertValidateOas3Param(param, value, [])
// valid (empty object) JSON object
param = {
content: {
"application/json": {
schema: {
required: true,
type: "object"
}
}
}
}
value = {}
assertValidateOas3Param(param, value, [])
// should skip JSON validation for non-JSON media types
param = {
content: {
"application/definitely-not-json": {
schema: {
required: true,
type: "object"
}
}
}
}
value = "{{}"
assertValidateOas3Param(param, value, [])
})
it("validates required strings with min and max length", function() { it("validates required strings with min and max length", function() {
// invalid string with max length // invalid string with max length
param = { param = {