feat: patch fetch and xhr inside cy.origin to get resourceType and credential Level (#23822)

* chore: modify xhr-fetch-requests to handle onload and prep for use in patches tests

* feat: add patches for fetch and xmlhttprequest

* chore: short circuit fetch and xmlHttpRequests if conditions aren't met

* chore: refactor xmlHttpRequest and fetch patches into individual files and add some basic types

* chore: fix typo
This commit is contained in:
Bill Glesias
2022-09-18 23:22:12 -04:00
committed by GitHub
parent 0c265638ce
commit f356065ec4
10 changed files with 777 additions and 26 deletions
@@ -1,10 +1,10 @@
describe('src/cross-origin/patches', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
context('submit', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('correctly submits a form when the target is _top for HTMLFormElement', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.get('form').then(($form) => {
@@ -18,6 +18,11 @@ describe('src/cross-origin/patches', () => {
})
context('setAttribute', () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('renames integrity to cypress-stripped-integrity for HTMLScriptElement', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win: Window) => {
@@ -52,4 +57,533 @@ describe('src/cross-origin/patches', () => {
})
})
})
context('fetch', () => {
describe('from the AUT', () => {
beforeEach(() => {
cy.intercept('/test-request').as('testRequest')
cy.origin('http://www.foobar.com:3500', () => {
cy.stub(Cypress, 'backend').callThrough()
})
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="xhr-fetch-requests"]').click()
})
describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => {
[undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => {
describe(`for credential option ${credentialOption || 'default'}`, () => {
const postfixedSelector = !credentialOption || credentialOption === 'same-origin' ? '' : `-${credentialOption}`
const assertCredentialStatus = credentialOption || 'same-origin'
it('with a url string', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
postfixedSelector,
assertCredentialStatus,
},
},
({ postfixedSelector, assertCredentialStatus }) => {
cy.get(`[data-cy="trigger-fetch${postfixedSelector}"]`).click()
cy.wait('@testRequest')
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
it('with a request object', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
postfixedSelector,
assertCredentialStatus,
},
},
({ postfixedSelector, assertCredentialStatus }) => {
cy.get(`[data-cy="trigger-fetch-with-request-object${postfixedSelector}"]`).click()
cy.wait('@testRequest')
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
it('with a url object', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
postfixedSelector,
assertCredentialStatus,
},
},
({ postfixedSelector, assertCredentialStatus }) => {
cy.get(`[data-cy="trigger-fetch-with-url-object${postfixedSelector}"]`).click()
cy.wait('@testRequest')
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
})
})
it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('404')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
return false
})
cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click()
})
})
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('CORS ERROR')
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
resourceType: 'fetch',
credentialStatus: 'include',
})
return false
})
cy.get(`[data-cy="trigger-fetch-with-preflight"]`).click()
})
})
})
})
describe('from the spec bridge', () => {
beforeEach(() => {
cy.intercept('/test-request').as('testRequest')
cy.stub(Cypress, 'backend').callThrough()
cy.origin('http://www.foobar.com:3500', () => {
cy.stub(Cypress, 'backend').callThrough()
})
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="xhr-fetch-requests"]').click()
})
describe('patches fetch in the AUT when going cross origin and sends credential status to server socket', () => {
[undefined, 'same-origin', 'omit', 'include'].forEach((credentialOption) => {
const assertCredentialStatus = credentialOption || 'same-origin'
// NOTE: Even if the request fails, this should be popped off the queue in the proxy and not be an issue moving forward.
// only thing we should be concerned with is urls that go over the socket but somehow do NOT make a request.
// This MIGHT be an issue for preflight requests failing if the browser fails the request and the request doesn't actually make it through the proxy
describe(`for credential option ${credentialOption || 'default'}`, () => {
it('with a url string', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
credentialOption,
assertCredentialStatus,
},
}, ({ credentialOption, assertCredentialStatus }) => {
cy.then(() => {
if (credentialOption) {
return fetch('http://www.foobar.com:3500/test-request-credentials', {
credentials: credentialOption as RequestCredentials,
})
}
return fetch('http://www.foobar.com:3500/test-request-credentials')
})
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
it('with a request object', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
credentialOption,
assertCredentialStatus,
},
}, ({ credentialOption, assertCredentialStatus }) => {
cy.then(() => {
let req
if (credentialOption) {
req = new Request('http://www.foobar.com:3500/test-request-credentials', {
credentials: credentialOption as RequestCredentials,
})
} else {
req = new Request('http://www.foobar.com:3500/test-request-credentials')
}
return fetch(req)
})
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
it('with a url object', () => {
cy.origin('http://www.foobar.com:3500', {
args: {
credentialOption,
assertCredentialStatus,
},
}, ({ credentialOption, assertCredentialStatus }) => {
cy.then(() => {
let urlObj = new URL('/test-request-credentials', 'http://www.foobar.com:3500')
if (credentialOption) {
return fetch(urlObj, {
credentials: credentialOption as RequestCredentials,
})
}
return fetch(urlObj)
})
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
})
})
})
})
it('fails gracefully if fetch is called with Bad arguments and we don\'t single to the socket (must match the fetch api spec), but fetch request still proceeds', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('404')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
return false
})
cy.get(`[data-cy="trigger-fetch-with-bad-options"]`).click()
})
})
})
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.then(() => {
let url = new URL('/test-request', 'http://app.foobar.com:3500').toString()
return new Promise<void>((resolve, reject) => {
fetch(url, {
credentials: 'include',
headers: {
'foo': 'bar',
},
}).catch(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
resourceType: 'fetch',
credentialStatus: 'include',
})
resolve()
}).then(() => {
// if this fetch does not fail, fail the test
reject()
})
})
})
})
})
})
it('does not patch fetch in the spec window or the AUT if the AUT is on the primary', () => {
cy.stub(Cypress, 'backend').callThrough()
cy.visit('fixtures/xhr-fetch-requests.html')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html'
})
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html'
})
})
// expect spec to NOT be patched in primary
cy.then(async () => {
await fetch('/test-request')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
})
// expect AUT to NOT be patched in primary
cy.window().then(async (win) => {
await win.fetch('/test-request')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
})
})
})
context('xmlHttpRequest', () => {
describe('from the AUT', () => {
beforeEach(() => {
cy.intercept('/test-request').as('testRequest')
cy.origin('http://www.foobar.com:3500', () => {
cy.stub(Cypress, 'backend').callThrough()
})
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="xhr-fetch-requests"]').click()
})
describe('patches xmlHttpRequest in the AUT when going cross origin and sends credential status to server socket', () => {
[false, true].forEach((withCredentials) => {
it(`for withCredentials option ${withCredentials}`, () => {
const postfixedSelector = withCredentials ? '-with-credentials' : ''
cy.origin('http://www.foobar.com:3500', {
args: {
postfixedSelector,
withCredentials,
},
},
({ postfixedSelector, withCredentials = false }) => {
cy.get(`[data-cy="trigger-xml-http-request${postfixedSelector}"]`).click()
cy.wait('@testRequest')
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
})
})
})
it('fails gracefully if xhr is called with Bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('404')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
return false
})
cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click()
})
})
})
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('CORS ERROR')
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
resourceType: 'xhr',
credentialStatus: true,
})
return false
})
cy.get(`[data-cy="trigger-xml-http-request-with-preflight"]`).click()
})
})
})
describe('from the spec bridge', () => {
beforeEach(() => {
cy.intercept('/test-request').as('testRequest')
cy.stub(Cypress, 'backend').callThrough()
cy.origin('http://www.foobar.com:3500', () => {
cy.stub(Cypress, 'backend').callThrough()
})
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="xhr-fetch-requests"]').click()
})
describe('patches xmlHttpRequest in the spec bridge', () => {
[false, true].forEach((withCredentials) => {
it(`for withCredentials option ${withCredentials}`, () => {
cy.origin('http://www.foobar.com:3500', {
args: {
withCredentials,
},
},
({ withCredentials = false }) => {
cy.then(() => {
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
return new Promise<void>((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.withCredentials = withCredentials
xhr.onload = function () {
resolve(xhr.response)
}
xhr.onerror = function () {
reject(xhr.response)
}
xhr.send()
})
})
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
})
})
})
it('fails gracefully if xmlHttpRequest is called with bad arguments and we don\'t signal to the socket (must match the legit URL), but xhr request still proceeds', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.on('uncaught:exception', (err) => {
expect(err.message).to.contain('404')
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
return false
})
cy.get(`[data-cy="trigger-xml-http-request-with-bad-options"]`).click()
})
})
it('works as expected with requests that require preflight that ultimately fail and the request does not succeed', () => {
cy.origin('http://www.foobar.com:3500',
() => {
cy.then(() => {
let url = new URL('/test-request', 'http://app.foobar.com:3500').toString()
return new Promise<void>((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.withCredentials = true
xhr.onload = function () {
// if this request passes, fail the test
reject(xhr.response)
}
xhr.onerror = function () {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
resourceType: 'xhr',
credentialStatus: true,
})
resolve()
}
xhr.send()
})
})
})
})
})
})
it('does not patch xmlHttpRequest in the spec window or the AUT if the AUT is on the primary', () => {
cy.stub(Cypress, 'backend').callThrough()
cy.visit('fixtures/xhr-fetch-requests.html')
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/secondary-origin.html'
})
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.location.href = 'http://localhost:3500/fixtures/xhr-fetch-requests.html'
})
})
// expect spec to NOT be patched in primary
cy.then(async () => {
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
await new Promise<void>((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onload = function () {
resolve(xhr.response)
}
xhr.onerror = function () {
reject(xhr.response)
}
xhr.send()
})
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
})
// expect AUT to NOT be patched in primary
cy.window().then(async (win) => {
let url = new URL('/test-request-credentials', 'http://www.foobar.com:3500').toString()
await new Promise<void>((resolve, reject) => {
let xhr = new win.XMLHttpRequest()
xhr.open('GET', url)
xhr.onload = function () {
resolve(xhr.response)
}
xhr.onerror = function () {
reject(xhr.response)
}
xhr.send()
})
expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials')
})
})
})
})
@@ -23,6 +23,7 @@ describe('cy.origin - snapshots', () => {
})
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="xhr-fetch-requests-onload"]').click()
})
// TODO: the xhr event is showing up twice in the log, which is wrong and causing flake. skipping until: https://github.com/cypress-io/cypress/issues/23840 is addressed.
@@ -47,7 +48,7 @@ describe('cy.origin - snapshots', () => {
// TODO: Since we have two events, one of them does not have a request snapshot
expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!')
expect(snapshots[1].querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!')
})
})
@@ -70,7 +71,7 @@ describe('cy.origin - snapshots', () => {
const snapshots = xhrLogFromSecondaryOrigin.snapshots.map((snapshot) => snapshot.body.get()[0])
snapshots.forEach((snapshot) => {
expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes!')
expect(snapshot.querySelector(`[data-cy="assertion-header"]`)).to.have.property('innerText').that.equals('Making XHR and Fetch Requests behind the scenes if fireOnload is true!')
})
})
})
@@ -13,7 +13,8 @@
<li><a data-cy="files-form-link" href="http://www.foobar.com:3500/fixtures/files-form.html">http://www.foobar.com:3500/fixtures/files-form.html</a></li>
<li><a data-cy="errors-link" href="http://www.foobar.com:3500/fixtures/errors.html">http://www.foobar.com:3500/fixtures/errors.html</a></li>
<li><a data-cy="screenshots-link" href="http://www.foobar.com:3500/fixtures/screenshots.html">http://www.foobar.com:3500/fixtures/screenshots.html</a></li>
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html">http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html</a></li>
<li><a data-cy="xhr-fetch-requests-onload" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html?fireOnload=true">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html onLoad</a></li>
<li><a data-cy="xhr-fetch-requests" href="http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html">http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html</a></li>
<li><a data-cy="integrity-link" href="http://www.foobar.com:3500/fixtures/scripts-with-integrity.html">http://www.foobar.com:3500/fixtures/scripts-with-integrity.html</a></li>
<li><a data-cy="cookie-login">Login with Social</a></li>
<li><a data-cy="cookie-login-https">Login with Social (https)</a></li>
@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<h1 data-cy="assertion-header">Making XHR and Fetch Requests behind the scenes!</h1>
<script>
function fireXHRAndFetchRequests() {
xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3500/foo.bar.baz.json");
xhr.responseType = "json";
xhr.send();
fetch("http://localhost:3500/foo.bar.baz.json")
}
fireXHRAndFetchRequests()
</script>
</body>
</html>
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<body>
<h1 data-cy="assertion-header">Making XHR and Fetch Requests behind the scenes if fireOnload is true!</h1>
<button data-cy="trigger-fetch" onclick="triggerFetch('/test-request')"> trigger fetch </button>
<button data-cy="trigger-fetch-with-request-object" onclick="triggerFetchWithRequestObject('/test-request')"> trigger fetch with Request Object</button>
<button data-cy="trigger-fetch-with-url-object" onclick="triggerFetchWithUrlObject('/test-request')" > trigger fetch with URL Object</button>
<button data-cy="trigger-fetch-omit" onclick="triggerFetch('/test-request', 'omit')"> trigger fetch w/ omit credentials </button>
<button data-cy="trigger-fetch-with-request-object-omit" onclick="triggerFetchWithRequestObject('/test-request', 'omit')"> trigger fetch with Request Object w/ omit credentials</button>
<button data-cy="trigger-fetch-with-url-object-omit" onclick="triggerFetchWithUrlObject('/test-request', 'omit')" > trigger fetch with URL Object w/ omit credentials</button>
<button data-cy="trigger-fetch-include" onclick="triggerFetch('/test-request', 'include')"> trigger fetch w/ include credentials </button>
<button data-cy="trigger-fetch-with-request-object-include" onclick="triggerFetchWithRequestObject('/test-request', 'include')"> trigger fetch with Request Object w/ include credentials</button>
<button data-cy="trigger-fetch-with-url-object-include" onclick="triggerFetchWithUrlObject('/test-request', 'include')" > trigger fetch with URL Object w/ include credentials</button>
<button data-cy="trigger-fetch-with-bad-options" onclick="triggerFetch(null)">trigger fetch with bad option</button>
<button data-cy="trigger-fetch-with-preflight" onclick="triggerFailingFetchPreflight('/test-request')">trigger fetch w/ preflight</button>
<button data-cy="trigger-xml-http-request" onclick="triggerXmlHttpRequest('/test-request')">trigger xmlHttpRequest</button>
<button data-cy="trigger-xml-http-request-with-credentials" onclick="triggerXmlHttpRequest('/test-request', true)">trigger xmlHttpRequest w/ credentials</button>
<button data-cy="trigger-xml-http-request-with-bad-options" onclick="triggerXmlHttpRequest(null)">trigger xmlHttpRequest w/ bad options</button>
<button data-cy="trigger-xml-http-request-with-preflight" onclick="triggerFailingXmlHttpRequestPreflight('/test-request')">trigger xmlHttpRequest w/ preflight</button>
<script>
function triggerFetch(requestOrUrlObjOrString, credentials){
let fetchReq
if(credentials){
fetchReq = fetch(requestOrUrlObjOrString, {
credentials
})
} else {
fetchReq = fetch(requestOrUrlObjOrString)
}
return fetchReq.then(function(response) {
// throw errors in our application to test when fetch fails
if (!response.ok) {
throw Error(response.status);
}
return response;
})
}
function triggerFetchWithRequestObject(urlString, credentials){
let req = new Request(urlString)
if(credentials){
// credentials must either match options passed into fetch or must exist on the Request object itself
req = new Request(urlString, {
credentials
})
}
return triggerFetch(req)
}
function triggerFetchWithUrlObject(urlString, credentials){
return triggerFetch(new URL(urlString, window.location.origin), credentials)
}
function triggerFailingFetchPreflight(relativeUrl){
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()
return fetch(url, {
headers: {
'foo': 'bar'
},
credentials: 'include'
}).catch(() => {
throw new Error('CORS ERROR');
}).then(() => {
throw new Error('request succeeded when it shouldn\'t have')
})
}
function triggerXmlHttpRequest(relativeUrl, withCredentials = false){
const url = new URL(relativeUrl, window.location.origin)
xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.withCredentials = withCredentials
xhr.send();
}
function triggerFailingXmlHttpRequestPreflight(relativeUrl){
// might need a cross origin req here
let url = new URL(relativeUrl, 'http://app.foobar.com:3500').toString()
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
// adding headers to trigger a preflight request. @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
xhr.setRequestHeader('foo', 'bar')
// since the plugin server sets the Access-Control-Allow-Origin to * (wildcard), this request will fail as a CORS errors. The
xhr.withCredentials = true
xhr.onerror = function () {
throw new Error('CORS ERROR')
}
xhr.send()
}
function fireXHRAndFetchRequests() {
if(window.location.search.includes('fireOnload=true')){
xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3500/foo.bar.baz.json");
xhr.responseType = "json";
xhr.send();
fetch("http://localhost:3500/foo.bar.baz.json")
}
}
fireXHRAndFetchRequests()
</script>
</body>
</html>
@@ -21,6 +21,8 @@ import { handleTestEvents } from './events/test'
import { handleMiscEvents } from './events/misc'
import { handleUnsupportedAPIs } from './unsupported_apis'
import { patchFormElementSubmit } from './patches/submit'
import { patchFetch } from './patches/fetch'
import { patchXmlHttpRequest } from './patches/xmlHttpRequest'
import $Mocha from '../cypress/mocha'
import * as cors from '@packages/network/lib/cors'
@@ -173,6 +175,15 @@ const attachToWindow = (autWindow: Window) => {
cy.overrides.wrapNativeMethods(autWindow)
// place after override incase fetch is polyfilled in the AUT injection
// this can be in the beforeLoad code as we only want to patch fetch/xmlHttpRequest
// when the cy.origin block is active to track credential use
patchFetch(Cypress, autWindow)
patchXmlHttpRequest(Cypress, autWindow)
// also patch it in the spec bridge as well
patchFetch(Cypress, window)
patchXmlHttpRequest(Cypress, window)
// TODO: DRY this up with the mostly-the-same code in src/cypress/cy.js
// https://github.com/cypress-io/cypress/issues/20972
bindToListeners(autWindow, {
@@ -0,0 +1,51 @@
import { captureFullRequestUrl } from './utils'
export const patchFetch = (Cypress: Cypress.Cypress, window) => {
// if fetch is available in the browser, or is polyfilled by whatwg fetch
// intercept method calls and add cypress headers to determine cookie applications in the proxy
// for simulated top. @see https://github.github.io/fetch/ for default options
if (!Cypress.config('experimentalSessionAndOrigin') || !window.fetch) {
return
}
const originalFetch = window.fetch
window.fetch = function (...args) {
try {
let url: string | undefined = undefined
let credentials: string | undefined = undefined
const resource = args[0]
// @see https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters for fetch resource options. We will only support Request, URL, and strings
if (resource instanceof window.Request) {
({ url, credentials } = resource)
} else if (resource instanceof window.URL) {
// should be a no-op for URL
url = resource.toString()
;({ credentials } = args[1] || {})
} else if (Cypress._.isString(resource)) {
url = captureFullRequestUrl(resource, window)
;({ credentials } = args[1] || {})
}
credentials = credentials || 'same-origin'
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the resource type in the proxy
if (url) {
// @ts-expect-error
Cypress.backend('request:sent:with:credentials', {
// TODO: might need to go off more information here or at least make collisions less likely
url,
resourceType: 'fetch',
credentialStatus: credentials,
})
}
} finally {
// if our internal logic errors for whatever reason, do NOT block the end user and continue the request
return originalFetch.apply(this, args)
}
}
}
@@ -0,0 +1,17 @@
export const captureFullRequestUrl = (relativeOrAbsoluteUrlString: string, window: Window) => {
// need to pass the window here by reference to generate the correct absolute URL if needed. Spec Bridge does NOT contain sub domain
let url
try {
url = new URL(relativeOrAbsoluteUrlString).toString()
} catch (err1) {
try {
// likely a relative path, construct the full url
url = new URL(relativeOrAbsoluteUrlString, window.location.origin).toString()
} catch (err2) {
return undefined
}
}
return url
}
@@ -0,0 +1,42 @@
import { captureFullRequestUrl } from './utils'
export const patchXmlHttpRequest = (Cypress: Cypress.Cypress, window: Window) => {
// intercept method calls and add cypress headers to determine cookie applications in the proxy
// for simulated top
if (!Cypress.config('experimentalSessionAndOrigin')) {
return
}
const originalXmlHttpRequestOpen = window.XMLHttpRequest.prototype.open
const originalXmlHttpRequestSend = window.XMLHttpRequest.prototype.send
window.XMLHttpRequest.prototype.open = function (...args) {
try {
// since the send method does NOT have access to the arguments passed into open or have the request information,
// we need to store a reference here to what we need in the send method
this._url = captureFullRequestUrl(args[1], window)
} finally {
return originalXmlHttpRequestOpen.apply(this, args as any)
}
}
window.XMLHttpRequest.prototype.send = function (...args) {
try {
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the resource type in the proxy
if (this._url) {
// @ts-expect-error
Cypress.backend('request:sent:with:credentials', {
// TODO: might need to go off more information here or at least make collisions less likely
url: this._url,
resourceType: 'xhr',
credentialStatus: this.withCredentials,
})
}
} finally {
// if our internal logic errors for whatever reason, do NOT block the end user and continue the request
return originalXmlHttpRequestSend.apply(this, args)
}
}
}
+3
View File
@@ -484,6 +484,9 @@ export class SocketBase {
}
case 'cross:origin:automation:cookies:received':
return this.localBus.emit('cross:origin:automation:cookies:received')
case 'request:sent:with:credentials':
// NOTE: this is currently a no-op until the server logic is implemented
return this.localBus.emit('request:sent:with:credentials', args[0])
default:
throw new Error(`You requested a backend event we cannot handle: ${eventName}`)
}