fix: Boolean and null literals should be considered valid request bodies (#28835)

* fix(types): RequestBody type should be able to accept booleans and null values, which are all valid JSON literals

* refactor: boolean literals are valid JSON objects. Null values should also be considered valid when explicitly passed to the request function.

* refactor: body is explicitly defined when passed as positional argument or when supplied through the options object

* test: JSON literals should be parsed as valid JSON and set json=true

* docs: issue reference

* fix: boolean and null literal should be send to request promise as strings

* docs: fixes #28789 -- added issue reference

* test: tests proper conversion of JSON literals to strings.

* docs: added isssue reference

* docs: fixes #28789 -- changelog entry

* refactor: change isValidJsonObj to isValidBody

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* refactor: change isValidJsonObj to isValidBody

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* refactor: use lodash utils

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* Update cli/CHANGELOG.md

Co-authored-by: Bill Glesias <bglesias@gmail.com>

* docs: moved entry to 13.6.5

* docs: fixed changelog entry

* Update CHANGELOG.md

---------

Co-authored-by: Bill Glesias <bglesias@gmail.com>
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Teo Anastasiadis
2024-02-23 23:11:18 +02:00
committed by GitHub
parent f3348bcb75
commit 3e5fabce2c
6 changed files with 134 additions and 4 deletions

View File

@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.6.7
_Released 2/27/2024 (PENDING)_
**Bugfixes:**
- Changed RequestBody type to allow for boolean and null literals to be passed as body values. [#28789](https://github.com/cypress-io/cypress/issues/28789)
## 13.6.6
_Released 2/22/2024_

View File

@@ -7,7 +7,7 @@ declare namespace Cypress {
type FileContents = string | any[] | object
type HistoryDirection = 'back' | 'forward'
type HttpMethod = string
type RequestBody = string | object
type RequestBody = string | object | boolean | null
type ViewportOrientation = 'portrait' | 'landscape'
type PrevSubject = keyof PrevSubjectMap
type TestingType = 'e2e' | 'component'

View File

@@ -129,6 +129,60 @@ describe('src/cy/commands/request', () => {
})
})
// https://github.com/cypress-io/cypress/issues/28789
context('accepts trivial RFC 8259 compliant body objects', () => {
it('accepts body equal to true', () => {
cy.request({ method: 'POST', url: 'http://www.github.com/projects/foo', body: true }).then(function () {
this.expectOptionsToBe({
method: 'POST',
url: 'http://www.github.com/projects/foo',
body: true,
json: true,
})
})
})
it('accepts body equal to false', () => {
cy.request({ method: 'POST', url: 'http://www.github.com/projects/foo', body: false }).then(function () {
this.expectOptionsToBe({
method: 'POST',
url: 'http://www.github.com/projects/foo',
body: false,
json: true,
})
})
})
it('accepts (explicitly defined) null body', () => {
cy.request({ method: 'POST', url: 'http://www.github.com/projects/foo', body: null }).then(function () {
this.expectOptionsToBe({
method: 'POST',
url: 'http://www.github.com/projects/foo',
//body: null,
json: true,
})
})
cy.request('POST', 'http://www.github.com/projects/foo', null).then(function () {
this.expectOptionsToBe({
method: 'POST',
url: 'http://www.github.com/projects/foo',
//body: null,
json: true,
})
})
cy.request('http://www.github.com/projects/foo', null).then(function () {
this.expectOptionsToBe({
method: 'POST',
url: 'http://www.github.com/projects/foo',
//body: null,
json: true,
})
})
})
})
context('method normalization', () => {
it('uppercases method', () => {
cy.request('post', 'https://www.foo.com').then(function () {

View File

@@ -43,8 +43,9 @@ const hasFormUrlEncodedContentTypeHeader = (headers) => {
return header && (_.toLower(header) === 'content-type')
}
const isValidJsonObj = (body) => {
return _.isObject(body) && !_.isFunction(body)
const isValidBody = (body, isExplicitlyDefined: boolean = false) => {
return (_.isObject(body) || _.isBoolean(body) || (isExplicitlyDefined && _.isNull(body)))
&& !_.isFunction(body)
}
const whichAreOptional = (val, key) => {
@@ -81,9 +82,11 @@ export default (Commands, Cypress, cy, state, config) => {
request (...args) {
const o: any = {}
const userOptions = o
let bodyIsExplicitlyDefined = false
if (_.isObject(args[0])) {
_.extend(userOptions, args[0])
bodyIsExplicitlyDefined = _.has(args[0], 'body')
} else if (args.length === 1) {
o.url = args[0]
} else if (args.length === 2) {
@@ -96,11 +99,13 @@ export default (Commands, Cypress, cy, state, config) => {
// set url + body
o.url = args[0]
o.body = args[1]
bodyIsExplicitlyDefined = true
}
} else if (args.length === 3) {
o.method = args[0]
o.url = args[1]
o.body = args[2]
bodyIsExplicitlyDefined = true
}
let options = _.defaults({}, userOptions, REQUEST_DEFAULTS, {
@@ -222,7 +227,7 @@ export default (Commands, Cypress, cy, state, config) => {
// only set json to true if form isnt true
// and we have a valid object for body
if ((options.form !== true) && isValidJsonObj(options.body)) {
if ((options.form !== true) && isValidBody(options.body, bodyIsExplicitlyDefined)) {
options.json = true
}

View File

@@ -698,6 +698,11 @@ module.exports = function (options = {}) {
// either turn these both on or off
options.followAllRedirects = options.followRedirect
// https://github.com/cypress-io/cypress/issues/28789
if (options.json === true) {
if (_.isBoolean(options.body) || _.isNull(options.body)) options.body = String(options.body)
}
if (options.form === true) {
// reset form to whatever body is
// and nuke body

View File

@@ -937,6 +937,64 @@ describe('lib/request', () => {
})
})
// https://github.com/cypress-io/cypress/issues/28789
context('json=true', () => {
beforeEach(() => {
nock('http://localhost:8080')
.matchHeader('Content-Type', 'application/json')
.post('/login')
.reply(200, '<html></html>')
})
it('does not modify regular JSON objects', function () {
const init = sinon.spy(request.rp.Request.prototype, 'init')
const body = {
foo: 'bar',
}
return request.sendPromise({}, this.fn, {
url: 'http://localhost:8080/login',
method: 'POST',
cookies: false,
json: true,
body,
})
.then(() => {
expect(init).to.be.calledWithMatch({ body })
})
})
it('converts boolean JSON literals to strings', function () {
const init = sinon.spy(request.rp.Request.prototype, 'init')
return request.sendPromise({}, this.fn, {
url: 'http://localhost:8080/login',
method: 'POST',
cookies: false,
json: true,
body: true,
})
.then(() => {
expect(init).to.be.calledWithMatch({ body: 'true' })
})
})
it('converts null JSON literals to \'null\'', function () {
const init = sinon.spy(request.rp.Request.prototype, 'init')
return request.sendPromise({}, this.fn, {
url: 'http://localhost:8080/login',
method: 'POST',
cookies: false,
json: true,
body: null,
})
.then(() => {
expect(init).to.be.calledWithMatch({ body: 'null' })
})
})
})
context('bad headers', () => {
beforeEach(function (done) {
this.srv = http.createServer((req, res) => {