feat: experimental skip domain injection (#25307)

* feat: set up experimentalUseDefaultDocumentDomain to disallow document.domain overwritting

* use default domain around experimentalUseDefaultDocumentDomain in main iframe and spec bridge iframes. Also adapt CORS policy to use same-origin if experimental flag is set

* run ci

* fix: add insertion of experimental flag where is was needed/missing

* chore: add system test to exercise experimental flag for expected behavior

* fix: fix issues with template updates to conform to squirrelly v7

* fix: update config tests to include new experimental flag

* run ci

* fix: trailing whitespace [run ci]

* chore: update snapshot

* run ci

* fix: update proxy unit tests to account for experimentalUseDefaultDocumentDomain

* run ci

* fix: Allow component tests with special characters in filepath (#25299)

feat: cut over experimental flag to take list of known problematic domains via string/glob pattern

run ci

chore: update system test and fix broken config

* fix: fix server unit and integration tests. integration tests should no longer use google to test against injection as we do not inject document.domain on google domains

* run ci

* run ci

* fix: server integration tests where google documents are expected to receive document.domain injection. Kept test same by changing URL

* run ci

* fix: update server test with mssing unupdated assertions

* run ci

* fix: turn off experimental flag by default while recommending sane defaults to users to configure

* run ci

* chore: fix typings [run ci]

* run ci

* chore: make experiment an e2e option only

* run ci

* chore: address comments in code review

* chore: rename experimentalUseDefaultDocumentDomain to experimentalSkipDomainInjection

* fix regression in shouldInjectionDocumentDomain utility function and add unit tests

* run ci

* chore: rename documentSuperDomainIfExists to superDomain [run ci]

* chore: address comments from code review

* chore: just pass opts through to policyForDomain

* run ci

Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com>
This commit is contained in:
Bill Glesias
2023-01-09 10:00:05 -05:00
committed by GitHub
parent ca53ee0b1d
commit d470f59ea2
35 changed files with 533 additions and 111 deletions
+10
View File
@@ -3019,6 +3019,16 @@ declare namespace Cypress {
* @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode
*/
experimentalModifyObstructiveThirdPartyCode: boolean
/**
* Disables setting document.domain to the applications super domain on injection.
* This experiment is to be used for sites that do not work with setting document.domain
* due to cross-origin issues. Enabling this option no longer allows for default subdomain
* navigations, and will require the use of cy.origin(). This option takes an array of
* strings/string globs.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/domain
* @default null
*/
experimentalSkipDomainInjection: string[] | null
/**
* Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm.
* @default false
@@ -38,6 +38,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
'experimentalModifyObstructiveThirdPartyCode': false,
'experimentalSkipDomainInjection': null,
'experimentalOriginDependencies': false,
'experimentalSourceRewriting': false,
'experimentalSingleTabRunMode': false,
@@ -123,6 +124,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
'experimentalInteractiveRunEvents': false,
'experimentalRunAllSpecs': false,
'experimentalModifyObstructiveThirdPartyCode': false,
'experimentalSkipDomainInjection': null,
'experimentalOriginDependencies': false,
'experimentalSourceRewriting': false,
'experimentalSingleTabRunMode': false,
@@ -204,6 +206,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
'experimentalInteractiveRunEvents',
'experimentalRunAllSpecs',
'experimentalModifyObstructiveThirdPartyCode',
'experimentalSkipDomainInjection',
'experimentalOriginDependencies',
'experimentalSourceRewriting',
'experimentalSingleTabRunMode',
+17
View File
@@ -215,6 +215,12 @@ const driverConfigOptions: Array<DriverConfigOption> = [
validation: validate.isBoolean,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
name: 'experimentalSkipDomainInjection',
defaultValue: null,
validation: validate.isNullOrArrayOfStrings,
isExperimental: true,
requireRestartOnChange: 'server',
}, {
name: 'experimentalOriginDependencies',
defaultValue: false,
@@ -679,6 +685,12 @@ export const breakingRootOptions: Array<BreakingOption> = [
isWarning: false,
testingTypes: ['e2e'],
},
{
name: 'experimentalSkipDomainInjection',
errorKey: 'EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY',
isWarning: false,
testingTypes: ['e2e'],
},
]
export const testingTypeBreakingOptions: { e2e: Array<BreakingOption>, component: Array<BreakingOption> } = {
@@ -720,5 +732,10 @@ export const testingTypeBreakingOptions: { e2e: Array<BreakingOption>, component
errorKey: 'EXPERIMENTAL_ORIGIN_DEPENDENCIES_E2E_ONLY',
isWarning: false,
},
{
name: 'experimentalSkipDomainInjection',
errorKey: 'EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY',
isWarning: false,
},
],
}
+8
View File
@@ -328,3 +328,11 @@ export function isStringOrArrayOfStrings (key: string, value: any): ErrResult |
return errMsg(key, value, 'a string or an array of strings')
}
export function isNullOrArrayOfStrings (key: string, value: any): ErrResult | true {
if (_.isNull(value) || isArrayOfStrings(value)) {
return true
}
return errMsg(key, value, 'an array of strings or null')
}
@@ -1052,6 +1052,7 @@ describe('config/src/project/utils', () => {
env: {},
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },
@@ -1147,6 +1148,7 @@ describe('config/src/project/utils', () => {
downloadsFolder: { value: 'cypress/downloads', from: 'default' },
execTimeout: { value: 60000, from: 'default' },
experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' },
experimentalSkipDomainInjection: { value: null, from: 'default' },
experimentalFetchPolyfill: { value: false, from: 'default' },
experimentalInteractiveRunEvents: { value: false, from: 'default' },
experimentalOriginDependencies: { value: false, from: 'default' },
@@ -85,10 +85,14 @@ export class Validator {
}
// Users would be better off not using cy.origin if the origin is part of the same super domain.
if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref)) {
if (cors.urlMatchesPolicyBasedOnDomain(originLocation.href, specHref, {
skipDomainInjectionForDomains: Cypress.config('experimentalSkipDomainInjection'),
})) {
// this._isSameSuperDomainOriginWithExceptions({ originLocation, specLocation })) {
const policy = cors.policyForDomain(originLocation.href)
const policy = cors.policyForDomain(originLocation.href, {
skipDomainInjectionForDomains: Cypress.config('experimentalSkipDomainInjection'),
})
$errUtils.throwErrByPath('origin.invalid_url_argument_same_origin', {
onFail: this.log,
+6 -1
View File
@@ -44,6 +44,7 @@ import { PrimaryOriginCommunicator, SpecBridgeCommunicator } from './cross-origi
import { setupAutEventHandlers } from './cypress/aut_event_handlers'
import type { CachedTestState } from '@packages/types'
import * as cors from '@packages/network/lib/cors'
const debug = debugFn('cypress:driver:cypress')
@@ -182,7 +183,11 @@ class $Cypress {
// set domainName but allow us to turn
// off this feature in testing
if (domainName && config.testingType === 'e2e') {
const shouldInjectDocumentDomain = cors.shouldInjectDocumentDomain(window.location.origin, {
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
})
if (domainName && config.testingType === 'e2e' && shouldInjectDocumentDomain) {
document.domain = domainName
}
+1
View File
@@ -255,6 +255,7 @@ const commandCanCommunicateWithAUT = (cy: $Cy, err?): boolean => {
const crossOriginCommandError = $errUtils.errByPath('miscellaneous.cross_origin_command', {
commandOrigin: window.location.origin,
autOrigin: cy.state('autLocation').origin,
isSkipDomainInjectionEnabled: !!Cypress.config('experimentalSkipDomainInjection'),
})
if (err) {
@@ -912,13 +912,15 @@ export default {
return `Timed out retrying after ${ms}ms: `
},
test_stopped: 'Cypress test was stopped while running this command.',
cross_origin_command ({ commandOrigin, autOrigin }) {
cross_origin_command ({ commandOrigin, autOrigin, isSkipDomainInjectionEnabled }) {
return {
message: stripIndent`\
The command was expected to run against origin \`${commandOrigin}\` but the application is at origin \`${autOrigin}\`.
This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.
${isSkipDomainInjectionEnabled ? `
If \`experimentalSkipDomainInjection\` is enabled for this domain, a ${cmd('origin')} command is required.
` : ''}
Using ${cmd('origin')} to wrap the commands run on \`${autOrigin}\` will likely fix this issue.
\`cy.origin('${autOrigin}', () => {\`
+14
View File
@@ -1185,6 +1185,20 @@ export const AllCypressErrors = {
${fmt.code(code)}`
},
EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY: () => {
const code = errPartial`
{
e2e: {
experimentalSkipDomainInjection: ['*.salesforce.com', '*.force.com', '*.google.com', 'google.com']
},
}`
return errTemplate`\
The ${fmt.highlight(`experimentalSkipDomainInjection`)} experiment is currently only supported for End to End Testing and must be configured as an e2e testing type property: ${fmt.highlightSecondary(`e2e.experimentalSkipDomainInjection`)}.
The suggested values are only a recommendation.
${fmt.code(code)}`
},
FIREFOX_GC_INTERVAL_REMOVED: () => {
return errTemplate`\
The ${fmt.highlight(`firefoxGcInterval`)} configuration option was removed in ${fmt.cypressVersion(`8.0.0`)}. It was introduced to work around a bug in Firefox 79 and below.
@@ -1268,5 +1268,11 @@ describe('visual error templates', () => {
default: [],
}
},
EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY: () => {
return {
default: [],
}
},
})
})
+1
View File
@@ -800,6 +800,7 @@ enum ErrorTypeEnum {
EXPERIMENTAL_SINGLE_TAB_RUN_MODE
EXPERIMENTAL_STUDIO_E2E_ONLY
EXPERIMENTAL_STUDIO_REMOVED
EXPERIMENTAL_USE_DEFAULT_DOCUMENT_DOMAIN_E2E_ONLY
EXTENSION_NOT_LOADED
FIREFOX_COULD_NOT_CONNECT
FIREFOX_GC_INTERVAL_REMOVED
+58 -7
View File
@@ -1,4 +1,5 @@
import _ from 'lodash'
import minimatch from 'minimatch'
import * as uri from './uri'
import debugModule from 'debug'
import _parseDomain from '@cypress/parse-domain'
@@ -11,6 +12,8 @@ const debug = debugModule('cypress:network:cors')
// match IP addresses or anything following the last .
const customTldsRe = /(^[\d\.]+$|\.[^\.]+$)/
// TODO: if experimentalSkipDomainInjection plans to go GA, we can likely lump this strictSameOriginDomains
// into that config option by default. @see https://github.com/cypress-io/cypress/issues/25317
const strictSameOriginDomains = Object.freeze(['google.com'])
export function getSuperDomain (url) {
@@ -158,15 +161,58 @@ export const urlSameSiteMatch = (frameUrl: string, topUrl: string): boolean => {
})
}
/**
* @param url - the url to check the policy against.
* @param arrayOfStringOrGlobPatterns - an array of url strings or globs to match against
* @returns {boolean} - whether or not a match was found
*/
const doesUrlHostnameMatchGlobArray = (url: string, arrayOfStringOrGlobPatterns: string[]): boolean => {
let { hostname } = uri.parse(url)
return !!arrayOfStringOrGlobPatterns.find((globPattern) => {
return minimatch(hostname || '', globPattern)
})
}
/**
* Returns the policy that will be used for the specified url.
* @param url - the url to check the policy against.
* @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined.
* @returns a Policy string.
*/
export const policyForDomain = (url: string): Policy => {
export const policyForDomain = (url: string, opts?: {
skipDomainInjectionForDomains: string[] | null | undefined
}): Policy => {
const obj = parseUrlIntoHostProtocolDomainTldPort(url)
let shouldUseSameOriginPolicy = strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`)
return strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin'
if (!shouldUseSameOriginPolicy && _.isArray(opts?.skipDomainInjectionForDomains)) {
// if the strict same origins matches came up false, we should check the user provided config value for skipDomainInjectionForDomains, if one exists
shouldUseSameOriginPolicy = doesUrlHostnameMatchGlobArray(url, opts?.skipDomainInjectionForDomains as string[])
}
return shouldUseSameOriginPolicy ?
'same-origin' :
'same-super-domain-origin'
}
/**
* @param url - The url to check for injection
* @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined.
* @returns {boolean} whether or not document.domain should be injected solely based on the url.
*/
export const shouldInjectDocumentDomain = (url: string, opts?: {
skipDomainInjectionForDomains: string[] | null
}) => {
// When determining if we want to injection document domain,
// We need to make sure the experimentalSkipDomainInjection feature flag is off.
// If on, we need to make sure the glob pattern doesn't exist in the array so we cover possible intersections (google).
if (_.isArray(opts?.skipDomainInjectionForDomains)) {
// if we match the glob, we want to return false
return !doesUrlHostnameMatchGlobArray(url, opts?.skipDomainInjectionForDomains as string[])
}
return true
}
/**
@@ -175,11 +221,14 @@ export const policyForDomain = (url: string): Policy => {
* in which case the policy is 'same-origin'
* @param frameUrl - The url you are testing the policy for.
* @param topUrl - The url you are testing the policy in context of.
* @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined.
* @returns boolean, true if matching, false if not.
*/
export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string): boolean => {
export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string, opts?: {
skipDomainInjectionForDomains: string[] | null
}): boolean => {
return urlMatchesPolicy({
policy: policyForDomain(frameUrl),
policy: policyForDomain(frameUrl, opts),
frameUrl,
topUrl,
})
@@ -191,11 +240,13 @@ export const urlMatchesPolicyBasedOnDomain = (frameUrl: string, topUrl: string):
* in which case the policy is 'same-origin'
* @param frameUrl - The url you are testing the policy for.
* @param topProps - The props of the url you are testing the policy in context of.
* @param opts - an options object containing the skipDomainInjectionForDomains config. Default is undefined.
* @returns boolean, true if matching, false if not.
*/
export const urlMatchesPolicyBasedOnDomainProps = (frameUrl: string, topProps: ParsedHostWithProtocolAndHost): boolean => {
const obj = parseUrlIntoHostProtocolDomainTldPort(frameUrl)
const policy = strictSameOriginDomains.includes(`${obj.domain}.${obj.tld}`) ? 'same-origin' : 'same-super-domain-origin'
export const urlMatchesPolicyBasedOnDomainProps = (frameUrl: string, topProps: ParsedHostWithProtocolAndHost, opts?: {
skipDomainInjectionForDomains: string[]
}): boolean => {
const policy = policyForDomain(frameUrl, opts)
return urlMatchesPolicyProps({
policy,
+1
View File
@@ -20,6 +20,7 @@
"debug": "^4.3.2",
"fs-extra": "9.1.0",
"lodash": "^4.17.21",
"minimatch": "3.0.5",
"node-forge": "1.3.0",
"proxy-from-env": "1.0.0"
},
+82
View File
@@ -657,4 +657,86 @@ describe('lib/cors', () => {
expect(cors.getOrigin('http://www.app.herokuapp.com:8080')).to.equal('http://www.app.herokuapp.com:8080')
})
})
context('.policyForDomain', () => {
const recommendedSameOriginPolicyUrlGlobs = ['*.salesforce.com', '*.force.com', '*.google.com', 'google.com']
context('returns "same-origin" for google domains', () => {
it('accounts.google.com', () => {
expect(cors.policyForDomain('https://accounts.google.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
it('www.google.com', () => {
expect(cors.policyForDomain('https://www.google.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
})
context('returns "same-origin" for salesforce domains', () => {
it('https://the-host.develop.lightning.force.com', () => {
expect(cors.policyForDomain('https://the-host.develop.lightning.force.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
it('https://the-host.develop.my.salesforce.com', () => {
expect(cors.policyForDomain('https://the-host.develop.my.salesforce.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
it('https://the-host.develop.file.force.com', () => {
expect(cors.policyForDomain('https://the-host.develop.file.force.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
it('https://the-host.develop.my.salesforce.com', () => {
expect(cors.policyForDomain('https://the-host.develop.my.salesforce.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-origin')
})
})
describe('returns "same-super-domain-origin" for non exception urls', () => {
it('www.cypress.io', () => {
expect(cors.policyForDomain('http://www.cypress.io', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-super-domain-origin')
})
it('docs.cypress.io', () => {
expect(cors.policyForDomain('http://docs.cypress.io', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-super-domain-origin')
})
it('stackoverflow.com', () => {
expect(cors.policyForDomain('https://stackoverflow.com', {
skipDomainInjectionForDomains: recommendedSameOriginPolicyUrlGlobs,
})).to.equal('same-super-domain-origin')
})
})
})
context('.shouldInjectDocumentDomain', () => {
it('returns false when "skipDomainInjectionForDomains" is configured and contains a matching blob pattern ', () => {
expect(cors.shouldInjectDocumentDomain('http://www.cypress.io', {
skipDomainInjectionForDomains: ['*.cypress.io'],
})).to.be.false
})
it('returns true when "skipDomainInjectionForDomains" exists, but doesn\'t contain a matching glob pattern', () => {
expect(cors.shouldInjectDocumentDomain('http://www.cypress.io', {
skipDomainInjectionForDomains: ['*.foobar.com'],
})).to.be.true
})
it('returns true otherwise', () => {
expect(cors.shouldInjectDocumentDomain('http://www.cypress.io')).to.be.true
})
})
})
@@ -162,8 +162,8 @@ const MaybeEndRequestWithBufferedResponse: RequestMiddleware = function () {
if (buffer) {
this.debug('ending request with buffered response')
// NOTE: Only inject fullCrossOrigin here if experimental is on and
// the super domain origins do not match in order to keep parity with cypress application reloads
// NOTE: Only inject fullCrossOrigin here if the super domain origins do not match in order to keep parity with cypress application reloads
this.res.wantsInjection = buffer.urlDoesNotMatchPolicyBasedOnDomain ? 'fullCrossOrigin' : 'full'
return this.onResponse(buffer.response, buffer.stream)
@@ -55,9 +55,11 @@ function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer,
return 'latin1'
}
function reqMatchesPolicyBasedOnDomain (req: CypressIncomingRequest, remoteState) {
function reqMatchesPolicyBasedOnDomain (req: CypressIncomingRequest, remoteState, skipDomainInjectionForDomains) {
if (remoteState.strategy === 'http') {
return cors.urlMatchesPolicyBasedOnDomainProps(req.proxiedUrl, remoteState.props)
return cors.urlMatchesPolicyBasedOnDomainProps(req.proxiedUrl, remoteState.props, {
skipDomainInjectionForDomains,
})
}
if (remoteState.strategy === 'file') {
@@ -250,7 +252,7 @@ const SetInjectionLevel: ResponseMiddleware = function () {
this.debug('determine injection')
const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.current())
const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.current(), this.config.experimentalSkipDomainInjection)
const getInjectionLevel = () => {
if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) {
this.debug('- partial injection (x-cypress-file-server-error)')
@@ -259,7 +261,7 @@ const SetInjectionLevel: ResponseMiddleware = function () {
}
// NOTE: Only inject fullCrossOrigin if the super domain origins do not match in order to keep parity with cypress application reloads
const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.getPrimary())
const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(this.req, this.remoteStates.getPrimary(), this.config.experimentalSkipDomainInjection)
const isAUTFrame = this.req.isAUTFrame
const isHTMLLike = isHTML || isRenderedHTML
@@ -544,6 +546,9 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
isNotJavascript: !resContentTypeIsJavaScript(this.incomingRes),
useAstSourceRewriting: this.config.experimentalSourceRewriting,
modifyObstructiveThirdPartyCode: this.config.experimentalModifyObstructiveThirdPartyCode && !this.remoteStates.isPrimarySuperDomainOrigin(this.req.proxiedUrl),
shouldInjectDocumentDomain: cors.shouldInjectDocumentDomain(this.req.proxiedUrl, {
skipDomainInjectionForDomains: this.config.experimentalSkipDomainInjection,
}),
modifyObstructiveCode: this.config.modifyObstructiveCode,
url: this.req.proxiedUrl,
deferSourceMapRewrite: this.deferSourceMapRewrite,
+29 -6
View File
@@ -2,25 +2,42 @@ import { oneLine } from 'common-tags'
import { getRunnerInjectionContents, getRunnerCrossOriginInjectionContents } from '@packages/resolve-dist'
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
interface InjectionOpts {
shouldInjectDocumentDomain: boolean
}
interface FullCrossOriginOpts {
modifyObstructiveThirdPartyCode: boolean
modifyObstructiveCode: boolean
simulatedCookies: AutomationCookie[]
}
export function partial (domain) {
export function partial (domain, options: InjectionOpts) {
let documentDomainInjection = `document.domain = '${domain}';`
if (!options.shouldInjectDocumentDomain) {
documentDomainInjection = ''
}
// With useDefaultDocumentDomain=true we continue to inject an empty script tag in order to be consistent with our other forms of injection.
// This is also diagnostic in nature is it will allow us to debug easily to make sure injection is still occurring.
return oneLine`
<script type='text/javascript'>
document.domain = '${domain}';
${documentDomainInjection}
</script>
`
}
export function full (domain) {
export function full (domain, options: InjectionOpts) {
return getRunnerInjectionContents().then((contents) => {
let documentDomainInjection = `document.domain = '${domain}';`
if (!options.shouldInjectDocumentDomain) {
documentDomainInjection = ''
}
return oneLine`
<script type='text/javascript'>
document.domain = '${domain}';
${documentDomainInjection}
${contents}
</script>
@@ -28,12 +45,18 @@ export function full (domain) {
})
}
export async function fullCrossOrigin (domain, options: FullCrossOriginOpts) {
export async function fullCrossOrigin (domain, options: InjectionOpts & FullCrossOriginOpts) {
const contents = await getRunnerCrossOriginInjectionContents()
let documentDomainInjection = `document.domain = '${domain}';`
if (!options.shouldInjectDocumentDomain) {
documentDomainInjection = ''
}
return oneLine`
<script type='text/javascript'>
document.domain = '${domain}';
${documentDomainInjection}
(function (cypressConfig) {
${contents}
+9 -2
View File
@@ -18,6 +18,7 @@ export type InjectionOpts = {
wantsInjection: CypressWantsInjection
wantsSecurityRemoved: any
simulatedCookies: AutomationCookie[]
shouldInjectDocumentDomain: boolean
}
const doctypeRe = /<\!doctype.*?>/i
@@ -36,19 +37,25 @@ function getHtmlToInject (opts: InjectionOpts & SecurityOpts) {
modifyObstructiveThirdPartyCode,
modifyObstructiveCode,
simulatedCookies,
shouldInjectDocumentDomain,
} = opts
switch (wantsInjection) {
case 'full':
return inject.full(domainName)
return inject.full(domainName, {
shouldInjectDocumentDomain,
})
case 'fullCrossOrigin':
return inject.fullCrossOrigin(domainName, {
modifyObstructiveThirdPartyCode,
modifyObstructiveCode,
simulatedCookies,
shouldInjectDocumentDomain,
})
case 'partial':
return inject.partial(domainName)
return inject.partial(domainName, {
shouldInjectDocumentDomain,
})
default:
return
}
@@ -1395,6 +1395,7 @@ describe('http/response-middleware', function () {
'isNotJavascript': true,
'modifyObstructiveCode': true,
'modifyObstructiveThirdPartyCode': true,
'shouldInjectDocumentDomain': true,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
@@ -1418,6 +1419,7 @@ describe('http/response-middleware', function () {
'isNotJavascript': true,
'modifyObstructiveCode': true,
'modifyObstructiveThirdPartyCode': false,
'shouldInjectDocumentDomain': true,
'url': 'http://127.0.0.1:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
@@ -1435,6 +1437,7 @@ describe('http/response-middleware', function () {
config: {
modifyObstructiveCode: false,
experimentalModifyObstructiveThirdPartyCode: false,
experimentalSkipDomainInjection: null,
},
simulatedCookies: [],
})
@@ -1448,6 +1451,7 @@ describe('http/response-middleware', function () {
'isNotJavascript': true,
'modifyObstructiveCode': false,
'modifyObstructiveThirdPartyCode': false,
'shouldInjectDocumentDomain': true,
'url': 'http://www.foobar.com:3501/primary-origin.html',
'useAstSourceRewriting': undefined,
'wantsInjection': 'full',
@@ -1485,6 +1489,7 @@ describe('http/response-middleware', function () {
config: {
modifyObstructiveCode: true,
experimentalModifyObstructiveThirdPartyCode: true,
experimentalSkipDomainInjection: null,
},
remoteStates,
debug: (formatter, ...args) => {
+1
View File
@@ -22,6 +22,7 @@ export namespace CyServer {
experimentalSourceRewriting: boolean
modifyObstructiveCode: boolean
experimentalModifyObstructiveThirdPartyCode: boolean
experimentalSkipDomainInjection: string[] | null
/**
* URL to Cypress's runner.
*/
+18 -6
View File
@@ -26,9 +26,15 @@ module.exports = {
debug('all files to send %o', _.map(allFilesToSend, 'relative'))
const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, {
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
}) ?
remoteStates.getPrimary().domainName :
undefined
const iframeOptions = {
superDomain,
title: this.getTitle(test),
domain: remoteStates.getPrimary().domainName,
scripts: JSON.stringify(allFilesToSend),
}
@@ -38,14 +44,20 @@ module.exports = {
})
},
handleCrossOriginIframe (req, res, namespace) {
handleCrossOriginIframe (req, res, config) {
const iframePath = cwd('lib', 'html', 'spec-bridge-iframe.html')
const domain = cors.getSuperDomain(req.proxiedUrl)
const superDomain = cors.shouldInjectDocumentDomain(req.proxiedUrl, {
skipDomainInjectionForDomains: config.experimentalSkipDomainInjection,
}) ?
cors.getSuperDomain(req.proxiedUrl) :
undefined
const origin = cors.getOrigin(req.proxiedUrl)
const iframeOptions = {
domain,
title: `Cypress for ${domain}`,
namespace,
superDomain,
title: `Cypress for ${origin}`,
namespace: config.namespace,
}
debug('cross origin iframe with options %o', iframeOptions)
+2
View File
@@ -54,6 +54,7 @@ const _summaries: StringValues = {
experimentalFetchPolyfill: 'Polyfills `window.fetch` to enable Network spying and stubbing.',
experimentalInteractiveRunEvents: 'Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode.',
experimentalModifyObstructiveThirdPartyCode: 'Applies `modifyObstructiveCode` to third party `.html` and `.js`, removes subresource integrity, and modifies the user agent in Electron.',
experimentalSkipDomainInjection: 'Disables setting document.domain to the document\'s super domain on injection.',
experimentalSourceRewriting: 'Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm.',
experimentalSingleTabRunMode: 'Runs all component specs in a single tab, trading spec isolation for faster run mode execution.',
experimentalStudio: 'Generate and save commands directly to your test suite by interacting with your app as an end user would.',
@@ -76,6 +77,7 @@ const _names: StringValues = {
experimentalFetchPolyfill: 'Fetch Polyfill',
experimentalInteractiveRunEvents: 'Interactive Mode Run Events',
experimentalModifyObstructiveThirdPartyCode: 'Modify Obstructive Third Party Code',
experimentalSkipDomainInjection: 'Use Default document.domain',
experimentalSingleTabRunMode: 'Single Tab Run Mode',
experimentalSourceRewriting: 'Improved Source Rewriting',
experimentalStudio: 'Studio',
+4 -2
View File
@@ -6,8 +6,10 @@
</head>
<body>
<script type="text/javascript">
document.domain = '{{domain}}';
{{if(options.superDomain)}}
document.domain = '{{superDomain}}';
{{/if}}
(function(parent) {
var Cypress = window.Cypress = parent.Cypress;
if (!Cypress) {
@@ -6,7 +6,9 @@
</head>
<body>
<script type="text/javascript">
document.domain = '{{domain}}';
{{if(options.superDomain)}}
document.domain = '{{superDomain}}';
{{/if}}
</script>
<script src="/{{namespace}}/runner/cypress_cross_origin_runner.js"></script>
</body>
+1 -1
View File
@@ -105,7 +105,7 @@ export const createRoutesE2E = ({
// @see https://github.com/cypress-io/cypress/issues/25010
res.setHeader('Origin-Agent-Cluster', '?0')
files.handleCrossOriginIframe(req, res, config.namespace)
files.handleCrossOriginIframe(req, res, config)
})
return routesE2E
+7 -3
View File
@@ -45,6 +45,8 @@ const isResponseHtml = function (contentType, responseBuffer) {
export class ServerE2E extends ServerBase<SocketE2E> {
private _urlResolver: Bluebird<Record<string, any>> | null
// the initialization of this variable is only precautionary as the actual config value is applied when the server is created
private skipDomainInjectionForDomains: string[] | null = null
constructor () {
super()
@@ -58,10 +60,10 @@ export class ServerE2E extends ServerBase<SocketE2E> {
createServer (app, config, onWarning): Bluebird<[number, WarningErr?]> {
return new Bluebird((resolve, reject) => {
const { port, fileServerFolder, socketIoRoute, baseUrl } = config
const { port, fileServerFolder, socketIoRoute, baseUrl, experimentalSkipDomainInjection } = config
this._server = this._createHttpServer(app)
this.skipDomainInjectionForDomains = experimentalSkipDomainInjection
const onError = (err) => {
// if the server bombs before starting
// and the err no is EADDRINUSE
@@ -308,7 +310,9 @@ export class ServerE2E extends ServerBase<SocketE2E> {
// TODO: think about moving this logic back into the frontend so that the driver can be in control
// of when to buffer and set the remote state
if (isOk && details.isHtml) {
const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl && !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '') || options.isFromSpecBridge
const urlDoesNotMatchPolicyBasedOnDomain = options.hasAlreadyVisitedUrl
&& !cors.urlMatchesPolicyBasedOnDomain(primaryRemoteState.origin, newUrl || '', { skipDomainInjectionForDomains: this.skipDomainInjectionForDomains })
|| options.isFromSpecBridge
if (!handlingLocalFile) {
this._remoteStates.set(newUrl as string, options, !urlDoesNotMatchPolicyBasedOnDomain)
@@ -2199,7 +2199,7 @@ describe('Routes', () => {
context('content injection', () => {
beforeEach(function () {
return this.setup('http://www.google.com')
return this.setup('http://www.cypress.io')
})
it('injects when head has attributes', async function () {
@@ -2212,7 +2212,7 @@ describe('Routes', () => {
const injection = await getRunnerInjectionContents()
const contents = removeWhitespace(Fixtures.get('server/expected_head_inject.html').replace('{{injection}}', injection))
const res = await this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2234,7 +2234,7 @@ describe('Routes', () => {
const contents = removeWhitespace(Fixtures.get('server/expected_no_head_tag_inject.html').replace('{{injection}}', injection))
const res = await this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2253,7 +2253,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2261,7 +2261,7 @@ describe('Routes', () => {
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.include('<HTML> <HEAD> <script type=\'text/javascript\'> document.domain = \'google.com\';')
expect(res.body).to.include('<HTML> <HEAD> <script type=\'text/javascript\'> document.domain = \'cypress.io\';')
})
})
@@ -2273,7 +2273,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2281,7 +2281,7 @@ describe('Routes', () => {
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.include('<html> <head> <script type=\'text/javascript\'> document.domain = \'google.com\';')
expect(res.body).to.include('<html> <head> <script type=\'text/javascript\'> document.domain = \'cypress.io\';')
expect(res.body).to.include('</head> <body><nav>some nav</nav><header>header</header></body> </html>')
})
@@ -2295,7 +2295,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2315,7 +2315,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2337,7 +2337,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2359,7 +2359,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
@@ -2379,7 +2379,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=false',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
@@ -2388,7 +2388,7 @@ describe('Routes', () => {
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.eq('<html> <head> <script type=\'text/javascript\'> document.domain = \'google.com\'; </script> </head> <body>hello from bar!</body> </html>')
expect(res.body).to.eq('<html> <head> <script type=\'text/javascript\'> document.domain = \'cypress.io\'; </script> </head> <body>hello from bar!</body> </html>')
})
})
@@ -2397,7 +2397,7 @@ describe('Routes', () => {
.get('/bar')
.reply(302, undefined, {
// redirect us to google.com!
'Location': 'http://www.google.com/foo',
'Location': 'http://www.cypress.io/foo',
})
nock(this.server.remoteStates.current().origin)
@@ -2407,14 +2407,14 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=true',
},
})
.then((res) => {
expect(res.statusCode).to.eq(302)
expect(res.headers['location']).to.eq('http://www.google.com/foo')
expect(res.headers['location']).to.eq('http://www.cypress.io/foo')
expect(res.headers['set-cookie']).to.match(/initial=true/)
return this.rp(res.headers['location'])
@@ -2437,7 +2437,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/elements.html',
url: 'http://www.cypress.io/elements.html',
headers: {
'Cookie': '__cypress.initial=true',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
@@ -2446,7 +2446,7 @@ describe('Routes', () => {
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.include('document.domain = \'google.com\';')
expect(res.body).to.include('document.domain = \'cypress.io\';')
})
})
@@ -2479,7 +2479,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/bar',
url: 'http://www.cypress.io/bar',
headers: {
'Cookie': '__cypress.initial=false',
},
@@ -2534,10 +2534,10 @@ describe('Routes', () => {
})
it('injects even on 5xx responses', function () {
return this.setup('https://www.google.com')
return this.setup('https://www.cypress.io')
.then(() => {
this.server.onRequest((req, res) => {
return nock('https://www.google.com')
return nock('https://www.cypress.io')
.get('/')
.reply(500, '<html><head></head><body>google</body></html>', {
'Content-Type': 'text/html',
@@ -2545,7 +2545,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'https://www.google.com/',
url: 'https://www.cypress.io/',
headers: {
'Accept': 'text/html, application/xhtml+xml, */*',
},
@@ -2553,7 +2553,7 @@ describe('Routes', () => {
.then((res) => {
expect(res.statusCode).to.eq(500)
expect(res.body).to.include('document.domain = \'google.com\'')
expect(res.body).to.include('document.domain = \'cypress.io\'')
})
})
})
@@ -2624,7 +2624,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/iframe',
url: 'http://www.cypress.io/iframe',
headers: {
'Cookie': '__cypress.initial=false',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
@@ -2635,19 +2635,19 @@ describe('Routes', () => {
const body = cleanResponseBody(res.body)
expect(body).to.eq('<html><head> <script type=\'text/javascript\'> document.domain = \'google.com\'; </script></head></html>')
expect(body).to.eq('<html><head> <script type=\'text/javascript\'> document.domain = \'cypress.io\'; </script></head></html>')
})
})
it('does not inject document.domain on matching super domains but different subdomain - when the domain is set to strict same origin (google)', function () {
nock('http://mail.google.com')
nock('http://www.google.com')
.get('/iframe')
.reply(200, '<html><head></head></html>', {
'Content-Type': 'text/html',
})
return this.rp({
url: 'http://mail.google.com/iframe',
url: 'http://www.google.com/iframe',
headers: {
'Cookie': '__cypress.initial=false',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
@@ -2694,7 +2694,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/json',
url: 'http://www.cypress.io/json',
json: true,
headers: {
'Cookie': '__cypress.initial=false',
@@ -2734,7 +2734,7 @@ describe('Routes', () => {
.reply(200, { foo: 'bar' })
return this.rp({
url: 'http://www.google.com/json',
url: 'http://www.cypress.io/json',
headers: {
'Cookie': '__cypress.initial=true',
'Accept': 'application/json',
@@ -2757,7 +2757,7 @@ describe('Routes', () => {
})
return this.rp({
url: 'http://www.google.com/iframe',
url: 'http://www.cypress.io/iframe',
headers: {
'Cookie': '__cypress.initial=false',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
@@ -2788,7 +2788,7 @@ describe('Routes', () => {
headers['Accept'] = type
return this.rp({
url: 'http://www.google.com/iframe',
url: 'http://www.cypress.io/iframe',
headers,
})
.then((res) => {
+38 -38
View File
@@ -823,7 +823,7 @@ describe('Server', () => {
})
it('can serve non 2xx status code requests when option set', function () {
nock('http://google.com')
nock('http://cypress.io')
.matchHeader('user-agent', 'foobarbaz')
.matchHeader('accept', 'text/html,*/*')
.get('/foo')
@@ -837,29 +837,29 @@ describe('Server', () => {
headers['user-agent'] = 'foobarbaz'
return this.server._onResolveUrl('http://google.com/foo', headers, this.automationRequest, { failOnStatusCode: false })
return this.server._onResolveUrl('http://cypress.io/foo', headers, this.automationRequest, { failOnStatusCode: false })
.then((obj = {}) => {
return expectToEqDetails(obj, {
isOkStatusCode: true,
isPrimarySuperDomainOrigin: true,
isHtml: true,
contentType: 'text/html',
url: 'http://google.com/foo',
originalUrl: 'http://google.com/foo',
url: 'http://cypress.io/foo',
originalUrl: 'http://cypress.io/foo',
status: 404,
statusText: 'Not Found',
redirects: [],
cookies: [],
})
}).then(() => {
return this.rp('http://google.com/foo')
return this.rp('http://cypress.io/foo')
.then((res) => {
expect(res.statusCode).to.eq(404)
expect(res.headers['set-cookie']).not.to.match(/initial=;/)
expect(res.headers['x-foo-bar']).to.eq('true')
expect(res.headers['cache-control']).to.eq('no-cache, no-store, must-revalidate')
expect(res.body).to.include('content')
expect(res.body).to.include('document.domain = \'google.com\'')
expect(res.body).to.include('document.domain = \'cypress.io\'')
expect(res.body).to.include('.action("app:window:before:load",window)')
expect(res.body).to.include('</head>content</html>')
@@ -1132,7 +1132,7 @@ describe('Server', () => {
})
it('can go from file -> http -> file', function () {
nock('http://www.google.com')
nock('http://www.cypress.io')
.get('/')
.reply(200, 'content page', {
'Content-Type': 'text/html',
@@ -1159,35 +1159,35 @@ describe('Server', () => {
expect(res.statusCode).to.eq(200)
})
}).then(() => {
return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest)
return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest)
}).then((obj = {}) => {
return expectToEqDetails(obj, {
isOkStatusCode: true,
isPrimarySuperDomainOrigin: true,
isHtml: true,
contentType: 'text/html',
url: 'http://www.google.com/',
originalUrl: 'http://www.google.com/',
url: 'http://www.cypress.io/',
originalUrl: 'http://www.cypress.io/',
status: 200,
statusText: 'OK',
redirects: [],
cookies: [],
})
}).then(() => {
return this.rp('http://www.google.com/')
return this.rp('http://www.cypress.io/')
.then((res) => {
expect(res.statusCode).to.eq(200)
})
}).then(() => {
expect(this.server.remoteStates.current()).to.deep.eq({
auth: undefined,
origin: 'http://www.google.com',
origin: 'http://www.cypress.io',
strategy: 'http',
domainName: 'google.com',
domainName: 'cypress.io',
fileServer: null,
props: {
domain: 'google',
tld: 'com',
domain: 'cypress',
tld: 'io',
port: '80',
subdomain: 'www',
protocol: 'http:',
@@ -1228,50 +1228,50 @@ describe('Server', () => {
})
it('can go from http -> file -> http', function () {
nock('http://www.google.com')
nock('http://www.cypress.io')
.get('/')
.reply(200, '<html><head></head><body>google</body></html>', {
.reply(200, '<html><head></head><body>cypress</body></html>', {
'Content-Type': 'text/html',
})
.get('/')
.reply(200, '<html><head></head><body>google</body></html>', {
.reply(200, '<html><head></head><body>cypress</body></html>', {
'Content-Type': 'text/html',
})
return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest)
return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest)
.then((obj = {}) => {
return expectToEqDetails(obj, {
isOkStatusCode: true,
isPrimarySuperDomainOrigin: true,
isHtml: true,
contentType: 'text/html',
url: 'http://www.google.com/',
originalUrl: 'http://www.google.com/',
url: 'http://www.cypress.io/',
originalUrl: 'http://www.cypress.io/',
status: 200,
statusText: 'OK',
redirects: [],
cookies: [],
})
}).then(() => {
return this.rp('http://www.google.com/')
return this.rp('http://www.cypress.io/')
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.include('document.domain')
expect(res.body).to.include('google.com')
expect(res.body).to.include('cypress.io')
expect(res.body).to.include('.action("app:window:before:load",window)')
expect(res.body).to.include('</script></head><body>google</body></html>')
expect(res.body).to.include('</script></head><body>cypress</body></html>')
})
}).then(() => {
expect(this.server.remoteStates.current()).to.deep.eq({
auth: undefined,
origin: 'http://www.google.com',
origin: 'http://www.cypress.io',
strategy: 'http',
domainName: 'google.com',
domainName: 'cypress.io',
fileServer: null,
props: {
domain: 'google',
tld: 'com',
domain: 'cypress',
tld: 'io',
port: '80',
subdomain: 'www',
protocol: 'http:',
@@ -1313,40 +1313,40 @@ describe('Server', () => {
props: null,
})
}).then(() => {
return this.server._onResolveUrl('http://www.google.com/', {}, this.automationRequest)
return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest)
.then((obj = {}) => {
return expectToEqDetails(obj, {
isOkStatusCode: true,
isPrimarySuperDomainOrigin: true,
isHtml: true,
contentType: 'text/html',
url: 'http://www.google.com/',
originalUrl: 'http://www.google.com/',
url: 'http://www.cypress.io/',
originalUrl: 'http://www.cypress.io/',
status: 200,
statusText: 'OK',
redirects: [],
cookies: [],
})
}).then(() => {
return this.rp('http://www.google.com/')
return this.rp('http://www.cypress.io/')
.then((res) => {
expect(res.statusCode).to.eq(200)
expect(res.body).to.include('document.domain')
expect(res.body).to.include('google.com')
expect(res.body).to.include('cypress.io')
expect(res.body).to.include('.action("app:window:before:load",window)')
expect(res.body).to.include('</script></head><body>google</body></html>')
expect(res.body).to.include('</script></head><body>cypress</body></html>')
})
}).then(() => {
expect(this.server.remoteStates.current()).to.deep.eq({
auth: undefined,
origin: 'http://www.google.com',
origin: 'http://www.cypress.io',
strategy: 'http',
domainName: 'google.com',
domainName: 'cypress.io',
fileServer: null,
props: {
domain: 'google',
tld: 'com',
domain: 'cypress',
tld: 'io',
port: '80',
subdomain: 'www',
protocol: 'http:',
@@ -1,7 +1,7 @@
<html>
<head prefix="og: foo">
<script type='text/javascript'>
document.domain = 'google.com';
document.domain = 'cypress.io';
{{injection}}
</script>
@@ -1,7 +1,7 @@
<html>
<head>
<script type='text/javascript'>
document.domain = 'google.com';
document.domain = 'cypress.io';
{{injection}}
</script>
+1 -1
View File
@@ -30,7 +30,7 @@ export interface FullConfig extends Partial<Cypress.RuntimeConfigOptions & Cypre
// and are required when creating a project.
export type ReceivedCypressOptions =
Pick<Cypress.RuntimeConfigOptions, 'hosts' | 'projectName' | 'clientRoute' | 'devServerPublicPathRoute' | 'namespace' | 'report' | 'socketIoCookie' | 'configFile' | 'isTextTerminal' | 'isNewProject' | 'proxyUrl' | 'browsers' | 'browserUrl' | 'socketIoRoute' | 'arch' | 'platform' | 'spec' | 'specs' | 'browser' | 'version' | 'remote'>
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'excludeSpecPattern' | 'specPattern' | 'experimentalModifyObstructiveThirdPartyCode' | 'video' | 'videoCompression' | 'videosFolder' | 'videoUploadOnPasses' | 'resolvedNodeVersion' | 'resolvedNodePath' | 'trashAssetsBeforeRuns' | 'experimentalWebKitSupport'> // TODO: Figure out how to type this better.
& Pick<Cypress.ResolvedConfigOptions, 'chromeWebSecurity' | 'supportFolder' | 'experimentalSourceRewriting' | 'fixturesFolder' | 'reporter' | 'reporterOptions' | 'screenshotsFolder' | 'supportFile' | 'baseUrl' | 'viewportHeight' | 'viewportWidth' | 'port' | 'experimentalInteractiveRunEvents' | 'userAgent' | 'downloadsFolder' | 'env' | 'excludeSpecPattern' | 'specPattern' | 'experimentalModifyObstructiveThirdPartyCode' | 'experimentalSkipDomainInjection' | 'video' | 'videoCompression' | 'videosFolder' | 'videoUploadOnPasses' | 'resolvedNodeVersion' | 'resolvedNodePath' | 'trashAssetsBeforeRuns' | 'experimentalWebKitSupport'> // TODO: Figure out how to type this better.
export interface SettingsOptions {
testingType?: 'component' |'e2e'
@@ -0,0 +1,65 @@
exports['e2e experimentalSkipDomainInjection=true / passes'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (experimental_skip_domain_injection.cy.ts)
Searched: cypress/e2e/experimental_skip_domain_injection.cy.ts
Experiments: experimentalSkipDomainInjection=*.foobar.com
Running: experimental_skip_domain_injection.cy.ts (1 of 1)
expected behavior when experimentalSkipDomainInjection=true
Handles cross-site/cross-origin navigation the same way without the experimental flag enabled
errors appropriately when doing a sub domain navigation w/o cy.origin()
allows sub-domain navigations with the use of cy.origin()
3 passing
(Results)
Tests: 3
Passing: 3
Failing: 0
Pending: 0
Skipped: 0
Screenshots: 0
Video: true
Duration: X seconds
Spec Ran: experimental_skip_domain_injection.cy.ts
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/experimental_skip_domain_inject (X second)
ion.cy.ts.mp4
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
experimental_skip_domain_injection. XX:XX 3 3 - - -
cy.ts
All specs passed! XX:XX 3 3 - - -
`
@@ -0,0 +1,43 @@
describe('expected behavior when experimentalSkipDomainInjection=true', () => {
it('Handles cross-site/cross-origin navigation the same way without the experimental flag enabled', () => {
cy.visit('/primary_origin.html')
cy.get('a[data-cy="cross_origin_secondary_link"]').click()
cy.origin('http://www.foobar.com:4466', () => {
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
it('errors appropriately when doing a sub domain navigation w/o cy.origin()', () => {
const timeout = 500
cy.on('fail', (err) => {
expect(err.name).to.equal('CypressError')
expect(err.message).to.contain(`Timed out retrying after ${timeout}ms: The command was expected to run against origin \`http://app.foobar.com:4466\` but the application is at origin \`http://www.foobar.com:4466\`.`)
expect(err.message).to.contain('This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.')
expect(err.message).to.contain('Using `cy.origin()` to wrap the commands run on `http://www.foobar.com:4466` will likely fix this issue.')
expect(err.message).to.include(`cy.origin('http://www.foobar.com:4466', () => {\`\n\` <commands targeting http://www.foobar.com:4466 go here>\`\n\`})`)
expect(err.message).to.include('If `experimentalSkipDomainInjection` is enabled for this domain, a `cy.origin()` command is required.')
// make sure that the secondary origin failures do NOT show up as spec failures or AUT failures
expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`)
expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`)
})
// with experimentalSkipDomainInjection, sub domain navigations require a cy.origin() block
cy.visit('http://app.foobar.com:4466/primary_origin.html')
cy.get('a[data-cy="cross_origin_secondary_link"]').click()
cy.get('[data-cy="dom-check"]', {
timeout,
}).should('have.text', 'From a secondary origin')
})
it('allows sub-domain navigations with the use of cy.origin()', () => {
cy.visit('http://app.foobar.com:4466/primary_origin.html')
cy.get('a[data-cy="cross_origin_secondary_link"]').click()
// with experimentalSkipDomainInjection, sub domain navigations require a cy.origin() block
cy.origin('http://www.foobar.com:4466', () => {
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary origin')
})
})
})
@@ -0,0 +1,44 @@
import path from 'path'
import systemTests from '../lib/system-tests'
import Fixtures from '../lib/fixtures'
const e2ePath = Fixtures.projectPath('e2e')
const PORT = 3500
const onServer = function (app) {
app.get('/primary_origin.html', (_, res) => {
res.sendFile(path.join(e2ePath, `primary_origin.html`))
})
app.get('/secondary_origin.html', (_, res) => {
res.sendFile(path.join(e2ePath, `secondary_origin.html`))
})
}
describe('e2e experimentalSkipDomainInjection=true', () => {
systemTests.setup({
servers: [{
port: 4466,
onServer,
}],
settings: {
hosts: {
'*.foobar.com': '127.0.0.1',
},
e2e: {},
},
})
systemTests.it('passes', {
browser: '!webkit', // TODO(webkit): fix+unskip (needs multidomain support)
// keep the port the same to prevent issues with the snapshot
port: PORT,
spec: 'experimental_skip_domain_injection.cy.ts',
snapshot: true,
expectedExitCode: 0,
config: {
retries: 0,
experimentalSkipDomainInjection: ['*.foobar.com'],
},
})
})