Skip to content

Commit

Permalink
fix(oas3): switching media types should update schema properties (#6518)
Browse files Browse the repository at this point in the history
* When the media-type is changed, there is a new `onChangeMediaType` method to handle actions.
* If target schema properties key/value pairs does NOT equals current schema properties, clear the requestBodyValue, try-it-out request/response and validation params.
* If target schema properties key/value pairs DOES equals current schema properties, do not change or re-render schema properties
* oas3Selector `validateShallowRequired` now also validates required keys against target media-type

Fixes #6201, #6250, #6476
  • Loading branch information
tim-lai committed Oct 14, 2020
1 parent b9137dc commit 3905fad
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 14 deletions.
7 changes: 6 additions & 1 deletion src/core/components/execute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class Execute extends Component {
let oas3RequiredRequestBodyContentType = specSelectors.getOAS3RequiredRequestBodyContentType([path, method])
let oas3RequestBodyValue = oas3Selectors.requestBodyValue(path, method)
let oas3ValidateBeforeExecuteSuccess = oas3Selectors.validateBeforeExecute([path, method])
let oas3RequestContentType = oas3Selectors.requestContentType(path, method)

if (!oas3ValidateBeforeExecuteSuccess) {
validationErrors.missingBodyValue = true
Expand All @@ -40,7 +41,11 @@ export default class Execute extends Component {
if (!oas3RequiredRequestBodyContentType) {
return true
}
let missingRequiredKeys = oas3Selectors.validateShallowRequired({ oas3RequiredRequestBodyContentType, oas3RequestBodyValue })
let missingRequiredKeys = oas3Selectors.validateShallowRequired({
oas3RequiredRequestBodyContentType,
oas3RequestContentType,
oas3RequestBodyValue
})
if (!missingRequiredKeys || missingRequiredKeys.length < 1) {
return true
}
Expand Down
18 changes: 16 additions & 2 deletions src/core/components/parameters/parameters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ export default class Parameters extends Component {
}
}

onChangeMediaType = ( { value, pathMethod } ) => {
let { specSelectors, specActions, oas3Selectors, oas3Actions } = this.props
let targetMediaType = value
let currentMediaType = oas3Selectors.requestContentType(...pathMethod)
let schemaPropertiesMatch = specSelectors.isMediaTypeSchemaPropertiesEqual(pathMethod, currentMediaType, targetMediaType)
if (!schemaPropertiesMatch) {
oas3Actions.clearRequestBodyValue({ pathMethod })
specActions.clearResponse(...pathMethod)
specActions.clearRequest(...pathMethod)
specActions.clearValidateParams(pathMethod)
}
oas3Actions.setRequestContentType({ value, pathMethod })
oas3Actions.initRequestBodyValidateError({ pathMethod })
}

render(){

let {
Expand Down Expand Up @@ -187,8 +202,7 @@ export default class Parameters extends Component {
value={oas3Selectors.requestContentType(...pathMethod)}
contentTypes={ requestBody.get("content", List()).keySeq() }
onChange={(value) => {
oas3Actions.setRequestContentType({ value, pathMethod })
oas3Actions.initRequestBodyValidateError({ pathMethod })
this.onChangeMediaType({ value, pathMethod })
}}
className="body-param-content-type" />
</label>
Expand Down
8 changes: 8 additions & 0 deletions src/core/plugins/oas3/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 const CLEAR_REQUEST_BODY_VALUE = "oas3_clear_request_body_value"

export function setSelectedServer (selectedServerUrl, namespace) {
return {
Expand Down Expand Up @@ -80,3 +81,10 @@ export const initRequestBodyValidateError = ({ pathMethod } ) => {
payload: { path: pathMethod[0], method: pathMethod[1] }
}
}

export const clearRequestBodyValue = ({ pathMethod }) => {
return {
type: CLEAR_REQUEST_BODY_VALUE,
payload: { pathMethod }
}
}
12 changes: 12 additions & 0 deletions src/core/plugins/oas3/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UPDATE_RESPONSE_CONTENT_TYPE,
SET_REQUEST_BODY_VALIDATE_ERROR,
CLEAR_REQUEST_BODY_VALIDATE_ERROR,
CLEAR_REQUEST_BODY_VALUE,
} from "./actions"

export default {
Expand Down Expand Up @@ -94,4 +95,15 @@ export default {
}, bodyValues)
})
},
[CLEAR_REQUEST_BODY_VALUE]: (state, { payload: { pathMethod }}) => {
let [path, method] = pathMethod
const requestBodyValue = state.getIn(["requestData", path, method, "bodyValue"])
if (!requestBodyValue) {
return state
}
if (!Map.isMap(requestBodyValue)) {
return state.setIn(["requestData", path, method, "bodyValue"], "")
}
return state.setIn(["requestData", path, method, "bodyValue"], Map())
}
}
22 changes: 11 additions & 11 deletions src/core/plugins/oas3/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,23 +154,23 @@ export const validateBeforeExecute = validateRequestBodyIsRequired(
(state, pathMethod) => validateRequestBodyValueExists(state, pathMethod)
)

export const validateShallowRequired = ( state, {oas3RequiredRequestBodyContentType, oas3RequestBodyValue} ) => {
export const validateShallowRequired = (state, { oas3RequiredRequestBodyContentType, oas3RequestContentType, oas3RequestBodyValue} ) => {
let missingRequiredKeys = []
// context: json => String; urlencoded => Map
// context: json => String; urlencoded, form-data => 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
// Cycle through list of possible contentTypes for matching contentType and defined requiredKeys
Object.keys(oas3RequiredRequestBodyContentType.requestContentType).forEach((contentType) => {
let contentTypeVal = oas3RequiredRequestBodyContentType.requestContentType[contentType]
contentTypeVal.forEach((requiredKey) => {
if (requiredKeys.indexOf(requiredKey) < 0 ) {
requiredKeys.push(requiredKey)
}
})
if (contentType === oas3RequestContentType) {
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"])
Expand Down
11 changes: 11 additions & 0 deletions src/core/plugins/spec/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,17 @@ export const getOAS3RequiredRequestBodyContentType = (state, pathMethod) => {
return requiredObj
}

export const isMediaTypeSchemaPropertiesEqual = ( state, pathMethod, currentMediaType, targetMediaType) => {
let requestBodyContent = state.getIn(["resolvedSubtrees", "paths", ...pathMethod, "requestBody", "content"], fromJS([]))
if (requestBodyContent.size < 2 || !currentMediaType || !targetMediaType) {
// nothing to compare
return false
}
let currentMediaTypeSchemaProperties = requestBodyContent.getIn([currentMediaType, "schema", "properties"], fromJS([]))
let targetMediaTypeSchemaProperties = requestBodyContent.getIn([targetMediaType, "schema", "properties"], fromJS([]))
return currentMediaTypeSchemaProperties.equals(targetMediaTypeSchemaProperties) ? true: false
}

function returnSelfOrNewMap(obj) {
// returns obj if obj is an Immutable map, else returns a new Map
return Map.isMap(obj) ? obj : new Map()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
openapi: 3.0.0
info:
title: Switching between multiple content-type test
version: 1.0.0
servers:
- url: https://httpbin.org
paths:
/post:
post:
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/Bar'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Foo'
application/json:
schema:
$ref: '#/components/schemas/FooBar'
responses:
'200':
description: ok

components:
schemas:
Foo:
type: object
properties:
foo:
type: string
example: ''
Bar:
type: object
required: [bar]
properties:
bar:
type: integer
example: 1
FooBar:
type: object
required:
- bar
properties:
foo:
type: string
example: ''
bar:
type: integer
example: 1
174 changes: 174 additions & 0 deletions test/e2e-cypress/tests/features/oas3-multiple-media-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// https://github.com/swagger-api/swagger-ui/issues/6201
// https://github.com/swagger-api/swagger-ui/issues/6250
// https://github.com/swagger-api/swagger-ui/issues/6476

describe("OpenAPI 3.0 Multiple Media Types with different schemas", () => {
const mediaTypeFormData = "multipart/form-data"
const mediaTypeUrlencoded = "application/x-www-form-urlencoded"
const mediaTypeJson = "application/json"

beforeEach(() => {
cy.visit(
"/?url=/documents/features/oas3-multiple-media-type.yaml"
)
.get("#operations-default-post_post")
.click()
// Expand Try It Out
.get(".try-out__btn")
.click()
// @alias Execute Button
cy.get(".execute.opblock-control__btn").as("executeBtn")
// @alias Media Type Dropdown
cy.get(".opblock-section-request-body .content-type").as("selectMediaType")
})

// In all cases,
// - assume that examples are populated based on schema (not explicitly tested)
// - assume validation passes based on successful "execute"
// - expect final cURL command result doees not contain unexpected artifacts from other content-type schemas
describe("multipart/form-data (only 'bar')", () => {
it("should execute multipart/form-data", () => {
cy.get("@selectMediaType")
.select(mediaTypeFormData)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "bar")
.should("not.contains.text", "foo")
})
it("should execute application/x-www-form-urlencoded THEN execute multipart/form-data", () => {
cy.get("@selectMediaType")
.select(mediaTypeUrlencoded)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeFormData)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "bar")
.should("not.contains.text", "foo")
})
it("should execute application/json THEN execute multipart/form-data", () => {
cy.get("@selectMediaType")
.select(mediaTypeJson)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeFormData)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "bar")
.should("not.contains.text", "foo")
})
})

describe("application/x-www-form-urlencoded (only 'foo')", () => {
it("should execute application/x-www-form-urlencoded", () => {
cy.get("@selectMediaType")
.select(mediaTypeUrlencoded)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("not.contains.text", "bar")
})
it("should execute multipart/form-data THEN execute application/x-www-form-urlencoded", () => {
cy.get("@selectMediaType")
.select(mediaTypeFormData)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeUrlencoded)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("not.contains.text", "bar")
})
it("should execute application/json THEN execute application/x-www-form-urlencoded", () => {
cy.get("@selectMediaType")
.select(mediaTypeJson)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeUrlencoded)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("not.contains.text", "bar")
})
})

describe("application/json (both 'foo' and 'bar')", () => {
// note: form input for "application/json" is a string; not multiple form fields
it("should execute application/json", () => {
// final curl should have both "bar" and "foo"
cy.get("@selectMediaType")
.select(mediaTypeJson)
cy.get("@executeBtn")
.click()
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("contains.text", "bar")
})
it("should execute multipart/form-data THEN execute application/json", () => {
cy.get("@selectMediaType")
.select(mediaTypeFormData)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeJson)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("contains.text", "bar")
})
it("should execute application/x-www-form-urlencoded THEN execute application/json", () => {
// final curl should have both "bar" and "foo"
cy.get("@selectMediaType")
.select(mediaTypeUrlencoded)
cy.get("@executeBtn")
.click()
cy.get("@selectMediaType")
.select(mediaTypeJson)
cy.get("@executeBtn")
.click()
// cURL component
cy.get(".responses-wrapper .curl-command")
.should("exist")
.get(".responses-wrapper .curl-command span")
.should("contains.text", "foo")
.should("contains.text", "bar")
})
})
})

0 comments on commit 3905fad

Please sign in to comment.