Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improve document.cookie patch #23643

Merged
merged 44 commits into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
075def8
patches/cookies.js -> patches/cookies.ts
chrisbreiding Aug 24, 2022
3d1976f
wip - optimistically update document.cookie
chrisbreiding Aug 29, 2022
6e3e1b4
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Aug 31, 2022
38b8bed
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Sep 1, 2022
846679b
revert firefox automation changes
chrisbreiding Sep 7, 2022
b23cc8d
fix: optimistically update document.cookie value when possible
chrisbreiding Sep 7, 2022
cc625db
convert back to js
chrisbreiding Sep 7, 2022
2227311
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Sep 7, 2022
d106c4a
fix test
chrisbreiding Sep 7, 2022
39e85d3
use default value
chrisbreiding Sep 7, 2022
c2d18ee
refactor how state is used/reset
chrisbreiding Sep 7, 2022
97355ac
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Sep 7, 2022
5f0b0ea
remove app build script
chrisbreiding Sep 7, 2022
483e914
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Sep 8, 2022
69fb577
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Oct 3, 2022
2ca2a46
wip
chrisbreiding Oct 5, 2022
ed4203d
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Oct 5, 2022
5d8bb52
wip
chrisbreiding Oct 6, 2022
517d430
wire up cy commands, get cookie login spec passing
chrisbreiding Oct 7, 2022
e728aed
convert cookies patch to typescript
chrisbreiding Oct 7, 2022
dc231b0
improve injection logic
chrisbreiding Oct 13, 2022
512a268
use cookie jar to store document.cookie cookies
chrisbreiding Oct 13, 2022
d8a4186
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Oct 13, 2022
8e3227e
remove polling for automation cookies
chrisbreiding Oct 14, 2022
b90c040
remove need for injection globals
chrisbreiding Oct 14, 2022
02b7ac2
add test for spec bridge existing before page load
chrisbreiding Oct 14, 2022
0300279
cleanup
chrisbreiding Oct 14, 2022
54255fc
set document.cookie cookies in server-side jar as well as automation
chrisbreiding Oct 17, 2022
7ddbc2b
only send one event to set automation cookies
chrisbreiding Oct 17, 2022
f2a1d0e
fix unit tests
chrisbreiding Oct 17, 2022
dd631eb
update injection to match previous whitespace
chrisbreiding Oct 17, 2022
e951c1f
put back listener
chrisbreiding Oct 17, 2022
5cb6c15
remove comments
chrisbreiding Oct 17, 2022
2102ad7
fix typescript issue
chrisbreiding Oct 17, 2022
f9bdea5
fix unit tests
chrisbreiding Oct 18, 2022
23a31de
fix xhr cookies
chrisbreiding Oct 18, 2022
5b7822e
improve simulated cookies logic
chrisbreiding Oct 18, 2022
12277d6
removed obsolete code
chrisbreiding Oct 18, 2022
f1441f0
upsense before:stability:release
chrisbreiding Oct 18, 2022
8c9d60f
refactor cross:origin:cookies handling
chrisbreiding Oct 18, 2022
7b91ba5
use import type
chrisbreiding Oct 18, 2022
1c91af7
update comments
chrisbreiding Oct 18, 2022
1dc801f
remove no-longer-used event listener
chrisbreiding Oct 18, 2022
bb2e37e
Merge branch 'develop' into issue-23531-document-cookie-timing
chrisbreiding Oct 18, 2022
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
2 changes: 1 addition & 1 deletion packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const driverToSocketEvents = 'backend:request automation:request mocha recorder:
const driverTestEvents = 'test:before:run:async test:after:run'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ')
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:automation:cookies'.split(' ')
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies'.split(' ')
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')

