From 58fc6d830d5c9b417d1adbf0d0b841ca2691084e Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 6 Sep 2022 12:08:29 -0400 Subject: [PATCH 1/5] chore: modify xhr-fetch-requests to handle onload and prep for use in patches tests --- .../cypress/e2e/e2e/origin/snapshots.cy.ts | 5 +++-- .../cypress/fixtures/xhr-fetch-onload.html | 18 ----------------- .../cypress/fixtures/xhr-fetch-requests.html | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 20 deletions(-) delete mode 100644 packages/driver/cypress/fixtures/xhr-fetch-onload.html create mode 100644 packages/driver/cypress/fixtures/xhr-fetch-requests.html diff --git a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts index 2e9b703f1b33..a9dd06757337 100644 --- a/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/snapshots.cy.ts @@ -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!') }) }) }) diff --git a/packages/driver/cypress/fixtures/xhr-fetch-onload.html b/packages/driver/cypress/fixtures/xhr-fetch-onload.html deleted file mode 100644 index 65fa87ea0cc4..000000000000 --- a/packages/driver/cypress/fixtures/xhr-fetch-onload.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -

Making XHR and Fetch Requests behind the scenes!

- - - \ No newline at end of file diff --git a/packages/driver/cypress/fixtures/xhr-fetch-requests.html b/packages/driver/cypress/fixtures/xhr-fetch-requests.html new file mode 100644 index 000000000000..3e3d0ee603f6 --- /dev/null +++ b/packages/driver/cypress/fixtures/xhr-fetch-requests.html @@ -0,0 +1,20 @@ + + + +

Making XHR and Fetch Requests behind the scenes if fireOnload is true!

+ + + \ No newline at end of file From 1e795658c89c38a249d55554a25dad4d76a93d71 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 6 Sep 2022 15:28:55 -0400 Subject: [PATCH 2/5] feat: add patches for fetch and xmlhttprequest --- .../cypress/e2e/e2e/origin/patches.cy.ts | 544 +++++++++++++++++- .../cypress/fixtures/primary-origin.html | 3 +- .../cypress/fixtures/xhr-fetch-requests.html | 89 +++ packages/driver/src/cross-origin/cypress.ts | 10 + .../patches/fetchAndXMLHttpRequest.ts | 102 ++++ packages/server/lib/socket-base.ts | 3 + 6 files changed, 745 insertions(+), 6 deletions(-) create mode 100644 packages/driver/src/cross-origin/patches/fetchAndXMLHttpRequest.ts diff --git a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts index 5111fcfdaeb7..d87511a5fedd 100644 --- a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts @@ -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((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((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((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((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((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') + }) + }) + }) }) diff --git a/packages/driver/cypress/fixtures/primary-origin.html b/packages/driver/cypress/fixtures/primary-origin.html index a4046cde4b67..7d98511e9a10 100644 --- a/packages/driver/cypress/fixtures/primary-origin.html +++ b/packages/driver/cypress/fixtures/primary-origin.html @@ -13,7 +13,8 @@
  • http://www.foobar.com:3500/fixtures/files-form.html
  • http://www.foobar.com:3500/fixtures/errors.html
  • http://www.foobar.com:3500/fixtures/screenshots.html
  • -
  • http://www.foobar.com:3500/fixtures/xhr-fetch-onload.html
  • +
  • http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html onLoad
  • +
  • http://www.foobar.com:3500/fixtures/xhr-fetch-requests.html
  • http://www.foobar.com:3500/fixtures/scripts-with-integrity.html
  • Login with Social
  • Login with Social (https)
  • diff --git a/packages/driver/cypress/fixtures/xhr-fetch-requests.html b/packages/driver/cypress/fixtures/xhr-fetch-requests.html index 3e3d0ee603f6..0692419a74bd 100644 --- a/packages/driver/cypress/fixtures/xhr-fetch-requests.html +++ b/packages/driver/cypress/fixtures/xhr-fetch-requests.html @@ -2,7 +2,96 @@

    Making XHR and Fetch Requests behind the scenes if fireOnload is true!

    + + + + + + + + + + + + + + +