Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/core/plugins/oas3/after-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @prettier
*/

function afterLoad({ fn }) {
const originalBuildRequest = fn.buildRequest

this.fn.buildRequest = (options) => {
const { requestBody, requestContentType, spec, pathName, method } = options

if (
requestBody &&
requestContentType === "application/x-www-form-urlencoded" &&
spec &&
pathName &&
method
) {
const operation =
spec.paths &&
spec.paths[pathName] &&
spec.paths[pathName][method.toLowerCase()]

if (operation && operation.requestBody) {
const mediaType =
operation.requestBody.content &&
operation.requestBody.content["application/x-www-form-urlencoded"]

if (mediaType && mediaType.schema && mediaType.schema.properties) {
const properties = mediaType.schema.properties
const existingEncoding = mediaType.encoding || {}

const needsDefaults = Object.keys(properties).some((propName) => {
const prop = properties[propName]
const isArray =
prop &&
(prop.type === "array" ||
(Array.isArray(prop.type) && prop.type.includes("array")))
return isArray && !existingEncoding[propName]
})

if (needsDefaults) {
const encoding = { ...existingEncoding }

Object.keys(properties).forEach((propName) => {
if (!encoding[propName]) {
const prop = properties[propName]
const isArray =
prop &&
(prop.type === "array" ||
(Array.isArray(prop.type) && prop.type.includes("array")))
if (isArray) {
encoding[propName] = { style: "form", explode: true }
}
}
})

mediaType.encoding = encoding
}
}
}
}

return originalBuildRequest(options)
}
}

export default afterLoad
2 changes: 2 additions & 0 deletions src/core/plugins/oas3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import * as actions from "./actions"
import * as selectors from "./selectors"
import reducers from "./reducers"
import { makeIsFileUploadIntended } from "./fn"
import afterLoad from "./after-load"

export default function ({ getSystem }) {
const isFileUploadIntended = makeIsFileUploadIntended(getSystem)

return {
afterLoad,
components,
wrapComponents,
statePlugins: {
Expand Down
269 changes: 269 additions & 0 deletions test/unit/core/plugins/oas3/after-load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import afterLoad from "core/plugins/oas3/after-load"

describe("OAS3 plugin - afterLoad", function () {
describe("buildRequest wrapper", function () {
it("should apply default encoding (style=form, explode=true) for array properties in form-urlencoded when encoding is omitted", function () {
const spec = {
openapi: "3.0.0",
paths: {
"/implicit-encoding": {
post: {
requestBody: {
content: {
"application/x-www-form-urlencoded": {
schema: {
type: "object",
properties: {
numbers: {
type: "array",
items: {
type: "integer",
},
},
},
},
},
},
},
},
},
},
}

const mockBuildRequest = jest.fn().mockReturnValue({})
const context = { fn: {} }

afterLoad.call(context, { fn: { buildRequest: mockBuildRequest } })

context.fn.buildRequest({
spec,
pathName: "/implicit-encoding",
method: "post",
requestBody: { numbers: [1, 2, 3] },
requestContentType: "application/x-www-form-urlencoded",
})

expect(mockBuildRequest).toHaveBeenCalledTimes(1)

const operation = spec.paths["/implicit-encoding"].post
const mediaType =
operation.requestBody.content["application/x-www-form-urlencoded"]
expect(mediaType.encoding).toBeDefined()
expect(mediaType.encoding.numbers).toEqual({
style: "form",
explode: true,
})
})

it("should not override existing encoding when explicitly specified", function () {
const spec = {
openapi: "3.0.0",
paths: {
"/explicit-encoding": {
post: {
requestBody: {
content: {
"application/x-www-form-urlencoded": {
schema: {
type: "object",
properties: {
numbers: {
type: "array",
items: {
type: "integer",
},
},
},
},
encoding: {
numbers: {
style: "pipeDelimited",
explode: false,
},
},
},
},
},
},
},
},
}

const mockBuildRequest = jest.fn().mockReturnValue({})
const context = { fn: {} }

afterLoad.call(context, { fn: { buildRequest: mockBuildRequest } })

context.fn.buildRequest({
spec,
pathName: "/explicit-encoding",
method: "post",
requestBody: { numbers: [1, 2, 3] },
requestContentType: "application/x-www-form-urlencoded",
})

const operation = spec.paths["/explicit-encoding"].post
const mediaType =
operation.requestBody.content["application/x-www-form-urlencoded"]
expect(mediaType.encoding.numbers).toEqual({
style: "pipeDelimited",
explode: false,
})
})

it("should not add encoding for non-array properties", function () {
const spec = {
openapi: "3.0.0",
paths: {
"/no-arrays": {
post: {
requestBody: {
content: {
"application/x-www-form-urlencoded": {
schema: {
type: "object",
properties: {
name: {
type: "string",
},
age: {
type: "integer",
},
},
},
},
},
},
},
},
},
}

const mockBuildRequest = jest.fn().mockReturnValue({})
const context = { fn: {} }

afterLoad.call(context, { fn: { buildRequest: mockBuildRequest } })

context.fn.buildRequest({
spec,
pathName: "/no-arrays",
method: "post",
requestBody: { name: "test", age: 25 },
requestContentType: "application/x-www-form-urlencoded",
})

const operation = spec.paths["/no-arrays"].post
const mediaType =
operation.requestBody.content["application/x-www-form-urlencoded"]
expect(mediaType.encoding).toBeUndefined()
})

it("should not modify request for non-form-urlencoded content types", function () {
const spec = {
openapi: "3.0.0",
paths: {
"/json": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
numbers: {
type: "array",
items: {
type: "integer",
},
},
},
},
},
},
},
},
},
},
}

const mockBuildRequest = jest.fn().mockReturnValue({})
const context = { fn: {} }

afterLoad.call(context, { fn: { buildRequest: mockBuildRequest } })

context.fn.buildRequest({
spec,
pathName: "/json",
method: "post",
requestBody: { numbers: [1, 2, 3] },
requestContentType: "application/json",
})

const operation = spec.paths["/json"].post
const mediaType = operation.requestBody.content["application/json"]
expect(mediaType.encoding).toBeUndefined()
})

it("should apply default encoding only for array properties among mixed properties", function () {
const spec = {
openapi: "3.0.0",
paths: {
"/mixed": {
post: {
requestBody: {
content: {
"application/x-www-form-urlencoded": {
schema: {
type: "object",
properties: {
name: {
type: "string",
},
tags: {
type: "array",
items: {
type: "string",
},
},
ids: {
type: "array",
items: {
type: "integer",
},
},
},
},
},
},
},
},
},
},
}

const mockBuildRequest = jest.fn().mockReturnValue({})
const context = { fn: {} }

afterLoad.call(context, { fn: { buildRequest: mockBuildRequest } })

context.fn.buildRequest({
spec,
pathName: "/mixed",
method: "post",
requestBody: { name: "test", tags: ["a", "b"], ids: [1, 2] },
requestContentType: "application/x-www-form-urlencoded",
})

const operation = spec.paths["/mixed"].post
const mediaType =
operation.requestBody.content["application/x-www-form-urlencoded"]
expect(mediaType.encoding).toBeDefined()
expect(mediaType.encoding.name).toBeUndefined()
expect(mediaType.encoding.tags).toEqual({
style: "form",
explode: true,
})
expect(mediaType.encoding.ids).toEqual({ style: "form", explode: true })
})
})
})