Skip to content

Commit

Permalink
Merge branch 'feature-multidomain' into md-clean-up-sync-global-listener
Browse files Browse the repository at this point in the history
  • Loading branch information
AtofStryker authored Mar 29, 2022
2 parents 68b1dc9 + fedb65c commit 8204d49
Show file tree
Hide file tree
Showing 41 changed files with 1,477 additions and 599 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
"processId": "${command:PickProcess}",
"continueOnAttach": true
},
{
"type": "node",
Expand Down
9 changes: 2 additions & 7 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,12 @@ declare namespace Cypress {
}

interface RemoteState {
auth?: {
username: string
password: string
}
auth?: Auth
domainName: string
strategy: 'file' | 'http'
origin: string
fileServer: string
fileServer: string | null
props: Record<string, any>
visiting: string
}

interface Backend {
Expand All @@ -68,7 +64,6 @@ declare namespace Cypress {
* @see https://on.cypress.io/firefox-gc-issue
*/
(task: 'firefox:force:gc'): Promise<void>
(task: 'ready:for:domain'): Promise<void>
(task: 'net', eventName: string, frame: any): Promise<void>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,19 @@ context('multi-domain navigation', { experimentalSessionSupport: true }, () => {
})
})

// TODO: un-skip once multiple remote states are supported
it.skip('supports auth options and adding auth to subsequent requests', () => {
// TODO: test currently fails when redirecting
it.skip('supports visit redirects', () => {
cy.visit('/fixtures/multi-domain.html')
cy.get('a[data-cy="dom-link"]').click()

cy.switchToDomain('http://www.foobar.com:3500', () => {
cy.visit('/redirect?href=http://localhost:3500/fixtures/multi-domain-secondary.html')
})

cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain')
})

it('supports auth options and adding auth to subsequent requests', () => {
cy.switchToDomain('http://foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/basic_auth', {
auth: {
Expand All @@ -260,26 +271,83 @@ context('multi-domain navigation', { experimentalSessionSupport: true }, () => {

cy.get('body').should('have.text', 'basic auth worked')

cy.window().then({ timeout: 60000 }, (win) => {
return new Cypress.Promise(((resolve, reject) => {
const xhr = new win.XMLHttpRequest()
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})

cy.get('body').should('have.text', 'basic auth worked')
})

// attaches the auth options for the foobar domain even from another switchToDomain
cy.switchToDomain('http://www.idp.com:3500', () => {
cy.visit('/fixtures/multi-domain.html')

cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
})

cy.switchToDomain('http://foobar.com:3500', () => {
cy.get('body').should('have.text', 'basic auth worked')
})

cy.visit('/fixtures/multi-domain.html')

// attaches the auth options for the foobar domain from the top-level
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})

cy.switchToDomain('http://foobar.com:3500', () => {
cy.get('body').should('have.text', 'basic auth worked')
})
})

xhr.open('GET', '/basic_auth')
xhr.onload = function () {
try {
expect(this.responseText).to.include('basic auth worked')
it('does not propagate the auth options across tests', (done) => {
cy.intercept('/basic_auth', (req) => {
req.on('response', (res) => {
// clear the www-authenticate header so the browser doesn't prompt for username/password
res.headers['www-authenticate'] = ''
expect(res.statusCode).to.equal(401)
done()
})
})

return resolve(win)
} catch (err) {
return reject(err)
}
}
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/fixtures/multi-domain.html'
})

return xhr.send()
}))
cy.switchToDomain('http://foobar.com:3500', () => {
cy.window().then((win) => {
win.location.href = 'http://www.foobar.com:3500/basic_auth'
})
})
})

it('succeeds when visiting local file server first', { baseUrl: undefined }, () => {
cy.visit('cypress/fixtures/multi-domain.html')

cy.switchToDomain('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/multi-domain-secondary.html')
cy.get('[data-cy="dom-check"]').should('have.text', 'From a secondary domain')
})
})

it('handles visit failures', { baseUrl: undefined }, (done) => {
cy.on('fail', (e) => {
expect(e.message).to.include('failed trying to load:\n\nhttp://www.foobar.com:3500/fixtures/multi-domain-secondary.html')
expect(e.message).to.include('500: Internal Server Error')

done()
})

cy.intercept('*/multi-domain-secondary.html', { statusCode: 500 })

cy.visit('cypress/fixtures/multi-domain.html')
cy.switchToDomain('http://www.foobar.com:3500', () => {
cy.visit('fixtures/multi-domain-secondary.html')
})
})
})

