Skip to content

Commit

Permalink
slight refactor to util class to make easier to test
Browse files Browse the repository at this point in the history
  • Loading branch information
cacieprins committed Dec 16, 2024
1 parent a6b486f commit a857a38
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 88 deletions.
2 changes: 1 addition & 1 deletion packages/driver/src/cy/commands/origin/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class Validator {
})
}

const injector = new DocumentDomainInjection(Cypress.config())
const injector = DocumentDomainInjection.InjectionBehavior(Cypress.config())

const policy = cors.policyFromConfig({ injectDocumentDomain: Cypress.config('injectDocumentDomain') })

Expand Down
2 changes: 1 addition & 1 deletion packages/driver/src/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class $Cypress {
configure (config: Record<string, any> = {}) {
const domainName = config.remote ? config.remote.domainName : undefined

if (new DocumentDomainInjection(config).shouldSetDomainForUrl(domainName)) {
if (DocumentDomainInjection.InjectionBehavior(config).shouldInjectDocumentDomain(domainName)) {
document.domain = domainName
}

Expand Down
100 changes: 47 additions & 53 deletions packages/network/lib/document-domain-injection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,78 @@ of this logic, which should help inform a subsequent refactor strategy.
- whether to inject document.domain in proxied files (proxy/lib/http/response-middleware)
- how to verify stack traces of privileged commands in chrome
*/

//TODO: determine what to do about /cors
import Debug from 'debug'
import { isString, isEqual } from 'lodash'
import { getSuperDomainOrigin, getSuperDomain, parseUrlIntoHostProtocolDomainTldPort, Policy } from './cors'
import { getSuperDomainOrigin, getSuperDomain, parseUrlIntoHostProtocolDomainTldPort } from './cors'
import type { ParsedHostWithProtocolAndHost } from './types'

const debug = Debug('cypress:network:document-domain-injection')

export class DocumentDomainInjection {
constructor (
private config: { injectDocumentDomain?: boolean, testingType?: 'e2e' | 'component' },
) {}

private get policy (): Policy {
return this.config.injectDocumentDomain ? 'same-super-domain-origin' : 'same-origin'
}

// primarily used by `packages/server/lib/remote_states` to determine ??
export class DocumentDomainBehavior implements DocumentDomainInjection {
public getOrigin (url: string) {
if (this.config.injectDocumentDomain) {
return getSuperDomainOrigin(url)
}

return new URL(url).origin
return getSuperDomainOrigin(url)
}
public getHostname (url: string): string {
return getSuperDomain(url)
}
public urlsMatch (frameUrl: string | ParsedHostWithProtocolAndHost, topUrl: string | ParsedHostWithProtocolAndHost): boolean {
const frameProps = isString(frameUrl) ? parseUrlIntoHostProtocolDomainTldPort(frameUrl) : frameUrl
const topProps = isString(topUrl) ? parseUrlIntoHostProtocolDomainTldPort(topUrl) : topUrl

public getHostname (url: string) {
if (this.config.injectDocumentDomain) {
debug('Hostname returning superdomain. Config %s url %s', this.config.injectDocumentDomain, url)
debug('superdomain:', getSuperDomain(url))
const { subdomain: frameSubdomain, ...parsedFrameUrl } = frameProps
const { subdomain: topSubdomain, ...parsedTopUrl } = topProps

return getSuperDomain(url)
}
return isEqual(parsedFrameUrl, parsedTopUrl)
}
public shouldInjectDocumentDomain (url: string | undefined) {
debug('document-domain behavior: should inject document domain -> true')

debug('hostname returning URL hostname')
return !!url
}
}

export class OriginBehavior implements DocumentDomainInjection {
public getOrigin (url: string) {
return new URL(url).origin
}
public getHostname (url: string): string {
return new URL(url).hostname
}

public urlsMatch (frameUrl: string | ParsedHostWithProtocolAndHost, topUrl: string | ParsedHostWithProtocolAndHost): boolean {
const frameProps = isString(frameUrl) ? parseUrlIntoHostProtocolDomainTldPort(frameUrl) : frameUrl
const topProps = isString(topUrl) ? parseUrlIntoHostProtocolDomainTldPort(topUrl) : topUrl

debug('urlsMatch %s policy %o', this.policy, { frameUrl, topUrl })
switch (this.policy) {
case 'same-origin':
debug('match? ', isEqual(frameProps, topProps))
return isEqual(frameProps, topProps)
}
public shouldInjectDocumentDomain (url: string | undefined) {
debug('origin-behavior: should inject document domain -> false')

return isEqual(frameProps, topProps)
case 'same-super-domain-origin':
case 'schemeful-same-site': {
const { port: framePort, subdomain: frameSubdomain, ...parsedFrameUrl } = frameProps
const { port: topPort, subdomain: topSubdomain, ...parsedTopUrl } = topProps
return false
}
}

const doPortsPassSameSchemeCheck = this.policy === 'same-super-domain-origin' ?
framePort === topPort : // ports have to match precisely with same-super-domain-origin
(framePort === topPort) || (framePort !== '443' && topPort !== '443') // schemeful-same-site needs them to match, unless neither are https
export abstract class DocumentDomainInjection {
public static InjectionBehavior (config: { injectDocumentDomain?: boolean, testingType?: 'e2e' | 'component'}): DocumentDomainInjection {
debug('Determining injection behavior for config values: %o', {
injectDocumentDomain: config.injectDocumentDomain,
testingType: config.testingType,
})

debug('match? ', doPortsPassSameSchemeCheck, isEqual(parsedFrameUrl, parsedTopUrl))
debug('called from', new Error().stack)

return doPortsPassSameSchemeCheck && isEqual(parsedFrameUrl, parsedTopUrl)
}
default:
return false
}
}
if (config.injectDocumentDomain && config.testingType === 'e2e') {
debug('Returning document domain injection behavior')

public shouldSetDomainForUrl (url: string | undefined): boolean {
if (!url || this.config.testingType === 'component') {
return false
return new DocumentDomainBehavior()
}

// localhost is special, and we need to always set document domain for
// localhost pages

debug('should set domain for url %s? config: %s, result:', url, this.config.injectDocumentDomain, !!(this.config.injectDocumentDomain))
debug('Returning origin behavior - no document domain injection')

return !!(this.config.injectDocumentDomain)
return new OriginBehavior()
}

public abstract getOrigin (url: string): string
public abstract getHostname (url: string): string
public abstract urlsMatch (frameUrl: string | ParsedHostWithProtocolAndHost, topUrl: string | ParsedHostWithProtocolAndHost): boolean
public abstract shouldInjectDocumentDomain (url: string | undefined): boolean
}
188 changes: 172 additions & 16 deletions packages/network/test/unit/document_domain_injection_spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,187 @@
import { expect } from 'chai'
import { DocumentDomainInjection } from '../../lib/document-domain-injection'
import { DocumentDomainInjection, OriginBehavior, DocumentDomainBehavior } from '../../lib/document-domain-injection'
import { URL } from 'url'

describe('DocumentDomainInjection', () => {
/*
describe('when injectDocumentDomain config is true')
*/
describe('when injectDocumentDomain config is false', () => {
const injectDocumentDomain = false
describe('InjectionBehavior', () => {
let injectDocumentDomain: boolean
let testingType: 'e2e' | 'component'

describe('and testingType is e2e', () => {
const testingType = 'e2e'
const cfg = () => {
return { injectDocumentDomain, testingType }
}

let injection: DocumentDomainInjection
describe('when injectDocumentDomain config is false', () => {
beforeEach(() => {
injectDocumentDomain = false
})

describe('and testingType is e2e', () => {
beforeEach(() => {
testingType = 'e2e'
})

it('returns OriginBehavior', () => {
expect(DocumentDomainInjection.InjectionBehavior(cfg())).to.be.instanceOf(OriginBehavior)
})
})

describe('and testing type is component', () => {
beforeEach(() => {
testingType = 'component'
})

it('returns OriginBehavior', () => {
expect(DocumentDomainInjection.InjectionBehavior(cfg())).to.be.instanceOf(OriginBehavior)
})
})
})

describe('when injectDocumentDomain config is true', () => {
beforeEach(() => {
injection = new DocumentDomainInjection({
injectDocumentDomain,
testingType,
injectDocumentDomain = true
})

describe('and testingType is e2e', () => {
beforeEach(() => {
testingType = 'e2e'
})

it('returns OriginBehavior', () => {
expect(DocumentDomainInjection.InjectionBehavior(cfg())).to.be.instanceOf(DocumentDomainBehavior)
})
})

describe('and testing type is component', () => {
beforeEach(() => {
testingType = 'component'
})

it('returns OriginBehavior', () => {
expect(DocumentDomainInjection.InjectionBehavior(cfg())).to.be.instanceOf(OriginBehavior)
})
})
})
})

describe('DocumentDomainBehavior', () => {
let behavior: DocumentDomainBehavior

beforeEach(() => {
behavior = new DocumentDomainBehavior()
})

describe('getOrigin()', () => {
it('returns superdomain origin with ports', () => {
expect(behavior.getOrigin('https://example.com')).to.equal('https://example.com')
expect(behavior.getOrigin('http://example.com:8080')).to.equal('http://example.com:8080')
})

it('returns superdomain origin with subdomains', () => {
expect(behavior.getOrigin('http://www.example.com')).to.equal('http://example.com')
expect(behavior.getOrigin('http://www.app.herokuapp.com:8080')).to.equal('http://app.herokuapp.com:8080')
})
})

describe('.getHostname()', () => {
it('returns superdomain hostname with ip address', () => {
expect(behavior.getHostname('http://127.0.0.1')).to.equal('127.0.0.1')
})

it('returns superdomain hostname with domain', () => {
expect(behavior.getHostname('http://foo.com')).to.equal('foo.com')
})

it('returns superdomain hostname with subdomains', () => {
expect(behavior.getHostname('http://some.subdomain.foo.com')).to.equal('foo.com')
})
})

describe('urlsMatch', () => {
describe('when ports match', () => {
describe('and superdomain matches', () => {
it('returns true', () => {
expect(behavior.urlsMatch('http://www.foo.com:8080', 'http://baz.foo.com:8080')).to.be.true
})
})

describe('and superdomains do not match', () => {
it('returns false', () => {
expect(behavior.urlsMatch('http://www.foo.com:8080', 'http://baz.com:8080')).to.be.false
})
})
})

describe('.getHostname', () => {
describe('For localhost', () => {
it('returns localhost', () => {
expect(injection.getHostname('http://localhost:8080')).to.eq('localhost')
describe('when ports do not match', () => {
describe('but superdomains match', () => {
it('returns false', () => {
expect(behavior.urlsMatch('https://staging.google.com', 'http://staging.google.com')).to.be.false
expect(behavior.urlsMatch('http://staging.google.com:8080', 'http://staging.google.com:4444')).to.be.false
})
})

describe('and superdomains do not match', () => {
it('returns false', () => {
expect(behavior.urlsMatch('https://staging.google.com', 'http://www.yahoo.com')).to.be.false
expect(behavior.urlsMatch('http://staging.google.com:8080', 'http://staging.yahoo.com:4444')).to.be.false
})
})
})
})

describe('shouldInjectDocumentDomain()', () => {
describe('when param is defined', () => {
it('returns true', () => {
expect(behavior.shouldInjectDocumentDomain('http://some.url')).to.be.true
})
})

describe('when param is undefined', () => {
it('returns false', () => {
expect(behavior.shouldInjectDocumentDomain(undefined)).to.be.false
})
})
})
})

describe('OriginBehavior', () => {
let behavior: OriginBehavior
let url: string

beforeEach(() => {
url = 'http://some.url.com'
behavior = new OriginBehavior()
})

describe('getOrigin', () => {
it('returns the .origin returned from URL', () => {
expect(behavior.getOrigin(url)).to.equal(new URL(url).origin)
})
})

describe('.getHostname', () => {
it('returns the .hostname returned by URL()', () => {
expect(behavior.getHostname(url)).to.equal(new URL(url).hostname)
})
})

describe('urlsMatch', () => {
describe('same superdomain', () => {
it('returns false', () => {
expect(behavior.urlsMatch('http://staging.foo.com', 'http://dev.foo.com')).to.be.false
})
})

describe('same hostname', () => {
it('returns true', () => {
expect(behavior.urlsMatch('http://staging.foo.com', 'http://staging.foo.com')).to.be.true
})
})

describe('different hostname', () => {
it('returns false', () => {
expect(behavior.urlsMatch('http://foo.com', 'http://bar.com')).to.be.false
})
})
})
})
Expand Down
6 changes: 3 additions & 3 deletions packages/proxy/lib/http/response-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ const SetInjectionLevel: ResponseMiddleware = function () {
const isReqMatchSuperDomainOrigin = reqMatchesPolicyBasedOnDomain(
this.req,
this.remoteStates.current(),
new DocumentDomainInjection(this.config),
DocumentDomainInjection.InjectionBehavior(this.config),
)

span?.setAttributes({
Expand All @@ -442,7 +442,7 @@ const SetInjectionLevel: ResponseMiddleware = function () {
return 'partial'
}

const documentDomainInjection = new DocumentDomainInjection(this.config)
const documentDomainInjection = DocumentDomainInjection.InjectionBehavior(this.config)

// NOTE: Only inject fullCrossOrigin if the super domain origins do not match in order to keep parity with cypress application reloads
const urlDoesNotMatchPolicyBasedOnDomain = !reqMatchesPolicyBasedOnDomain(
Expand Down Expand Up @@ -844,7 +844,7 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
isNotJavascript: !resContentTypeIsJavaScript(this.incomingRes),
useAstSourceRewriting: this.config.experimentalSourceRewriting,
modifyObstructiveThirdPartyCode: this.config.experimentalModifyObstructiveThirdPartyCode && !this.remoteStates.isPrimarySuperDomainOrigin(this.req.proxiedUrl),
shouldInjectDocumentDomain: new DocumentDomainInjection(this.config).shouldSetDomainForUrl(this.req.proxiedUrl),
shouldInjectDocumentDomain: DocumentDomainInjection.InjectionBehavior(this.config).shouldInjectDocumentDomain(this.req.proxiedUrl),
modifyObstructiveCode: this.config.modifyObstructiveCode,
url: this.req.proxiedUrl,
deferSourceMapRewrite: this.deferSourceMapRewrite,
Expand Down
4 changes: 2 additions & 2 deletions packages/proxy/test/integration/net-stubbing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ context('network stubbing', () => {
let server
let destinationPort
let socket
let documentDomainInjection
let documentDomainInjection: DocumentDomainInjection

const serverPort = 3030
const fileServerPort = 3030
Expand All @@ -43,7 +43,7 @@ context('network stubbing', () => {
experimentalCspAllowList: false,
}

documentDomainInjection = new DocumentDomainInjection({ injectDocumentDomain: false, testingType: 'e2e' })
documentDomainInjection = DocumentDomainInjection.InjectionBehavior({ injectDocumentDomain: false, testingType: 'e2e' })

remoteStates = createRemoteStates()
socket = new EventEmitter()
Expand Down
Loading

0 comments on commit a857a38

Please sign in to comment.