Skip to content

Commit 75a0e5d

Browse files
authoredOct 11, 2019
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
1 parent 71a17f8 commit 75a0e5d

File tree

5 files changed

+190
-51
lines changed

5 files changed

+190
-51
lines changed
 

‎src/core/components/parameter-row.jsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default class ParameterRow extends Component {
4141
let enumValue
4242

4343
if(isOAS3) {
44-
let schema = getParameterSchema(parameterWithMeta, { isOAS3 })
44+
let { schema } = getParameterSchema(parameterWithMeta, { isOAS3 })
4545
enumValue = schema.get("enum")
4646
} else {
4747
enumValue = parameterWithMeta ? parameterWithMeta.get("enum") : undefined
@@ -98,7 +98,7 @@ export default class ParameterRow extends Component {
9898

9999
const paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
100100

101-
const schema = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() })
101+
const { schema } = getParameterSchema(paramWithMeta, { isOAS3: specSelectors.isOAS3() })
102102

103103
const parameterMediaType = paramWithMeta
104104
.get("content", Map())
@@ -209,9 +209,10 @@ export default class ParameterRow extends Component {
209209
const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer")
210210
const Example = getComponent("Example")
211211

212+
let { schema } = getParameterSchema(param, { isOAS3 })
212213
let paramWithMeta = specSelectors.parameterWithMetaByIdentity(pathMethod, rawParam) || Map()
214+
213215
let format = param.get("format")
214-
let schema = getParameterSchema(param, { isOAS3 })
215216
let type = schema.get("type")
216217
let isFormData = inType === "formData"
217218
let isFormDataSupported = "FormData" in win

‎src/core/utils.js

+20-15
Original file line numberDiff line numberDiff line change
@@ -501,12 +501,14 @@ export const validatePattern = (val, rxPattern) => {
501501
export const validateParam = (param, value, { isOAS3 = false, bypassRequiredCheck = false } = {}) => {
502502

503503
let errors = []
504-
let required = param.get("required")
505504

506-
let paramDetails = getParameterSchema(param, { isOAS3 })
505+
let paramRequired = param.get("required")
506+
507+
let { schema: paramDetails, parameterContentMediaType } = getParameterSchema(param, { isOAS3 })
507508

508509
if(!paramDetails) return errors
509510

511+
let required = paramDetails.get("required")
510512
let maximum = paramDetails.get("maximum")
511513
let minimum = paramDetails.get("minimum")
512514
let type = paramDetails.get("type")
@@ -520,7 +522,7 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
520522
then we should do our validation routine.
521523
Only bother validating the parameter if the type was specified.
522524
*/
523-
if ( type && (required || value) ) {
525+
if ( type && (paramRequired || required || value) ) {
524526
// These checks should evaluate to true if there is a parameter
525527
let stringCheck = type === "string" && value
526528
let arrayCheck = type === "array" && Array.isArray(value) && value.length
@@ -533,29 +535,32 @@ export const validateParam = (param, value, { isOAS3 = false, bypassRequiredChec
533535
let objectCheck = type === "object" && typeof value === "object" && value !== null
534536
let objectStringCheck = type === "object" && typeof value === "string" && value
535537

536-
// if(type === "object" && typeof value === "string") {
537-
// // Disabled because `validateParam` doesn't consider the MediaType of the
538-
// // `Parameter.content` hint correctly.
539-
// try {
540-
// JSON.parse(value)
541-
// } catch(e) {
542-
// errors.push("Parameter string value must be valid JSON")
543-
// return errors
544-
// }
545-
// }
546-
547538
const allChecks = [
548539
stringCheck, arrayCheck, arrayListCheck, arrayStringCheck, fileCheck,
549540
booleanCheck, numberCheck, integerCheck, objectCheck, objectStringCheck,
550541
]
551542

552543
const passedAnyCheck = allChecks.some(v => !!v)
553544

554-
if (required && !passedAnyCheck && !bypassRequiredCheck ) {
545+
if ((paramRequired || required) && !passedAnyCheck && !bypassRequiredCheck ) {
555546
errors.push("Required field is not provided")
556547
return errors
557548
}
558549

550+
if (
551+
type === "object" &&
552+
typeof value === "string" &&
553+
(parameterContentMediaType === null ||
554+
parameterContentMediaType === "application/json")
555+
) {
556+
try {
557+
JSON.parse(value)
558+
} catch (e) {
559+
errors.push("Parameter string value must be valid JSON")
560+
return errors
561+
}
562+
}
563+
559564
if (pattern) {
560565
let err = validatePattern(value, pattern)
561566
if (err) errors.push(err)

‎src/helpers/get-parameter-schema.js

+34-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ const swagger2SchemaKeys = Im.Set.of(
2323
"multipleOf"
2424
)
2525

26+
/**
27+
* @typedef {Object} ParameterSchemaDescriptor
28+
* @property {Immutable.Map} schema - the parameter schema
29+
* @property {string|null} parameterContentMediaType - the effective media type, for `content`-based OpenAPI 3.0 Parameters, or `null` otherwise
30+
*/
31+
2632
/**
2733
* Get the effective schema value for a parameter, or an empty Immutable.Map if
2834
* no suitable schema can be found.
@@ -35,18 +41,29 @@ const swagger2SchemaKeys = Im.Set.of(
3541
* @param {object} config
3642
* @param {boolean} config.isOAS3 Whether the parameter is from an OpenAPI 2.0
3743
* or OpenAPI 3.0 definition
38-
* @return {Immutable.Map} The desired schema
44+
* @return {ParameterSchemaDescriptor} Information about the parameter schema
3945
*/
4046
export default function getParameterSchema(parameter, { isOAS3 } = {}) {
4147
// Return empty Map if `parameter` isn't a Map
42-
if (!Im.Map.isMap(parameter)) return Im.Map()
48+
if (!Im.Map.isMap(parameter)) {
49+
return {
50+
schema: Im.Map(),
51+
parameterContentMediaType: null,
52+
}
53+
}
4354

4455
if (!isOAS3) {
4556
// Swagger 2.0
4657
if (parameter.get("in") === "body") {
47-
return parameter.get("schema", Im.Map())
58+
return {
59+
schema: parameter.get("schema", Im.Map()),
60+
parameterContentMediaType: null,
61+
}
4862
} else {
49-
return parameter.filter((v, k) => swagger2SchemaKeys.includes(k))
63+
return {
64+
schema: parameter.filter((v, k) => swagger2SchemaKeys.includes(k)),
65+
parameterContentMediaType: null,
66+
}
5067
}
5168
}
5269

@@ -57,11 +74,19 @@ export default function getParameterSchema(parameter, { isOAS3 } = {}) {
5774
.get("content", Im.Map({}))
5875
.keySeq()
5976

60-
return parameter.getIn(
61-
["content", parameterContentMediaTypes.first(), "schema"],
62-
Im.Map()
63-
)
77+
const parameterContentMediaType = parameterContentMediaTypes.first()
78+
79+
return {
80+
schema: parameter.getIn(
81+
["content", parameterContentMediaType, "schema"],
82+
Im.Map()
83+
),
84+
parameterContentMediaType,
85+
}
6486
}
6587

66-
return parameter.get("schema", Im.Map())
88+
return {
89+
schema: parameter.get("schema", Im.Map()),
90+
parameterContentMediaType: null,
91+
}
6792
}

‎test/mocha/core/helpers/get-parameter-schema.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33
*/
44

55
import expect from "expect"
6-
import Im, { fromJS } from "immutable"
6+
import { fromJS } from "immutable"
77
import getParameterSchema from "../../../../src/helpers/get-parameter-schema"
88

99
describe("getParameterSchema", () => {
1010
it("should return an empty Map when given no parameters", () => {
1111
const result = getParameterSchema()
1212

13-
expect(result).toEqual(fromJS({}))
13+
expect(result.schema.toJS()).toEqual({})
14+
expect(result.parameterContentMediaType).toEqual(null)
1415
})
1516

1617
it("should return an empty Map when given an empty Map", () => {
1718
const result = getParameterSchema(fromJS({}))
1819

19-
expect(result).toEqual(fromJS({}))
20+
expect(result.schema.toJS()).toEqual({})
21+
expect(result.parameterContentMediaType).toEqual(null)
2022
})
2123

2224
it("should return a schema for a Swagger 2.0 query parameter", () => {
@@ -34,12 +36,13 @@ describe("getParameterSchema", () => {
3436
})
3537
)
3638

37-
expect(result.toJS()).toEqual({
39+
expect(result.schema.toJS()).toEqual({
3840
type: "array",
3941
items: {
4042
type: "string",
4143
},
4244
})
45+
expect(result.parameterContentMediaType).toEqual(null)
4346
})
4447

4548
it("should return a schema for a Swagger 2.0 body parameter", () => {
@@ -58,12 +61,13 @@ describe("getParameterSchema", () => {
5861
})
5962
)
6063

61-
expect(result.toJS()).toEqual({
64+
expect(result.schema.toJS()).toEqual({
6265
type: "array",
6366
items: {
6467
type: "string",
6568
},
6669
})
70+
expect(result.parameterContentMediaType).toEqual(null)
6771
})
6872

6973
it("should return a schema for an OpenAPI 3.0 query parameter", () => {
@@ -87,12 +91,13 @@ describe("getParameterSchema", () => {
8791
}
8892
)
8993

90-
expect(result.toJS()).toEqual({
94+
expect(result.schema.toJS()).toEqual({
9195
type: "array",
9296
items: {
9397
type: "string",
9498
},
9599
})
100+
expect(result.parameterContentMediaType).toEqual(null)
96101
})
97102

98103
it("should return a schema for an OpenAPI 3.0 query parameter with `content`", () => {
@@ -126,7 +131,7 @@ describe("getParameterSchema", () => {
126131
}
127132
)
128133

129-
expect(result.toJS()).toEqual({
134+
expect(result.schema.toJS()).toEqual({
130135
type: "object",
131136
required: ["lat", "long"],
132137
properties: {
@@ -138,5 +143,6 @@ describe("getParameterSchema", () => {
138143
},
139144
},
140145
})
146+
expect(result.parameterContentMediaType).toEqual(`application/json`)
141147
})
142148
})

‎test/mocha/core/utils.js

+119-17
Original file line numberDiff line numberDiff line change
@@ -414,15 +414,15 @@ describe("utils", function() {
414414
})
415415
assertValidateOas3Param(param, value, [])
416416

417-
// // invalid object-as-string
418-
// param = {
419-
// required: true,
420-
// schema: {
421-
// type: "object"
422-
// }
423-
// }
424-
// value = "{{}"
425-
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
417+
// invalid object-as-string
418+
param = {
419+
required: true,
420+
schema: {
421+
type: "object"
422+
}
423+
}
424+
value = "{{}"
425+
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
426426

427427
// missing when required
428428
param = {
@@ -458,14 +458,14 @@ describe("utils", function() {
458458
})
459459
assertValidateOas3Param(param, value, [])
460460

461-
// // invalid object-as-string
462-
// param = {
463-
// schema: {
464-
// type: "object"
465-
// }
466-
// }
467-
// value = "{{}"
468-
// assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
461+
// invalid object-as-string
462+
param = {
463+
schema: {
464+
type: "object"
465+
}
466+
}
467+
value = "{{}"
468+
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
469469

470470
// missing when not required
471471
param = {
@@ -505,6 +505,108 @@ describe("utils", function() {
505505
assertValidateParam(param, value, [])
506506
})
507507

508+
it("handles OAS3 `Parameter.content`", function() {
509+
// invalid string
510+
param = {
511+
content: {
512+
"text/plain": {
513+
schema: {
514+
required: true,
515+
type: "string"
516+
}
517+
}
518+
}
519+
}
520+
value = ""
521+
assertValidateOas3Param(param, value, ["Required field is not provided"])
522+
523+
// valid string
524+
param = {
525+
content: {
526+
"text/plain": {
527+
schema: {
528+
required: true,
529+
type: "string"
530+
}
531+
}
532+
}
533+
}
534+
value = "test string"
535+
assertValidateOas3Param(param, value, [])
536+
537+
538+
// invalid (empty) JSON string
539+
param = {
540+
content: {
541+
"application/json": {
542+
schema: {
543+
required: true,
544+
type: "object"
545+
}
546+
}
547+
}
548+
}
549+
value = ""
550+
assertValidateOas3Param(param, value, ["Required field is not provided"])
551+
552+
// invalid (malformed) JSON string
553+
param = {
554+
content: {
555+
"application/json": {
556+
schema: {
557+
required: true,
558+
type: "object"
559+
}
560+
}
561+
}
562+
}
563+
value = "{{}"
564+
assertValidateOas3Param(param, value, ["Parameter string value must be valid JSON"])
565+
566+
567+
// valid (empty object) JSON string
568+
param = {
569+
content: {
570+
"application/json": {
571+
schema: {
572+
required: true,
573+
type: "object"
574+
}
575+
}
576+
}
577+
}
578+
value = "{}"
579+
assertValidateOas3Param(param, value, [])
580+
581+
// valid (empty object) JSON object
582+
param = {
583+
content: {
584+
"application/json": {
585+
schema: {
586+
required: true,
587+
type: "object"
588+
}
589+
}
590+
}
591+
}
592+
value = {}
593+
assertValidateOas3Param(param, value, [])
594+
595+
// should skip JSON validation for non-JSON media types
596+
param = {
597+
content: {
598+
"application/definitely-not-json": {
599+
schema: {
600+
required: true,
601+
type: "object"
602+
}
603+
}
604+
}
605+
}
606+
value = "{{}"
607+
assertValidateOas3Param(param, value, [])
608+
})
609+
508610
it("validates required strings with min and max length", function() {
509611
// invalid string with max length
510612
param = {

0 commit comments

Comments
 (0)
Please sign in to comment.