From fedb65c97cbfbb7b7cb1228dcfd6aaac70f122c4 Mon Sep 17 00:00:00 2001 From: Matt Schile Date: Mon, 28 Mar 2022 15:26:51 -0600 Subject: [PATCH] chore: (multi-domain) support multiple remote states (#20752) --- .vscode/launch.json | 3 +- cli/types/cypress.d.ts | 9 +- .../commands/multi_domain_navigation.spec.ts | 100 +++- .../multi-domain/multi_domain_rerun_spec.ts | 2 - packages/driver/cypress/plugins/server.js | 1 + packages/driver/src/cy/multi-domain/index.ts | 27 +- packages/driver/src/cypress/location.ts | 8 +- packages/driver/src/multi-domain/cypress.ts | 2 - packages/driver/types/internal-types.d.ts | 5 + packages/network/lib/cors.ts | 8 + packages/network/test/unit/cors_spec.ts | 12 + packages/proxy/lib/http/index.ts | 10 +- packages/proxy/lib/http/request-middleware.ts | 9 +- .../proxy/lib/http/response-middleware.ts | 27 +- packages/proxy/package.json | 1 + .../test/integration/net-stubbing.spec.ts | 29 +- packages/proxy/test/unit/http/helpers.ts | 9 +- packages/proxy/test/unit/http/index.spec.ts | 7 +- .../test/unit/http/request-middleware.spec.ts | 114 +++++ .../unit/http/response-middleware.spec.ts | 269 +++++++---- packages/runner/injection/main.js | 2 +- packages/runner/injection/multi-domain.js | 2 +- packages/server/index.d.ts | 2 - packages/server/lib/controllers/files.js | 4 +- packages/server/lib/controllers/iframes.ts | 7 +- packages/server/lib/controllers/runner.ts | 6 +- packages/server/lib/remote_states.ts | 186 ++++++++ packages/server/lib/routes.ts | 15 +- packages/server/lib/server-base.ts | 136 ++---- packages/server/lib/server-ct.ts | 5 +- packages/server/lib/server-e2e.ts | 42 +- packages/server/lib/socket-base.ts | 6 +- .../test/integration/http_requests_spec.js | 194 ++++---- .../server/test/integration/server_spec.js | 252 +++++++--- .../test/integration/websockets_spec.js | 12 +- .../performance/proxy_performance_spec.js | 4 +- packages/server/test/unit/iframes_spec.js | 4 +- .../server/test/unit/remote_states.spec.ts | 431 ++++++++++++++++++ packages/server/test/unit/server_spec.js | 88 +--- packages/server/test/unit/socket_spec.js | 22 +- yarn.lock | 4 +- 41 files changed, 1477 insertions(+), 599 deletions(-) create mode 100644 packages/server/lib/remote_states.ts create mode 100644 packages/server/test/unit/remote_states.spec.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b55f65350daa..4e76bbcdfd22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,8 @@ "type": "node", "request": "attach", "name": "Attach by Process ID", - "processId": "${command:PickProcess}" + "processId": "${command:PickProcess}", + "continueOnAttach": true }, { "type": "node", diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 8d8bffc2ac5a..4780cf90bef7 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -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 - visiting: string } interface Backend { @@ -68,7 +64,6 @@ declare namespace Cypress { * @see https://on.cypress.io/firefox-gc-issue */ (task: 'firefox:force:gc'): Promise - (task: 'ready:for:domain'): Promise (task: 'net', eventName: string, frame: any): Promise } diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts index c01adbd68dc5..67ad9f6bc89c 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_navigation.spec.ts @@ -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: { @@ -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', () => { diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_rerun_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_rerun_spec.ts index 9286dd61c6fc..e3f596bac6a5 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_rerun_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_rerun_spec.ts @@ -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', () => { diff --git a/packages/driver/cypress/plugins/server.js b/packages/driver/cypress/plugins/server.js index 91694154bcca..bff94b2b55f3 100644 --- a/packages/driver/cypress/plugins/server.js +++ b/packages/driver/cypress/plugins/server.js @@ -129,6 +129,7 @@ const createApp = (port) => { return res .set('WWW-Authenticate', 'Basic') + .type('html') .sendStatus(401) }) diff --git a/packages/driver/src/cy/multi-domain/index.ts b/packages/driver/src/cy/multi-domain/index.ts index f0cf0c66f6ff..89a1251b8d27 100644 --- a/packages/driver/src/cy/multi-domain/index.ts +++ b/packages/driver/src/cy/multi-domain/index.ts @@ -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 @@ -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({ @@ -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({ @@ -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) } @@ -135,7 +134,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) diff --git a/packages/driver/src/cypress/location.ts b/packages/driver/src/cypress/location.ts index 96837afd3b4c..90669130ea1f 100644 --- a/packages/driver/src/cypress/location.ts +++ b/packages/driver/src/cypress/location.ts @@ -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 () { diff --git a/packages/driver/src/multi-domain/cypress.ts b/packages/driver/src/multi-domain/cypress.ts index ca09c7b03590..85b35cab54da 100644 --- a/packages/driver/src/multi-domain/cypress.ts +++ b/packages/driver/src/multi-domain/cypress.ts @@ -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 diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 8fd48f10c59b..00798c915719 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -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. diff --git a/packages/network/lib/cors.ts b/packages/network/lib/cors.ts index 0a56b96ecc05..a6a3fa119ded 100644 --- a/packages/network/lib/cors.ts +++ b/packages/network/lib/cors.ts @@ -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(':') +} diff --git a/packages/network/test/unit/cors_spec.ts b/packages/network/test/unit/cors_spec.ts index 5031badf4c9a..76870b113624 100644 --- a/packages/network/test/unit/cors_spec.ts +++ b/packages/network/test/unit/cors_spec.ts @@ -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') + }) + }) }) diff --git a/packages/proxy/lib/http/index.ts b/packages/proxy/lib/http/index.ts index 1b176391c5cb..c2e81ed0288c 100644 --- a/packages/proxy/lib/http/index.ts +++ b/packages/proxy/lib/http/index.ts @@ -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') @@ -61,7 +62,7 @@ export type ServerCtx = Readonly<{ shouldCorrelatePreRequests?: () => boolean getCurrentBrowser: () => Browser | Partial & Pick | null getFileServerToken: () => string - getRemoteState: CyServer.getRemoteState + remoteStates: RemoteStates getRenderedHTMLOrigins: Http['getRenderedHTMLOrigins'] netStubbingState: NetStubbingState middleware: HttpMiddlewareStacks @@ -74,7 +75,6 @@ const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [ 'buffers', 'config', 'getFileServerToken', - 'getRemoteState', 'netStubbingState', 'next', 'end', @@ -201,7 +201,7 @@ export class Http { deferredSourceMapCache: DeferredSourceMapCache getCurrentBrowser: () => Browser | Partial & Pick | null getFileServerToken: () => string - getRemoteState: () => any + remoteStates: RemoteStates middleware: HttpMiddlewareStacks netStubbingState: NetStubbingState preRequests: PreRequests = new PreRequests() @@ -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 @@ -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, diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts index de288d016dcb..ca08870ecb6b 100644 --- a/packages/proxy/lib/http/request-middleware.ts +++ b/packages/proxy/lib/http/request-middleware.ts @@ -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') @@ -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) { diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 4f0915256abb..8d93476276e4 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -230,16 +230,22 @@ const PatchExpressSetHeader: ResponseMiddleware = function () { } const MaybeDelayForMultiDomain: ResponseMiddleware = function () { - const isCrossDomain = !reqMatchesOriginPolicy(this.req, this.getRemoteState()) + const isCrossDomain = !reqMatchesOriginPolicy(this.req, this.remoteStates.current()) + const isPreviousOrigin = this.remoteStates.isInOriginStack(this.req.proxiedUrl) const isHTML = resContentTypeIs(this.incomingRes, 'text/html') const isRenderedHTML = reqWillRenderHtml(this.req) const isAUTFrame = this.req.isAUTFrame - if (this.config.experimentalMultiDomain && isCrossDomain && isAUTFrame && (isHTML || isRenderedHTML)) { - this.debug('is cross-domain, delay until domain:ready event') + // delay the response if this is a cross-origin (and not returning to a previous origin) html request from the AUT iframe + if (this.config.experimentalMultiDomain && isCrossDomain && !isPreviousOrigin && isAUTFrame && (isHTML || isRenderedHTML)) { + this.debug('is cross-domain, delay until ready:for:domain event') - this.serverBus.once('ready:for:domain', () => { - this.debug('ready for domain, let it go') + this.serverBus.once('ready:for:domain', ({ failed }) => { + this.debug(`ready for domain${failed ? ' failed' : ''}, let it go`) + + if (!failed) { + this.res.wantsInjection = 'fullMultiDomain' + } this.next() }) @@ -267,7 +273,7 @@ const SetInjectionLevel: ResponseMiddleware = function () { this.debug('determine injection') - const isReqMatchOriginPolicy = reqMatchesOriginPolicy(this.req, this.getRemoteState()) + const isReqMatchOriginPolicy = reqMatchesOriginPolicy(this.req, this.remoteStates.current()) const getInjectionLevel = () => { if (this.incomingRes.headers['x-cypress-file-server-error'] && !this.res.isInitial) { this.debug('- partial injection (x-cypress-file-server-error)') @@ -275,10 +281,11 @@ const SetInjectionLevel: ResponseMiddleware = function () { return 'partial' } + const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin(this.req.proxiedUrl) const isHTML = resContentTypeIs(this.incomingRes, 'text/html') const isAUTFrame = this.req.isAUTFrame - if (this.config.experimentalMultiDomain && !isReqMatchOriginPolicy && isAUTFrame && (isHTML || isRenderedHTML)) { + if (this.config.experimentalMultiDomain && isSecondaryOrigin && isAUTFrame && (isHTML || isRenderedHTML)) { this.debug('- multi-domain injection') return 'fullMultiDomain' @@ -397,7 +404,7 @@ const determineIfNeedsMultiDomainHandling = (ctx: HttpMiddlewareThis {} context('network stubbing', () => { let config - let remoteState + let remoteStates: RemoteStates let netStubbingState: NetStubbingState let app let destinationApp @@ -27,7 +28,7 @@ context('network stubbing', () => { beforeEach((done) => { config = {} - remoteState = {} + remoteStates = new RemoteStates(() => {}) socket = new EventEmitter() socket.toDriver = sinon.stub() app = express() @@ -39,7 +40,7 @@ context('network stubbing', () => { config, middleware: defaultMiddleware, getCurrentBrowser: () => ({ family: 'chromium' }), - getRemoteState: () => remoteState, + remoteStates, getFileServerToken: () => 'fake-token', request: new Request(), getRenderedHTMLOrigins: () => ({}), @@ -62,6 +63,7 @@ context('network stubbing', () => { server = allowDestroy(destinationApp.listen(() => { destinationPort = server.address().port + remoteStates.set(`http://localhost:${destinationPort}`) done() })) }) @@ -71,26 +73,12 @@ context('network stubbing', () => { }) it('can make a vanilla request', (done) => { - remoteState.strategy = 'http' - remoteState.props = { - port: `${destinationPort}`, - tld: 'localhost', - domain: '', - } - supertest(app) .get(`/http://localhost:${destinationPort}`) .expect('it worked', done) }) it('does not add CORS headers to all responses', () => { - remoteState.strategy = 'http' - remoteState.props = { - port: `${destinationPort}`, - tld: 'localhost', - domain: '', - } - return supertest(app) .get(`/http://localhost:${destinationPort}`) .then((res) => { @@ -241,13 +229,6 @@ context('network stubbing', () => { }) }) - remoteState.strategy = 'http' - remoteState.props = { - port: `${destinationPort}`, - tld: 'localhost', - domain: '', - } - // capture unintercepted content-length await supertest(app) .post(`/http://localhost:${destinationPort}`) diff --git a/packages/proxy/test/unit/http/helpers.ts b/packages/proxy/test/unit/http/helpers.ts index a901078a68a9..654ffc710d30 100644 --- a/packages/proxy/test/unit/http/helpers.ts +++ b/packages/proxy/test/unit/http/helpers.ts @@ -1,4 +1,4 @@ -import { HttpMiddleware, _runStage } from '../../../lib/http' +import { HttpMiddleware, HttpStages, _runStage } from '../../../lib/http' export function testMiddleware (middleware: HttpMiddleware[], ctx = {}) { const fullCtx = { @@ -6,7 +6,6 @@ export function testMiddleware (middleware: HttpMiddleware[], ctx = {}) { req: {}, res: {}, config: {}, - getRemoteState: () => {}, middleware: { 0: middleware, @@ -15,5 +14,9 @@ export function testMiddleware (middleware: HttpMiddleware[], ctx = {}) { ...ctx, } - return _runStage(0, fullCtx) + const onError = (error) => { + throw error + } + + return _runStage(HttpStages.IncomingRequest, fullCtx, onError) } diff --git a/packages/proxy/test/unit/http/index.spec.ts b/packages/proxy/test/unit/http/index.spec.ts index 4055144e398e..5b9a4c49515d 100644 --- a/packages/proxy/test/unit/http/index.spec.ts +++ b/packages/proxy/test/unit/http/index.spec.ts @@ -5,7 +5,6 @@ import sinon from 'sinon' describe('http', function () { context('Http.handle', function () { let config - let getRemoteState let middleware let incomingRequest let incomingResponse @@ -14,8 +13,6 @@ describe('http', function () { beforeEach(function () { config = {} - getRemoteState = sinon.stub().returns({}) - incomingRequest = sinon.stub() incomingResponse = sinon.stub() error = sinon.stub() @@ -26,7 +23,7 @@ describe('http', function () { [HttpStages.Error]: [error], } - httpOpts = { config, getRemoteState, middleware } + httpOpts = { config, middleware } }) it('calls IncomingRequest stack, then IncomingResponse stack', function () { @@ -99,7 +96,7 @@ describe('http', function () { const resAdded = {} const errorAdded = {} - let expectedKeys = ['req', 'res', 'config', 'getRemoteState', 'middleware'] + let expectedKeys = ['req', 'res', 'config', 'middleware'] incomingRequest.callsFake(function () { expect(this).to.include.keys(expectedKeys) diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts index 8527afa113e8..1e3f6373bd18 100644 --- a/packages/proxy/test/unit/http/request-middleware.spec.ts +++ b/packages/proxy/test/unit/http/request-middleware.spec.ts @@ -4,6 +4,7 @@ import { expect } from 'chai' import { testMiddleware } from './helpers' import { CypressIncomingRequest, CypressOutgoingResponse } from '../../../lib' import { HttpBuffer, HttpBuffers } from '../../../lib/http/util/buffers' +import { RemoteStates } from '@packages/server/lib/remote_states' describe('http/request-middleware', () => { it('exports the members in the correct order', () => { @@ -119,4 +120,117 @@ describe('http/request-middleware', () => { }) }) }) + + describe('MaybeSetBasicAuthHeaders', () => { + const { MaybeSetBasicAuthHeaders } = RequestMiddleware + + it('adds auth header from remote state', async () => { + const headers = {} + const remoteStates = new RemoteStates(() => {}) + + remoteStates.set('https://www.cypress.io/', { auth: { username: 'u', password: 'p' } }) + + const ctx = { + req: { + proxiedUrl: 'https://www.cypress.io/', + headers, + }, + res: {} as Partial, + remoteStates, + } + + await testMiddleware([MaybeSetBasicAuthHeaders], ctx) + .then(() => { + const expectedAuthHeader = `Basic ${Buffer.from('u:p').toString('base64')}` + + expect(ctx.req.headers['authorization']).to.equal(expectedAuthHeader) + }) + }) + + it('does not add auth header if origins do not match', async () => { + const headers = {} + const remoteStates = new RemoteStates(() => {}) + + remoteStates.set('https://cypress.io/', { auth: { username: 'u', password: 'p' } }) // does not match due to subdomain + + const ctx = { + req: { + proxiedUrl: 'https://www.cypress.io/', + headers, + }, + res: {} as Partial, + remoteStates, + } + + await testMiddleware([MaybeSetBasicAuthHeaders], ctx) + .then(() => { + expect(ctx.req.headers['authorization']).to.be.undefined + }) + }) + + it('does not add auth header if remote does not have auth', async () => { + const headers = {} + const remoteStates = new RemoteStates(() => {}) + + remoteStates.set('https://www.cypress.io/') + + const ctx = { + req: { + proxiedUrl: 'https://www.cypress.io/', + headers, + }, + res: {} as Partial, + remoteStates, + } + + await testMiddleware([MaybeSetBasicAuthHeaders], ctx) + .then(() => { + expect(ctx.req.headers['authorization']).to.be.undefined + }) + }) + + it('does not add auth header if remote not found', async () => { + const headers = {} + const remoteStates = new RemoteStates(() => {}) + + remoteStates.set('http://localhost:3500', { auth: { username: 'u', password: 'p' } }) + + const ctx = { + req: { + proxiedUrl: 'https://www.cypress.io/', + headers, + }, + res: {} as Partial, + remoteStates, + } + + await testMiddleware([MaybeSetBasicAuthHeaders], ctx) + .then(() => { + expect(ctx.req.headers['authorization']).to.be.undefined + }) + }) + + it('does not update auth header from remote if request already has auth', async () => { + const headers = { + authorization: 'token', + } + const remoteStates = new RemoteStates(() => {}) + + remoteStates.set('https://www.cypress.io/', { auth: { username: 'u', password: 'p' } }) + + const ctx = { + req: { + proxiedUrl: 'https://www.cypress.io/', + headers, + }, + res: {} as Partial, + remoteStates, + } + + await testMiddleware([MaybeSetBasicAuthHeaders], ctx) + .then(() => { + expect(ctx.req.headers['authorization']).to.equal('token') + }) + }) + }) }) diff --git a/packages/proxy/test/unit/http/response-middleware.spec.ts b/packages/proxy/test/unit/http/response-middleware.spec.ts index 700bd94899db..8eee5d04eb38 100644 --- a/packages/proxy/test/unit/http/response-middleware.spec.ts +++ b/packages/proxy/test/unit/http/response-middleware.spec.ts @@ -3,9 +3,9 @@ import ResponseMiddleware from '../../../lib/http/response-middleware' import { debugVerbose } from '../../../lib/http' import { expect } from 'chai' import sinon from 'sinon' -import { - testMiddleware, -} from './helpers' +import { testMiddleware } from './helpers' +import { RemoteStates } from '@packages/server/lib/remote_states' +import EventEmitter from 'events' describe('http/response-middleware', function () { it('exports the members in the correct order', function () { @@ -173,6 +173,57 @@ describe('http/response-middleware', function () { }) }) + it('doesn\'t do anything when request is for a previous origin in the stack', function () { + prepareContext({ + req: { + isAUTFrame: true, + proxiedUrl: 'http://www.foobar.com/test', + }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + secondaryOrigins: ['http://foobar.com', 'http://example.com'], + config: { + experimentalMultiDomain: true, + }, + }) + + return testMiddleware([MaybeDelayForMultiDomain], ctx) + .then(() => { + expect(ctx.serverBus.emit).not.to.be.called + }) + }) + + it('waits for server signal if req is not of a previous origin, letting it continue after receiving ready:for:domain', function () { + prepareContext({ + req: { + isAUTFrame: true, + proxiedUrl: 'http://www.idp.com/test', + }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + secondaryOrigins: ['http://foobar.com', 'http://example.com'], + config: { + experimentalMultiDomain: true, + }, + }) + + const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) + + expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'http://www.idp.com/test' }) + + ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]({ originPolicy: 'http://idp.com' }) + + expect(ctx.res.wantsInjection).to.equal('fullMultiDomain') + + return promise + }) + it('waits for server signal if res is html, letting it continue after receiving ready:for:domain', function () { prepareContext({ incomingRes: { @@ -182,7 +233,7 @@ describe('http/response-middleware', function () { }, req: { isAUTFrame: true, - proxiedUrl: 'protocol://host/originalUrl', + proxiedUrl: 'http://www.foobar.com/test', }, config: { experimentalMultiDomain: true, @@ -191,9 +242,9 @@ describe('http/response-middleware', function () { const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) - expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'protocol://host/originalUrl' }) + expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'http://www.foobar.com/test' }) - ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]() + ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]({ originPolicy: 'http://foobar.com' }) return promise }) @@ -208,8 +259,36 @@ describe('http/response-middleware', function () { ], }, isAUTFrame: true, - proxiedUrl: 'protocol://host/originalUrl', + proxiedUrl: 'http://www.foobar.com/test', + }, + config: { + experimentalMultiDomain: true, + }, + }) + + const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) + + expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'http://www.foobar.com/test' }) + + ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]({ originPolicy: 'http://foobar.com' }) + + expect(ctx.res.wantsInjection).to.equal('fullMultiDomain') + + return promise + }) + + it('waits for server signal, letting it continue after receiving ready:for:domain failed', function () { + prepareContext({ + req: { + isAUTFrame: true, + proxiedUrl: 'http://www.idp.com/test', }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + secondaryOrigins: ['http://foobar.com', 'http://example.com'], config: { experimentalMultiDomain: true, }, @@ -217,9 +296,9 @@ describe('http/response-middleware', function () { const promise = testMiddleware([MaybeDelayForMultiDomain], ctx) - expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'protocol://host/originalUrl' }) + expect(ctx.serverBus.emit).to.be.calledWith('cross:domain:delaying:html', { href: 'http://www.idp.com/test' }) - ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]() + ctx.serverBus.once.withArgs('ready:for:domain').args[0][1]({ failed: true }) expect(ctx.res.wantsInjection).to.be.undefined @@ -227,6 +306,18 @@ describe('http/response-middleware', function () { }) function prepareContext (props) { + const remoteStates = new RemoteStates(() => {}) + const eventEmitter = new EventEmitter() + + // set the primary remote state + remoteStates.set('http://127.0.0.1:3501') + + // set the secondary remote states + remoteStates.addEventListeners(eventEmitter) + props.secondaryOrigins?.forEach((originPolicy) => { + eventEmitter.emit('ready:for:domain', { originPolicy }) + }) + ctx = { incomingRes: { headers: {}, @@ -245,11 +336,7 @@ describe('http/response-middleware', function () { emit: sinon.stub(), once: sinon.stub(), }, - getRemoteState () { - return { - strategy: 'foo', - } - }, + remoteStates, debug () {}, onError (error) { throw error @@ -353,6 +440,32 @@ describe('http/response-middleware', function () { 'content-type': 'text/html', }, }, + secondaryOrigins: ['http://foobar.com'], + config: { + experimentalMultiDomain: true, + }, + }) + + return testMiddleware([SetInjectionLevel], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.equal('fullMultiDomain') + }) + }) + + it('injects "fullMultiDomain" when request is in origin stack for cross-domain html"', function () { + prepareContext({ + req: { + proxiedUrl: 'http://example.com', + isAUTFrame: true, + cookies: {}, + headers: {}, + }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + secondaryOrigins: ['http://example.com', 'http://foobar.com'], config: { experimentalMultiDomain: true, }, @@ -416,6 +529,40 @@ describe('http/response-middleware', function () { }) }) + it('injects partial when request is for top-level origin', function () { + prepareContext({ + renderedHTMLOrigins: {}, + getRenderedHTMLOrigins () { + return this.renderedHTMLOrigins + }, + req: { + proxiedUrl: 'http://127.0.0.1:3501/', + isAUTFrame: true, + cookies: {}, + headers: { + 'accept': [ + 'text/html', + 'application/xhtml+xml', + ], + }, + }, + incomingRes: { + headers: { + 'content-type': 'text/html', + }, + }, + secondaryOrigins: ['http://foobar.com'], + config: { + experimentalMultiDomain: true, + }, + }) + + return testMiddleware([SetInjectionLevel], ctx) + .then(() => { + expect(ctx.res.wantsInjection).to.equal('partial') + }) + }) + it('does not set Origin-Agent-Cluster header to false when injection is not expected', function () { prepareContext({}) @@ -442,6 +589,18 @@ describe('http/response-middleware', function () { }) function prepareContext (props) { + const remoteStates = new RemoteStates(() => {}) + const eventEmitter = new EventEmitter() + + // set the primary remote state + remoteStates.set('http://127.0.0.1:3501') + + // set the secondary remote states + remoteStates.addEventListeners(eventEmitter) + props.secondaryOrigins?.forEach((originPolicy) => { + eventEmitter.emit('ready:for:domain', { originPolicy }) + }) + ctx = { incomingRes: { headers: {}, @@ -460,14 +619,7 @@ describe('http/response-middleware', function () { }, ...props.req, }, - getRemoteState () { - return { - strategy: 'http', - props: { - port: '3501', tld: '127.0.0.1', domain: '', - }, - } - }, + remoteStates, debug: (formatter, ...args) => { debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args) }, @@ -577,13 +729,6 @@ describe('http/response-middleware', function () { getPreviousAUTRequestUrl () { return 'https://different.site' }, - getRemoteState () { - // nonsense, but it's the simplest way to match origin policy - return { - strategy: 'file', - origin: 'http', - } - }, }) await testMiddleware([CopyCookiesFromIncomingRes], ctx) @@ -622,17 +767,11 @@ describe('http/response-middleware', function () { }, req: { isAUTFrame: true, + proxiedUrl: 'http://www.foobar.com/multi-domain.html', }, res: { append: appendStub, }, - getRemoteState () { - // nonsense, but it's the simplest way to match origin policy - return { - strategy: 'file', - origin: 'http', - } - }, }) await testMiddleware([CopyCookiesFromIncomingRes], ctx) @@ -651,17 +790,11 @@ describe('http/response-middleware', function () { }, req: { isAUTFrame: true, + proxiedUrl: 'http://www.foobar.com/multi-domain.html', }, res: { append: appendStub, }, - getRemoteState () { - // nonsense, but it's the simplest way to match origin policy - return { - strategy: 'file', - origin: 'http', - } - }, }) ctx.getPreviousAUTRequestUrl = () => ctx.req.proxiedUrl @@ -778,6 +911,18 @@ describe('http/response-middleware', function () { }) function prepareContext (props) { + const remoteStates = new RemoteStates(() => {}) + const eventEmitter = new EventEmitter() + + // set the primary remote state + remoteStates.set('http://foobar.com') + + // set the secondary remote states + remoteStates.addEventListeners(eventEmitter) + props.secondaryOrigins?.forEach((originPolicy) => { + eventEmitter.emit('ready:for:domain', { originPolicy }) + }) + return { incomingRes: { headers: {}, @@ -789,7 +934,7 @@ describe('http/response-middleware', function () { ...props.res, }, req: { - proxiedUrl: 'http:127.0.0.1:3501/multi-domain.html', + proxiedUrl: 'http://127.0.0.1:3501/multi-domain.html', headers: {}, ...props.req, }, @@ -805,11 +950,7 @@ describe('http/response-middleware', function () { return { family: 'chromium' } }, getPreviousAUTRequestUrl () {}, - getRemoteState () { - return { - strategy: 'foo', - } - }, + remoteStates, debug () {}, onError (error) { throw error @@ -841,33 +982,3 @@ describe('http/response-middleware', function () { } }) }) - -// beforeEach(function () { -// ctx = { -// req: { -// proxiedUrl: 'http://proxy.com', -// cookies: { -// '__cypress.initial': true, -// }, -// headers: { -// accept: ['text/html', 'application/xhtml+xml'], -// }, -// }, -// res: { -// setHeader: sinon.stub(), -// }, -// getRemoteState: () => { -// return { -// strategy: 'http', -// props: { -// domain: 'proxy', -// port: '80', -// tld: 'com', -// }, -// } -// }, -// getRenderedHTMLOrigins: () => { -// return {} -// }, -// } -// }) diff --git a/packages/runner/injection/main.js b/packages/runner/injection/main.js index 17d0d2f38f20..ad3eb91de45a 100644 --- a/packages/runner/injection/main.js +++ b/packages/runner/injection/main.js @@ -13,7 +13,7 @@ const Cypress = window.Cypress = parent.Cypress if (!Cypress) { throw new Error('Something went terribly wrong and we cannot proceed. We expected to find the global \ - Cypress in the parent window but it is missing. This should never happen and likely is a bug. Please open an issue.') +Cypress in the parent window but it is missing. This should never happen and likely is a bug. Please open an issue.') } // We wrap timers in the injection code because if we do it in the driver (like diff --git a/packages/runner/injection/multi-domain.js b/packages/runner/injection/multi-domain.js index 920a6a22c688..d36c1b1597c8 100644 --- a/packages/runner/injection/multi-domain.js +++ b/packages/runner/injection/multi-domain.js @@ -33,7 +33,7 @@ const Cypress = findCypress() // This may not be needed if we defer to the first command if (!Cypress) { throw new Error('Something went terribly wrong and we cannot proceed. We expected to find the global \ - Cypress in the spec bridge window but it is missing.') +Cypress in the spec bridge window but it is missing.') } // the timers are wrapped in the injection code similar to the primary domain diff --git a/packages/server/index.d.ts b/packages/server/index.d.ts index 5b3c15366d63..26b8a0ecc533 100644 --- a/packages/server/index.d.ts +++ b/packages/server/index.d.ts @@ -15,8 +15,6 @@ // types for the `server` package export namespace CyServer { - export type getRemoteState = () => Cypress.RemoteState - // TODO: pull this from main types export interface Config { blockHosts: string | string[] diff --git a/packages/server/lib/controllers/files.js b/packages/server/lib/controllers/files.js index c67c5f01b219..2064d0e0078b 100644 --- a/packages/server/lib/controllers/files.js +++ b/packages/server/lib/controllers/files.js @@ -22,7 +22,7 @@ module.exports = { }) }, - handleIframe (req, res, config, getRemoteState, extraOptions) { + handleIframe (req, res, config, remoteStates, extraOptions) { const test = req.params[0] const iframePath = cwd('lib', 'html', 'iframe.html') const specFilter = _.get(extraOptions, 'specFilter') @@ -39,7 +39,7 @@ module.exports = { const iframeOptions = { title: this.getTitle(test), - domain: getRemoteState().domainName, + domain: remoteStates.current().domainName, scripts: JSON.stringify(allFilesToSend), } diff --git a/packages/server/lib/controllers/iframes.ts b/packages/server/lib/controllers/iframes.ts index 3d807ae4cb65..fc2edebb8853 100644 --- a/packages/server/lib/controllers/iframes.ts +++ b/packages/server/lib/controllers/iframes.ts @@ -3,6 +3,7 @@ import type httpProxy from 'http-proxy' import Debug from 'debug' import files from './files' import type { Cfg } from '../project-base' +import type { RemoteStates } from '../remote_states' const debug = Debug('cypress:server:iframes') @@ -11,7 +12,7 @@ interface IFramesController { } interface E2E extends IFramesController { - getRemoteState: () => any + remoteStates: RemoteStates getSpec: () => Cypress.Cypress['spec'] | null } @@ -20,7 +21,7 @@ interface CT extends IFramesController { } export const iframesController = { - e2e: ({ getSpec, getRemoteState, config }: E2E, req: Request, res: Response) => { + e2e: ({ getSpec, remoteStates, config }: E2E, req: Request, res: Response) => { const extraOptions = { specFilter: getSpec()?.specFilter, specType: 'integration', @@ -37,7 +38,7 @@ export const iframesController = { // https://github.com/cypress-io/cypress/issues/20147 res.setHeader('Origin-Agent-Cluster', '?0') - files.handleIframe(req, res, config, getRemoteState, extraOptions) + files.handleIframe(req, res, config, remoteStates, extraOptions) }, component: ({ config, nodeProxy }: CT, req: Request, res: Response) => { diff --git a/packages/server/lib/controllers/runner.ts b/packages/server/lib/controllers/runner.ts index df0091b1668e..d12ce38bba78 100644 --- a/packages/server/lib/controllers/runner.ts +++ b/packages/server/lib/controllers/runner.ts @@ -22,7 +22,7 @@ const _serveNonProxiedError = (res: Response) => { }) } -export interface ServeOptions extends Pick { +export interface ServeOptions extends Pick { testingType: Cypress.TestingType } @@ -53,7 +53,7 @@ export const runner = { return _serveNonProxiedError(res) } - let { config, getRemoteState, getCurrentBrowser, getSpec, specsStore, exit } = options + let { config, remoteStates, getCurrentBrowser, getSpec, specsStore, exit } = options config = _.clone(config) // at any given point, rather than just arbitrarily modifying it. @@ -69,7 +69,7 @@ export const runner = { // } // TODO: Find out what the problem. if (options.testingType === 'e2e') { - config.remote = getRemoteState() + config.remote = remoteStates.current() } config.version = pkg.version diff --git a/packages/server/lib/remote_states.ts b/packages/server/lib/remote_states.ts new file mode 100644 index 000000000000..a43c997b4940 --- /dev/null +++ b/packages/server/lib/remote_states.ts @@ -0,0 +1,186 @@ +import { cors } from '@packages/network' +import origin from './util/origin' +import Debug from 'debug' +import _ from 'lodash' +import type EventEmitter from 'events' + +const DEFAULT_DOMAIN_NAME = 'localhost' +const fullyQualifiedRe = /^https?:\/\// + +const debug = Debug('cypress:server:remote-states') + +/** + * Class to maintain and manage the remote states of the server. + * + * Example file remote state: + * { + * auth: { + * username: 'name' + * password: 'pass' + * } + * origin: "http://localhost:2020" + * fileServer: "http://localhost:2021" + * strategy: "file" + * domainName: "localhost" + * props: null + * } + * + * Example http remote state: + * { + * auth: { + * username: 'name' + * password: 'pass' + * } + * origin: "https://foo.google.com" + * fileServer: null + * strategy: "http" + * domainName: "google.com" + * props: { + * port: 443 + * tld: "com" + * domain: "google" + * } + * } + */ +export class RemoteStates { + private remoteStates: Map = new Map() + private originStack: string[] = [] + private configure: () => { serverPort: number, fileServerPort: number } + private _config: { serverPort: number, fileServerPort: number } | undefined + + constructor (configure) { + this.configure = configure + } + + get (url: string) { + const state = this.remoteStates.get(cors.getOriginPolicy(url)) + + debug('Getting remote state: %o for: %s', state, url) + + return _.cloneDeep(state) + } + + isInOriginStack (url: string): boolean { + return this.originStack.includes(cors.getOriginPolicy(url)) + } + + isSecondaryOrigin (url: string): boolean { + // start at 1 to exclude the primary origin + return this.originStack.indexOf(cors.getOriginPolicy(url), 1) !== -1 + } + + isPrimaryOrigin (url: string): boolean { + return this.originStack[0] === cors.getOriginPolicy(url) + } + + reset () { + debug('Resetting remote state') + + const stateArray = Array.from(this.remoteStates.entries()) + + this.remoteStates = new Map([stateArray[0]]) + + this.originStack = [stateArray[0][0]] + } + + current (): Cypress.RemoteState { + return this.get(this.originStack[this.originStack.length - 1]) as Cypress.RemoteState + } + + set (urlOrState: string | Cypress.RemoteState, options: { auth?: {}, isMultiDomain?: boolean } = {}): Cypress.RemoteState { + let state + + if (_.isString(urlOrState)) { + const remoteOrigin = origin(urlOrState) + const remoteProps = cors.parseUrlIntoDomainTldPort(remoteOrigin) + + if ((urlOrState === '') || !fullyQualifiedRe.test(urlOrState)) { + state = { + auth: options.auth, + origin: `http://${DEFAULT_DOMAIN_NAME}:${this.config.serverPort}`, + strategy: 'file', + fileServer: _.compact([`http://${DEFAULT_DOMAIN_NAME}`, this.config.fileServerPort]).join(':'), + domainName: DEFAULT_DOMAIN_NAME, + props: null, + } + } else { + state = { + auth: options.auth, + origin: remoteOrigin, + strategy: 'http', + fileServer: null, + domainName: cors.getDomainNameFromParsedHost(remoteProps), + props: remoteProps, + } + } + } else { + state = urlOrState + } + + const remoteOriginPolicy = cors.getOriginPolicy(state.origin) + + if (options.isMultiDomain) { + this.remoteStates.set(remoteOriginPolicy, state) + } else { + // convert map to array + const stateArray = Array.from(this.remoteStates.entries()) + + // set the primary remote state and convert back to map + stateArray[0] = [remoteOriginPolicy, state] + this.remoteStates = new Map(stateArray) + + // automatically update the primary origin stack + this.originStack[0] = remoteOriginPolicy + } + + debug('Setting remote state %o for %s', state, remoteOriginPolicy) + + return this.get(remoteOriginPolicy) as Cypress.RemoteState + } + + addEventListeners (eventEmitter: EventEmitter) { + eventEmitter.on('ready:for:domain', ({ originPolicy, failed }) => { + if (failed) return + + const existingOrigin = this.remoteStates.get(originPolicy) + + // since this is just the switchToDomain starting, we don't want to override + // the existing origin if it already exists + if (!existingOrigin) { + this.set(originPolicy, { isMultiDomain: true }) + } + + this.addOrigin(originPolicy) + }) + + eventEmitter.on('cross:origin:finished', (originPolicy) => { + this.removeCurrentOrigin(originPolicy) + }) + } + + private get config () { + if (!this._config) { + this._config = this.configure() + } + + return this._config + } + + private addOrigin (originPolicy) { + this.originStack.push(originPolicy) + + debug('Added origin: ', originPolicy) + } + + private removeCurrentOrigin (originPolicy) { + const currentOriginPolicy = this.originStack[this.originStack.length - 1] + + if (originPolicy !== currentOriginPolicy) { + throw new Error(`Tried to remove origin ${originPolicy} but ${currentOriginPolicy} was found. This should never happen and likely is a bug. Please open an issue.`) + } + + this.originStack.pop() + + debug('Removed current origin: ', originPolicy) + } +} diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index f90a53102098..860054bd09b7 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -9,6 +9,7 @@ import type { Cfg } from './project-base' import xhrs from './controllers/xhrs' import { runner } from './controllers/runner' import { iframesController } from './controllers/iframes' +import type { RemoteStates } from './remote_states' const debug = Debug('cypress:server:routes') @@ -19,7 +20,7 @@ export interface InitializeRoutes { getCurrentBrowser: () => Browser nodeProxy: httpProxy networkProxy: NetworkProxy - getRemoteState: () => Cypress.RemoteState + remoteStates: RemoteStates onError: (...args: unknown[]) => any testingType: Cypress.TestingType exit?: boolean @@ -32,7 +33,7 @@ export const createCommonRoutes = ({ getSpec, getCurrentBrowser, specsStore, - getRemoteState, + remoteStates, nodeProxy, exit, }: InitializeRoutes) => { @@ -48,7 +49,10 @@ export const createCommonRoutes = ({ router.get('/__cypress/iframes/*', (req, res) => { if (testingType === 'e2e') { - iframesController.e2e({ config, getSpec, getRemoteState }, req, res) + // ensure the remote state gets cleaned up from any previous tests/runs + remoteStates.reset() + + iframesController.e2e({ config, getSpec, remoteStates }, req, res) } if (testingType === 'component') { @@ -65,12 +69,15 @@ export const createCommonRoutes = ({ router.get(clientRoute, (req, res) => { debug('Serving Cypress front-end by requested URL:', req.url) + // ensure the remote state gets cleaned up from any previous tests/runs + remoteStates.reset() + runner.serve(req, res, testingType === 'e2e' ? 'runner' : 'runner-ct', { config, testingType, getSpec, getCurrentBrowser, - getRemoteState, + remoteStates, specsStore, exit, }) diff --git a/packages/server/lib/server-base.ts b/packages/server/lib/server-base.ts index 07a1f1c17b79..1dab9de7ec37 100644 --- a/packages/server/lib/server-base.ts +++ b/packages/server/lib/server-base.ts @@ -13,7 +13,7 @@ import url from 'url' import la from 'lazy-ass' import type httpsProxy from '@packages/https-proxy' import { netStubbingState, NetStubbingState } from '@packages/net-stubbing' -import { agent, clientCertificates, cors, httpUtils, uri, ParsedHost } from '@packages/network' +import { agent, clientCertificates, cors, httpUtils, uri } from '@packages/network' import { NetworkProxy, BrowserPreRequest } from '@packages/proxy' import type { SocketCt } from './socket-ct' import errors from './errors' @@ -22,7 +22,6 @@ import Request from './request' import type { SocketE2E } from './socket-e2e' import templateEngine from './template_engine' import { ensureProp } from './util/class-helpers' -import origin from './util/origin' import { allowDestroy, DestroyableHttpServer } from './util/server_destroy' import { SocketAllowed } from './util/socket_allowed' import { createInitialWorkers } from '@packages/rewriter' @@ -32,6 +31,7 @@ import type { Browser } from '@packages/server/lib/browsers/types' import { InitializeRoutes, createCommonRoutes } from './routes' import { createRoutesE2E } from './routes-e2e' import { createRoutesCT } from './routes-ct' +import { RemoteStates } from './remote_states' const ALLOWED_PROXY_BYPASS_URLS = [ '/', @@ -39,8 +39,6 @@ const ALLOWED_PROXY_BYPASS_URLS = [ '/__cypress/runner/cypress_runner.js', // TODO: fix this '/__cypress/runner/favicon.ico', ] -const DEFAULT_DOMAIN_NAME = 'localhost' -const fullyQualifiedRe = /^https?:\/\// const debug = Debug('cypress:server:server-base') @@ -120,14 +118,7 @@ export abstract class ServerBase { protected _netStubbingState?: NetStubbingState protected _httpsProxy?: httpsProxy protected _eventBus: EventEmitter - - protected _remoteAuth: unknown - protected _remoteProps: ParsedHost | undefined | null - protected _remoteOrigin: unknown - protected _remoteStrategy: unknown - protected _remoteVisitingUrl: unknown - protected _remoteDomainName: unknown - protected _remoteFileServer: unknown + protected remoteStates: RemoteStates constructor () { this.isListening = false @@ -139,12 +130,11 @@ export abstract class ServerBase { this._baseUrl = null this._fileServer = null - this._eventBus.on('cross:domain:delaying:html', (request) => { - this.socket.localBus.once('ready:for:domain', () => { - this._eventBus.emit('ready:for:domain') - }) - - this.socket.toDriver('cross:domain:delaying:html', request) + this.remoteStates = new RemoteStates(() => { + return { + serverPort: this._port(), + fileServerPort: this._fileServer?.port(), + } }) } @@ -174,6 +164,16 @@ export abstract class ServerBase { return this.ensureProp(this._httpsProxy, 'open') } + setupMultiDomain () { + this._eventBus.on('cross:domain:delaying:html', (request) => { + this.socket.localBus.once('ready:for:domain', (args) => { + this._eventBus.emit('ready:for:domain', args) + }) + + this.socket.toDriver('cross:domain:delaying:html', request) + }) + } + abstract createServer ( app: Express, config: Cfg, @@ -212,11 +212,7 @@ export abstract class ServerBase { clientCertificates.loadClientCertificateConfig(config) - const getRemoteState = () => { - return this._getRemoteState() - } - - this.createNetworkProxy({ config, getCurrentBrowser, getRemoteState, shouldCorrelatePreRequests }) + this.createNetworkProxy({ config, getCurrentBrowser, remoteStates: this.remoteStates, shouldCorrelatePreRequests }) if (config.experimentalSourceRewriting) { createInitialWorkers() @@ -227,7 +223,7 @@ export abstract class ServerBase { const routeOptions: InitializeRoutes = { config, specsStore, - getRemoteState, + remoteStates: this.remoteStates, nodeProxy: this.nodeProxy, networkProxy: this._networkProxy!, onError, @@ -237,6 +233,9 @@ export abstract class ServerBase { exit, } + this.setupMultiDomain() + this.remoteStates.addEventListeners(this.socket.localBus) + const runnerSpecificRouter = testingType === 'e2e' ? createRoutesE2E(routeOptions) : createRoutesCT(routeOptions) @@ -304,7 +303,7 @@ export abstract class ServerBase { return e } - createNetworkProxy ({ config, getCurrentBrowser, getRemoteState, shouldCorrelatePreRequests }) { + createNetworkProxy ({ config, getCurrentBrowser, remoteStates, shouldCorrelatePreRequests }) { const getFileServerToken = () => { return this._fileServer.token } @@ -315,7 +314,7 @@ export abstract class ServerBase { config, shouldCorrelatePreRequests, getCurrentBrowser, - getRemoteState, + remoteStates, getFileServerToken, socket: this.socket, netStubbingState: this.netStubbingState, @@ -332,6 +331,7 @@ export abstract class ServerBase { options.onResetServerState = () => { this.networkProxy.reset() this.netStubbingState.reset() + this.remoteStates.reset() } const io = this.socket.startListening(this.server, automation, config, options) @@ -364,7 +364,7 @@ export abstract class ServerBase { return svr } - _port () { + _port = () => { return (this.server.address() as AddressInfo).port } @@ -417,86 +417,6 @@ export abstract class ServerBase { }) } - _getRemoteState (): Cypress.RemoteState { - // { - // origin: "http://localhost:2020" - // fileServer: - // strategy: "file" - // domainName: "localhost" - // props: null - // } - - // { - // origin: "https://foo.google.com" - // strategy: "http" - // domainName: "google.com" - // props: { - // port: 443 - // tld: "com" - // domain: "google" - // } - // } - - const props = _.extend({}, { - auth: this._remoteAuth, - props: this._remoteProps, - origin: this._remoteOrigin, - strategy: this._remoteStrategy, - visiting: this._remoteVisitingUrl, - domainName: this._remoteDomainName, - fileServer: this._remoteFileServer, - }) as Cypress.RemoteState - - debug('Getting remote state: %o', props) - - return props - } - - _onDomainSet (fullyQualifiedUrl, options: Record = {}) { - const l = (type, val) => { - return debug('Setting', type, val) - } - - this._remoteAuth = options.auth - - l('remoteAuth', this._remoteAuth) - - // if this isn't a fully qualified url - // or if this came to us as in our tests - // then we know to go back to our default domain - // which is the localhost server - if ((fullyQualifiedUrl === '') || !fullyQualifiedRe.test(fullyQualifiedUrl)) { - this._remoteOrigin = `http://${DEFAULT_DOMAIN_NAME}:${this._port()}` - this._remoteStrategy = 'file' - this._remoteFileServer = `http://${DEFAULT_DOMAIN_NAME}:${(this._fileServer != null ? this._fileServer.port() : undefined)}` - this._remoteDomainName = DEFAULT_DOMAIN_NAME - this._remoteProps = null - - l('remoteOrigin', this._remoteOrigin) - l('remoteStrategy', this._remoteStrategy) - l('remoteHostAndPort', this._remoteProps) - l('remoteDocDomain', this._remoteDomainName) - l('remoteFileServer', this._remoteFileServer) - } else { - this._remoteOrigin = origin(fullyQualifiedUrl) - - this._remoteStrategy = 'http' - - this._remoteFileServer = null - - // set an object with port, tld, and domain properties - // as the remoteHostAndPort - this._remoteProps = cors.parseUrlIntoDomainTldPort(this._remoteOrigin) - this._remoteDomainName = cors.getDomainNameFromParsedHost(this._remoteProps) - - l('remoteOrigin', this._remoteOrigin) - l('remoteHostAndPort', this._remoteProps) - l('remoteDocDomain', this._remoteDomainName) - } - - return this._getRemoteState() - } - proxyWebsockets (proxy, socketIoRoute, req, socket, head) { // bail if this is our own namespaced socket.io request @@ -546,7 +466,7 @@ export abstract class ServerBase { const baseUrl = this._baseUrl ?? '' - return this._onDomainSet(baseUrl) + return this.remoteStates.set(baseUrl) } _close () { diff --git a/packages/server/lib/server-ct.ts b/packages/server/lib/server-ct.ts index b02eb49202c7..92295bcf5938 100644 --- a/packages/server/lib/server-ct.ts +++ b/packages/server/lib/server-ct.ts @@ -35,11 +35,10 @@ export class ServerCt extends ServerBase { .then((httpsProxy) => { this._httpsProxy = httpsProxy - // once we open set the domain - // to root by default + // once we open set the domain to root by default // which prevents a situation where navigating // to http sites redirects to /__/ cypress - this._onDomainSet(baseUrl) + this.remoteStates.set(baseUrl) return resolve([port]) }) diff --git a/packages/server/lib/server-e2e.ts b/packages/server/lib/server-e2e.ts index 2c9234a26a52..26d56cb39098 100644 --- a/packages/server/lib/server-e2e.ts +++ b/packages/server/lib/server-e2e.ts @@ -118,11 +118,10 @@ export class ServerE2E extends ServerBase { }) } }).then((warning) => { - // once we open set the domain - // to root by default + // once we open set the domain to root by default // which prevents a situation where navigating // to http sites redirects to /__/ cypress - this._onDomainSet(baseUrl != null ? baseUrl : '') + this.remoteStates.set(baseUrl != null ? baseUrl : '') return resolve([port, warning]) }) @@ -160,7 +159,8 @@ export class ServerE2E extends ServerBase { const request = this.request let handlingLocalFile = false - const previousState = _.clone(this._getRemoteState()) + const previousRemoteState = this.remoteStates.current() + const previousRemoteStateIsPrimary = this.remoteStates.isPrimaryOrigin(previousRemoteState.origin) // nuke any hashes from our url since // those those are client only and do @@ -209,22 +209,17 @@ export class ServerE2E extends ServerBase { options.headers['x-cypress-authorization'] = this._fileServer.token - this._remoteVisitingUrl = true + const state = this.remoteStates.set(urlStr, options) - this._onDomainSet(urlStr, options) - - // TODO: instead of joining remoteOrigin here - // we can simply join our fileServer origin - // and bypass all the remoteState.visiting shit - urlFile = url.resolve(this._remoteFileServer as string, urlStr) - urlStr = url.resolve(this._remoteOrigin as string, urlStr) + urlFile = url.resolve(state.fileServer as string, urlStr) + urlStr = url.resolve(state.origin as string, urlStr) } const onReqError = (err) => { // only restore the previous state // if our promise is still pending if (p.isPending()) { - restorePreviousState() + restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary) } return reject(err) @@ -251,8 +246,6 @@ export class ServerE2E extends ServerBase { domain: cors.getSuperDomain(newUrl), }) .then((cookies) => { - this._remoteVisitingUrl = false - const statusIs2xxOrAllowedFailure = () => { // is our status code in the 2xx range, or have we disabled failing // on status code? @@ -303,10 +296,9 @@ export class ServerE2E extends ServerBase { // when the server should cache the request buffer // and set the domain vs not if (isOk && details.isHtml) { - // reset the domain to the new url if we're not - // handling a local file and we aren't in multi-domain - if (!handlingLocalFile && !options.isMultiDomain) { - this._onDomainSet(newUrl, options) + // if we're not handling a local file set the remote state + if (!handlingLocalFile) { + this.remoteStates.set(newUrl as string, options) } const responseBufferStream = new stream.PassThrough({ @@ -326,7 +318,7 @@ export class ServerE2E extends ServerBase { } else { // TODO: move this logic to the driver too for // the same reasons listed above - restorePreviousState() + restorePreviousRemoteState(previousRemoteState, previousRemoteStateIsPrimary) } return resolve(details) @@ -338,14 +330,8 @@ export class ServerE2E extends ServerBase { }) } - const restorePreviousState = () => { - this._remoteAuth = previousState.auth - this._remoteProps = previousState.props - this._remoteOrigin = previousState.origin - this._remoteStrategy = previousState.strategy - this._remoteFileServer = previousState.fileServer - this._remoteDomainName = previousState.domainName - this._remoteVisitingUrl = previousState.visiting + const restorePreviousRemoteState = (previousRemoteState: Cypress.RemoteState, previousRemoteStateIsPrimary: boolean) => { + this.remoteStates.set(previousRemoteState, { isMultiDomain: !previousRemoteStateIsPrimary }) } // if they're POSTing an object, querystringify their POST body diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 995288ef4f46..d080e2a4b6a9 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -415,9 +415,9 @@ export class SocketBase { return } case 'ready:for:domain': - this.localBus.emit('ready:for:domain', args[0]) - - return + return this.localBus.emit('ready:for:domain', args[0]) + case 'cross:origin:finished': + return this.localBus.emit('cross:origin:finished', args[0]) default: throw new Error( `You requested a backend event we cannot handle: ${eventName}`, diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 0de9613604f1..7a9372147915 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -173,7 +173,7 @@ describe('Routes', () => { await this.server.startWebsockets(automationStub, config, {}) if (initialUrl) { - this.server._onDomainSet(initialUrl) + this.server.remoteStates.set(initialUrl) } this.srv = this.server.getHttpServer() @@ -223,7 +223,7 @@ describe('Routes', () => { // this tests a situation where we open our browser in another browser // without proxy mode set - it('redirects to config.clientRoute without a _remoteOrigin and without a proxy', function () { + it('redirects to config.clientRoute without a remote origin and without a proxy', function () { return this.rp({ url: this.proxy, proxy: null, @@ -235,10 +235,10 @@ describe('Routes', () => { }) }) - it('does not redirect with _remoteOrigin set', function () { + it('does not redirect with remote origin set', function () { return this.setup('http://www.github.com') .then(() => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/') .reply(200, '', { 'Content-Type': 'text/html', @@ -1318,7 +1318,7 @@ describe('Routes', () => { }) it('basic 200 html response', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/') .reply(200, 'hello from bar!', { 'Content-Type': 'text/html', @@ -1344,7 +1344,7 @@ describe('Routes', () => { }) it('unzips, injects, and then rezips initial content', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/gzip') .matchHeader('accept-encoding', 'gzip') .replyWithFile(200, Fixtures.path('server/gzip.html.gz'), { @@ -1371,7 +1371,7 @@ describe('Routes', () => { }) it('unzips, injects, and then rezips regular http content', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/gzip') .matchHeader('accept-encoding', 'gzip') .replyWithFile(200, Fixtures.path('server/gzip.html.gz'), { @@ -1398,7 +1398,7 @@ describe('Routes', () => { }) it('does not inject on regular gzip\'d content', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/gzip') .matchHeader('accept-encoding', 'gzip') .replyWithFile(200, Fixtures.path('server/gzip.html.gz'), { @@ -1477,7 +1477,7 @@ describe('Routes', () => { }) it('strips unsupported deflate and br encoding', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/accept') .matchHeader('accept-encoding', 'gzip') .reply(200, 'accept') @@ -1497,7 +1497,7 @@ describe('Routes', () => { }) it('removes accept-encoding when nothing is supported', function () { - nock(this.server._remoteOrigin, { + nock(this.server.remoteStates.current().origin, { badheaders: ['accept-encoding'], }) .get('/accept') @@ -1524,7 +1524,7 @@ describe('Routes', () => { }) it('sends back a 304', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/assets/app.js') .reply(304) @@ -1546,7 +1546,7 @@ describe('Routes', () => { }) it('passes the location header through', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo') .reply(302, undefined, { 'Location': '/', @@ -1573,7 +1573,7 @@ describe('Routes', () => { // this fixes improper url merge where we took query params // and added them needlessly it('doesnt redirect with query params or hashes which werent in location header', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo?bar=baz') .reply(302, undefined, { 'Location': '/css', @@ -1594,7 +1594,7 @@ describe('Routes', () => { }) it('does redirect with query params if location header includes them', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo?bar=baz') .reply(302, undefined, { 'Location': '/css?q=search', @@ -1619,7 +1619,7 @@ describe('Routes', () => { }) it('does redirect with query params to external domain if location header includes them', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo?bar=baz') .reply(302, undefined, { 'Location': 'https://www.google.com/search?q=cypress', @@ -1644,7 +1644,7 @@ describe('Routes', () => { }) it('sets cookies and removes __cypress.initial when initial is originally false', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/css') .reply(302, undefined, { 'Set-Cookie': 'foo=bar; Path=/', @@ -1670,7 +1670,7 @@ describe('Routes', () => { it(`handles direct for status code: ${code}`, function () { return this.setup('http://auth.example.com') .then(() => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(code, undefined, { Location: 'http://app.example.com/users/1', @@ -1698,7 +1698,7 @@ describe('Routes', () => { }) it('passes through status code + content', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .reply(500, 'server error', { 'Content-Type': 'text/html', @@ -1827,7 +1827,7 @@ describe('Routes', () => { }) it('transparently proxies decoding gzip failures', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), { 'Content-Type': 'text/html', @@ -1872,7 +1872,7 @@ describe('Routes', () => { describe('when initial is true', () => { it('sets back to false', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.html') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -1894,7 +1894,7 @@ describe('Routes', () => { describe('when initial is false', () => { it('does not reset initial or remoteHost', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.html') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -1916,7 +1916,7 @@ describe('Routes', () => { }) it('sends with Transfer-Encoding: chunked without Content-Length', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(200, Buffer.from('foo'), { 'Content-Type': 'text/html', @@ -1938,7 +1938,7 @@ describe('Routes', () => { }) it('does not have Content-Length', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(200, 'foo', { 'Content-Type': 'text/html', @@ -1961,7 +1961,7 @@ describe('Routes', () => { }) it('forwards cookies from incoming responses', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(200, 'OK', { 'set-cookie': 'userId=123', @@ -1981,7 +1981,7 @@ describe('Routes', () => { }) it('appends to previous cookies from incoming responses', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(200, '', { 'set-cookie': 'userId=123; Path=/', @@ -2006,7 +2006,7 @@ describe('Routes', () => { }) it('appends cookies on redirects', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/login') .reply(302, undefined, { 'location': '/dashboard', @@ -2032,7 +2032,7 @@ describe('Routes', () => { }) it('passes invalid cookies', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/invalid') .reply(200, 'OK', { 'set-cookie': [ @@ -2055,7 +2055,7 @@ describe('Routes', () => { }) it('forwards other headers from incoming responses', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/auth') .reply(200, 'OK', { 'x-token': 'abc-123', @@ -2075,7 +2075,7 @@ describe('Routes', () => { }) it('forwards headers to outgoing requests', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .matchHeader('x-custom', 'value') .reply(200, 'hello from bar!', { @@ -2097,7 +2097,7 @@ describe('Routes', () => { }) it('omits x-frame-options', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -2113,7 +2113,7 @@ describe('Routes', () => { }) it('omits content-security-policy', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -2134,7 +2134,7 @@ describe('Routes', () => { }) it('omits content-security-policy-report-only', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -2155,7 +2155,7 @@ describe('Routes', () => { }) it('omits document-domain from Feature-Policy header', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'OK', { 'Content-Type': 'text/html', @@ -2180,7 +2180,7 @@ describe('Routes', () => { it('does not modify host origin header', function () { return this.setup('http://foobar.com') .then(() => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/css') .matchHeader('host', 'foobar.com') .reply(200) @@ -2198,7 +2198,7 @@ describe('Routes', () => { }) it('does not cache when initial response', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/') .reply(200, 'hello from bar!', { 'Content-Type': 'text/html', @@ -2218,7 +2218,7 @@ describe('Routes', () => { }) it('does cache requesting resource without injection', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/') .reply(200, 'hello from bar!', { 'Content-Type': 'text/plain', @@ -2234,7 +2234,7 @@ describe('Routes', () => { }) it('forwards origin header', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo') .matchHeader('host', 'localhost:8080') .matchHeader('origin', 'http://localhost:8080') @@ -2326,10 +2326,12 @@ describe('Routes', () => { const base64 = Buffer.from(`${username}:${password}`).toString('base64') - this.server._remoteAuth = { - username, - password, - } + this.server.remoteStates.set('http://localhost:8080', { + auth: { + username, + password, + }, + }) nock('http://localhost:8080') .get('/index') @@ -2358,10 +2360,12 @@ describe('Routes', () => { it('does not modify existing auth headers when matching origin', function () { const existing = 'Basic asdf' - this.server._remoteAuth = { - username: 'u', - password: 'p', - } + this.server.remoteStates.set('http://localhost:8080', { + auth: { + username: 'u', + password: 'p', + }, + }) nock('http://localhost:8080') .get('/index') @@ -2388,10 +2392,12 @@ describe('Routes', () => { const base64 = Buffer.from(`${username}:${password}`).toString('base64') - this.server._remoteAuth = { - username, - password, - } + this.server.remoteStates.set('http://beta.something.com', { + auth: { + username, + password, + }, + }) nock(/.*\.something.com/) .get('/index') @@ -2428,7 +2434,7 @@ describe('Routes', () => { this.setup('http://localhost:8881'), ]) .spread((size, bytes, setup) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/javascript-logo.png') .replyWithFile(200, image, { 'Content-Type': 'image/png', @@ -2460,7 +2466,7 @@ describe('Routes', () => { this.setup('http://localhost:8881'), ]) .spread((size, bytes, setup) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/javascript-logo.png') .replyWithFile(200, zipped, { 'Content-Type': 'image/png', @@ -2499,7 +2505,7 @@ describe('Routes', () => { this.setup('http://localhost:8881'), ]) .spread((size, bytes, setup) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/font.woff') .replyWithFile(200, font, { 'Content-Type': 'application/font-woff; charset=utf-8', @@ -2531,7 +2537,7 @@ describe('Routes', () => { // if this test finishes without timing out we know its all good const contents = removeWhitespace(Fixtures.get('server/err_response.html')) - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, contents, { 'Content-Type': 'text/html; charset=utf-8', @@ -2555,7 +2561,7 @@ describe('Routes', () => { }) it('injects when head has attributes', async function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', @@ -2576,7 +2582,7 @@ describe('Routes', () => { }) it('injects even when head tag is missing', async function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', @@ -2598,7 +2604,7 @@ describe('Routes', () => { }) it('injects when head is capitalized', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', @@ -2618,7 +2624,7 @@ describe('Routes', () => { }) it('injects when head missing but has
', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, '
header
', { 'Content-Type': 'text/html', @@ -2640,7 +2646,7 @@ describe('Routes', () => { }) it('injects when body is capitalized', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', @@ -2660,7 +2666,7 @@ describe('Routes', () => { }) it('injects when both head + body are missing', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'hello from bar!', { 'Content-Type': 'text/html', @@ -2682,7 +2688,7 @@ describe('Routes', () => { }) it('injects even when html + head + body are missing', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, '
hello from bar!
', { 'Content-Type': 'text/html', @@ -2704,7 +2710,7 @@ describe('Routes', () => { }) it('injects after DOCTYPE declaration when no other content', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, '', { 'Content-Type': 'text/html', @@ -2724,7 +2730,7 @@ describe('Routes', () => { }) it('injects superdomain even when head tag is missing', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, ' hello from bar! ', { 'Content-Type': 'text/html', @@ -2745,14 +2751,14 @@ describe('Routes', () => { }) it('injects content after following redirect', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(302, undefined, { // redirect us to google.com! 'Location': 'http://www.google.com/foo', }) - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/foo') .reply(200, ' foo hello from bar! ', { 'Content-Type': 'text/html', @@ -2782,7 +2788,7 @@ describe('Routes', () => { it('injects performantly on a huge amount of elements over http', function () { Fixtures.scaffold() - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/elements.html') .replyWithFile(200, Fixtures.projectPath('e2e/elements.html'), { 'Content-Type': 'text/html', @@ -2824,7 +2830,7 @@ describe('Routes', () => { }) it('does not inject when not initial and not html', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, '', { 'Content-Type': 'text/plain', @@ -2969,7 +2975,7 @@ describe('Routes', () => { }) it('injects document.domain on other http requests', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/iframe') .reply(200, '', { 'Content-Type': 'text/html', @@ -3022,7 +3028,7 @@ describe('Routes', () => { }) this.server._eventBus.on('cross:domain:delaying:html', () => { - this.server._eventBus.emit('ready:for:domain', { shouldInject: true }) + this.server._eventBus.emit('ready:for:domain', { originPolicy: 'http://foobar.com' }) }) return this.rp({ @@ -3043,7 +3049,7 @@ describe('Routes', () => { }) it('does not inject document.domain on non http requests', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/json') .reply(200, { foo: '', @@ -3085,7 +3091,7 @@ describe('Routes', () => { }) it('does not inject anything when not text/html response content-type even when __cypress.initial=true', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/json') .reply(200, { foo: 'bar' }) @@ -3106,7 +3112,7 @@ describe('Routes', () => { }) it('does not inject into x-requested-with request headers', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/iframe') .reply(200, '', { 'Content-Type': 'text/html', @@ -3131,7 +3137,7 @@ describe('Routes', () => { return ['text/html', 'application/xhtml+xml', 'text/plain, application/xhtml+xml', '', null].forEach((type) => { it(`does not inject unless both text/html and application/xhtml+xml is requested: tried to accept: ${type}`, function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/iframe') .reply(200, '', { 'Content-Type': 'text/html', @@ -3167,7 +3173,7 @@ describe('Routes', () => { it('replaces obstructive code in HTML files', function () { const html = '' - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .reply(200, html, { 'Content-Type': 'text/html', @@ -3189,7 +3195,7 @@ describe('Routes', () => { }) it('replaces obstructive code in JS files', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.js') .reply(200, 'if (top !== self) { }', { 'Content-Type': 'application/javascript', @@ -3212,7 +3218,7 @@ describe('Routes', () => { return zlib.gzipAsync(response) .then((resp) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .reply(200, resp, { 'Content-Type': 'text/html', @@ -3240,7 +3246,7 @@ describe('Routes', () => { return zlib.gzipAsync(response) .then((resp) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.js') .reply(200, resp, { 'Content-Type': 'application/javascript', @@ -3266,7 +3272,7 @@ describe('Routes', () => { return zlib.gzipAsync(response) .then((resp) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.js') // remove the last 8 characters which // truncates the CRC checksum and size check @@ -3293,7 +3299,7 @@ describe('Routes', () => { return zlib.gzipAsync(response) .then((resp) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') // remove the last 8 characters which // truncates the CRC checksum and size check @@ -3319,7 +3325,7 @@ describe('Routes', () => { }) it('ECONNRESETs bad gzip responses when not injecting', function (done) { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.js') .delayBody(100) .replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), { @@ -3339,7 +3345,7 @@ describe('Routes', () => { }) it('ECONNRESETs bad gzip responses when injecting', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), { 'Content-Type': 'text/html', @@ -3363,7 +3369,7 @@ describe('Routes', () => { it('does not die rewriting a huge JS file', function () { return getHugeJsFile() .then((hugeJsFile) => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.js') .reply(200, hugeJsFile, { 'Content-Type': 'application/javascript', @@ -3396,7 +3402,7 @@ describe('Routes', () => { it('can turn off security rewriting for HTML', function () { const html = '' - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/index.html') .reply(200, html, { 'Content-Type': 'text/html', @@ -3418,7 +3424,7 @@ describe('Routes', () => { }) it('does not replaces obstructive code in JS files', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/app.js') .reply(200, 'if (top !== self) { }', { 'Content-Type': 'application/javascript', @@ -3442,7 +3448,7 @@ describe('Routes', () => { }) it('does not rewrite html when initial', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'google', { 'Content-Type': 'text/html', @@ -3467,7 +3473,7 @@ describe('Routes', () => { }) it('does not rewrite html when not initial', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/bar') .reply(200, 'google', { 'Content-Type': 'text/html', @@ -3676,7 +3682,7 @@ describe('Routes', () => { beforeEach(function () { return this.setup('http://getbootstrap.com') .then(() => { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/components') .reply(200, 'content page', { 'Content-Type': 'text/html', @@ -3690,7 +3696,7 @@ describe('Routes', () => { }) it('proxies http requests', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/assets/css/application.css') .reply(200, 'html { color: #333 }', { 'Content-Type': 'text/css', @@ -3711,7 +3717,7 @@ describe('Routes', () => { .then(() => { // make an initial request to set the // session proxy! - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/css') .reply(200, 'css content page', { 'Content-Type': 'text/html', @@ -3725,7 +3731,7 @@ describe('Routes', () => { }) it('proxies to the remote session', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .get('/assets/css/application.css') .reply(200, 'html { color: #333 }', { 'Content-Type': 'text/css', @@ -4038,7 +4044,7 @@ describe('Routes', () => { }) it('processes POST + redirect on remote proxy', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .post('/login', { username: 'brian@cypress.io', password: 'foobar', @@ -4068,7 +4074,7 @@ describe('Routes', () => { // this happens on a real form submit because beforeunload fires // and initial=true gets set it('processes POST + redirect on remote initial', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .post('/login', { username: 'brian@cypress.io', password: 'foobar', @@ -4097,7 +4103,7 @@ describe('Routes', () => { }) it('does not alter request headers', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .matchHeader('x-csrf-token', 'abc-123') .post('/login', { username: 'brian@cypress.io', @@ -4124,7 +4130,7 @@ describe('Routes', () => { }) it('does not fail on a big cookie', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .post('/login') .reply(200) @@ -4145,7 +4151,7 @@ describe('Routes', () => { }) it('hands back 201 status codes', function () { - nock(this.server._remoteOrigin) + nock(this.server.remoteStates.current().origin) .post('/companies/validate', { payload: { name: 'Brian' }, }) diff --git a/packages/server/test/integration/server_spec.js b/packages/server/test/integration/server_spec.js index f6b5976368e0..44669723e054 100644 --- a/packages/server/test/integration/server_spec.js +++ b/packages/server/test/integration/server_spec.js @@ -98,7 +98,7 @@ describe('Server', () => { await this.server.startWebsockets(automationStub, config, {}) if (initialUrl) { - this.server._onDomainSet(initialUrl) + this.server.remoteStates.set(initialUrl) } this.srv = this.server.getHttpServer() @@ -259,11 +259,10 @@ describe('Server', () => { .then((res) => { expect(res.statusCode).to.eq(200) - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, props: null, @@ -584,11 +583,10 @@ describe('Server', () => { expect(res.body).to.include('.action("app:window:before:load",window)') expect(res.body).to.include('content') - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://espn.go.com', strategy: 'http', - visiting: false, domainName: 'go.com', fileServer: null, props: { @@ -893,11 +891,10 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth, origin: 'http://google.com', strategy: 'http', - visiting: false, domainName: 'google.com', fileServer: null, props: { @@ -909,53 +906,185 @@ describe('Server', () => { }) }) - it('can serve a multi-domain request', function () { - nock('http://www.cypress.io/') - .get('/') - .reply(200, 'content', { - 'Content-Type': 'text/html', - }) - - expect(this.server._getRemoteState()).to.deep.eq({ - auth: undefined, - props: null, - origin: 'http://localhost:2000', - strategy: 'file', - visiting: undefined, - domainName: 'localhost', - fileServer: this.fileServer, - }) - - return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isMultiDomain: true }) - .then((obj = {}) => { - expectToEqDetails(obj, { - isOkStatusCode: true, - isHtml: true, - contentType: 'text/html', - url: 'http://www.cypress.io/', - originalUrl: 'http://www.cypress.io/', - status: 200, - statusText: 'OK', - cookies: [], - redirects: [], + context('multi-domain', () => { + it('adds a secondary remote state', function () { + nock('http://www.cypress.io/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', }) - // Verify the multi-domain request was buffered - const buffer = this.buffers.take('http://www.cypress.io/') - - expect(buffer).to.not.be.empty - expect(buffer.isMultiDomain).to.be.true - - // Verify the remote state was not updated with the multi-domain request - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, props: null, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, }) + + this.server.socket.localBus.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: undefined, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) + + expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.true + + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isMultiDomain: true }) + .then((obj = {}) => { + expectToEqDetails(obj, { + isOkStatusCode: true, + isHtml: true, + contentType: 'text/html', + url: 'http://www.cypress.io/', + originalUrl: 'http://www.cypress.io/', + status: 200, + statusText: 'OK', + cookies: [], + redirects: [], + }) + + // Verify the multi-domain request was buffered + const buffer = this.buffers.take('http://www.cypress.io/') + + expect(buffer).to.not.be.empty + expect(buffer.isMultiDomain).to.be.true + + // Verify the secondary remote state is returned + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: undefined, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) + + this.server.socket.localBus.emit('cross:origin:finished', 'http://cypress.io') + + expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.false + + // Verify the primary remote state is now returned + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: undefined, + props: null, + origin: 'http://localhost:2000', + strategy: 'file', + domainName: 'localhost', + fileServer: this.fileServer, + }) + }) + }) + + it('doesn\'t override existing remote state on ready:for:domain', function () { + nock('http://www.cypress.io/') + .get('/') + .reply(200, 'content', { + 'Content-Type': 'text/html', + }) + + this.server.socket.localBus.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + return this.server._onResolveUrl('http://www.cypress.io/', {}, this.automationRequest, { isMultiDomain: true }) + .then(() => { + // Verify the secondary remote state is returned + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: undefined, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) + + this.server.socket.localBus.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + // Verify the existing secondary remote state is not overridden + expect(this.server.remoteStates.current()).to.deep.eq({ + auth: undefined, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) + }) + }) + + context('#get()', () => { + it('returns undefined for not found remote state', function () { + this.server.remoteStates.set('http://www.cypress.io/') + + expect(this.server.remoteStates.get('http://notfound.com/')).to.be.undefined + }) + + it('returns primary remote state', function () { + this.server.remoteStates.set('http://www.cypress.io/', { isMultiDomain: true }) + + expect(this.server.remoteStates.get('http://localhost:2000')).to.deep.eq({ + auth: undefined, + props: null, + origin: 'http://localhost:2000', + strategy: 'file', + domainName: 'localhost', + fileServer: this.fileServer, + }) + }) + + it('returns secondary remote state', function () { + this.server.remoteStates.set('http://www.cypress.io/', { isMultiDomain: true }) + + expect(this.server.remoteStates.get('http://cypress.io')).to.deep.eq({ + auth: undefined, + props: { + domain: 'cypress', + port: '80', + tld: 'io', + }, + origin: 'http://www.cypress.io', + strategy: 'http', + domainName: 'cypress.io', + fileServer: null, + }) + }) + }) + + context('#reset()', () => { + it('returns undefined for not found remote state', function () { + this.server.socket.localBus.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.true + expect(this.server.remoteStates.get('http://cypress.io')).to.not.be.undefined + + this.server.remoteStates.reset() + + expect(this.server.remoteStates.isSecondaryOrigin('http://cypress.io')).to.be.false + expect(this.server.remoteStates.get('http://cypress.io')).to.be.undefined + expect(this.server.remoteStates.isPrimaryOrigin('http://localhost:2000')).to.be.true + expect(this.server.remoteStates.get('http://localhost:2000')).to.not.be.undefined + }) }) }) }) @@ -1019,11 +1148,10 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://www.google.com', strategy: 'http', - visiting: false, domainName: 'google.com', fileServer: null, props: { @@ -1054,11 +1182,10 @@ describe('Server', () => { expect(res.statusCode).to.eq(200) }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, props: null, @@ -1101,11 +1228,10 @@ describe('Server', () => { expect(res.body).to.include('google') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://www.google.com', strategy: 'http', - visiting: false, domainName: 'google.com', fileServer: null, props: { @@ -1140,11 +1266,10 @@ describe('Server', () => { expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, props: null, @@ -1174,11 +1299,10 @@ describe('Server', () => { expect(res.body).to.include('google') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://www.google.com', strategy: 'http', - visiting: false, domainName: 'google.com', fileServer: null, props: { @@ -1218,11 +1342,10 @@ describe('Server', () => { expect(res.body).to.include('https server') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'https://www.foobar.com:8443', strategy: 'http', - visiting: false, domainName: 'foobar.com', fileServer: null, props: { @@ -1257,11 +1380,10 @@ describe('Server', () => { expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, props: null, @@ -1291,11 +1413,10 @@ describe('Server', () => { expect(res.body).to.include('https server') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'https://www.foobar.com:8443', strategy: 'http', - visiting: false, fileServer: null, domainName: 'foobar.com', props: { @@ -1343,11 +1464,10 @@ describe('Server', () => { expect(res.body).to.include('Cypress') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'https://s3.amazonaws.com', strategy: 'http', - visiting: false, domainName: 's3.amazonaws.com', fileServer: null, props: { @@ -1382,11 +1502,10 @@ describe('Server', () => { expect(res.body).to.include('.action("app:window:before:load",window)') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'http://localhost:2000', strategy: 'file', - visiting: false, domainName: 'localhost', fileServer: this.fileServer, props: null, @@ -1422,11 +1541,10 @@ describe('Server', () => { expect(res.body).to.include('Cypress') }) }).then(() => { - expect(this.server._getRemoteState()).to.deep.eq({ + expect(this.server.remoteStates.current()).to.deep.eq({ auth: undefined, origin: 'https://s3.amazonaws.com', strategy: 'http', - visiting: false, fileServer: null, domainName: 's3.amazonaws.com', props: { diff --git a/packages/server/test/integration/websockets_spec.js b/packages/server/test/integration/websockets_spec.js index 6f1bcb32e606..e3f6022461c8 100644 --- a/packages/server/test/integration/websockets_spec.js +++ b/packages/server/test/integration/websockets_spec.js @@ -74,7 +74,7 @@ describe('Web Sockets', () => { it('sends back ECONNRESET when error upgrading', function (done) { const agent = new httpsProxyAgent(`http://localhost:${cyPort}`) - this.server._onDomainSet(`http://localhost:${otherPort}`) + this.server.remoteStates.set(`http://localhost:${otherPort}`) const client = new ws(`ws://localhost:${otherPort}`, { agent, @@ -118,7 +118,7 @@ describe('Web Sockets', () => { // force node into legit proxy mode like a browser const agent = new httpsProxyAgent(`http://localhost:${cyPort}`) - this.server._onDomainSet(`http://localhost:${wsPort}`) + this.server.remoteStates.set(`http://localhost:${wsPort}`) this.ws.on('connection', (c) => { return c.on('message', (msg) => { @@ -149,7 +149,7 @@ describe('Web Sockets', () => { rejectUnauthorized: false, }) - this.server._onDomainSet(`https://localhost:${wssPort}`) + this.server.remoteStates.set(`https://localhost:${wssPort}`) this.wss.on('connection', (c) => { return c.on('message', (msg) => { @@ -189,7 +189,7 @@ describe('Web Sockets', () => { rejectUnauthorized: false, }) - this.server._onDomainSet(`https://foobar.com:${wssPort}`) + this.server.remoteStates.set(`https://foobar.com:${wssPort}`) this.wss.on('connection', (c) => { return c.on('message', (msg) => { @@ -306,13 +306,13 @@ describe('Web Sockets', () => { context('when http superDomain has been set', () => { return testSocketIo(`http://localhost:${otherPort}`, function () { - return this.server._onDomainSet(`http://localhost:${otherPort}`) + return this.server.remoteStates.set(`http://localhost:${otherPort}`) }) }) context('when https superDomain has been set', () => { return testSocketIo(`http://localhost:${wssPort}`, function () { - return this.server._onDomainSet(`http://localhost:${wssPort}`) + return this.server.remoteStates.set(`http://localhost:${wssPort}`) }) }) }) diff --git a/packages/server/test/performance/proxy_performance_spec.js b/packages/server/test/performance/proxy_performance_spec.js index 0afca66d23a1..e9b8326dd574 100644 --- a/packages/server/test/performance/proxy_performance_spec.js +++ b/packages/server/test/performance/proxy_performance_spec.js @@ -220,9 +220,9 @@ const runBrowserTest = (urlUnderTest, testCase) => { } if (testCase.cyIntercept) { - cyServer._onDomainSet(urlUnderTest) + cyServer.remoteStates.set(urlUnderTest) } else { - cyServer._onDomainSet('') + cyServer.remoteStates.set('') } let cmd = CHROME_PATH diff --git a/packages/server/test/unit/iframes_spec.js b/packages/server/test/unit/iframes_spec.js index 898cdeb2d3de..8993f4fea328 100644 --- a/packages/server/test/unit/iframes_spec.js +++ b/packages/server/test/unit/iframes_spec.js @@ -15,7 +15,7 @@ describe('controllers/iframes', () => { const controllerOptions = { getSpec: sinon.stub(), - getRemoteState: sinon.stub(), + remoteStates: sinon.stub(), config: {}, } @@ -23,7 +23,7 @@ describe('controllers/iframes', () => { expect(mockRes.setHeader).to.have.been.calledWith('Origin-Agent-Cluster', '?0') expect(files.handleIframe).to.have.been.calledWith( - mockReq, mockRes, controllerOptions.config, controllerOptions.getRemoteState, sinon.match({ + mockReq, mockRes, controllerOptions.config, controllerOptions.remoteStates, sinon.match({ specFilter: undefined, specType: 'integration', }), ) diff --git a/packages/server/test/unit/remote_states.spec.ts b/packages/server/test/unit/remote_states.spec.ts new file mode 100644 index 000000000000..c585a89a5e3d --- /dev/null +++ b/packages/server/test/unit/remote_states.spec.ts @@ -0,0 +1,431 @@ +require('../spec_helper') + +import { RemoteStates } from '../../lib/remote_states' +import EventEmitter from 'events' + +describe('remote states', () => { + beforeEach(function () { + this.remoteStates = new RemoteStates(() => { + return { + serverPort: 9999, + fileServerPort: 9998, + } + }) + + // set the initial state + this.remoteStates.set('http://localhost:3500') + + this.eventEmitter = new EventEmitter() + this.remoteStates.addEventListeners(this.eventEmitter) + }) + + context('#get', () => { + it('returns the remote state by for requested origin policy', function () { + const state = this.remoteStates.get('http://localhost:3500/foobar') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:3500', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '3500', + domain: '', + tld: 'localhost', + }, + }) + }) + + it('returns undefined when the remote state is not found', function () { + const state = this.remoteStates.get('http://notfound.com') + + expect(state).to.be.undefined + }) + + it('changing returned state does not mutate remote state', function () { + const originalState = this.remoteStates.get('http://localhost:3500/foobar') + + expect(originalState).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:3500', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '3500', + domain: '', + tld: 'localhost', + }, + }) + + originalState.auth = { username: 'u', password: 'p' } + + const currentState = this.remoteStates.get('http://localhost:3500/foobar') + + expect(currentState).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:3500', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '3500', + domain: '', + tld: 'localhost', + }, + }) + }) + }) + + context('#isInOriginStack', () => { + it('returns true when the requested url is in the origin stack', function () { + const isInOriginStack = this.remoteStates.isInOriginStack('http://localhost:3500') + + expect(isInOriginStack).to.be.true + }) + + it('returns false when the requested url is not in the origin stack', function () { + const isInOriginStack = this.remoteStates.isInOriginStack('http://notfound.com') + + expect(isInOriginStack).to.be.false + }) + }) + + context('#isSecondaryOrigin', () => { + it('returns true when the requested url is a secondary origin', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('https://staging.google.com') + + expect(isSecondaryOrigin).to.be.true + }) + + it('returns false when the requested url is the primary origin', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('http://localhost:3500') + + expect(isSecondaryOrigin).to.be.false + }) + + it('returns false when the requested url is not in the origin stack', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + const isSecondaryOrigin = this.remoteStates.isSecondaryOrigin('https://foobar.com') + + expect(isSecondaryOrigin).to.be.false + }) + }) + + context('#isPrimaryOrigin', () => { + it('returns true when the requested url is the primary origin', function () { + const isPrimaryOrigin = this.remoteStates.isPrimaryOrigin('http://localhost:3500') + + expect(isPrimaryOrigin).to.be.true + }) + + it('returns false when the requested url is not the primary origin', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + const isPrimaryOrigin = this.remoteStates.isPrimaryOrigin('http://google.com') + + expect(isPrimaryOrigin).to.be.false + }) + }) + + context('#removeCurrentOrigin', () => { + it('removes the current origin from the stack', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true + + this.remoteStates.removeCurrentOrigin('https://google.com') + + expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.false + }) + + it('throws an error when trying to remove the incorrect origin', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true + + expect(() => this.remoteStates.removeCurrentOrigin('http://notfound.com')) + .to.throw(Error, 'Tried to remove origin http://notfound.com but https://google.com was found. This should never happen and likely is a bug. Please open an issue.') + }) + }) + + context('#reset', () => { + it('resets the origin stack and remote states to the primary', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + + expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.true + + this.remoteStates.reset() + + expect(this.remoteStates.get('https://google.com')).to.be.undefined + expect(this.remoteStates.isInOriginStack('https://google.com')).to.be.false + }) + }) + + context('#current', () => { + it('returns the remote state for the current origin in the stack', function () { + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'https://google.com' }) + this.remoteStates.set('https://staging.google.com/foo/bar', { isMultiDomain: true }) + + const state = this.remoteStates.current() + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + }) + }) + + context('#set', () => { + it('sets primary state and origin when isMultiDomain is false', function () { + expect(this.remoteStates.isPrimaryOrigin('http://localhost:3500')).to.be.true + + const state = this.remoteStates.set('https://staging.google.com/foo/bar', { isMultiDomain: false }) + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + + expect(this.remoteStates.isPrimaryOrigin('https://staging.google.com')).to.be.true + }) + + it('sets a secondary state when isMultiDomain is true', function () { + expect(this.remoteStates.isPrimaryOrigin('http://localhost:3500')).to.be.true + + const state = this.remoteStates.set('https://staging.google.com/foo/bar', { isMultiDomain: true }) + + this.remoteStates.addOrigin('https://staging.google.com') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + + expect(this.remoteStates.isPrimaryOrigin('http://localhost:3500')).to.be.true + expect(this.remoteStates.isPrimaryOrigin('https://staging.google.com')).to.be.false + }) + + it('overrides the existing state', function () { + this.remoteStates.set('https://staging.google.com/foo/bar') + + let state = this.remoteStates.get('https://google.com') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + + this.remoteStates.set('https://prod.google.com/foo/bar') + + state = this.remoteStates.get('https://google.com') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://prod.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + }) + + it('sets port to 443 when omitted and https:', function () { + const state = this.remoteStates.set('https://staging.google.com/foo/bar') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'https://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '443', + domain: 'google', + tld: 'com', + }, + }) + }) + + it('sets port to 80 when omitted and http:', function () { + const state = this.remoteStates.set('http://staging.google.com/foo/bar') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://staging.google.com', + strategy: 'http', + domainName: 'google.com', + fileServer: null, + props: { + port: '80', + domain: 'google', + tld: 'com', + }, + }) + }) + + it('sets host + port to localhost', function () { + const state = this.remoteStates.set('http://localhost:4200/a/b?q=1#asdf') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:4200', + strategy: 'http', + domainName: 'localhost', + fileServer: null, + props: { + port: '4200', + domain: '', + tld: 'localhost', + }, + }) + }) + + it('sets local file', function () { + const state = this.remoteStates.set('/index.html') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:9999', + strategy: 'file', + domainName: 'localhost', + fileServer: 'http://localhost:9998', + props: null, + }) + }) + + it('sets ', function () { + const state = this.remoteStates.set('') + + expect(state).to.deep.equal({ + auth: undefined, + origin: 'http://localhost:9999', + strategy: 'file', + domainName: 'localhost', + fileServer: 'http://localhost:9998', + props: null, + }) + }) + + it('sets the remote state when passed a state object', function () { + const state = { + auth: undefined, + origin: 'http://www.foobar.com', + strategy: 'http', + domainName: 'foobar.com', + fileServer: null, + props: { + port: '80', + domain: 'foobar', + tld: 'com', + }, + } + + this.remoteStates.set(state) + + const actualState = this.remoteStates.get('http://www.foobar.com') + + expect(actualState).to.deep.equal(state) + }) + }) + + context('events', () => { + it('can add a secondary remote state on ready:for:domain', function () { + let currentState = this.remoteStates.current() + + expect(currentState.origin).to.equal('http://localhost:3500') + + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://cypress.io') + expect(this.remoteStates.isSecondaryOrigin(currentState.origin)).to.be.true + }) + + it('doesn\'t do anything if ready:for:domain failed', function () { + let currentState = this.remoteStates.current() + + expect(currentState.origin).to.equal('http://localhost:3500') + + this.eventEmitter.emit('ready:for:domain', { failed: true }) + + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://localhost:3500') + expect(this.remoteStates.isSecondaryOrigin(currentState.origin)).to.be.false + }) + + it('removes the current origin when cross:origin:finished is received', function () { + let currentState = this.remoteStates.current() + + expect(currentState.origin).to.equal('http://localhost:3500') + + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://cypress.io') + + this.eventEmitter.emit('cross:origin:finished', 'http://cypress.io') + + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://localhost:3500') + }) + + it('doesn\'t override an existing secondary remote state on ready:for:domain', function () { + let currentState = this.remoteStates.current() + + expect(currentState.origin).to.equal('http://localhost:3500') + + // simulate a switchToDomain by calling ready:for:domain followed by setting + // the origin with specific auth options and finally calling cross:origin:finished + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + this.remoteStates.set('http://cypress.io', { auth: { username: 'u', password: 'p' }, isMultiDomain: true }) + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://cypress.io') + expect(currentState.auth).to.deep.equal({ username: 'u', password: 'p' }) + this.eventEmitter.emit('cross:origin:finished', 'http://cypress.io') + + // verify calling ready:for:domain doesn't reset the previous state + this.eventEmitter.emit('ready:for:domain', { originPolicy: 'http://cypress.io' }) + + currentState = this.remoteStates.current() + expect(currentState.origin).to.equal('http://cypress.io') + expect(currentState.auth).to.deep.equal({ username: 'u', password: 'p' }) + }) + }) +}) diff --git a/packages/server/test/unit/server_spec.js b/packages/server/test/unit/server_spec.js index 7003fd935ab5..27ec3c34ffe0 100644 --- a/packages/server/test/unit/server_spec.js +++ b/packages/server/test/unit/server_spec.js @@ -374,7 +374,7 @@ xdescribe('lib/server', () => { }) it('calls proxy.ws with hostname + port', function () { - this.server._onDomainSet('https://www.google.com') + this.server.remoteStates.set('https://www.google.com') const req = { connection: { @@ -415,90 +415,4 @@ xdescribe('lib/server', () => { expect(this.socket.end).to.be.called }) }) - - context('#_onDomainSet', () => { - beforeEach(function () { - this.server = new ServerE2E() - }) - - it('sets port to 443 when omitted and https:', function () { - const ret = this.server._onDomainSet('https://staging.google.com/foo/bar') - - expect(ret).to.deep.eq({ - auth: undefined, - origin: 'https://staging.google.com', - strategy: 'http', - domainName: 'google.com', - visiting: undefined, - fileServer: null, - props: { - port: '443', - domain: 'google', - tld: 'com', - }, - }) - }) - - it('sets port to 80 when omitted and http:', function () { - const ret = this.server._onDomainSet('http://staging.google.com/foo/bar') - - expect(ret).to.deep.eq({ - auth: undefined, - origin: 'http://staging.google.com', - strategy: 'http', - domainName: 'google.com', - visiting: undefined, - fileServer: null, - props: { - port: '80', - domain: 'google', - tld: 'com', - }, - }) - }) - - it('sets host + port to localhost', function () { - const ret = this.server._onDomainSet('http://localhost:4200/a/b?q=1#asdf') - - expect(ret).to.deep.eq({ - auth: undefined, - origin: 'http://localhost:4200', - strategy: 'http', - domainName: 'localhost', - visiting: undefined, - fileServer: null, - props: { - port: '4200', - domain: '', - tld: 'localhost', - }, - }) - }) - - it('sets when not http url', function () { - this.server._server = { - address () { - return { port: 9999 } - }, - } - - this.server._fileServer = { - port () { - return 9998 - }, - } - - const ret = this.server._onDomainSet('/index.html') - - expect(ret).to.deep.eq({ - auth: undefined, - origin: 'http://localhost:9999', - strategy: 'file', - domainName: 'localhost', - fileServer: 'http://localhost:9998', - props: null, - visiting: undefined, - }) - }) - }) }) diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index b5fc100b87c2..381d1b637cee 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -565,13 +565,29 @@ describe('lib/socket', () => { context('on(ready:for:domain)', () => { it('emits ready:for:domain on local bus', function (done) { - this.server.socket.localBus.once('ready:for:domain', (arg) => { - expect(arg).to.deep.equal({ shouldInject: true }) + this.server.socket.localBus.once('ready:for:domain', ({ originPolicy, failed }) => { + expect(originPolicy).to.equal('http://foobar.com') + expect(failed).to.be.false done() }) - this.client.emit('backend:request', 'ready:for:domain', { shouldInject: true }, () => {}) + this.client.emit('backend:request', 'ready:for:domain', { originPolicy: 'http://foobar.com', failed: false }, () => {}) + }) + }) + + context('on(cross:origin:finished)', () => { + it('emits cross:origin:finished on local bus', function (done) { + this.server.socket.localBus.once('cross:origin:finished', (originPolicy) => { + expect(originPolicy).to.equal('http://foobar.com') + + done() + }) + + // add the origin before calling cross:origin:finished (otherwise we'll fail trying to remove the origin) + this.client.emit('backend:request', 'ready:for:domain', { originPolicy: 'http://foobar.com' }, () => {}) + + this.client.emit('backend:request', 'cross:origin:finished', 'http://foobar.com', () => {}) }) }) }) diff --git a/yarn.lock b/yarn.lock index c1d91c0d0d4a..4d775cd2957f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27933,12 +27933,12 @@ minimist@1.2.3: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.3.tgz#3db5c0765545ab8637be71f333a104a965a9ca3f" integrity sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw== -minimist@1.2.5, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: +minimist@1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@1.2.6, minimist@^1.2.6: +minimist@1.2.6, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==