diff --git a/src/core/components/execute.jsx b/src/core/components/execute.jsx
index d9e8b4f462f..0c005fcbbb4 100644
--- a/src/core/components/execute.jsx
+++ b/src/core/components/execute.jsx
@@ -9,28 +9,81 @@ export default class Execute extends Component {
operation: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
method: PropTypes.string.isRequired,
+ oas3Selectors: PropTypes.object.isRequired,
+ oas3Actions: PropTypes.object.isRequired,
onExecute: PropTypes.func
}
- onClick=()=>{
- let { specSelectors, specActions, operation, path, method } = this.props
+ handleValidateParameters = () => {
+ let { specSelectors, specActions, path, method } = this.props
+ specActions.validateParams([path, method])
+ return specSelectors.validateBeforeExecute([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 (!oas3ValidateBeforeExecuteSuccess) {
+ 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
+ }
- specActions.validateParams( [path, method] )
+ handleValidationResultPass = () => {
+ let { specActions, operation, path, method } = this.props
+ if (this.props.onExecute) {
+ // loading spinner
+ this.props.onExecute()
+ }
+ specActions.execute({ operation, path, method })
+ }
- if ( specSelectors.validateBeforeExecute([path, method]) ) {
- if(this.props.onExecute) {
- this.props.onExecute()
- }
- specActions.execute( { operation, path, method } )
+ handleValidationResultFail = () => {
+ let { specActions, path, method } = this.props
+ // deferred by 40ms, to give element class change time to settle.
+ specActions.clearValidateParams([path, method])
+ setTimeout(() => {
+ specActions.validateParams([path, method])
+ }, 40)
+ }
+
+ handleValidationResult = (isPass) => {
+ if (isPass) {
+ this.handleValidationResultPass()
} else {
- // deferred by 40ms, to give element class change time to settle.
- specActions.clearValidateParams( [path, method] )
- setTimeout(() => {
- specActions.validateParams([path, method])
- }, 40)
+ 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)
render(){
diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx
index 29130be2a90..a4a799772ff 100644
--- a/src/core/components/operation.jsx
+++ b/src/core/components/operation.jsx
@@ -192,6 +192,8 @@ export default class Operation extends PureComponent {
operation={ operation }
specActions={ specActions }
specSelectors={ specSelectors }
+ oas3Selectors={ oas3Selectors }
+ oas3Actions={ oas3Actions }
path={ path }
method={ method }
onExecute={ onExecute } />
diff --git a/src/core/components/parameters/parameters.jsx b/src/core/components/parameters/parameters.jsx
index 0f574b32622..49929e817de 100644
--- a/src/core/components/parameters/parameters.jsx
+++ b/src/core/components/parameters/parameters.jsx
@@ -187,6 +187,7 @@ export default class Parameters extends Component {
contentTypes={ requestBody.get("content", List()).keySeq() }
onChange={(value) => {
oas3Actions.setRequestContentType({ value, pathMethod })
+ oas3Actions.initRequestBodyValidateError({ pathMethod })
}}
className="body-param-content-type" />
@@ -197,6 +198,7 @@ export default class Parameters extends Component {
requestBody={requestBody}
requestBodyValue={oas3Selectors.requestBodyValue(...pathMethod)}
requestBodyInclusionSetting={oas3Selectors.requestBodyInclusionSetting(...pathMethod)}
+ requestBodyErrors={oas3Selectors.requestBodyErrors(...pathMethod)}
isExecute={isExecute}
activeExamplesKey={oas3Selectors.activeExamplesMember(
...pathMethod,
diff --git a/src/core/plugins/oas3/actions.js b/src/core/plugins/oas3/actions.js
index 0736489a00b..ef6a7ae6b9b 100644
--- a/src/core/plugins/oas3/actions.js
+++ b/src/core/plugins/oas3/actions.js
@@ -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_RESPONSE_CONTENT_TYPE = "oas3_set_response_content_type"
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) {
return {
@@ -57,3 +59,24 @@ export function setServerVariableValue ({ 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] }
+ }
+}
diff --git a/src/core/plugins/oas3/components/request-body-editor.jsx b/src/core/plugins/oas3/components/request-body-editor.jsx
index 6d6d09d2d77..ea729ba564e 100644
--- a/src/core/plugins/oas3/components/request-body-editor.jsx
+++ b/src/core/plugins/oas3/components/request-body-editor.jsx
@@ -1,5 +1,6 @@
import React, { PureComponent } from "react"
import PropTypes from "prop-types"
+import cx from "classnames"
import { stringify } from "core/utils"
const NOOP = Function.prototype
@@ -11,6 +12,7 @@ export default class RequestBodyEditor extends PureComponent {
getComponent: PropTypes.func.isRequired,
value: PropTypes.string,
defaultValue: PropTypes.string,
+ errors: PropTypes.array,
};
static defaultProps = {
@@ -74,19 +76,22 @@ export default class RequestBodyEditor extends PureComponent {
render() {
let {
- getComponent
+ getComponent,
+ errors
} = this.props
let {
value
} = this.state
+ let isInvalid = errors.size > 0 ? true : false
const TextArea = getComponent("TextArea")
return (
diff --git a/src/core/plugins/oas3/components/request-body.jsx b/src/core/plugins/oas3/components/request-body.jsx
index b33dd5efe4b..05aa3dd0fcd 100644
--- a/src/core/plugins/oas3/components/request-body.jsx
+++ b/src/core/plugins/oas3/components/request-body.jsx
@@ -38,6 +38,7 @@ const RequestBody = ({
requestBody,
requestBodyValue,
requestBodyInclusionSetting,
+ requestBodyErrors,
getComponent,
getConfigs,
specSelectors,
@@ -88,6 +89,7 @@ const RequestBody = ({
const handleExamplesSelect = (key /*, { isSyntheticChange } */) => {
updateActiveExamplesKey(key)
}
+ requestBodyErrors = List.isList(requestBodyErrors) ? requestBodyErrors : List()
if(!mediaTypeValue.size) {
return null
@@ -138,7 +140,8 @@ const RequestBody = ({
const type = prop.get("type")
const format = prop.get("format")
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") || ""
@@ -179,6 +182,8 @@ const RequestBody = ({
description={key}
getComponent={getComponent}
value={currentValue === undefined ? initialValue : currentValue}
+ required = { required }
+ errors = { currentErrors }
onChange={(value) => {
onChange(value, [key])
}}
@@ -223,6 +228,7 @@ const RequestBody = ({
{
let [path, method] = pathMethod
- return state.setIn( [ "requestData", path, method, "bodyValue" ], value)
+ if (!Map.isMap(value)) {
+ // context: application/json is always a String (instead of Map)
+ 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 } } ) =>{
let [path, method] = pathMethod
@@ -36,4 +60,38 @@ export default {
const path = namespace ? [ namespace, "serverVariableValues", server, key ] : [ "serverVariableValues", server, key ]
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)
+ })
+ },
}
diff --git a/src/core/plugins/oas3/selectors.js b/src/core/plugins/oas3/selectors.js
index 1bdc55891e5..1c5df118bfa 100644
--- a/src/core/plugins/oas3/selectors.js
+++ b/src/core/plugins/oas3/selectors.js
@@ -1,7 +1,6 @@
import { OrderedMap, Map } from "immutable"
import { isOAS3 as isOAS3Helper } from "./helpers"
-
// Helpers
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) => {
const path = namespace ? [namespace, "selectedServer"] : ["selectedServer"]
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) => {
return state.getIn(["examples", path, method, type, name, "activeExample"]) || null
}
@@ -116,3 +149,34 @@ export const serverEffectiveValue = onlyOAS3((state, locationData) => {
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
+}
diff --git a/src/core/plugins/spec/actions.js b/src/core/plugins/spec/actions.js
index bd88db44923..293fb1aef6b 100644
--- a/src/core/plugins/spec/actions.js
+++ b/src/core/plugins/spec/actions.js
@@ -406,7 +406,19 @@ export const executeRequest = (req) =>
if(isJSONObject(requestBody)) {
req.requestBody = JSON.parse(requestBody)
} 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{
req.requestBody = requestBody
}
diff --git a/src/core/plugins/spec/selectors.js b/src/core/plugins/spec/selectors.js
index 88ac0102979..2c854729b1b 100644
--- a/src/core/plugins/spec/selectors.js
+++ b/src/core/plugins/spec/selectors.js
@@ -314,7 +314,6 @@ export const parameterWithMetaByIdentity = (state, pathMethod, param) => {
hashKeyedMeta
)
})
-
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) => {
const opParams = specJsonWithResolvedSubtrees(state).getIn(["paths", ...pathMethod, "parameters"], OrderedMap())
const currentParam = opParams.find(param => param.get("in") === paramIn && param.get("name") === paramName, OrderedMap())
-
return parameterWithMetaByIdentity(state, pathMethod, currentParam)
}
@@ -364,7 +362,6 @@ export const hasHost = createSelector(
// Get the parameter values, that the user filled out
export function parameterValues(state, pathMethod, isXml) {
pathMethod = pathMethod || []
- // let paramValues = state.getIn(["meta", "paths", ...pathMethod, "parameters"], fromJS([]))
let paramValues = operationWithMeta(state, ...pathMethod).get("parameters", List())
return paramValues.reduce( (hash, p) => {
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
}
+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) {
// returns obj if obj is an Immutable map, else returns a new Map
return Map.isMap(obj) ? obj : new Map()
diff --git a/test/e2e-cypress/static/documents/bugs/5181.yaml b/test/e2e-cypress/static/documents/bugs/5181.yaml
new file mode 100644
index 00000000000..5a0051a7bd8
--- /dev/null
+++ b/test/e2e-cypress/static/documents/bugs/5181.yaml
@@ -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
\ No newline at end of file
diff --git a/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml b/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml
index 2d5d0060e5d..d83d7b1e826 100644
--- a/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml
+++ b/test/e2e-cypress/static/documents/features/petstore-only-pet.openapi.yaml
@@ -415,4 +415,4 @@ components:
api_key:
type: apiKey
name: api_key
- in: header
\ No newline at end of file
+ in: header
diff --git a/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js b/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js
index 65c7383c6fa..0ef31e2085b 100644
--- a/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js
+++ b/test/e2e-cypress/tests/features/oas3-request-body-allow-empty-values.js
@@ -67,6 +67,9 @@ describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => {
// Expand Try It Out
.get(".try-out__btn")
.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
.get(".execute.opblock-control__btn")
.click()
@@ -91,6 +94,9 @@ describe("OpenAPI 3.0 Allow Empty Values in 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")
.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
.get(".execute.opblock-control__btn")
.click()
@@ -118,6 +124,9 @@ describe("OpenAPI 3.0 Allow Empty Values in Request Body", () => {
.uncheck()
.get(".opblock-body .opblock-section .opblock-section-request-body .parameters:nth-child(6) > .parameters-col_description .parameter__empty_value_toggle input")
.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
.get(".execute.opblock-control__btn")
.click()
diff --git a/test/e2e-cypress/tests/features/oas3-request-body-required.js b/test/e2e-cypress/tests/features/oas3-request-body-required.js
new file mode 100644
index 00000000000..37cb1358dcd
--- /dev/null
+++ b/test/e2e-cypress/tests/features/oas3-request-body-required.js
@@ -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")
+ })
+ })
+})
diff --git a/test/mocha/core/plugins/oas3/reducers.js b/test/mocha/core/plugins/oas3/reducers.js
new file mode 100644
index 00000000000..c192ba44475
--- /dev/null
+++ b/test/mocha/core/plugins/oas3/reducers.js
@@ -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)
+ })
+ })
+ })
+
+})