/**
Expand Down
44 changes: 31 additions & 13 deletions packages/driver/cypress/e2e/e2e/origin/cookie_login.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,13 @@ describe('cy.origin - cookie login', () => {
const expires = (new Date()).toUTCString()

cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="localhostCookieProps"]').type(`Expires=${expires}`)
cy.get('[data-cy="cookieProps"]').type(`Expires=${expires}`)
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', () => {
cy.clearCookie('user')
cy.origin('http://idp.com:3501', () => {
cy.wait(1000) // give cookie time to expire
cy.reload()
cy.document().its('cookie').should('not.include', 'user=')
})
})
Expand Down Expand Up @@ -499,15 +500,18 @@ describe('cy.origin - cookie login', () => {
cy.getCookie('user').should('be.null')
})

it('past max-age -> not accessible via document.cookie', () => {
// expiring cookies set by automation don't seem to get unset appropriately
// in Firefox. this issue doesn't seem to be specific to cross-origin tests,
// as it happens even using cy.setCookie()
it('past max-age -> not accessible via document.cookie', { browser: '!firefox' }, () => {
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
cy.origin('http://foobar.com:3500', { args: { username } }, ({ username }) => {
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="localhostCookieProps"]').type('Max-Age=1')
cy.get('[data-cy="cookieProps"]').type('Max-Age=1')
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', () => {
cy.origin('http://idp.com:3501', () => {
cy.wait(1000) // give cookie time to expire
cy.reload()
cy.document().its('cookie').should('not.include', 'user=')
Expand Down Expand Up @@ -661,14 +665,15 @@ describe('cy.origin - cookie login', () => {
})

it('gets cookie set by http request', () => {
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
cy.get('[data-cy="cookie-login-land-on-document-cookie"]').click()
cy.origin('http://foobar.com:3500', { args: { username } }, ({ username }) => {
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', { args: { username } }, ({ username }) => {
cy.document().its('cookie').should('include', `user=${username}`)
cy.origin('http://idp.com:3501', { args: { username } }, ({ username }) => {
cy.get('[data-cy="document-cookie"]').invoke('text')
.should('include', `user=${username}`)
})
})

Expand Down Expand Up @@ -738,7 +743,7 @@ describe('cy.origin - cookie login', () => {
doc.cookie = 'key2=value2'
})

cy.document().its('cookie').should('equal', 'key2=value2; key1=value1')
cy.document().its('cookie').should('equal', 'key1=value1; key2=value2')
})
})

Expand All @@ -753,14 +758,27 @@ describe('cy.origin - cookie login', () => {
})
})

it('returns cookie set by cy.setCookie()', () => {
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
cy.origin('http://foobar.com:3500', { args: { username } }, ({ username }) => {
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3501', () => {
cy.setCookie('foo', 'bar')
cy.document().its('cookie').should('include', 'foo=bar')
})
})

it('no longer returns cookie after cy.clearCookie()', () => {
cy.get('[data-cy="cookie-login-land-on-idp"]').click()
cy.origin('http://foobar.com:3500', { args: { username } }, ({ username }) => {
cy.get('[data-cy="username"]').type(username)
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', () => {
cy.origin('http://idp.com:3501', () => {
cy.clearCookie('user')
cy.document().its('cookie').should('equal', '')
})
Expand All @@ -773,7 +791,7 @@ describe('cy.origin - cookie login', () => {
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', () => {
cy.origin('http://idp.com:3501', () => {
cy.clearCookies()
cy.document().its('cookie').should('equal', '')
})
Expand All @@ -786,7 +804,7 @@ describe('cy.origin - cookie login', () => {
cy.get('[data-cy="login"]').click()
})

cy.origin('http://idp.com:3500', { args: { username } }, ({ username }) => {
cy.origin('http://idp.com:3501', { args: { username } }, ({ username }) => {
cy.document().then((doc) => {
doc.cookie = 'key=value'
})
Expand Down
15 changes: 15 additions & 0 deletions packages/driver/cypress/fixtures/document-cookie.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>document.cookie</title>
</head>
<body>
<p>
<strong>document.cookie:</strong>
<span data-cy="document-cookie"></span>
</p>
<script>
document.querySelector('[data-cy="document-cookie"]').textContent = document.cookie;
</script>
</body>
</html>
8 changes: 5 additions & 3 deletions packages/driver/cypress/fixtures/primary-origin.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
<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="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-http" href="http://www.foobar.com:3500/fixtures/secondary-origin.html">Visit foobar.com over http</a></li>
<li><a data-cy="cookie-https" href="https://www.foobar.com:3502/fixtures/secondary-origin.html">Visit foobar.com over https</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>
<li><a data-cy="cookie-login-subdomain">Login with Social (subdomain)</a></li>
<li><a data-cy="cookie-login-alias">Login with Social (aliased localhost)</a></li>
<li><a data-cy="cookie-login-override">Login with Social (cookie override)</a></li>
<li><a data-cy="cookie-login-land-on-idp">Login with Social (lands on idp)</a></li>
<li><a data-cy="cookie-http" href="http://www.foobar.com:3500/fixtures/secondary-origin.html">Visit foobar.com over http</a></li>
<li><a data-cy="cookie-https" href="https://www.foobar.com:3502/fixtures/secondary-origin.html">Visit foobar.com over https</a></li>
<li><a data-cy="cookie-login-land-on-document-cookie">Login with Social (lands on document.cookie)</a></li>
</ul>
<script>
function setHref (dataCy, redirect2, primaryQueryAddition = '') {
Expand All @@ -43,7 +44,8 @@
setHref('cookie-login-subdomain', 'http://localhost:3500/login&subdomain=true')
setHref('cookie-login-alias', 'http://localhost:3500/login&alias=true')
setHref('cookie-login-override', 'https://localhost:3502/login', '&override=true')
setHref('cookie-login-land-on-idp', 'http://www.idp.com:3500/welcome')
setHref('cookie-login-land-on-idp', 'http://www.idp.com:3501/welcome')
setHref('cookie-login-land-on-document-cookie', 'http://www.idp.com:3501/fixtures/document-cookie.html')
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions packages/driver/src/cross-origin/communicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ export class PrimaryOriginCommunicator extends EventEmitter {
data: preprocessedData,
}, '*')
}

isConnectedToSpecBridge (originPolicy: string) {
return !!this.crossOriginDriverWindows[originPolicy]
}
}

/**
Expand Down
5 changes: 2 additions & 3 deletions packages/driver/src/cross-origin/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { handleScreenshots } from './events/screenshots'
import { handleTestEvents } from './events/test'
import { handleMiscEvents } from './events/misc'
import { handleUnsupportedAPIs } from './unsupported_apis'
import { patchDocumentCookie } from './patches/cookies'
import { handleDocumentCookie } from './patches/cookies'
import { patchFormElementSubmit } from './patches/submit'
import { patchElementIntegrity } from './patches/setAttribute'
import $Mocha from '../cypress/mocha'
Expand Down Expand Up @@ -110,6 +110,7 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => {
handleScreenshots(Cypress)
handleTestEvents(Cypress)
handleUnsupportedAPIs(Cypress, cy)
handleDocumentCookie(Cypress)

cy.onBeforeAppWindowLoad = onBeforeAppWindowLoad(Cypress, cy)
}
Expand All @@ -126,8 +127,6 @@ const onBeforeAppWindowLoad = (Cypress: Cypress.Cypress, cy: $Cy) => (autWindow:
patchElementIntegrity(autWindow)
}

patchDocumentCookie(Cypress, autWindow)

// This is typically called by the cy function `urlNavigationEvent` but it is private. For the primary origin this is called in 'onBeforeAppWindowLoad'.
Cypress.action('app:navigation:changed', 'page navigation event (\'before:load\')')

Expand Down
139 changes: 112 additions & 27 deletions packages/driver/src/cross-origin/patches/cookies.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
const parseCookieString = (cookieString) => {
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
if (!cookieString || !cookieString.trim().length) return []

return cookieString.split(';').map((cookieString) => {
const [name, value] = cookieString.split('=')

return {
name: name.trim(),
value: value.trim(),
}
})
}

const stringifyCookies = (cookies) => {
return cookies
.map((cookie) => `${cookie.name}=${cookie.value}`)
.join('; ')
}

const mergeCookies = (documentCookieValue, newCookies = []) => {
const existingCookies = parseCookieString(documentCookieValue)
const newCookieNameIndex = newCookies.reduce((nameIndex, cookie) => {
nameIndex[cookie.name] = cookie.value

return nameIndex
}, {})
const filteredExistingCookies = existingCookies.filter((cookie) => {
return !newCookieNameIndex[cookie.name]
})

return stringifyCookies(filteredExistingCookies.concat(newCookies))
}

const clearCookie = (documentCookieValue, name) => {
const cookies = parseCookieString(documentCookieValue)

const filteredCookies = cookies.filter((cookie) => {
return cookie.name !== name
})

return stringifyCookies(filteredCookies)
}

// document.cookie monkey-patching
// -------------------------------
// We monkey-patch document.cookie when in a cross-origin injection, because
Expand All @@ -13,43 +56,59 @@
// - On an interval, get the browser's cookies for the given domain, so that
// updates to the cookie jar (via http requests, cy.setCookie, etc) are
// reflected in the document.cookie value.
export const patchDocumentCookie = (Cypress, window) => {
const setAutomationCookie = (toughCookie) => {
const { superDomain } = Cypress.Location.create(window.location.href)
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(toughCookie, superDomain)
export const handleDocumentCookie = (Cypress) => {
// if cookies were sent before the spec bridge was created, they'll come
// through as state('crossOriginCookies'), so use them then reset them
const cookies = Cypress.state('crossOriginCookies')
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
let documentCookieValue = mergeCookies(document.cookie, cookies)

Cypress.state('crossOriginCookies', [])

Cypress.automation('set:cookie', automationCookie)
.catch(() => {
const patch = (window) => {
// Cookies added to the server-side cookie jar are optimistically
// added here so that if a cross-origin request sets cookies, they're
// available via document.cookie synchronously on page load
const setAutomationCookie = (toughCookie) => {
const { superDomain } = Cypress.Location.create(window.location.href)
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
const automationCookie = Cypress.Cookies.toughCookieToAutomationCookie(toughCookie, superDomain)

Cypress.automation('set:cookie', automationCookie)
.catch(() => {
// unlikely there will be errors, but ignore them in any case, since
// they're not user-actionable
})
}
})
}

let documentCookieValue = ''
Object.defineProperty(window.document, 'cookie', {
get () {
return documentCookieValue
},

Object.defineProperty(window.document, 'cookie', {
get () {
return documentCookieValue
},
set (newValue) {
const cookie = Cypress.Cookies.parse(newValue)

set (newValue) {
const cookie = Cypress.Cookies.parse(newValue)
// If cookie is undefined, it was invalid and couldn't be parsed
if (!cookie) return documentCookieValue

// If cookie is undefined, it was invalid and couldn't be parsed
if (!cookie) return documentCookieValue
documentCookieValue = mergeCookies(documentCookieValue, [{
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
name: cookie.key,
value: cookie.value,
}])

const cookieString = `${cookie.key}=${cookie.value}`
setAutomationCookie(cookie)

// New cookies get prepended to existing cookies
documentCookieValue = documentCookieValue.length
? `${cookieString}; ${documentCookieValue}`
: cookieString
return documentCookieValue
},
})
}

setAutomationCookie(cookie)
const updateCookies = (cookies) => {
documentCookieValue = mergeCookies(documentCookieValue, cookies)
}

return documentCookieValue
},
})
const reset = () => {
documentCookieValue = ''
}

// The interval value is arbitrary; it shouldn't be too often, but needs to
// be fairly frequent so that the local value is kept as up-to-date as
Expand All @@ -62,7 +121,7 @@ export const patchDocumentCookie = (Cypress, window) => {

chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
try {
const cookies = await Cypress.automation('get:cookies', { domain })
const cookiesString = (cookies || []).map((c) => `${c.name}=${c.value}`).join('; ')
const cookiesString = stringifyCookies(cookies || [])

documentCookieValue = cookiesString
} catch (err) {
Expand All @@ -77,4 +136,30 @@ export const patchDocumentCookie = (Cypress, window) => {
}

window.addEventListener('unload', onUnload)

Cypress.on('window:before:load', patch)
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved

Cypress.specBridgeCommunicator.on('cross:origin:cookies', (cookies) => {
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
updateCookies(cookies)

Cypress.state('crossOriginCookies', cookies)

Cypress.specBridgeCommunicator.toPrimary('cross:origin:cookies:received')
})

Cypress.on('test:before:run', () => {
reset()
})

Cypress.on('set:cookie', (cookie) => {
updateCookies([cookie])
})

Cypress.on('clear:cookie', (name) => {
documentCookieValue = clearCookie(documentCookieValue, name)
})

Cypress.on('clear:cookies', () => {
reset()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we still need to do this if we store the reference to get:cookies, or just clear the reference out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to optimistically clear out the cookies when a user calls cy.clearCookies() so there's no delay/race condition waiting to sync with the automation cookies. There's actually no longer any polling to sync with automation cookies. It just caused issues and now all cases of cookies changing in automation are taken care of in a more deterministic fashion

})
chrisbreiding marked this conversation as resolved.
Show resolved Hide resolved
}
Loading