feat(RequestBody): validation support for required fields (#6223)
fixes #5181 * application/json * application/xml * application/x-www-form-urlencoded * Set requestBodyValue values to be an immutable Map, as "value". Previously stored as a normal String. * This enables adding "errors" to the Map, for validation use * note: getOAS3RequiredRequestBodyContentType requires state.spec, * which is not available to state.oas3
This commit is contained in:
@@ -9,26 +9,79 @@ export default class Execute extends Component {
|
|||||||
operation: PropTypes.object.isRequired,
|
operation: PropTypes.object.isRequired,
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
method: PropTypes.string.isRequired,
|
method: PropTypes.string.isRequired,
|
||||||
|
oas3Selectors: PropTypes.object.isRequired,
|
||||||
|
oas3Actions: PropTypes.object.isRequired,
|
||||||
onExecute: PropTypes.func
|
onExecute: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick=()=>{
|
handleValidateParameters = () => {
|
||||||
let { specSelectors, specActions, operation, path, method } = this.props
|
let { specSelectors, specActions, path, method } = this.props
|
||||||
|
specActions.validateParams([path, method])
|
||||||
|
return specSelectors.validateBeforeExecute([path, method])
|
||||||
|
}
|
||||||
|
|
||||||
specActions.validateParams( [path, method] )
|
handleValidateRequestBody = () => {
|
||||||
|
let { path, method, specSelectors, oas3Selectors, oas3Actions } = this.props
|
||||||
|
let validationErrors = {
|
||||||
|
missingBodyValue: false,
|
||||||
|
missingRequiredKeys: []
|
||||||
|
}
|
||||||
|
// 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])
|
||||||
|
|
||||||
if ( specSelectors.validateBeforeExecute([path, method]) ) {
|
if (!oas3ValidateBeforeExecuteSuccess) {
|
||||||
if(this.props.onExecute) {
|
validationErrors.missingBodyValue = true
|
||||||
|
oas3Actions.setRequestBodyValidateError({ path, method, validationErrors })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!oas3RequiredRequestBodyContentType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let missingRequiredKeys = oas3Selectors.validateShallowRequired({ oas3RequiredRequestBodyContentType, oas3RequestBodyValue })
|
||||||
|
if (!missingRequiredKeys || missingRequiredKeys.length < 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
missingRequiredKeys.forEach((missingKey) => {
|
||||||
|
validationErrors.missingRequiredKeys.push(missingKey)
|
||||||
|
})
|
||||||
|
oas3Actions.setRequestBodyValidateError({ path, method, validationErrors })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
handleValidationResultPass = () => {
|
||||||
|
let { specActions, operation, path, method } = this.props
|
||||||
|
if (this.props.onExecute) {
|
||||||
|
// loading spinner
|
||||||
this.props.onExecute()
|
this.props.onExecute()
|
||||||
}
|
}
|
||||||
specActions.execute( { operation, path, method } )
|
specActions.execute({ operation, path, method })
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
handleValidationResultFail = () => {
|
||||||
|
let { specActions, path, method } = this.props
|
||||||
// deferred by 40ms, to give element class change time to settle.
|
// deferred by 40ms, to give element class change time to settle.
|
||||||
specActions.clearValidateParams( [path, method] )
|
specActions.clearValidateParams([path, method])
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
specActions.validateParams([path, method])
|
specActions.validateParams([path, method])
|
||||||
}, 40)
|
}, 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleValidationResult = (isPass) => {
|
||||||
|
if (isPass) {
|
||||||
|
this.handleValidationResultPass()
|
||||||
|
} else {
|
||||||
|
this.handleValidationResultFail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
let paramsResult = this.handleValidateParameters()
|
||||||
|
let requestBodyResult = this.handleValidateRequestBody()
|
||||||
|
let isPass = paramsResult && requestBodyResult
|
||||||
|
this.handleValidationResult(isPass)
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val)
|
onChangeProducesWrapper = ( val ) => this.props.specActions.changeProducesValue([this.props.path, this.props.method], val)
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export default class Operation extends PureComponent {
|
|||||||
operation={ operation }
|
operation={ operation }
|
||||||
specActions={ specActions }
|
specActions={ specActions }
|
||||||
specSelectors={ specSelectors }
|
specSelectors={ specSelectors }
|
||||||
|
oas3Selectors={ oas3Selectors }
|
||||||
|
oas3Actions={ oas3Actions }
|
||||||
path={ path }
|
path={ path }
|
||||||
method={ method }
|
method={ method }
|
||||||
onExecute={ onExecute } />
|
onExecute={ onExecute } />
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export default class Parameters extends Component {
|
|||||||
contentTypes={ requestBody.get("content", List()).keySeq() }
|
contentTypes={ requestBody.get("content", List()).keySeq() }
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
oas3Actions.setRequestContentType({ value, pathMethod })
|
oas3Actions.setRequestContentType({ value, pathMethod })
|
||||||
|
oas3Actions.initRequestBodyValidateError({ pathMethod })
|
||||||
}}
|
}}
|
||||||
className="body-param-content-type" />
|
className="body-param-content-type" />
|
||||||
</label>
|
</label>
|
||||||
@@ -197,6 +198,7 @@ export default class Parameters extends Component {
|
|||||||
requestBody={requestBody}
|
requestBody={requestBody}
|
||||||
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
|
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
|
||||||
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}
|
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}
|
||||||
|
requestBodyErrors={oas3Selectors.requestBodyErrors(...pathMethod)}
|
||||||
isExecute={isExecute}
|
isExecute={isExecute}
|
||||||
activeExamplesKey={oas3Selectors.activeExamplesMember(
|
activeExamplesKey={oas3Selectors.activeExamplesMember(
|
||||||
...pathMethod,
|
...pathMethod,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const UPDATE_ACTIVE_EXAMPLES_MEMBER = "oas3_set_active_examples_member"
|
|||||||
export const UPDATE_REQUEST_CONTENT_TYPE = "oas3_set_request_content_type"
|
export const UPDATE_REQUEST_CONTENT_TYPE = "oas3_set_request_content_type"
|
||||||
export const UPDATE_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type"
|
export const UPDATE_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type"
|
||||||
export const UPDATE_SERVER_VARIABLE_VALUE = "oas3_set_server_variable_value"
|
export const UPDATE_SERVER_VARIABLE_VALUE = "oas3_set_server_variable_value"
|
||||||
|
export const SET_REQUEST_BODY_VALIDATE_ERROR = "oas3_set_request_body_validate_error"
|
||||||
|
export const CLEAR_REQUEST_BODY_VALIDATE_ERROR = "oas3_clear_request_body_validate_error"
|
||||||
|
|
||||||
export function setSelectedServer (selectedServerUrl, namespace) {
|
export function setSelectedServer (selectedServerUrl, namespace) {
|
||||||
return {
|
return {
|
||||||
@@ -57,3 +59,24 @@ export function setServerVariableValue ({ server, namespace, key, val }) {
|
|||||||
payload: { server, namespace, key, val }
|
payload: { server, namespace, key, val }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setRequestBodyValidateError = ({ path, method, validationErrors }) => {
|
||||||
|
return {
|
||||||
|
type: SET_REQUEST_BODY_VALIDATE_ERROR,
|
||||||
|
payload: { path, method, validationErrors }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearRequestBodyValidateError = ({ path, method }) => {
|
||||||
|
return {
|
||||||
|
type: CLEAR_REQUEST_BODY_VALIDATE_ERROR,
|
||||||
|
payload: { path, method }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initRequestBodyValidateError = ({ pathMethod } ) => {
|
||||||
|
return {
|
||||||
|
type: CLEAR_REQUEST_BODY_VALIDATE_ERROR,
|
||||||
|
payload: { path: pathMethod[0], method: pathMethod[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { PureComponent } from "react"
|
import React, { PureComponent } from "react"
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
|
import cx from "classnames"
|
||||||
import { stringify } from "core/utils"
|
import { stringify } from "core/utils"
|
||||||
|
|
||||||
const NOOP = Function.prototype
|
const NOOP = Function.prototype
|
||||||
@@ -11,6 +12,7 @@ export default class RequestBodyEditor extends PureComponent {
|
|||||||
getComponent: PropTypes.func.isRequired,
|
getComponent: PropTypes.func.isRequired,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
defaultValue: PropTypes.string,
|
defaultValue: PropTypes.string,
|
||||||
|
errors: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -74,19 +76,22 @@ export default class RequestBodyEditor extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let {
|
let {
|
||||||
getComponent
|
getComponent,
|
||||||
|
errors
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value
|
value
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
|
let isInvalid = errors.size > 0 ? true : false
|
||||||
const TextArea = getComponent("TextArea")
|
const TextArea = getComponent("TextArea")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="body-param">
|
<div className="body-param">
|
||||||
<TextArea
|
<TextArea
|
||||||
className={"body-param__text"}
|
className={cx("body-param__text", { invalid: isInvalid } )}
|
||||||
|
title={errors.size ? errors.join(", ") : ""}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={ this.onDomChange }
|
onChange={ this.onDomChange }
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const RequestBody = ({
|
|||||||
requestBody,
|
requestBody,
|
||||||
requestBodyValue,
|
requestBodyValue,
|
||||||
requestBodyInclusionSetting,
|
requestBodyInclusionSetting,
|
||||||
|
requestBodyErrors,
|
||||||
getComponent,
|
getComponent,
|
||||||
getConfigs,
|
getConfigs,
|
||||||
specSelectors,
|
specSelectors,
|
||||||
@@ -88,6 +89,7 @@ const RequestBody = ({
|
|||||||
const handleExamplesSelect = (key /*, { isSyntheticChange } */) => {
|
const handleExamplesSelect = (key /*, { isSyntheticChange } */) => {
|
||||||
updateActiveExamplesKey(key)
|
updateActiveExamplesKey(key)
|
||||||
}
|
}
|
||||||
|
requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List()
|
||||||
|
|
||||||
if(!mediaTypeValue.size) {
|
if(!mediaTypeValue.size) {
|
||||||
return null
|
return null
|
||||||
@@ -138,7 +140,8 @@ const RequestBody = ({
|
|||||||
const type = prop.get("type")
|
const type = prop.get("type")
|
||||||
const format = prop.get("format")
|
const format = prop.get("format")
|
||||||
const description = prop.get("description")
|
const description = prop.get("description")
|
||||||
const currentValue = requestBodyValue.get(key)
|
const currentValue = requestBodyValue.getIn([key, "value"])
|
||||||
|
const currentErrors = requestBodyValue.getIn([key, "errors"]) || requestBodyErrors
|
||||||
|
|
||||||
let initialValue = prop.get("default") || prop.get("example") || ""
|
let initialValue = prop.get("default") || prop.get("example") || ""
|
||||||
|
|
||||||
@@ -179,6 +182,8 @@ const RequestBody = ({
|
|||||||
description={key}
|
description={key}
|
||||||
getComponent={getComponent}
|
getComponent={getComponent}
|
||||||
value={currentValue === undefined ? initialValue : currentValue}
|
value={currentValue === undefined ? initialValue : currentValue}
|
||||||
|
required = { required }
|
||||||
|
errors = { currentErrors }
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onChange(value, [key])
|
onChange(value, [key])
|
||||||
}}
|
}}
|
||||||
@@ -223,6 +228,7 @@ const RequestBody = ({
|
|||||||
<div>
|
<div>
|
||||||
<RequestBodyEditor
|
<RequestBodyEditor
|
||||||
value={requestBodyValue}
|
value={requestBodyValue}
|
||||||
|
errors={requestBodyErrors}
|
||||||
defaultValue={getDefaultRequestBodyValue(
|
defaultValue={getDefaultRequestBodyValue(
|
||||||
requestBody,
|
requestBody,
|
||||||
contentType,
|
contentType,
|
||||||
@@ -270,6 +276,7 @@ RequestBody.propTypes = {
|
|||||||
requestBody: ImPropTypes.orderedMap.isRequired,
|
requestBody: ImPropTypes.orderedMap.isRequired,
|
||||||
requestBodyValue: ImPropTypes.orderedMap.isRequired,
|
requestBodyValue: ImPropTypes.orderedMap.isRequired,
|
||||||
requestBodyInclusionSetting: ImPropTypes.Map.isRequired,
|
requestBodyInclusionSetting: ImPropTypes.Map.isRequired,
|
||||||
|
requestBodyErrors: ImPropTypes.list.isRequired,
|
||||||
getComponent: PropTypes.func.isRequired,
|
getComponent: PropTypes.func.isRequired,
|
||||||
getConfigs: PropTypes.func.isRequired,
|
getConfigs: PropTypes.func.isRequired,
|
||||||
fn: PropTypes.object.isRequired,
|
fn: PropTypes.object.isRequired,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { fromJS, Map } from "immutable"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UPDATE_SELECTED_SERVER,
|
UPDATE_SELECTED_SERVER,
|
||||||
UPDATE_REQUEST_BODY_VALUE,
|
UPDATE_REQUEST_BODY_VALUE,
|
||||||
@@ -5,7 +7,9 @@ import {
|
|||||||
UPDATE_ACTIVE_EXAMPLES_MEMBER,
|
UPDATE_ACTIVE_EXAMPLES_MEMBER,
|
||||||
UPDATE_REQUEST_CONTENT_TYPE,
|
UPDATE_REQUEST_CONTENT_TYPE,
|
||||||
UPDATE_SERVER_VARIABLE_VALUE,
|
UPDATE_SERVER_VARIABLE_VALUE,
|
||||||
UPDATE_RESPONSE_CONTENT_TYPE
|
UPDATE_RESPONSE_CONTENT_TYPE,
|
||||||
|
SET_REQUEST_BODY_VALIDATE_ERROR,
|
||||||
|
CLEAR_REQUEST_BODY_VALIDATE_ERROR,
|
||||||
} from "./actions"
|
} from "./actions"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -15,7 +19,27 @@ export default {
|
|||||||
},
|
},
|
||||||
[UPDATE_REQUEST_BODY_VALUE]: (state, { payload: { value, pathMethod } } ) =>{
|
[UPDATE_REQUEST_BODY_VALUE]: (state, { payload: { value, pathMethod } } ) =>{
|
||||||
let [path, method] = pathMethod
|
let [path, method] = pathMethod
|
||||||
|
if (!Map.isMap(value)) {
|
||||||
|
// context: application/json is always a String (instead of Map)
|
||||||
return state.setIn( [ "requestData", path, method, "bodyValue" ], value)
|
return state.setIn( [ "requestData", path, method, "bodyValue" ], value)
|
||||||
|
}
|
||||||
|
let currentVal = state.getIn(["requestData", path, method, "bodyValue"]) || Map()
|
||||||
|
if (!Map.isMap(currentVal)) {
|
||||||
|
// context: user switch from application/json to application/x-www-form-urlencoded
|
||||||
|
currentVal = Map()
|
||||||
|
}
|
||||||
|
let newVal
|
||||||
|
const [...valueKeys] = value.keys()
|
||||||
|
valueKeys.forEach((valueKey) => {
|
||||||
|
let valueKeyVal = value.getIn([valueKey])
|
||||||
|
if (!currentVal.has(valueKey)) {
|
||||||
|
newVal = currentVal.setIn([valueKey, "value"], valueKeyVal)
|
||||||
|
} else if (!Map.isMap(valueKeyVal)) {
|
||||||
|
// context: user input will be received as String
|
||||||
|
newVal = currentVal.setIn([valueKey, "value"], valueKeyVal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return state.setIn(["requestData", path, method, "bodyValue"], newVal)
|
||||||
},
|
},
|
||||||
[UPDATE_REQUEST_BODY_INCLUSION]: (state, { payload: { value, pathMethod, name } } ) =>{
|
[UPDATE_REQUEST_BODY_INCLUSION]: (state, { payload: { value, pathMethod, name } } ) =>{
|
||||||
let [path, method] = pathMethod
|
let [path, method] = pathMethod
|
||||||
@@ -36,4 +60,38 @@ export default {
|
|||||||
const path = namespace ? [ namespace, "serverVariableValues", server, key ] : [ "serverVariableValues", server, key ]
|
const path = namespace ? [ namespace, "serverVariableValues", server, key ] : [ "serverVariableValues", server, key ]
|
||||||
return state.setIn(path, val)
|
return state.setIn(path, val)
|
||||||
},
|
},
|
||||||
|
[SET_REQUEST_BODY_VALIDATE_ERROR]: (state, { payload: { path, method, validationErrors } } ) => {
|
||||||
|
let errors = []
|
||||||
|
errors.push("Required field is not provided")
|
||||||
|
if (validationErrors.missingBodyValue) {
|
||||||
|
// context: is application/json or application/xml, where typeof (missing) bodyValue = String
|
||||||
|
return state.setIn(["requestData", path, method, "errors"], fromJS(errors))
|
||||||
|
}
|
||||||
|
if (validationErrors.missingRequiredKeys && validationErrors.missingRequiredKeys.length > 0) {
|
||||||
|
// context: is application/x-www-form-urlencoded, with list of missing keys
|
||||||
|
const { missingRequiredKeys } = validationErrors
|
||||||
|
return state.updateIn(["requestData", path, method, "bodyValue"], fromJS({}), missingKeyValues => {
|
||||||
|
return missingRequiredKeys.reduce((bodyValue, currentMissingKey) => {
|
||||||
|
return bodyValue.setIn([currentMissingKey, "errors"], fromJS(errors))
|
||||||
|
}, missingKeyValues)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.warn("unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR")
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
[CLEAR_REQUEST_BODY_VALIDATE_ERROR]: (state, { payload: { path, method } }) => {
|
||||||
|
const requestBodyValue = state.getIn(["requestData", path, method, "bodyValue"])
|
||||||
|
if (!Map.isMap(requestBodyValue)) {
|
||||||
|
return state.setIn(["requestData", path, method, "errors"], fromJS([]))
|
||||||
|
}
|
||||||
|
const [...valueKeys] = requestBodyValue.keys()
|
||||||
|
if (!valueKeys) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
return state.updateIn(["requestData", path, method, "bodyValue"], fromJS({}), bodyValues => {
|
||||||
|
return valueKeys.reduce((bodyValue, curr) => {
|
||||||
|
return bodyValue.setIn([curr, "errors"], fromJS([]))
|
||||||
|
}, bodyValues)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { OrderedMap, Map } from "immutable"
|
import { OrderedMap, Map } from "immutable"
|
||||||
import { isOAS3 as isOAS3Helper } from "./helpers"
|
import { isOAS3 as isOAS3Helper } from "./helpers"
|
||||||
|
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
function onlyOAS3(selector) {
|
function onlyOAS3(selector) {
|
||||||
@@ -15,6 +14,35 @@ function onlyOAS3(selector) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateRequestBodyIsRequired(selector) {
|
||||||
|
return (...args) => (system) => {
|
||||||
|
const specJson = system.getSystem().specSelectors.specJson()
|
||||||
|
const argsList = [...args]
|
||||||
|
// expect argsList[0] = state
|
||||||
|
let pathMethod = argsList[1] || []
|
||||||
|
let isOas3RequestBodyRequired = specJson.getIn(["paths", ...pathMethod, "requestBody", "required"])
|
||||||
|
|
||||||
|
if (isOas3RequestBodyRequired) {
|
||||||
|
return selector(...args)
|
||||||
|
} else {
|
||||||
|
// validation pass b/c not required
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateRequestBodyValueExists = (state, pathMethod) => {
|
||||||
|
pathMethod = pathMethod || []
|
||||||
|
let oas3RequestBodyValue = state.getIn(["requestData", ...pathMethod, "bodyValue"])
|
||||||
|
// context: bodyValue can be a String, or a Map
|
||||||
|
if (!oas3RequestBodyValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// validation pass if String is not empty, or if Map exists
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const selectedServer = onlyOAS3((state, namespace) => {
|
export const selectedServer = onlyOAS3((state, namespace) => {
|
||||||
const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"]
|
const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"]
|
||||||
return state.getIn(path) || ""
|
return state.getIn(path) || ""
|
||||||
@@ -31,6 +59,11 @@ export const requestBodyInclusionSetting = onlyOAS3((state, path, method) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const requestBodyErrors = onlyOAS3((state, path, method) => {
|
||||||
|
return state.getIn(["requestData", path, method, "errors"]) || null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const activeExamplesMember = onlyOAS3((state, path, method, type, name) => {
|
export const activeExamplesMember = onlyOAS3((state, path, method, type, name) => {
|
||||||
return state.getIn(["examples", path, method, type, name, "activeExample"]) || null
|
return state.getIn(["examples", path, method, type, name, "activeExample"]) || null
|
||||||
}
|
}
|
||||||
@@ -116,3 +149,34 @@ export const serverEffectiveValue = onlyOAS3((state, locationData) => {
|
|||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const validateBeforeExecute = validateRequestBodyIsRequired(
|
||||||
|
(state, pathMethod) => validateRequestBodyValueExists(state, pathMethod)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateShallowRequired = ( state, {oas3RequiredRequestBodyContentType, oas3RequestBodyValue} ) => {
|
||||||
|
let missingRequiredKeys = []
|
||||||
|
// context: json => String; urlencoded => Map
|
||||||
|
if (!Map.isMap(oas3RequestBodyValue)) {
|
||||||
|
return missingRequiredKeys
|
||||||
|
}
|
||||||
|
let requiredKeys = []
|
||||||
|
// We intentionally cycle through list of contentTypes for defined requiredKeys
|
||||||
|
// instead of assuming first contentType will accurately list all expected requiredKeys
|
||||||
|
// Alternatively, we could try retrieving the contentType first, and match exactly. This would be a more accurate representation of definition
|
||||||
|
Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach((contentType) => {
|
||||||
|
let contentTypeVal = oas3RequiredRequestBodyContentType.requestContentType[contentType]
|
||||||
|
contentTypeVal.forEach((requiredKey) => {
|
||||||
|
if (requiredKeys.indexOf(requiredKey) < 0 ) {
|
||||||
|
requiredKeys.push(requiredKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
requiredKeys.forEach((key) => {
|
||||||
|
let requiredKeyValue = oas3RequestBodyValue.getIn([key, "value"])
|
||||||
|
if (!requiredKeyValue) {
|
||||||
|
missingRequiredKeys.push(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return missingRequiredKeys
|
||||||
|
}
|
||||||
|
|||||||
@@ -406,7 +406,19 @@ export const executeRequest = (req) =>
|
|||||||
if(isJSONObject(requestBody)) {
|
if(isJSONObject(requestBody)) {
|
||||||
req.requestBody = JSON.parse(requestBody)
|
req.requestBody = JSON.parse(requestBody)
|
||||||
} else if(requestBody && requestBody.toJS) {
|
} else if(requestBody && requestBody.toJS) {
|
||||||
req.requestBody = requestBody.filter((value, key) => !isEmptyValue(value) || requestBodyInclusionSetting.get(key)).toJS()
|
req.requestBody = requestBody
|
||||||
|
.map(
|
||||||
|
(val) => {
|
||||||
|
if (Map.isMap(val)) {
|
||||||
|
return val.get("value")
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(value, key) => !isEmptyValue(value) || requestBodyInclusionSetting.get(key)
|
||||||
|
)
|
||||||
|
.toJS()
|
||||||
} else{
|
} else{
|
||||||
req.requestBody = requestBody
|
req.requestBody = requestBody
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,7 +314,6 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
|
|||||||
hashKeyedMeta
|
hashKeyedMeta
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return mergedParams.find(curr => curr.get("in") === param.get("in") && curr.get("name") === param.get("name"), OrderedMap())
|
return mergedParams.find(curr => curr.get("in") === param.get("in") && curr.get("name") === param.get("name"), OrderedMap())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +326,6 @@ export const parameterInclusionSettingFor = (state, pathMethod, paramName, param
|
|||||||
export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {
|
export const parameterWithMeta = (state, pathMethod, paramName, paramIn) => {
|
||||||
const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], OrderedMap())
|
const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], OrderedMap())
|
||||||
const currentParam = opParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, OrderedMap())
|
const currentParam = opParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, OrderedMap())
|
||||||
|
|
||||||
return parameterWithMetaByIdentity(state, pathMethod, currentParam)
|
return parameterWithMetaByIdentity(state, pathMethod, currentParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +362,6 @@ export const hasHost = createSelector(
|
|||||||
// Get the parameter values, that the user filled out
|
// Get the parameter values, that the user filled out
|
||||||
export function parameterValues(state, pathMethod, isXml) {
|
export function parameterValues(state, pathMethod, isXml) {
|
||||||
pathMethod = pathMethod || []
|
pathMethod = pathMethod || []
|
||||||
// let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
|
|
||||||
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")
|
||||||
@@ -495,6 +492,28 @@ export const validateBeforeExecute = ( state, pathMethod ) => {
|
|||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getOAS3RequiredRequestBodyContentType = (state, pathMethod) => {
|
||||||
|
let requiredObj = {
|
||||||
|
requestBody: false,
|
||||||
|
requestContentType: {}
|
||||||
|
}
|
||||||
|
let requestBody = state.getIn(["resolvedSubtrees", "paths", ...pathMethod, "requestBody"], fromJS([]))
|
||||||
|
if (requestBody.size < 1) {
|
||||||
|
return requiredObj
|
||||||
|
}
|
||||||
|
if (requestBody.getIn(["required"])) {
|
||||||
|
requiredObj.requestBody = requestBody.getIn(["required"])
|
||||||
|
}
|
||||||
|
requestBody.getIn(["content"]).entrySeq().forEach((contentType) => { // e.g application/json
|
||||||
|
const key = contentType[0]
|
||||||
|
if (contentType[1].getIn(["schema", "required"])) {
|
||||||
|
const val = contentType[1].getIn(["schema", "required"]).toJS()
|
||||||
|
requiredObj.requestContentType[key] = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return requiredObj
|
||||||
|
}
|
||||||
|
|
||||||
function returnSelfOrNewMap(obj) {
|
function returnSelfOrNewMap(obj) {
|
||||||
// returns obj if obj is an Immutable map, else returns a new Map
|
// returns obj if obj is an Immutable map, else returns a new Map
|
||||||
return Map.isMap(obj) ? obj : new Map()
|
return Map.isMap(obj) ? obj : new Map()
|
||||||
|
|||||||
27
test/e2e-cypress/static/documents/bugs/5181.yaml
Normal file
27
test/e2e-cypress/static/documents/bugs/5181.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
info:
|
||||||
|
title: Required parameter missing, doesn't block request from executing.
|
||||||
|
version: '1'
|
||||||
|
openapi: 3.0.0
|
||||||
|
servers:
|
||||||
|
- url: http://httpbin.org/anything
|
||||||
|
paths:
|
||||||
|
/foos:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/x-www-form-urlencoded:
|
||||||
|
# application/json:
|
||||||
|
# application/xml:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
foo:
|
||||||
|
type: string
|
||||||
|
bar:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- foo
|
||||||
|
type: object
|
||||||
|
required: true # Note this doesn't have an impact
|
||||||
|
responses:
|
||||||
|
default:
|
||||||
|
description: ok
|
||||||
@@ -67,6 +67,9 @@ describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => {
|
|||||||
// Expand Try It Out
|
// Expand Try It Out
|
||||||
.get(".try-out__btn")
|
.get(".try-out__btn")
|
||||||
.click()
|
.click()
|
||||||
|
// add item to pass required validation
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description button")
|
||||||
|
.click()
|
||||||
// Execute
|
// Execute
|
||||||
.get(".execute.opblock-control__btn")
|
.get(".execute.opblock-control__btn")
|
||||||
.click()
|
.click()
|
||||||
@@ -91,6 +94,9 @@ describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => {
|
|||||||
// Request Body
|
// Request Body
|
||||||
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input")
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(5) > .parameters-col_description .parameter__empty_value_toggle input")
|
||||||
.uncheck()
|
.uncheck()
|
||||||
|
// add item to pass required validation
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description button")
|
||||||
|
.click()
|
||||||
// Execute
|
// Execute
|
||||||
.get(".execute.opblock-control__btn")
|
.get(".execute.opblock-control__btn")
|
||||||
.click()
|
.click()
|
||||||
@@ -118,6 +124,9 @@ describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => {
|
|||||||
.uncheck()
|
.uncheck()
|
||||||
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(6) > .parameters-col_description .parameter__empty_value_toggle input")
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(6) > .parameters-col_description .parameter__empty_value_toggle input")
|
||||||
.uncheck()
|
.uncheck()
|
||||||
|
// add item to pass required validation
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description button")
|
||||||
|
.click()
|
||||||
// Execute
|
// Execute
|
||||||
.get(".execute.opblock-control__btn")
|
.get(".execute.opblock-control__btn")
|
||||||
.click()
|
.click()
|
||||||
|
|||||||
214
test/e2e-cypress/tests/features/oas3-request-body-required.js
Normal file
214
test/e2e-cypress/tests/features/oas3-request-body-required.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("OpenAPI 3.0 Validation for Required Request Body and Request Body Fields", () => {
|
||||||
|
describe("Request Body required bug/5181", () => {
|
||||||
|
it("on execute, if empty value, SHOULD render class 'invalid' and should NOT render cURL component", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/bugs/5181.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-default-post_foos")
|
||||||
|
.click()
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get input
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(1) > .parameters-col_description input")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
// class "invalid" should now exist (and render red, which we won't check)
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(1) > .parameters-col_description input")
|
||||||
|
.should("have.class", "invalid")
|
||||||
|
// cURL component should not exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("not.exist")
|
||||||
|
})
|
||||||
|
it("on execute, if value exists, should NOT render class 'invalid' and SHOULD render cURL component", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/bugs/5181.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-default-post_foos")
|
||||||
|
.click()
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get input
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(1) > .parameters-col_description input")
|
||||||
|
.type("abc")
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
// cURL component should exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Request Body required fields - application/json", () => {
|
||||||
|
it("on execute, if empty value, SHOULD render class 'invalid' and should NOT render cURL component", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get and clear textarea
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
.clear()
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
// class "invalid" should now exist (and render red, which we won't check)
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("have.class", "invalid")
|
||||||
|
// cURL component should not exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("not.exist")
|
||||||
|
})
|
||||||
|
it("on execute, if value exists, even if just single space, should NOT render class 'invalid' and SHOULD render cURL component that contains the single space", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get, clear, then modify textarea
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.clear()
|
||||||
|
.type(" ")
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
// cURL component should exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("exist")
|
||||||
|
.get(".responses-wrapper .copy-paste textarea")
|
||||||
|
.should("contains.text", "-d \" \"")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
petstore ux notes:
|
||||||
|
- required field, but if example value exists, will populate the field. So this test will clear the example value.
|
||||||
|
- "add item" will insert an empty array, and display an input text box. This establishes a value for the field.
|
||||||
|
*/
|
||||||
|
describe("Request Body required fields - application/x-www-form-urlencoded", () => {
|
||||||
|
it("on execute, if empty value, SHOULD render class 'invalid' and should NOT render cURL component", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-section .opblock-section-request-body .body-param-content-type > select")
|
||||||
|
.select("application/x-www-form-urlencoded")
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get and clear input populated from example value
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.clear()
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
// class "invalid" should now exist (and render red, which we won't check)
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.should("have.class", "invalid")
|
||||||
|
// cURL component should not exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("not.exist")
|
||||||
|
})
|
||||||
|
it("on execute, if all values exist, even if array exists but is empty, should NOT render class 'invalid' and SHOULD render cURL component", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-section .opblock-section-request-body .body-param-content-type > select")
|
||||||
|
.select("application/x-www-form-urlencoded")
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// add item to get input
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description button")
|
||||||
|
.click()
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.should("have.value", "doggie")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description input")
|
||||||
|
.should("have.value", "")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
// cURL component should exist
|
||||||
|
.get(".responses-wrapper .copy-paste")
|
||||||
|
.should("exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Request Body: switching between Content Types", () => {
|
||||||
|
it("after application/json 'invalid' error, on switch content type to application/x-www-form-urlencoded, SHOULD be free of errors", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get and clear textarea
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
.clear()
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("have.class", "invalid")
|
||||||
|
// switch content type
|
||||||
|
.get(".opblock-section .opblock-section-request-body .body-param-content-type > select")
|
||||||
|
.select("application/x-www-form-urlencoded")
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(4) > .parameters-col_description input")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
})
|
||||||
|
it("after application/x-www-form-urlencoded 'invalid' error, on switch content type to application/json, SHOULD be free of errors", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/features/petstore-only-pet.openapi.yaml"
|
||||||
|
)
|
||||||
|
.get("#operations-pet-addPet")
|
||||||
|
.click()
|
||||||
|
.get(".opblock-section .opblock-section-request-body .body-param-content-type > select")
|
||||||
|
.select("application/x-www-form-urlencoded")
|
||||||
|
// Expand Try It Out
|
||||||
|
.get(".try-out__btn")
|
||||||
|
.click()
|
||||||
|
// get and clear input
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.clear()
|
||||||
|
// Execute
|
||||||
|
.get(".execute.opblock-control__btn")
|
||||||
|
.click()
|
||||||
|
// class "invalid" should now exist (and render red, which we won't check)
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(2) > .parameters-col_description input")
|
||||||
|
.should("have.class", "invalid")
|
||||||
|
// switch content type
|
||||||
|
.get(".opblock-section .opblock-section-request-body .body-param-content-type > select")
|
||||||
|
.select("application/json")
|
||||||
|
.get(".opblock-body .opblock-section .opblock-section-request-body .body-param textarea")
|
||||||
|
.should("not.have.class", "invalid")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
508
test/mocha/core/plugins/oas3/reducers.js
Normal file
508
test/mocha/core/plugins/oas3/reducers.js
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
/* eslint-env mocha */
|
||||||
|
import expect from "expect"
|
||||||
|
import { fromJS } from "immutable"
|
||||||
|
import reducer from "corePlugins/oas3/reducers"
|
||||||
|
|
||||||
|
describe("oas3 plugin - reducer", function () {
|
||||||
|
describe("SET_REQUEST_BODY_VALIDATE_ERROR", () => {
|
||||||
|
const setRequestBodyValidateError = reducer["oas3_set_request_body_validate_error"]
|
||||||
|
|
||||||
|
describe("missingBodyValue exists, e.g. application/json", () => {
|
||||||
|
it("should set errors", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: "",
|
||||||
|
requestContentType: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: true,
|
||||||
|
missingRequiredKeys: []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: "",
|
||||||
|
requestContentType: "application/json",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("missingRequiredKeys exists with length, e.g. application/x-www-form-urleconded", () => {
|
||||||
|
it("should set nested errors", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
missingRequiredKeys: ["name"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should overwrite nested errors, for keys listed in missingRequiredKeys", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["some fake error"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
missingRequiredKeys: ["name"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not overwrite nested errors, for keys not listed in missingRequiredKeys", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
errors: ["random error should not be overwritten"]
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["some fake error"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
missingRequiredKeys: ["name"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
errors: ["random error should not be overwritten"]
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set multiple nested errors", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
missingRequiredKeys: ["id", "name"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("missingRequiredKeys is empty list", () => {
|
||||||
|
it("should not set any errors, and return state unchanged", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
missingRequiredKeys: []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = setRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
validationErrors: {
|
||||||
|
missingBodyValue: null,
|
||||||
|
// missingRequiredKeys: ["none provided"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("CLEAR_REQUEST_BODY_VALIDATE_ERROR", function() {
|
||||||
|
const clearRequestBodyValidateError = reducer["oas3_clear_request_body_validate_error"]
|
||||||
|
|
||||||
|
describe("bodyValue is String, e.g. application/json", () => {
|
||||||
|
it("should clear errors", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: "{}",
|
||||||
|
requestContentType: "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = clearRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: "{}",
|
||||||
|
requestContentType: "application/json",
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bodyValue is Map with entries, e.g. application/x-www-form-urleconded", () => {
|
||||||
|
it("should clear nested errors, and apply empty error list to all entries", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
errors: ["some random error"]
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "doggie",
|
||||||
|
errors: ["Required field is not provided"]
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
value: "available"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = clearRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
id: {
|
||||||
|
value: "10",
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "doggie",
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
value: "available",
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bodyValue is empty Map", () => {
|
||||||
|
it("should return state unchanged", () => {
|
||||||
|
const state = fromJS({
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = clearRequestBodyValidateError(state, {
|
||||||
|
payload: {
|
||||||
|
path: "/pet",
|
||||||
|
method: "post",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
requestData: {
|
||||||
|
"/pet": {
|
||||||
|
post: {
|
||||||
|
bodyValue: {
|
||||||
|
},
|
||||||
|
requestContentType: "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result.toJS()).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user