Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(oas3): request body validation #9698

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/core/components/execute.jsx
Expand Up @@ -25,14 +25,16 @@ export default class Execute extends Component {
let { path, method, specSelectors, oas3Selectors, oas3Actions } = this.props
let validationErrors = {
missingBodyValue: false,
missingRequiredKeys: []
missingRequiredKeys: [],
valueErrors: []
}
// context: reset errors, then (re)validate
oas3Actions.clearRequestBodyValidateError({ path, method })
let oas3RequiredRequestBodyContentType = specSelectors.getOAS3RequiredRequestBodyContentType([path, method])
let oas3RequestBodyValue = oas3Selectors.requestBodyValue(path, method)
let oas3ValidateBeforeExecuteSuccess = oas3Selectors.validateBeforeExecute([path, method])
let oas3RequestContentType = oas3Selectors.requestContentType(path, method)
let oas3RequestBody = specSelectors.getOAS3RequestBody([path, method])

if (!oas3ValidateBeforeExecuteSuccess) {
validationErrors.missingBodyValue = true
Expand All @@ -47,12 +49,21 @@ export default class Execute extends Component {
oas3RequestContentType,
oas3RequestBodyValue
})
if (!missingRequiredKeys || missingRequiredKeys.length < 1) {
let valueErrors = oas3Selectors.validateValues({
oas3RequestBody,
oas3RequestContentType,
oas3RequestBodyValue
})
if ((!missingRequiredKeys || missingRequiredKeys.length < 1)
&& (!valueErrors || valueErrors.length < 1)) {
return true
}
missingRequiredKeys.forEach((missingKey) => {
validationErrors.missingRequiredKeys.push(missingKey)
})
valueErrors.forEach((error) => {
validationErrors.valueErrors.push(error)
})
oas3Actions.setRequestBodyValidateError({ path, method, validationErrors })
return false
}
Expand Down
5 changes: 4 additions & 1 deletion src/core/components/operation.jsx
Expand Up @@ -115,7 +115,10 @@ export default class Operation extends PureComponent {

let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method )

const validationErrors = specSelectors.validationErrors([path, method])
const oas3ValidationErrors = specSelectors.isOAS3()
? oas3Selectors.validationErrors(path, method)
: []
const validationErrors = specSelectors.validationErrors([path, method]).concat(oas3ValidationErrors)

return (
<div className={deprecated ? "opblock opblock-deprecated" : isShown ? `opblock opblock-${method} is-open` : `opblock opblock-${method}`} id={escapeDeepLinkPath(isShownKey.join("-"))} >
Expand Down
4 changes: 4 additions & 0 deletions src/core/plugins/oas3/reducers.js
Expand Up @@ -81,6 +81,10 @@ export default {
}, missingKeyValues)
})
}
if (validationErrors.valueErrors && validationErrors.valueErrors.length > 0) {
// context: is application/json, with list of errors
return state.setIn(["requestData", path, method, "errors"], fromJS(validationErrors.valueErrors))
}
console.warn("unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR")
return state
},
Expand Down
32 changes: 31 additions & 1 deletion src/core/plugins/oas3/selectors.js
Expand Up @@ -5,7 +5,7 @@ import { OrderedMap, Map, List } from "immutable"
import constant from "lodash/constant"

import { getDefaultRequestBodyValue } from "./components/request-body"
import { stringify } from "core/utils"
import { stringify, validateParam } from "core/utils"

// Helpers

Expand Down Expand Up @@ -165,6 +165,19 @@ export const requestBodyErrors = onlyOAS3((state, path, method) => {
return state.getIn(["requestData", path, method, "errors"]) || null
})

export const validationErrors = onlyOAS3((state, path, method) => {
const errors = state.getIn(["requestData", path, method, "errors"]) || null
const result = []

if (errors && errors.count()) {
errors
.map((e) => (Map.isMap(e) ? `${e.get("propKey")}: ${e.get("error")}` : e))
.forEach((e) => result.push(e))
}

return result
})