it('supports navigating through changing the window.location.href', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ describe('multi-domain - rerun', { }, () => {
cy.get('a[data-cy="multi-domain-secondary-link"]').click()
})

// this test will hang without the fix for multi-domain rerun
// https://github.com/cypress-io/cypress/issues/18043
it('successfully reruns tests', () => {
// @ts-ignore
cy.switchToDomain('http://foobar.com:3500', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/driver/cypress/plugins/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const createApp = (port) => {

return res
.set('WWW-Authenticate', 'Basic')
.type('html')
.sendStatus(401)
})

Expand Down
27 changes: 14 additions & 13 deletions packages/driver/src/cy/multi-domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,8 @@ const normalizeDomain = (domain) => {
export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State, config: Cypress.InternalConfig) {
let timeoutId

// @ts-ignore
const communicator = Cypress.multiDomainCommunicator

const sendReadyForDomain = () => {
// lets the proxy know to allow the response for the secondary
// domain html through, so the page will finish loading
Cypress.backend('ready:for:domain')
}

communicator.on('delaying:html', (request) => {
// when a secondary domain is detected by the proxy, it holds it up
// to provide time for the spec bridge to be set up. normally, the queue
Expand All @@ -38,10 +31,13 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
// @ts-ignore
cy.isAnticipatingMultiDomainFor(request.href)

// cy.isAnticipatingMultiDomainFor(href) will free the queue to move forward.
// if the next command isn't switchToDomain, this timeout will hit and
// the test will fail with a cross-origin error
timeoutId = setTimeout(sendReadyForDomain, 2000)
// If we haven't seen a switchToDomain and cleared the timeout within 300ms,
// go ahead and inform the server 'ready:for:domain' failed and to release the
// response. This typically happens during a redirect where the user does
// not have a switchToDomain for the intermediary domain.
timeoutId = setTimeout(() => {
Cypress.backend('ready:for:domain', { failed: true })
}, 300)
})

Commands.addAll({
Expand Down Expand Up @@ -80,7 +76,9 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,

const validator = new Validator({
log,
onFailure: sendReadyForDomain,
onFailure: () => {
Cypress.backend('ready:for:domain', { failed: true })
},
})

validator.validate({
Expand All @@ -101,6 +99,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,

return new Bluebird((resolve, reject, onCancel) => {
const cleanup = () => {
Cypress.backend('cross:origin:finished', location.originPolicy)
communicator.off('queue:finished', onQueueFinished)
communicator.off('sync:globals', onSyncGlobals)
}
Expand Down Expand Up @@ -138,7 +137,9 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
communicator.once('ran:domain:fn', (details) => {
const { subject, unserializableSubjectType, err, finished } = details

sendReadyForDomain()
// lets the proxy know to allow the response for the secondary
// domain html through, so the page will finish loading
Cypress.backend('ready:for:domain', { originPolicy: location.originPolicy })

if (err) {
return _reject(err)
Expand Down
8 changes: 1 addition & 7 deletions packages/driver/src/cypress/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,7 @@ export class $Location {
}

getOriginPolicy () {
// origin policy is comprised of
// protocol + superdomain
// and subdomain is not factored in
return _.compact([
`${this.getProtocol()}//${this.getSuperDomain()}`,
this.getPort(),
]).join(':')
return cors.getOriginPolicy(this.remote.href)
}

getSuperDomain () {
Expand Down
2 changes: 0 additions & 2 deletions packages/driver/src/multi-domain/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => {
handleUnsupportedAPIs(Cypress, cy)

cy.onBeforeAppWindowLoad = onBeforeAppWindowLoad(Cypress, cy)

return cy
}

// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
Expand Down
5 changes: 5 additions & 0 deletions packages/driver/types/internal-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ declare namespace Cypress {
(action: 'after:screenshot', config: {})
}

interface Backend {
(task: 'ready:for:domain', args: { originPolicy?: string , failed?: boolean}): boolean
(task: 'cross:origin:finished', originPolicy: string): boolean
}

interface cy {
/**
* If `as` is chained to the current command, return the alias name used.
Expand Down
8 changes: 8 additions & 0 deletions packages/network/lib/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,11 @@ export function urlMatchesOriginProtectionSpace (urlStr, origin) {

return _.startsWith(normalizedUrl, normalizedOrigin)
}

export function getOriginPolicy (url: string) {
const { port, protocol } = new URL(url)

// origin policy is comprised of:
// protocol + superdomain + port (subdomain is not factored in)
return _.compact([`${protocol}//${getSuperDomain(url)}`, port]).join(':')
}
12 changes: 12 additions & 0 deletions packages/network/test/unit/cors_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,16 @@ describe('lib/cors', () => {
isNotMatch('http://foo.example.com/', 'http://foo.bar.example.com')
})
})

context('.getOriginPolicy', () => {
it('ports', () => {
expect(cors.getOriginPolicy('https://example.com')).to.equal('https://example.com')
expect(cors.getOriginPolicy('http://example.com:8080')).to.equal('http://example.com:8080')
})

it('subdomain', () => {
expect(cors.getOriginPolicy('http://www.example.com')).to.equal('http://example.com')
expect(cors.getOriginPolicy('http://www.app.herokuapp.com:8080')).to.equal('http://app.herokuapp.com:8080')
})
})
})
10 changes: 5 additions & 5 deletions packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { DeferredSourceMapCache } from '@packages/rewriter'
import type { Browser } from '@packages/server/lib/browsers/types'
import type { RemoteStates } from '@packages/server/lib/remote_states'

export const debugVerbose = Debug('cypress-verbose:proxy:http')

Expand Down Expand Up @@ -61,7 +62,7 @@ export type ServerCtx = Readonly<{
shouldCorrelatePreRequests?: () => boolean
getCurrentBrowser: () => Browser | Partial<Browser> & Pick<Browser, 'family'> | null
getFileServerToken: () => string
getRemoteState: CyServer.getRemoteState
remoteStates: RemoteStates
getRenderedHTMLOrigins: Http['getRenderedHTMLOrigins']
netStubbingState: NetStubbingState
middleware: HttpMiddlewareStacks
Expand All @@ -74,7 +75,6 @@ const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [
'buffers',
'config',
'getFileServerToken',
'getRemoteState',
'netStubbingState',
'next',
'end',
Expand Down Expand Up @@ -201,7 +201,7 @@ export class Http {
deferredSourceMapCache: DeferredSourceMapCache
getCurrentBrowser: () => Browser | Partial<Browser> & Pick<Browser, 'family'> | null
getFileServerToken: () => string
getRemoteState: () => any
remoteStates: RemoteStates
middleware: HttpMiddlewareStacks
netStubbingState: NetStubbingState
preRequests: PreRequests = new PreRequests()
Expand All @@ -219,7 +219,7 @@ export class Http {
this.shouldCorrelatePreRequests = opts.shouldCorrelatePreRequests || (() => false)
this.getCurrentBrowser = opts.getCurrentBrowser
this.getFileServerToken = opts.getFileServerToken
this.getRemoteState = opts.getRemoteState
this.remoteStates = opts.remoteStates
this.middleware = opts.middleware
this.netStubbingState = opts.netStubbingState
this.socket = opts.socket
Expand All @@ -240,7 +240,7 @@ export class Http {
shouldCorrelatePreRequests: this.shouldCorrelatePreRequests,
getCurrentBrowser: this.getCurrentBrowser,
getFileServerToken: this.getFileServerToken,
getRemoteState: this.getRemoteState,
remoteStates: this.remoteStates,
request: this.request,
middleware: _.cloneDeep(this.middleware),
netStubbingState: this.netStubbingState,
Expand Down
9 changes: 5 additions & 4 deletions packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ function reqNeedsBasicAuthHeaders (req, { auth, origin }: Cypress.RemoteState) {
}

const MaybeSetBasicAuthHeaders: RequestMiddleware = function () {
const remoteState = this.getRemoteState()
// get the remote state for the proxied url
const remoteState = this.remoteStates.get(this.req.proxiedUrl)

if (remoteState.auth && reqNeedsBasicAuthHeaders(this.req, remoteState)) {
if (remoteState?.auth && reqNeedsBasicAuthHeaders(this.req, remoteState)) {
const { auth } = remoteState
const base64 = Buffer.from(`${auth.username}:${auth.password}`).toString('base64')

Expand All @@ -164,12 +165,12 @@ const SendRequestOutgoing: RequestMiddleware = function () {

const requestBodyBuffered = !!this.req.body

const { strategy, origin, fileServer } = this.getRemoteState()
const { strategy, origin, fileServer } = this.remoteStates.current()

if (strategy === 'file' && requestOptions.url.startsWith(origin)) {
this.req.headers['x-cypress-authorization'] = this.getFileServerToken()

requestOptions.url = requestOptions.url.replace(origin, fileServer)
requestOptions.url = requestOptions.url.replace(origin, fileServer as string)
}

if (requestBodyBuffered) {
Expand Down
Loading

0 comments on commit 8204d49

Please sign in to comment.