export const activeExamplesMember = onlyOAS3(
(state, path, method, type, name) => {
return (
Expand Down Expand Up @@ -295,6 +308,23 @@ export const validateShallowRequired = (
return missingRequiredKeys
}

export const validateValues = (
state,
{
oas3RequestBody,
oas3RequestContentType,
oas3RequestBodyValue
}
) => {
if (oas3RequestContentType !== "application/json") {
return []
}
return validateParam(oas3RequestBody, oas3RequestBodyValue, {
bypassRequiredCheck: false,
isOAS3: true,
})
}

export const validOperationMethods = constant([
"get",
"put",
Expand Down
4 changes: 4 additions & 0 deletions src/core/plugins/spec/selectors.js
Expand Up @@ -509,6 +509,10 @@ export const validateBeforeExecute = (state, pathMethod) => {
return validationErrors(state, pathMethod).length === 0
}

export const getOAS3RequestBody = (state, pathMethod) => {
return state.getIn(["resolvedSubtrees", "paths", ...pathMethod, "requestBody"], fromJS([]))
}

export const getOAS3RequiredRequestBodyContentType = (state, pathMethod) => {
let requiredObj = {
requestBody: false,
Expand Down
4 changes: 2 additions & 2 deletions src/core/utils/index.js
Expand Up @@ -9,7 +9,7 @@
If you're refactoring something in here, feel free to break it out to a file
in `./helpers` if you have the time.
*/
import Im, { fromJS, Set } from "immutable"
import Im, { fromJS, Set, List } from "immutable"
import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"
import camelCase from "lodash/camelCase"
import upperFirst from "lodash/upperFirst"
Expand Down Expand Up @@ -486,7 +486,7 @@ function validateValueBySchema(value, schema, requiredByParam, bypassRequiredChe
return errors
}
}
if(schema && schema.has("required") && isFunc(requiredBySchema.isList) && requiredBySchema.isList()) {
if(schema && requiredBySchema && List.isList(requiredBySchema)) {
requiredBySchema.forEach(key => {
if(objectVal[key] === undefined) {
errors.push({ propKey: key, error: "Required property not found" })
Expand Down
136 changes: 136 additions & 0 deletions test/unit/core/plugins/oas3/reducers.js
Expand Up @@ -314,6 +314,142 @@ describe("oas3 plugin - reducer", function () {
})
})

describe("valueErrors exists with length, e.g. application/json", () => {
it("should set errors", () => {
const state = fromJS({
requestData: {
"/pet": {
post: {
bodyValue: {
id: {
value: "10",
},
name: {
value: "",
},
},
requestContentType: "application/json"
}
}
}
})

const result = setRequestBodyValidateError(state, {
payload: {
path: "/pet",
method: "post",
validationErrors: {
missingBodyValue: null,
valueErrors: [
{
propKey: "name",
error: "Required property not found"
}
]
},
}
})

const expectedResult = {
requestData: {
"/pet": {
post: {
bodyValue: {
id: {
value: "10",
},
name: {
value: "",
},
},
requestContentType: "application/json",
errors: [
{
propKey: "name",
error: "Required property not found"
}
]
}
}
}
}

expect(result.toJS()).toEqual(expectedResult)
})

it("should overwrite errors", () => {
const state = fromJS({
requestData: {
"/pet": {
post: {
bodyValue: {
id: {
value: "10",
},
name: {
value: "",
},
},
requestContentType: "application/json",
errors: [
{
propKey: "id",
error: "some fake error"
},
{
propKey: "name",
error: "some fake error"
}
]
}
}
}
})

const result = setRequestBodyValidateError(state, {
payload: {
path: "/pet",
method: "post",
validationErrors: {
missingBodyValue: null,
valueErrors: [
{
propKey: "name",
error: "Required property not found"
}
]
},
}
})

const expectedResult = {
requestData: {
"/pet": {
post: {
bodyValue: {
id: {
value: "10",
},
name: {
value: "",
},
},
requestContentType: "application/json",
errors: [
{
propKey: "name",
error: "Required property not found"
}
]
}
}
}
}

expect(result.toJS()).toEqual(expectedResult)
})
})

describe("other unexpected payload, e.g. no missingBodyValue or missingRequiredKeys", () => {
it("should not throw error if receiving unexpected validationError format. return state unchanged", () => {
const state = fromJS({
Expand Down