mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-22 07:00:22 -05:00
71c5b864ea
* feat: Selective CSP header directive stripping from HTTPResponse - uses `stripCspDirectives` config option * feat: Selective CSP header directive permission from HTTPResponse - uses `experimentalCspAllowList` config option * Address Review Comments: - Add i18n for `experimentalCspAllowList` - Remove PR link in changelog - Fix docs link in changelog - Remove extra typedef additions - Update validation error message and snapshot - Fix middleware negated conditional * chore: refactor driver test into system tests to get better test coverage on experimentalCspAllowList options * Address Review Comments: - Remove legacyOption for `experimentalCspAllowList` - Update App desc for `experimentalCspAllowList` to include "Content-Security-Policy-Report-Only" - Modify CHANGELOG wording - Specify “never” overrideLevel - Remove unused validator (+2 squashed commits) - Add "Addresses" note in CHANGELOG to satisfy automation - Set `canUpdateDuringTestTime` to `false` to prevent confusion * chore: Add `frame-src` and `child-src` to conditional CSP directives * chore: Rename `isSubsetOf` to `isArrayIncludingAny` * chore: fix CLI linting types * chore: fix server unit tests * chore: fix system tests within firefox and webkit * chore: add form-action test * chore: update system test snapshots * chore: skip tests in webkit due to form-action flakiness * chore: Move 'sandbox' and 'navigate-to' into `unsupportedCSPDirectives` - Add additional system tests - Update snapshots and unit test * chore: update system test snapshots * chore: fix system tests * chore: do not run csp tests within firefox or webkit due to flake issues in CI * chore: attempt to increase intercept delay to avoid race condition * chore: update new snapshots with video defaults work * chore: update changelog --------- Co-authored-by: Bill Glesias <bglesias@gmail.com> Co-authored-by: Matt Schile <mschile@cypress.io>
4228 lines
144 KiB
JavaScript
4228 lines
144 KiB
JavaScript
require('../spec_helper')
|
||
|
||
const _ = require('lodash')
|
||
let r = require('@cypress/request')
|
||
const rp = require('@cypress/request-promise')
|
||
const compression = require('compression')
|
||
const dns = require('dns')
|
||
const express = require('express')
|
||
const http = require('http')
|
||
const url = require('url')
|
||
let zlib = require('zlib')
|
||
const str = require('underscore.string')
|
||
const evilDns = require('evil-dns')
|
||
const Promise = require('bluebird')
|
||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||
|
||
const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
|
||
const SseStream = require('ssestream')
|
||
const EventSource = require('eventsource')
|
||
const { setupFullConfigWithDefaults } = require('@packages/config')
|
||
const config = require(`../../lib/config`)
|
||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||
const pluginsModule = require(`../../lib/plugins`)
|
||
const preprocessor = require(`../../lib/plugins/preprocessor`)
|
||
const resolve = require(`../../lib/util/resolve`)
|
||
const { fs } = require(`../../lib/util/fs`)
|
||
const CacheBuster = require(`../../lib/util/cache_buster`)
|
||
const Fixtures = require('@tooling/system-tests')
|
||
const { scaffoldCommonNodeModules } = require('@tooling/system-tests/lib/dep-installer')
|
||
/**
|
||
* @type {import('@packages/resolve-dist')}
|
||
*/
|
||
const { getRunnerInjectionContents } = require(`@packages/resolve-dist`)
|
||
const { createRoutes } = require(`../../lib/routes`)
|
||
const { getCtx } = require(`../../lib/makeDataContext`)
|
||
const dedent = require('dedent')
|
||
const { unsupportedCSPDirectives } = require('@packages/proxy/lib/http/util/csp-header')
|
||
|
||
zlib = Promise.promisifyAll(zlib)
|
||
|
||
// force supertest-session to use promises provided in supertest
|
||
const session = proxyquire('supertest-session', { supertest })
|
||
|
||
const absolutePathRegex = /"\/[^{}]*?cy-projects/g
|
||
let sourceMapRegex = /\n\/\/# sourceMappingURL\=.*/
|
||
|
||
const replaceAbsolutePaths = (content) => {
|
||
return content.replace(absolutePathRegex, '"/<path-to-project>')
|
||
}
|
||
|
||
const removeWhitespace = function (c) {
|
||
c = str.clean(c)
|
||
c = str.lines(c).join(' ')
|
||
|
||
return c
|
||
}
|
||
|
||
const cleanResponseBody = (body) => {
|
||
return replaceAbsolutePaths(removeWhitespace(body))
|
||
}
|
||
|
||
function getHugeJsFile () {
|
||
const pathToHugeAppJs = Fixtures.path('server/libs/huge_app.js')
|
||
|
||
const getHugeFile = () => {
|
||
return rp('https://s3.amazonaws.com/internal-test-runner-assets.cypress.io/huge_app.js')
|
||
.then((resp) => {
|
||
return fs
|
||
.outputFileAsync(pathToHugeAppJs, resp)
|
||
.return(resp)
|
||
})
|
||
}
|
||
|
||
return fs
|
||
.readFileAsync(pathToHugeAppJs, 'utf8')
|
||
.catch(getHugeFile)
|
||
}
|
||
|
||
let ctx
|
||
|
||
describe('Routes', () => {
|
||
require('mocha-banner').register()
|
||
|
||
beforeEach(async function () {
|
||
await scaffoldCommonNodeModules()
|
||
ctx = getCtx()
|
||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||
|
||
sinon.stub(CacheBuster, 'get').returns('-123')
|
||
sinon.stub(ServerE2E.prototype, 'reset')
|
||
sinon.stub(pluginsModule, 'has').returns(false)
|
||
|
||
nock.enableNetConnect()
|
||
|
||
Fixtures.scaffold()
|
||
|
||
this.setup = async (initialUrl, obj = {}, spec) => {
|
||
if (_.isObject(initialUrl)) {
|
||
obj = initialUrl
|
||
initialUrl = null
|
||
}
|
||
|
||
if (!obj.projectRoot) {
|
||
obj.projectRoot = Fixtures.projectPath('e2e')
|
||
}
|
||
|
||
await ctx.lifecycleManager.setCurrentProject(obj.projectRoot)
|
||
|
||
// get all the config defaults
|
||
// and allow us to override them
|
||
// for each test
|
||
return setupFullConfigWithDefaults(obj, getCtx().file.getFilesByGlob)
|
||
.then((cfg) => {
|
||
// use a jar for each test
|
||
// but reset it automatically
|
||
// between test
|
||
const jar = rp.jar()
|
||
|
||
this.r = function (options = {}, cb) {
|
||
_.defaults(options, {
|
||
proxy: this.proxy,
|
||
jar,
|
||
simple: false,
|
||
followRedirect: false,
|
||
resolveWithFullResponse: true,
|
||
})
|
||
|
||
return r(options, cb)
|
||
}
|
||
|
||
// use a custom request promise
|
||
// to automatically backfill these
|
||
// options including our proxy
|
||
this.rp = (options = {}) => {
|
||
let targetUrl
|
||
|
||
if (_.isString(options)) {
|
||
targetUrl = options
|
||
options = {}
|
||
}
|
||
|
||
_.defaults(options, {
|
||
url: targetUrl,
|
||
proxy: this.proxy,
|
||
jar,
|
||
simple: false,
|
||
followRedirect: false,
|
||
resolveWithFullResponse: true,
|
||
})
|
||
|
||
return rp(options)
|
||
}
|
||
|
||
const open = () => {
|
||
cfg.pluginsFile = false
|
||
|
||
return ctx.lifecycleManager.waitForInitializeSuccess()
|
||
.then(() => {
|
||
return Promise.all([
|
||
// open our https server
|
||
httpsServer.start(8443),
|
||
|
||
// and open our cypress server
|
||
(this.server = new ServerE2E()),
|
||
|
||
this.server.open(cfg, {
|
||
SocketCtor: SocketE2E,
|
||
getSpec: () => spec,
|
||
getCurrentBrowser: () => null,
|
||
createRoutes,
|
||
testingType: 'e2e',
|
||
exit: false,
|
||
})
|
||
.spread(async (port) => {
|
||
const automationStub = {
|
||
use: () => { },
|
||
}
|
||
|
||
await this.server.startWebsockets(automationStub, config, {})
|
||
|
||
if (initialUrl) {
|
||
this.server.remoteStates.set(initialUrl)
|
||
}
|
||
|
||
this.srv = this.server.getHttpServer()
|
||
|
||
this.session = session(this.srv)
|
||
|
||
this.proxy = `http://localhost:${port}`
|
||
}),
|
||
])
|
||
})
|
||
.then(() => {
|
||
ctx.lifecycleManager.setAndLoadCurrentTestingType('e2e')
|
||
})
|
||
}
|
||
|
||
if (this.server) {
|
||
return Promise.join(
|
||
httpsServer.stop(),
|
||
this.server.close(),
|
||
)
|
||
.then(open)
|
||
}
|
||
|
||
return open()
|
||
})
|
||
}
|
||
})
|
||
|
||
afterEach(function () {
|
||
evilDns.clear()
|
||
nock.cleanAll()
|
||
this.session.destroy()
|
||
preprocessor.close()
|
||
this.project = null
|
||
|
||
return Promise.join(
|
||
this.server.close(),
|
||
httpsServer.stop(),
|
||
ctx.actions.project.clearCurrentProject(),
|
||
)
|
||
.then(() => {
|
||
Fixtures.remove()
|
||
})
|
||
})
|
||
|
||
context('GET /', () => {
|
||
beforeEach(function () {
|
||
return this.setup()
|
||
})
|
||
|
||
// this tests a situation where we open our browser in another browser
|
||
// without proxy mode set
|
||
it('redirects to config.clientRoute without a remote origin and without a proxy', function () {
|
||
return this.rp({
|
||
url: this.proxy,
|
||
proxy: null,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
|
||
expect(res.headers.location).to.eq('/__/')
|
||
})
|
||
})
|
||
|
||
it('does not redirect with remote origin set', function () {
|
||
return this.setup('http://www.github.com')
|
||
.then(() => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/')
|
||
.reply(200, '<html></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('<html>')
|
||
expect(res.body).to.include('document.domain = \'github.com\'')
|
||
|
||
expect(res.body).to.include('</html>')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('does not redirect when visiting http site which isnt cypress server', function () {
|
||
// this tests the 'default' state of cypress when a server
|
||
// is instantiated. i noticed that before you do your first
|
||
// cy.visit() that all http sites would redirect which is
|
||
// incorrect. we only want the cypress port to redirect initially
|
||
|
||
nock('http://www.momentjs.com')
|
||
.get('/')
|
||
.reply(200)
|
||
|
||
return this.rp('http://www.momentjs.com/')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers.location).not.to.eq('/__/')
|
||
})
|
||
})
|
||
|
||
it('proxies through https', function () {
|
||
return this.setup('https://localhost:8443')
|
||
.then(() => {
|
||
return this.rp({
|
||
url: 'https://localhost:8443/',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).not.to.include('Cypress')
|
||
expect(res.body).to.include('document.domain = \'localhost\'')
|
||
|
||
expect(res.body).to.include('<body>https server</body>')
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('GET /__', () => {
|
||
beforeEach(function () {
|
||
return this.setup({ projectName: 'foobarbaz' })
|
||
})
|
||
|
||
it('routes config.clientRoute to serve cypress client app html', function () {
|
||
return this.rp('http://localhost:2020/__')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/window.__Cypress__ = true/)
|
||
|
||
expect(res.headers['origin-agent-cluster']).to.eq('?0')
|
||
})
|
||
})
|
||
|
||
it('correctly sets the "origin-agent-cluster" to opt in to setting document.domain on spec bridge iframes', function () {
|
||
return this.rp('http://localhost:2020/__cypress/spec-bridge-iframes')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/document.domain = \'localhost\'/)
|
||
|
||
expect(res.headers['origin-agent-cluster']).to.eq('?0')
|
||
})
|
||
})
|
||
|
||
it('sets title to projectName', function () {
|
||
return this.rp('http://localhost:2020/__')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<title>e2e</title>')
|
||
})
|
||
})
|
||
|
||
it('omits x-powered-by', function () {
|
||
return this.rp('http://localhost:2020/__')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['x-powered-by']).not.to.exist
|
||
})
|
||
})
|
||
|
||
it('proxies through https', function () {
|
||
return this.setup('https://localhost:8443')
|
||
.then(() => {
|
||
return this.rp('https://localhost:8443/__')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/window.__Cypress__ = true/)
|
||
})
|
||
})
|
||
})
|
||
|
||
// TODO: no automation in unified app
|
||
it('clientRoute routes to \'not launched through Cypress\' without a proxy set', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/__`,
|
||
proxy: null,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/This browser was not launched through Cypress\./)
|
||
})
|
||
})
|
||
|
||
it('other URLs redirect to clientRoute without a proxy set', function () {
|
||
// test something that isn't the clientRoute
|
||
return this.rp({
|
||
url: `${this.proxy}/__cypress/xhrs/foo`,
|
||
proxy: null,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
|
||
expect(res.headers['location']).to.eq('/__/')
|
||
})
|
||
})
|
||
|
||
it('routes when baseUrl is set', function () {
|
||
return this.setup({ baseUrl: 'http://localhost:9999/app' })
|
||
.then(() => {
|
||
return this.rp('http://localhost:9999/__')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/window.__Cypress__ = true/)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('GET /__cypress/runner/*', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8443')
|
||
})
|
||
|
||
it('can get cypress_runner.js', function () {
|
||
return this.rp('http://localhost:8443/__cypress/runner/cypress_runner.js')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/React/)
|
||
})
|
||
})
|
||
|
||
it('can get cypress_runner.css', function () {
|
||
return this.rp('http://localhost:8443/__cypress/runner/cypress_runner.css')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.match(/\.reporter/)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('GET /__cypress/automation', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8443')
|
||
})
|
||
|
||
it('sends getLocalStorage', function () {
|
||
return this.rp(`http://localhost:8443/__cypress/automation/getLocalStorage`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to
|
||
.match(/parent\.postMessage/)
|
||
.match(/localStorage/)
|
||
.match(/sessionStorage/)
|
||
})
|
||
})
|
||
|
||
it('sends setLocalStorage', function () {
|
||
return this.rp(`http://localhost:8443/__cypress/automation/setLocalStorage`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to
|
||
.match(/parent\.postMessage/)
|
||
.match(/set:storage/)
|
||
.match(/localStorage/)
|
||
.match(/sessionStorage/)
|
||
})
|
||
})
|
||
})
|
||
|
||
function pollUntilEventsIpcLoaded () {
|
||
return new Promise((resolve) => {
|
||
let i = 0
|
||
|
||
const interval = setInterval(() => {
|
||
if (ctx.lifecycleManager.isFullConfigReady) {
|
||
clearInterval(interval)
|
||
resolve()
|
||
}
|
||
|
||
i += 1
|
||
|
||
if (i > 50) {
|
||
throw Error(dedent`
|
||
setupNodeEvents and plugins did not complete after 10 seconds.
|
||
There might be an endless loop or an uncaught exception that isn't bubbling up.`)
|
||
}
|
||
}, 200)
|
||
})
|
||
}
|
||
|
||
context('GET /__cypress/tests', () => {
|
||
describe('ids with typescript', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold('ids')
|
||
|
||
return this.setup({
|
||
projectRoot: Fixtures.projectPath('ids'),
|
||
})
|
||
})
|
||
|
||
it('processes foo.coffee spec', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/foo.coffee')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include('expect("foo.coffee")')
|
||
})
|
||
|
||
it('processes dom.jsx spec', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/baz.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include('React.createElement(')
|
||
})
|
||
|
||
it('processes spec into modern javascript', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/es6.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
// "modern" features should remain and not be transpiled into es5
|
||
expect(res.body).to.include('const numbers')
|
||
expect(res.body).to.include('[...numbers]')
|
||
expect(res.body).to.include('async function')
|
||
expect(res.body).to.include('await Promise')
|
||
})
|
||
|
||
it('serves error javascript file when the file is missing', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=does/not/exist.coffee')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('Module not found')
|
||
expect(res.body).to.include('Cypress.action("spec:script:error", {')
|
||
})
|
||
})
|
||
|
||
describe('ids without typescript', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold('ids')
|
||
|
||
sinon.stub(resolve, 'typescript').callsFake(() => {
|
||
return null
|
||
})
|
||
|
||
return this.setup({
|
||
projectRoot: Fixtures.projectPath('ids'),
|
||
})
|
||
})
|
||
|
||
it('processes foo.coffee spec', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/foo.coffee')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include('expect("foo.coffee")')
|
||
})
|
||
|
||
it('processes dom.jsx spec', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/baz.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include('React.createElement(')
|
||
})
|
||
|
||
it('serves error javascript file when the file is missing', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=does/not/exist.coffee')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('Cypress.action("spec:script:error", {')
|
||
expect(res.body).to.include('Module not found')
|
||
})
|
||
})
|
||
|
||
describe('failures', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold('failures')
|
||
|
||
return this.setup({
|
||
projectRoot: Fixtures.projectPath('failures'),
|
||
config: {
|
||
supportFile: false,
|
||
},
|
||
})
|
||
})
|
||
|
||
it('serves error javascript file when there\'s a syntax error', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=cypress/e2e/syntax_error.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('Cypress.action("spec:script:error", {')
|
||
expect(res.body).to.include('Unexpected token')
|
||
})
|
||
})
|
||
|
||
describe('no-server', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold('no-server')
|
||
|
||
return this.setup({
|
||
projectRoot: Fixtures.projectPath('no-server'),
|
||
config: {
|
||
specPattern: 'my-tests/**/*',
|
||
supportFile: 'helpers/includes.js',
|
||
},
|
||
})
|
||
})
|
||
|
||
it('processes my-tests/test1.js spec', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=my-tests/test1.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include(`expect('no-server')`)
|
||
})
|
||
|
||
it('processes helpers/includes.js supportFile', async function () {
|
||
await pollUntilEventsIpcLoaded()
|
||
const res = await this.rp('http://localhost:2020/__cypress/tests?p=helpers/includes.js')
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.match(sourceMapRegex)
|
||
expect(res.body).to.include(`console.log('includes')`)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('ALL /__cypress/xhrs/*', () => {
|
||
beforeEach(function () {
|
||
return this.setup()
|
||
})
|
||
|
||
describe('delay', () => {
|
||
it('can set delay to 10ms', function () {
|
||
const delay = sinon.spy(Promise, 'delay')
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-delay': '10',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(delay).to.be.calledWith(10)
|
||
})
|
||
})
|
||
|
||
it('does not call Promise.delay when no delay', function () {
|
||
const delay = sinon.spy(Promise, 'delay')
|
||
|
||
return this.rp('http://localhost:2020/__cypress/xhrs/users/1')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(delay).not.to.be.called
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('status', () => {
|
||
it('can set status', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-status': '401',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(401)
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('headers', () => {
|
||
it('can set headers', function () {
|
||
const headers = JSON.stringify({
|
||
'x-token': '123',
|
||
'content-type': 'text/plain',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-headers': headers,
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/text\/plain/)
|
||
|
||
expect(res.headers['x-token']).to.eq('123')
|
||
})
|
||
})
|
||
|
||
it('sets headers from response type', function () {
|
||
const headers = JSON.stringify({
|
||
'x-token': '123',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
json: true,
|
||
headers: {
|
||
'x-cypress-headers': headers,
|
||
'x-cypress-response': JSON.stringify({ foo: 'bar' }),
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
expect(res.headers['x-token']).to.eq('123')
|
||
|
||
expect(res.body).to.deep.eq({ foo: 'bar' })
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('response', () => {
|
||
it('sets response to json', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
json: true,
|
||
headers: {
|
||
'x-cypress-response': JSON.stringify([1, 2, 3]),
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
|
||
expect(res.body).to.deep.eq([1, 2, 3])
|
||
})
|
||
})
|
||
|
||
it('sets response to text/html', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-response': '<html>foo</html>',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/text\/html/)
|
||
|
||
expect(res.body).to.eq('<html>foo</html>')
|
||
})
|
||
})
|
||
|
||
it('sets response to text/plain', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-response': 'foobarbaz',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/text\/plain/)
|
||
|
||
expect(res.body).to.eq('foobarbaz')
|
||
})
|
||
})
|
||
|
||
it('sets response to text/plain on empty response', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
headers: {
|
||
'x-cypress-response': '',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/text\/plain/)
|
||
|
||
expect(res.body).to.eq('')
|
||
})
|
||
})
|
||
|
||
it('decodes responses', function () {
|
||
const response = encodeURI(JSON.stringify({
|
||
'test': 'We’ll',
|
||
}))
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
json: true,
|
||
headers: {
|
||
'x-cypress-response': response,
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.deep.eq({ test: 'We’ll' })
|
||
})
|
||
})
|
||
|
||
context('fixture', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold('todos')
|
||
|
||
return this.setup({
|
||
projectRoot: Fixtures.projectPath('todos'),
|
||
config: {
|
||
specPattern: 'tests/**/*',
|
||
supportFile: false,
|
||
fixturesFolder: 'tests/_fixtures',
|
||
},
|
||
})
|
||
})
|
||
|
||
it('returns fixture contents', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/bar',
|
||
json: true,
|
||
headers: {
|
||
'x-cypress-response': 'fixture:foo',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
|
||
expect(res.body).to.deep.eq([{ json: true }])
|
||
})
|
||
})
|
||
|
||
it('returns __error on fixture errors', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/bar',
|
||
json: true,
|
||
headers: {
|
||
'x-cypress-response': 'fixture:bad_json',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(400)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
|
||
expect(res.body.__error).to.include('\'bad_json\' is not valid JSON.')
|
||
})
|
||
})
|
||
|
||
it('can change the fixture encoding', function () {
|
||
return fs.readFileAsync(Fixtures.projectPath('todos/tests/_fixtures/images/flower.png'), 'binary')
|
||
.then((bin) => {
|
||
return this.rp({
|
||
url: 'http://localhost:2020/__cypress/xhrs/bar',
|
||
headers: {
|
||
'x-cypress-response': 'fixture:images/flower.png,binary',
|
||
'x-cypress-headers': JSON.stringify({
|
||
'content-type': 'binary/octet-stream',
|
||
}),
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.include('binary/octet-stream')
|
||
|
||
expect(res.headers['content-length']).to.eq(`${bin.length}`)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('PUT', () => {
|
||
it('can issue PUT requests', function () {
|
||
return this.rp({
|
||
method: 'put',
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
json: true,
|
||
body: {
|
||
name: 'brian',
|
||
},
|
||
headers: {
|
||
'x-cypress-response': JSON.stringify({ id: 123, name: 'brian' }),
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
|
||
expect(res.body).to.deep.eq({ id: 123, name: 'brian' })
|
||
})
|
||
})
|
||
})
|
||
|
||
context('POST', () => {
|
||
it('can issue POST requests', function () {
|
||
return this.rp({
|
||
method: 'post',
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
json: true,
|
||
body: {
|
||
name: 'brian',
|
||
},
|
||
headers: {
|
||
'x-cypress-response': JSON.stringify({ id: 123, name: 'brian' }),
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['content-type']).to.match(/application\/json/)
|
||
|
||
expect(res.body).to.deep.eq({ id: 123, name: 'brian' })
|
||
})
|
||
})
|
||
})
|
||
|
||
context('HEAD', () => {
|
||
it('can issue PUT requests', function () {
|
||
return this.rp({
|
||
method: 'head',
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['content-type']).to.match(/text\/plain/)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('DELETE', () => {
|
||
it('can issue DELETE requests', function () {
|
||
return this.rp({
|
||
method: 'delete',
|
||
url: 'http://localhost:2020/__cypress/xhrs/users/1',
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['content-type']).to.match(/text\/plain/)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
// describe "maximum header size", ->
|
||
// ## https://github.com/cypress-io/cypress/issues/76
|
||
// it "does not bomb on huge headers", ->
|
||
// json = Fixtures.get("server/really_big_json.json")
|
||
|
||
// supertest(@srv)
|
||
// .get("/__cypress/xhrs/users")
|
||
// .set("x-cypress-response", json)
|
||
// .set("x-cypress-response-2", json)
|
||
// .expect(200)
|
||
// .expect("Content-Type", /application\/json/)
|
||
// .expect(JSON.parse(json))
|
||
|
||
context('GET *', () => {
|
||
context('basic request', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.github.com')
|
||
})
|
||
|
||
it('basic 200 html response', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/')
|
||
.reply(200, 'hello from bar!', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('hello from bar!')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('gzip', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.github.com')
|
||
})
|
||
|
||
it('unzips, injects, and then rezips initial content', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/gzip')
|
||
.matchHeader('accept-encoding', 'gzip')
|
||
.replyWithFile(200, Fixtures.path('server/gzip.html.gz'), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/gzip',
|
||
gzip: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('<html>')
|
||
expect(res.body).to.include('gzip')
|
||
expect(res.body).to.include('parent.Cypress')
|
||
expect(res.body).to.include('document.domain = \'github.com\'')
|
||
|
||
expect(res.body).to.include('</html>')
|
||
})
|
||
})
|
||
|
||
it('unzips, injects, and then rezips regular http content', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/gzip')
|
||
.matchHeader('accept-encoding', 'gzip')
|
||
.replyWithFile(200, Fixtures.path('server/gzip.html.gz'), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/gzip',
|
||
gzip: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('<html>')
|
||
expect(res.body).to.include('gzip')
|
||
expect(res.body).to.include('document.domain = \'github.com\'')
|
||
|
||
expect(res.body).to.include('</html>')
|
||
})
|
||
})
|
||
|
||
it('does not inject on regular gzip\'d content', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/gzip')
|
||
.matchHeader('accept-encoding', 'gzip')
|
||
.replyWithFile(200, Fixtures.path('server/gzip.html.gz'), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/gzip',
|
||
gzip: true,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('<html>')
|
||
expect(res.body).to.include('gzip')
|
||
expect(res.body).not.to.include('document.domain = \'github.com\'')
|
||
|
||
expect(res.body).to.include('</html>')
|
||
})
|
||
})
|
||
|
||
// https://github.com/cypress-io/cypress/issues/1746
|
||
it('can ungzip utf-8 javascript and inject without corrupting it', function () {
|
||
let js = ''
|
||
|
||
const app = express()
|
||
|
||
app.use(compression({ chunkSize: 64, threshold: 1 }))
|
||
|
||
app.get('/', (req, res) => {
|
||
res.setHeader('content-type', 'application/javascript; charset=UTF-8')
|
||
res.setHeader('transfer-encoding', 'chunked')
|
||
|
||
const write = (chunk) => {
|
||
js += chunk
|
||
|
||
return res.write(chunk)
|
||
}
|
||
|
||
// note - this is unintentionally invalid JS, just try executing it anywhere
|
||
write('function ')
|
||
_.times(100, () => {
|
||
return write('😡😈'.repeat(10))
|
||
})
|
||
|
||
write(' () { }')
|
||
|
||
return res.end()
|
||
})
|
||
|
||
const server = http.createServer(app)
|
||
|
||
return Promise.fromCallback((cb) => {
|
||
return server.listen(12345, cb)
|
||
}).then(() => {
|
||
return this.rp({
|
||
url: 'http://localhost:12345',
|
||
gzip: true,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.deep.eq(js)
|
||
})
|
||
}).finally(() => {
|
||
return Promise.fromCallback((cb) => {
|
||
return server.close(cb)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('accept-encoding', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.github.com')
|
||
})
|
||
|
||
it('strips unsupported deflate and br encoding', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/accept')
|
||
.matchHeader('accept-encoding', 'gzip')
|
||
.reply(200, '<html>accept</html>')
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/accept',
|
||
gzip: true,
|
||
headers: {
|
||
'accept-encoding': 'gzip,deflate,br',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('<html>accept</html>')
|
||
})
|
||
})
|
||
|
||
it('removes accept-encoding when nothing is supported', function () {
|
||
nock(this.server.remoteStates.current().origin, {
|
||
badheaders: ['accept-encoding'],
|
||
})
|
||
.get('/accept')
|
||
.reply(200, '<html>accept</html>')
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/accept',
|
||
gzip: true,
|
||
headers: {
|
||
'accept-encoding': 'foo,bar,baz',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('<html>accept</html>')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('304 Not Modified', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8080')
|
||
})
|
||
|
||
it('sends back a 304', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/assets/app.js')
|
||
.reply(304)
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/assets/app.js',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(304)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('redirects', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://getbootstrap.com')
|
||
})
|
||
|
||
it('passes the location header through', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/foo')
|
||
.reply(302, undefined, {
|
||
'Location': '/',
|
||
})
|
||
.get('/')
|
||
.reply(200, '<html></html', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://getbootstrap.com/foo',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['set-cookie']).to.include('__cypress.initial=true; Domain=getbootstrap.com; Path=/')
|
||
|
||
expect(res.headers['location']).to.eq('/')
|
||
})
|
||
})
|
||
|
||
// 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.remoteStates.current().origin)
|
||
.get('/foo?bar=baz')
|
||
.reply(302, undefined, {
|
||
'Location': '/css',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://getbootstrap.com/foo?bar=baz',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['set-cookie']).to.include('__cypress.initial=true; Domain=getbootstrap.com; Path=/')
|
||
|
||
expect(res.headers['location']).to.eq('/css')
|
||
})
|
||
})
|
||
|
||
it('does redirect with query params if location header includes them', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/foo?bar=baz')
|
||
.reply(302, undefined, {
|
||
'Location': '/css?q=search',
|
||
})
|
||
.get('/')
|
||
.reply(200, '<html></html', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://getbootstrap.com/foo?bar=baz',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['set-cookie']).to.include('__cypress.initial=true; Domain=getbootstrap.com; Path=/')
|
||
|
||
expect(res.headers['location']).to.eq('/css?q=search')
|
||
})
|
||
})
|
||
|
||
it('does redirect with query params to external domain if location header includes them', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/foo?bar=baz')
|
||
.reply(302, undefined, {
|
||
'Location': 'https://www.google.com/search?q=cypress',
|
||
})
|
||
.get('/')
|
||
.reply(200, '<html></html', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://getbootstrap.com/foo?bar=baz',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['set-cookie']).to.include('__cypress.initial=true; Domain=getbootstrap.com; Path=/')
|
||
|
||
expect(res.headers['location']).to.eq('https://www.google.com/search?q=cypress')
|
||
})
|
||
})
|
||
|
||
it('sets cookies and removes __cypress.initial when initial is originally false', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/css')
|
||
.reply(302, undefined, {
|
||
'Set-Cookie': 'foo=bar; Path=/',
|
||
'Location': '/css/',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://getbootstrap.com/css',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
// since this is not a cypress cookie we do not set the domain
|
||
expect(res.headers['set-cookie']).to.deep.eq(['foo=bar; Path=/'])
|
||
|
||
expect(res.headers['location']).to.eq('/css/')
|
||
})
|
||
})
|
||
|
||
return [301, 302, 303, 307, 308].forEach((code) => {
|
||
it(`handles direct for status code: ${code}`, function () {
|
||
return this.setup('http://auth.example.com')
|
||
.then(() => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(code, undefined, {
|
||
Location: 'http://app.example.com/users/1',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://auth.example.com/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(code)
|
||
|
||
expect(res.headers['set-cookie']).to.include('__cypress.initial=true; Domain=example.com; Path=/')
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('error handling', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.github.com')
|
||
})
|
||
|
||
it('passes through status code + content', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.reply(500, 'server error', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(500)
|
||
expect(res.body).to.include('server error')
|
||
expect(res.body).to.include('document.domain = \'github.com\'')
|
||
|
||
expect(res.headers['set-cookie']).to.match(/__cypress.initial=;/)
|
||
})
|
||
})
|
||
|
||
it('sends back socket hang up on request errors which match origin', function () {
|
||
nock('http://app.github.com')
|
||
.get('/')
|
||
.replyWithError('ECONNREFUSED')
|
||
|
||
return this.rp({
|
||
url: 'http://app.github.com/',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
.then(() => {
|
||
throw new Error('should not reach')
|
||
}).catch((err) => {
|
||
expect(err.message).to.eq('Error: socket hang up')
|
||
})
|
||
})
|
||
|
||
it('sends back socket hang up on actual request errors', function () {
|
||
return this.setup('http://localhost:64644')
|
||
.then(() => {
|
||
return this.rp({
|
||
url: 'http://localhost:64644',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
}).then(() => {
|
||
throw new Error('should not reach')
|
||
}).catch((err) => {
|
||
expect(err.message).to.eq('Error: socket hang up')
|
||
})
|
||
})
|
||
|
||
it('sends back socket hang up on http errors when no matching origin', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:64644',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
.then(() => {
|
||
throw new Error('should not reach')
|
||
}).catch((err) => {
|
||
expect(err.message).to.eq('Error: socket hang up')
|
||
})
|
||
})
|
||
|
||
it('sends back 401 when file server does not receive correct auth', function () {
|
||
return this.setup('<root>', {
|
||
config: {
|
||
fileServerFolder: '/Users/bmann/Dev/projects',
|
||
},
|
||
})
|
||
.then(() => {
|
||
return rp(`http://localhost:${this.server._fileServer.port()}/foo/views/test/index.html`, {
|
||
resolveWithFullResponse: true,
|
||
simple: false,
|
||
})
|
||
}).then((res) => {
|
||
expect(res.statusCode).to.eq(401)
|
||
})
|
||
})
|
||
|
||
it('sends back 404 when file does not exist locally', function () {
|
||
return this.setup('<root>', {
|
||
config: {
|
||
fileServerFolder: '/Users/bmann/Dev/projects',
|
||
},
|
||
})
|
||
.then(() => {
|
||
return this.rp(`${this.proxy}/foo/views/test/index.html`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(404)
|
||
expect(res.body).to.include('Cypress errored trying to serve this file from your system:')
|
||
expect(res.body).to.include('/Users/bmann/Dev/projects/foo/views/test/index.html')
|
||
expect(res.body).to.include('The file was not found.')
|
||
expect(res.body).to.include('<html>\n<head> <script')
|
||
expect(res.body).to.include('</script> </head> <body>')
|
||
|
||
expect(res.body).to.include('document.domain = \'localhost\';')
|
||
})
|
||
})
|
||
}) // should continue to inject
|
||
|
||
it('does not inject on file server errors when origin does not match', function () {
|
||
return this.setup('<root>', {
|
||
config: {
|
||
fileServerFolder: '/Users/bmann/Dev/projects',
|
||
},
|
||
})
|
||
.then(() => {
|
||
nock('http://www.github.com')
|
||
.get('/index.html')
|
||
.reply(500, 'server error', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp('http://www.github.com/index.html')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(500)
|
||
|
||
expect(res.body).to.eq('server error')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('transparently proxies decoding gzip failures', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.github.com/index.html',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
gzip: true,
|
||
})
|
||
.then(() => {
|
||
throw new Error('should not reach')
|
||
}).catch((err) => {
|
||
expect(err.error.code).to.eq('ECONNRESET')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('headers', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8080')
|
||
})
|
||
|
||
describe('when unload is true', () => {
|
||
it('automatically redirects back to clientRoute', function () {
|
||
return this.rp({
|
||
url: 'http://localhost:8080/_',
|
||
headers: {
|
||
'Cookie': '__cypress.unload=true; __cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
|
||
expect(res.headers['location']).to.eq('/__/')
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('when initial is true', () => {
|
||
it('sets back to false', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.html')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/app.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['set-cookie']).to.match(/initial=;/)
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('when initial is false', () => {
|
||
it('does not reset initial or remoteHost', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.html')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/app.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
// there shouldnt be any cookies set here by us
|
||
expect(res.headers['set-cookie']).not.to.exist
|
||
})
|
||
})
|
||
})
|
||
|
||
it('sends with Transfer-Encoding: chunked without Content-Length', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(200, Buffer.from('foo'), {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('foo')
|
||
expect(res.headers['transfer-encoding']).to.eq('chunked')
|
||
|
||
expect(res.headers).not.to.have.property('content-length')
|
||
})
|
||
})
|
||
|
||
it('does not have Content-Length', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(200, 'foo', {
|
||
'Content-Type': 'text/html',
|
||
'Content-Length': 123,
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('foo')
|
||
expect(res.headers['transfer-encoding']).to.eq('chunked')
|
||
|
||
expect(res.headers).not.to.have.property('content-length')
|
||
})
|
||
})
|
||
|
||
it('forwards cookies from incoming responses', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(200, 'OK', {
|
||
'set-cookie': 'userId=123',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['set-cookie']).to.match(/userId=123/)
|
||
})
|
||
})
|
||
|
||
it('appends to previous cookies from incoming responses', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(200, '<html></html>', {
|
||
'set-cookie': 'userId=123; Path=/',
|
||
'content-type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const setCookie = res.headers['set-cookie']
|
||
|
||
expect(setCookie[0]).to.eq('userId=123; Path=/')
|
||
|
||
expect(setCookie[1]).to.eq('__cypress.initial=; Domain=localhost; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT')
|
||
})
|
||
})
|
||
|
||
it('appends cookies on redirects', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/login')
|
||
.reply(302, undefined, {
|
||
'location': '/dashboard',
|
||
'set-cookie': 'userId=123; Path=/',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/login',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
|
||
expect(res.headers['location']).to.eq('/dashboard')
|
||
|
||
expect(res.headers['set-cookie']).to.deep.eq([
|
||
'userId=123; Path=/',
|
||
'__cypress.initial=true; Domain=localhost; Path=/',
|
||
])
|
||
})
|
||
})
|
||
|
||
it('passes invalid cookies', function (done) {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/invalid')
|
||
.reply(200, 'OK', {
|
||
'set-cookie': [
|
||
'foo=bar; Path=/',
|
||
'___utmvmXluIZsM=fidJKOsDSdm; path=/; Max-Age=900',
|
||
'___utmvbXluIZsM=bZM\n XtQOGalF: VtR; path=/; Max-Age=900',
|
||
],
|
||
})
|
||
|
||
http.get('http://localhost:8080/invalid', (res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['set-cookie']).to.deep.eq([
|
||
'foo=bar; Path=/',
|
||
'___utmvmXluIZsM=fidJKOsDSdm; path=/; Max-Age=900',
|
||
'___utmvbXluIZsM=bZM\n XtQOGalF: VtR; path=/; Max-Age=900',
|
||
])
|
||
|
||
done()
|
||
})
|
||
})
|
||
|
||
it('forwards other headers from incoming responses', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/auth')
|
||
.reply(200, 'OK', {
|
||
'x-token': 'abc-123',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/auth',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['x-token']).to.eq('abc-123')
|
||
})
|
||
})
|
||
|
||
it('forwards headers to outgoing requests', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.matchHeader('x-custom', 'value')
|
||
.reply(200, 'hello from bar!', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'x-custom': 'value',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('hello from bar!')
|
||
})
|
||
})
|
||
|
||
it('omits x-frame-options', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'x-frame-options': 'SAMEORIGIN',
|
||
})
|
||
|
||
return this.rp('http://localhost:8080/bar')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers).not.to.have.property('x-frame-options')
|
||
})
|
||
})
|
||
|
||
it('omits content-security-policy by default', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foobar;',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers).not.to.have.property('content-security-policy')
|
||
})
|
||
})
|
||
|
||
it('omits content-security-policy-report-only by default', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy-report-only': 'foobar;',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers).not.to.have.property('content-security-policy-report-only')
|
||
})
|
||
})
|
||
|
||
it('omits document-domain from Feature-Policy header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'Feature-Policy': 'camera *; document-domain \'none\'; autoplay \'self\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['feature-policy']).to.include('camera *')
|
||
expect(res.headers['feature-policy']).to.include('autoplay \'self\'')
|
||
|
||
expect(res.headers['feature-policy']).not.to.include('document-domain \'none\'')
|
||
})
|
||
})
|
||
|
||
it('does not modify host origin header', function () {
|
||
return this.setup('http://foobar.com')
|
||
.then(() => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/css')
|
||
.matchHeader('host', 'foobar.com')
|
||
.reply(200)
|
||
|
||
return this.rp({
|
||
url: 'http://foobar.com/css',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
})
|
||
|
||
it('does not cache when initial response', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/')
|
||
.reply(200, 'hello from bar!', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['cache-control']).to.eq('no-cache, no-store, must-revalidate')
|
||
})
|
||
})
|
||
|
||
it('does cache requesting resource without injection', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/')
|
||
.reply(200, 'hello from bar!', {
|
||
'Content-Type': 'text/plain',
|
||
'Cache-Control': 'max-age=86400',
|
||
})
|
||
|
||
return this.rp('http://localhost:8080/')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['cache-control']).to.eq('max-age=86400')
|
||
})
|
||
})
|
||
|
||
it('forwards origin header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/foo')
|
||
.matchHeader('host', 'localhost:8080')
|
||
.matchHeader('origin', 'http://localhost:8080')
|
||
.reply(200, '<html>origin</html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/foo',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Origin': 'http://localhost:8080',
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('origin')
|
||
expect(res.body).to.include('document.domain = \'localhost\'')
|
||
|
||
expect(res.body).not.to.include('Cypress')
|
||
})
|
||
})
|
||
|
||
it('issue #222 - correctly sets http host headers', function () {
|
||
const matches = (url, fn) => {
|
||
return this.setup(url)
|
||
.then(() => {
|
||
this.server.onRequest(fn)
|
||
|
||
return this.rp(url)
|
||
}).then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('{}')
|
||
})
|
||
}
|
||
|
||
return matches('http://localhost:8080/app.css', () => {
|
||
return nock('http://localhost:8080')
|
||
.matchHeader('host', 'localhost:8080')
|
||
.get('/app.css')
|
||
.reply(200, '{}', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
}).then(() => {
|
||
return matches('http://127.0.0.1/app.css', () => {
|
||
return nock('http://127.0.0.1')
|
||
.matchHeader('host', '127.0.0.1')
|
||
.get('/app.css')
|
||
.reply(200, '{}', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
})
|
||
}).then(() => {
|
||
return matches('http://127.0.0.1:80/app2.css', () => {
|
||
return nock('http://127.0.0.1:80')
|
||
.matchHeader('host', '127.0.0.1')
|
||
.get('/app2.css')
|
||
.reply(200, '{}', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
})
|
||
}).then(() => {
|
||
return matches('https://www.google.com:443/app.css', () => {
|
||
return nock('https://www.google.com')
|
||
.matchHeader('host', 'www.google.com')
|
||
.get('/app.css')
|
||
.reply(200, '{}', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
})
|
||
}).then(() => {
|
||
return matches('https://www.apple.com/app.css', () => {
|
||
return nock('https://www.apple.com')
|
||
.matchHeader('host', 'www.apple.com')
|
||
.get('/app.css')
|
||
.reply(200, '{}', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('CSP Header', () => {
|
||
describe('provided', () => {
|
||
describe('experimentalCspAllowList: false', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8080', {
|
||
config: {
|
||
experimentalCspAllowList: false,
|
||
},
|
||
})
|
||
})
|
||
|
||
it('strips all CSP headers for text/html content-type when "experimentalCspAllowList" is false', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).not.to.have.property('content-security-policy')
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('experimentalCspAllowList: true', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8080', {
|
||
config: {
|
||
experimentalCspAllowList: true,
|
||
},
|
||
})
|
||
})
|
||
|
||
it('does not append a "script-src" nonce to CSP header for text/html content-type when no valid directive exists', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'$/)
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('experimentalCspAllowList: ["script-src-element", "script-src", "default-src"]', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8080', {
|
||
config: {
|
||
experimentalCspAllowList: ['script-src-elem', 'script-src', 'default-src'],
|
||
},
|
||
})
|
||
})
|
||
|
||
it('appends a nonce to existing CSP header directive "script-src-elem" for text/html content-type when in CSP header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'; script-src-elem \'fake-src\';',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src-elem 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
|
||
})
|
||
})
|
||
|
||
it('appends a nonce to existing CSP header directive "script-src" for text/html content-type when in CSP header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'; script-src \'fake-src\';',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
|
||
})
|
||
})
|
||
|
||
it('appends a nonce to existing CSP header directive "default-src" for text/html content-type when in CSP header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'; default-src \'fake-src\';',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
|
||
})
|
||
})
|
||
|
||
it('appends a nonce to both CSP header directive "script-src" and "default-src" for text/html content-type when in CSP header when both exist', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'; script-src \'fake-src\'; default-src \'fake-src\';',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
|
||
})
|
||
})
|
||
|
||
it('appends a nonce to all valid CSP header directives for text/html content-type when in CSP header', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'; script-src-elem \'fake-src\'; script-src \'fake-src\'; default-src \'fake-src\';',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'; script-src-elem 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; script-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'; default-src 'fake-src' 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}';$/)
|
||
})
|
||
})
|
||
|
||
it('does not remove original CSP header for text/html content-type', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': 'foo \'bar\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/foo 'bar'/)
|
||
})
|
||
})
|
||
|
||
it('does not append a nonce to CSP header if request is not for html', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'application/json',
|
||
'content-security-policy': 'foo \'bar\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).not.to.match(/script-src 'nonce-[^-A-Za-z0-9+/=]|=[^=]|={3,}'/)
|
||
})
|
||
})
|
||
|
||
it('does not remove original CSP header if request is not for html', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'application/json',
|
||
'content-security-policy': 'foo \'bar\'',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'$/)
|
||
})
|
||
})
|
||
|
||
// The following directives are not supported by Cypress and should be stripped
|
||
unsupportedCSPDirectives.forEach((directive) => {
|
||
const headerValue = `${directive} 'none'`
|
||
|
||
it(`removes the "${directive}" CSP directive for text/html content-type`, function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
'content-security-policy': `foo \'bar\'; ${headerValue};`,
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).to.have.property('content-security-policy')
|
||
expect(res.headers['content-security-policy']).to.match(/^foo 'bar'/)
|
||
expect(res.headers['content-security-policy']).not.to.match(new RegExp(headerValue))
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('not provided', () => {
|
||
it('does not append a nonce to CSP header for text/html content-type', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).not.to.have.property('content-security-policy')
|
||
})
|
||
})
|
||
|
||
it('does not append a nonce to CSP header if request is not for html', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, 'OK', {
|
||
'Content-Type': 'application/json',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers).not.to.have.property('content-security-policy')
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('authorization', () => {
|
||
it('attaches auth headers when matches origin', function () {
|
||
const username = 'u'
|
||
const password = 'p'
|
||
|
||
const base64 = Buffer.from(`${username}:${password}`).toString('base64')
|
||
|
||
this.server.remoteStates.set('http://localhost:8080', {
|
||
auth: {
|
||
username,
|
||
password,
|
||
},
|
||
})
|
||
|
||
nock('http://localhost:8080')
|
||
.get('/index')
|
||
.matchHeader('authorization', `Basic ${base64}`)
|
||
.reply(200, '')
|
||
|
||
return this.rp('http://localhost:8080/index')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
|
||
it('does not attach auth headers when not matching origin', function () {
|
||
nock('http://localhost:8080', {
|
||
badheaders: ['authorization'],
|
||
})
|
||
.get('/index')
|
||
.reply(200, '')
|
||
|
||
return this.rp('http://localhost:8080/index')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
|
||
it('does not modify existing auth headers when matching origin', function () {
|
||
const existing = 'Basic asdf'
|
||
|
||
this.server.remoteStates.set('http://localhost:8080', {
|
||
auth: {
|
||
username: 'u',
|
||
password: 'p',
|
||
},
|
||
})
|
||
|
||
nock('http://localhost:8080')
|
||
.get('/index')
|
||
.matchHeader('authorization', existing)
|
||
.reply(200, '')
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8080/index',
|
||
headers: {
|
||
'Authorization': existing,
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
|
||
// https://github.com/cypress-io/cypress/issues/4267
|
||
it(`doesn't attach auth headers to a diff protection space on the same origin`, function () {
|
||
return this.setup('http://beta.something.com')
|
||
.then(() => {
|
||
const username = 'u'
|
||
const password = 'p'
|
||
|
||
const base64 = Buffer.from(`${username}:${password}`).toString('base64')
|
||
|
||
this.server.remoteStates.set('http://beta.something.com', {
|
||
auth: {
|
||
username,
|
||
password,
|
||
},
|
||
})
|
||
|
||
nock(/.*\.something.com/)
|
||
.get('/index')
|
||
.matchHeader('authorization', `Basic ${base64}`)
|
||
.reply(200, '')
|
||
.get('/index')
|
||
.matchHeader('authorization', _.isUndefined)
|
||
.reply(200, '')
|
||
|
||
return this.rp('http://beta.something.com/index')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
return this.rp('http://cdn.something.com/index')
|
||
}).then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('images', () => {
|
||
beforeEach(() => {
|
||
return Fixtures.scaffold()
|
||
})
|
||
|
||
it('passes the bytes through without injection on http servers', function () {
|
||
const image = Fixtures.projectPath('e2e/static/javascript-logo.png')
|
||
|
||
return Promise.all([
|
||
fs.statAsync(image).get('size'),
|
||
fs.readFileAsync(image, 'utf8'),
|
||
this.setup('http://localhost:8881'),
|
||
])
|
||
.spread((size, bytes, setup) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/javascript-logo.png')
|
||
.replyWithFile(200, image, {
|
||
'Content-Type': 'image/png',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8881/javascript-logo.png',
|
||
headers: {
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).not.to.include('<script')
|
||
|
||
expect(res.body).to.eq(bytes)
|
||
})
|
||
})
|
||
})
|
||
|
||
it('passes the bytes through without injection on http servers with gzip', function () {
|
||
const image = Fixtures.projectPath('e2e/static/javascript-logo.png')
|
||
const zipped = Fixtures.projectPath('e2e/static/javascript-logo.png.gz')
|
||
|
||
return Promise.all([
|
||
fs.statAsync(image).get('size'),
|
||
fs.readFileAsync(image, 'utf8'),
|
||
this.setup('http://localhost:8881'),
|
||
])
|
||
.spread((size, bytes, setup) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/javascript-logo.png')
|
||
.replyWithFile(200, zipped, {
|
||
'Content-Type': 'image/png',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8881/javascript-logo.png',
|
||
gzip: true,
|
||
headers: {
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).not.to.include('<script')
|
||
|
||
expect(res.body).to.eq(bytes)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('woff', () => {
|
||
beforeEach(() => {
|
||
return Fixtures.scaffold()
|
||
})
|
||
|
||
it('passes the bytes through without injection', function () {
|
||
const font = Fixtures.projectPath('e2e/static/FiraSans-Regular.woff')
|
||
|
||
return Promise.all([
|
||
fs.statAsync(font).get('size'),
|
||
fs.readFileAsync(font, 'utf8'),
|
||
this.setup('http://localhost:8881'),
|
||
])
|
||
.spread((size, bytes, setup) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/font.woff')
|
||
.replyWithFile(200, font, {
|
||
'Content-Type': 'application/font-woff; charset=utf-8',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://localhost:8881/font.woff',
|
||
headers: {
|
||
'Accept': '*/*',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).not.to.include('<script')
|
||
|
||
expect(res.body).to.eq(bytes)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('svg', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.google.com')
|
||
})
|
||
|
||
it('rewrites <svg> without hanging', function () {
|
||
// if this test finishes without timing out we know its all good
|
||
const contents = removeWhitespace(Fixtures.get('server/err_response.html'))
|
||
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, contents, {
|
||
'Content-Type': 'text/html; charset=utf-8',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('content injection', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.cypress.io')
|
||
})
|
||
|
||
it('injects when head has attributes', async function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html> <head prefix="og: foo"> <meta name="foo" content="bar"> </head> <body>hello from bar!</body> </html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
const injection = await getRunnerInjectionContents()
|
||
const contents = removeWhitespace(Fixtures.get('server/expected_head_inject.html').replace('{{injection}}', injection))
|
||
const res = await this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(body).to.eq(contents)
|
||
})
|
||
|
||
it('injects even when head tag is missing', async function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html> <body>hello from bar!</body> </html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
const injection = await getRunnerInjectionContents()
|
||
const contents = removeWhitespace(Fixtures.get('server/expected_no_head_tag_inject.html').replace('{{injection}}', injection))
|
||
|
||
const res = await this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(body).to.eq(contents)
|
||
})
|
||
|
||
it('injects when head is capitalized', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<HTML> <HEAD>hello from bar!</HEAD> </HTML>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<HTML> <HEAD> <script type=\'text/javascript\'> document.domain = \'cypress.io\';')
|
||
})
|
||
})
|
||
|
||
it('injects when head missing but has <header>', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html> <body><nav>some nav</nav><header>header</header></body> </html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<html> <head> <script type=\'text/javascript\'> document.domain = \'cypress.io\';')
|
||
|
||
expect(res.body).to.include('</head> <body><nav>some nav</nav><header>header</header></body> </html>')
|
||
})
|
||
})
|
||
|
||
it('injects when body is capitalized', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<HTML> <BODY>hello from bar!</BODY> </HTML>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('</script> </head> <BODY>hello from bar!</BODY> </HTML>')
|
||
})
|
||
})
|
||
|
||
it('injects when both head + body are missing', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<HTML>hello from bar!</HTML>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<HTML> <head> <script')
|
||
|
||
expect(res.body).to.include('</head>hello from bar!</HTML>')
|
||
})
|
||
})
|
||
|
||
it('injects even when html + head + body are missing', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<div>hello from bar!</div>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<head> <script')
|
||
|
||
expect(res.body).to.include('</head><div>hello from bar!</div>')
|
||
})
|
||
})
|
||
|
||
it('injects after DOCTYPE declaration when no other content', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<!DOCTYPE>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('<!DOCTYPE><head> <script')
|
||
})
|
||
})
|
||
|
||
it('injects superdomain even when head tag is missing', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html> <body>hello from bar!</body> </html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('<html> <head> <script type=\'text/javascript\'> document.domain = \'cypress.io\'; </script> </head> <body>hello from bar!</body> </html>')
|
||
})
|
||
})
|
||
|
||
it('injects content after following redirect', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(302, undefined, {
|
||
// redirect us to google.com!
|
||
'Location': 'http://www.cypress.io/foo',
|
||
})
|
||
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/foo')
|
||
.reply(200, '<html> <head prefix="og: foo"> <title>foo</title> </head> <body>hello from bar!</body> </html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['location']).to.eq('http://www.cypress.io/foo')
|
||
expect(res.headers['set-cookie']).to.match(/initial=true/)
|
||
|
||
return this.rp(res.headers['location'])
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.headers['set-cookie']).to.match(/initial=;/)
|
||
|
||
expect(res.body).to.include('parent.Cypress')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('injects performantly on a huge amount of elements over http', function () {
|
||
Fixtures.scaffold()
|
||
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/elements.html')
|
||
.replyWithFile(200, Fixtures.projectPath('e2e/elements.html'), {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/elements.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('document.domain = \'cypress.io\';')
|
||
})
|
||
})
|
||
|
||
it('injects performantly on a huge amount of elements over file', function () {
|
||
Fixtures.scaffold()
|
||
|
||
return this.setup('/index.html', {
|
||
projectRoot: Fixtures.projectPath('e2e'),
|
||
})
|
||
.then(() => {
|
||
return this.rp({
|
||
url: `${this.proxy}/elements.html`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('document.domain = \'localhost\';')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('does not inject when not initial and not html', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/plain',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('<html><head></head></html>')
|
||
})
|
||
})
|
||
|
||
it('injects into https server', async function () {
|
||
await this.setup('https://localhost:8443')
|
||
|
||
const injection = await getRunnerInjectionContents()
|
||
const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection))
|
||
const res = await this.rp({
|
||
url: 'https://localhost:8443/',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(body).to.eq(contents)
|
||
})
|
||
|
||
it('injects into https://www.google.com', function () {
|
||
return this.setup('https://www.google.com')
|
||
.then(() => {
|
||
this.server.onRequest((req, res) => {
|
||
return nock('https://www.google.com')
|
||
.get('/')
|
||
.reply(200, '<html><head></head><body>google</body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'https://www.google.com/',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('parent.Cypress')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('injects even on 5xx responses', function () {
|
||
return this.setup('https://www.cypress.io')
|
||
.then(() => {
|
||
this.server.onRequest((req, res) => {
|
||
return nock('https://www.cypress.io')
|
||
.get('/')
|
||
.reply(500, '<html><head></head><body>google</body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'https://www.cypress.io/',
|
||
headers: {
|
||
'Accept': 'text/html, application/xhtml+xml, */*',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(500)
|
||
|
||
expect(res.body).to.include('document.domain = \'cypress.io\'')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('works with host swapping', async function () {
|
||
await this.setup('https://www.foobar.com:8443')
|
||
evilDns.add('*.foobar.com', '127.0.0.1')
|
||
|
||
const injection = await getRunnerInjectionContents()
|
||
const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection))
|
||
const res = await this.rp({
|
||
url: 'https://www.foobar.com:8443/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(body).to.eq(contents.replace('localhost', 'foobar.com'))
|
||
})
|
||
|
||
it('continues to inject on the same https superdomain but different subdomain', async function () {
|
||
await this.setup('https://www.foobar.com:8443')
|
||
evilDns.add('*.foobar.com', '127.0.0.1')
|
||
|
||
const injection = await getRunnerInjectionContents()
|
||
const contents = removeWhitespace(Fixtures.get('server/expected_https_inject.html').replace('{{injection}}', injection))
|
||
const res = await this.rp({
|
||
url: 'https://docs.foobar.com:8443/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(body).to.eq(contents.replace('localhost', 'foobar.com'))
|
||
})
|
||
|
||
it('injects document.domain on https requests to same superdomain but different subdomain', function () {
|
||
return this.setup('https://www.foobar.com:8443')
|
||
.then(() => {
|
||
evilDns.add('*.foobar.com', '127.0.0.1')
|
||
|
||
return this.rp({
|
||
url: 'https://docs.foobar.com:8443/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head> <script type=\'text/javascript\'> document.domain = \'foobar.com\'; </script></head><body>https server</body></html>')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('injects document.domain on other http requests', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/iframe')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/iframe',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head> <script type=\'text/javascript\'> document.domain = \'cypress.io\'; </script></head></html>')
|
||
})
|
||
})
|
||
|
||
it('does not inject document.domain on matching super domains but different subdomain - when the domain is set to strict same origin (google)', function () {
|
||
nock('http://www.google.com')
|
||
.get('/iframe')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/iframe',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head></head></html>')
|
||
})
|
||
})
|
||
|
||
it('injects document.domain on AUT iframe requests that do not match current superDomain', function () {
|
||
nock('http://www.foobar.com')
|
||
.get('/')
|
||
.reply(200, '<html><head></head><body>hi</body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.foobar.com',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
'X-Cypress-Is-AUT-Frame': 'true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.include(`<html><head> <script type='text/javascript'> document.domain = 'foobar.com';`)
|
||
})
|
||
})
|
||
|
||
it('does not inject document.domain on non http requests', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/json')
|
||
.reply(200, {
|
||
foo: '<html><head></head></html>',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/json',
|
||
json: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.deep.eq({ foo: '<html><head></head></html>' })
|
||
})
|
||
})
|
||
|
||
it('does not inject document.domain on http requests which do not match current superDomain and are not the AUT iframe', function () {
|
||
nock('http://www.foobar.com')
|
||
.get('/')
|
||
.reply(200, '<html><head></head><body>hi</body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.foobar.com',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('<html><head></head><body>hi</body></html>')
|
||
})
|
||
})
|
||
|
||
it('does not inject anything when not text/html response content-type even when __cypress.initial=true', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/json')
|
||
.reply(200, { foo: 'bar' })
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/json',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'Accept': 'application/json',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.eq(JSON.stringify({ foo: 'bar' }))
|
||
|
||
// it should not be telling us to turn this off either
|
||
expect(res.headers['set-cookie']).not.to.match(/initial/)
|
||
})
|
||
})
|
||
|
||
it('does not inject into x-requested-with request headers', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/iframe')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/iframe',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head></head></html>')
|
||
})
|
||
})
|
||
|
||
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.remoteStates.current().origin)
|
||
.get('/iframe')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
const headers = {
|
||
'Cookie': '__cypress.initial=false',
|
||
}
|
||
|
||
headers['Accept'] = type
|
||
|
||
return this.rp({
|
||
url: 'http://www.cypress.io/iframe',
|
||
headers,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head></head></html>')
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('content injection', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.foo.com')
|
||
})
|
||
|
||
it('injects document.domain on matching super domains but different subdomain - non google domain', function () {
|
||
nock('http://mail.foo.com')
|
||
.get('/iframe')
|
||
.reply(200, '<html><head></head></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://mail.foo.com/iframe',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const body = cleanResponseBody(res.body)
|
||
|
||
expect(body).to.eq('<html><head> <script type=\'text/javascript\'> document.domain = \'foo.com\'; </script></head></html>')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('security rewriting', () => {
|
||
describe('on by default', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.google.com')
|
||
})
|
||
|
||
it('replaces obstructive code in HTML files', function () {
|
||
const html = '<html><body><script>if (top !== self) { }</script></body></html>'
|
||
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.reply(200, html, {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include(
|
||
'<script>if (self !== self) { }</script>',
|
||
)
|
||
})
|
||
})
|
||
|
||
it('replaces obstructive code in JS files', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.js')
|
||
.reply(200, 'if (top !== self) { }', {
|
||
'Content-Type': 'application/javascript',
|
||
})
|
||
|
||
return this.rp('http://www.google.com/app.js')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq(
|
||
'if (self !== self) { }',
|
||
)
|
||
})
|
||
})
|
||
|
||
it('handles multi-byte characters correctly when injecting', function () {
|
||
const bytes = `0${Array(16 * 1024 * 10).fill('λφ').join('')}`
|
||
|
||
const response = `<html>${bytes}</html>`
|
||
|
||
return zlib.gzipAsync(response)
|
||
.then((resp) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.reply(200, resp, {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.html',
|
||
gzip: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include(`${bytes}</html>`)
|
||
})
|
||
})
|
||
})
|
||
|
||
// https://github.com/cypress-io/cypress/issues/1396
|
||
it('handles multi-byte characters correctly on JS files', function () {
|
||
const response = `0${Array(16 * 1024 * 2).fill('λφ').join('')}`
|
||
|
||
return zlib.gzipAsync(response)
|
||
.then((resp) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.js')
|
||
.reply(200, resp, {
|
||
'Content-Type': 'application/javascript',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.js',
|
||
gzip: true,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq(response)
|
||
})
|
||
})
|
||
})
|
||
|
||
// https://github.com/cypress-io/cypress/issues/1756
|
||
// https://github.com/nodejs/node/issues/5761
|
||
it('handles gzip responses that are slightly truncated', function () {
|
||
const response = 'I am a gzipped response'
|
||
|
||
return zlib.gzipAsync(response)
|
||
.then((resp) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.js')
|
||
// remove the last 8 characters which
|
||
// truncates the CRC checksum and size check
|
||
// at the end of the stream
|
||
.reply(200, resp.slice(0, -8), {
|
||
'Content-Type': 'application/javascript',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/app.js',
|
||
gzip: true,
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq(response)
|
||
})
|
||
})
|
||
})
|
||
|
||
it('handles gzip responses that are slightly truncated when injecting', function () {
|
||
const response = '<html>I am a gzipped response</html>'
|
||
|
||
return zlib.gzipAsync(response)
|
||
.then((resp) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
// remove the last 8 characters which
|
||
// truncates the CRC checksum and size check
|
||
// at the end of the stream
|
||
.reply(200, resp.slice(0, -8), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.html',
|
||
gzip: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('I am a gzipped response</html>')
|
||
})
|
||
})
|
||
})
|
||
|
||
it('ECONNRESETs bad gzip responses when not injecting', function (done) {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.js')
|
||
.delayBody(100)
|
||
.replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), {
|
||
'Content-Type': 'application/javascript',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.r({
|
||
url: 'http://www.google.com/app.js',
|
||
gzip: true,
|
||
})
|
||
.on('error', (err) => {
|
||
expect(err.code).to.eq('ECONNRESET')
|
||
|
||
return done()
|
||
})
|
||
})
|
||
|
||
it('ECONNRESETs bad gzip responses when injecting', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.replyWithFile(200, Fixtures.path('server/gzip-bad.html.gz'), {
|
||
'Content-Type': 'text/html',
|
||
'Content-Encoding': 'gzip',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.html',
|
||
gzip: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then(() => {
|
||
throw new Error('should not reach')
|
||
}).catch((err) => {
|
||
expect(err.error.code).to.eq('ECONNRESET')
|
||
})
|
||
})
|
||
|
||
it('does not die rewriting a huge JS file', function () {
|
||
return getHugeJsFile()
|
||
.then((hugeJsFile) => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.js')
|
||
.reply(200, hugeJsFile, {
|
||
'Content-Type': 'application/javascript',
|
||
})
|
||
|
||
let reqTime = new Date()
|
||
|
||
return this.rp('http://www.google.com/app.js')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
reqTime = new Date() - reqTime
|
||
|
||
// shouldn't be more than 500ms
|
||
expect(reqTime).to.be.lt(500)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('off with config', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.google.com', {
|
||
config: {
|
||
modifyObstructiveCode: false,
|
||
},
|
||
})
|
||
})
|
||
|
||
it('can turn off security rewriting for HTML', function () {
|
||
const html = '<html><body><script>if (top !== self) { }</script></body></html>'
|
||
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/index.html')
|
||
.reply(200, html, {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/index.html',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include(
|
||
'<script>if (top !== self) { }</script>',
|
||
)
|
||
})
|
||
})
|
||
|
||
it('does not replaces obstructive code in JS files', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/app.js')
|
||
.reply(200, 'if (top !== self) { }', {
|
||
'Content-Type': 'application/javascript',
|
||
})
|
||
|
||
return this.rp('http://www.google.com/app.js')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq(
|
||
'if (top !== self) { }',
|
||
)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('FQDN rewriting', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://www.google.com')
|
||
})
|
||
|
||
it('does not rewrite html when initial', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html><body><a href=\'http://www.google.com\'>google</a></body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const {
|
||
body,
|
||
} = res
|
||
|
||
expect(body).to.include('<a href=\'http://www.google.com\'>google</a>')
|
||
})
|
||
})
|
||
|
||
it('does not rewrite html when not initial', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/bar')
|
||
.reply(200, '<html><body><a href=\'http://www.google.com\'>google</a></body></html>', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp({
|
||
url: 'http://www.google.com/bar',
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
const {
|
||
body,
|
||
} = res
|
||
|
||
expect(body).to.include('<a href=\'http://www.google.com\'>google</a>')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('file requests', () => {
|
||
beforeEach(function () {
|
||
Fixtures.scaffold()
|
||
|
||
return this.setup('/index.html', {
|
||
projectRoot: Fixtures.projectPath('no-server'),
|
||
config: {
|
||
fileServerFolder: 'dev',
|
||
specPattern: 'my-tests/**/*',
|
||
supportFile: false,
|
||
},
|
||
})
|
||
.then(() => {
|
||
return this.rp({
|
||
url: `${this.proxy}/index.html`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.include('index.html content')
|
||
expect(res.body).to.include('parent.Cypress')
|
||
|
||
expect(res.headers['set-cookie']).to.match(/initial=;/)
|
||
expect(res.headers['cache-control']).to.eq('no-cache, no-store, must-revalidate')
|
||
expect(res.headers['etag']).to.exist
|
||
|
||
expect(res.headers['last-modified']).to.exist
|
||
})
|
||
})
|
||
})
|
||
|
||
it('sets etag', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.eq('html { color: black; }')
|
||
|
||
expect(res.headers['etag']).to.be.a('string')
|
||
})
|
||
})
|
||
|
||
it('sets last-modified', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css`)
|
||
.then((res) => {
|
||
expect(res.headers['last-modified']).to.be.a('string')
|
||
})
|
||
})
|
||
|
||
it('streams from file system', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('html { color: black; }')
|
||
})
|
||
})
|
||
|
||
it('sets content-type', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.headers['content-type']).to.match(/text\/css/)
|
||
})
|
||
})
|
||
|
||
it('disregards anything past the pathname', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css?foo=bar#hash`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('html { color: black; }')
|
||
})
|
||
})
|
||
|
||
it('can serve files with spaces in the path', function () {
|
||
return this.rp(`${this.proxy}/a space/foo.txt`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('foo')
|
||
})
|
||
})
|
||
|
||
it('sets x-cypress-file-path headers', function () {
|
||
return this.rp(`${this.proxy}/assets/app.css`)
|
||
.then((res) => {
|
||
expect(res.headers).to.have.property('x-cypress-file-path', `${Fixtures.projectPath('no-server')}/dev/assets/app.css`)
|
||
})
|
||
})
|
||
|
||
it('sets x-cypress-file-server-error headers on error', function () {
|
||
return this.rp(`${this.proxy}/does-not-exist.html`)
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(404)
|
||
|
||
expect(res.headers).to.have.property('x-cypress-file-server-error', 'true')
|
||
})
|
||
})
|
||
|
||
it('injects document.domain on other http requests', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/index.html`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('document.domain = \'localhost\';')
|
||
})
|
||
})
|
||
|
||
it('injects document.domain on other http requests to root', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/sub/`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.include('document.domain = \'localhost\';')
|
||
})
|
||
})
|
||
|
||
it('does not inject injects document.domain on 301 redirects to folders', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/sub`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(301)
|
||
|
||
expect(res.body).not.to.include('document.domain = \'localhost\';')
|
||
})
|
||
})
|
||
|
||
it('does not inject document.domain on non http requests', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/assets/foo.json`,
|
||
json: true,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.deep.eq({ contents: '<html><head></head></html>' })
|
||
})
|
||
})
|
||
|
||
it('does not inject anything when not text/html response content-type even when __cypress.initial=true', function () {
|
||
return this.rp({
|
||
url: `${this.proxy}/assets/foo.json`,
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
'Accept': 'application/json',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
expect(res.body).to.deep.eq(JSON.stringify({ contents: '<html><head></head></html>' }, null, 2))
|
||
|
||
// it should not be telling us to turn this off either
|
||
expect(res.headers['set-cookie']).not.to.match(/initial/)
|
||
})
|
||
})
|
||
})
|
||
|
||
context('http requests', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://getbootstrap.com')
|
||
.then(() => {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/components')
|
||
.reply(200, 'content page', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp('http://getbootstrap.com/components')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
})
|
||
|
||
it('proxies http requests', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/assets/css/application.css')
|
||
.reply(200, 'html { color: #333 }', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
|
||
return this.rp('http://getbootstrap.com/assets/css/application.css')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('html { color: #333 }')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('when session is already set', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://getbootstrap.com')
|
||
.then(() => {
|
||
// make an initial request to set the
|
||
// session proxy!
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/css')
|
||
.reply(200, 'css content page', {
|
||
'Content-Type': 'text/html',
|
||
})
|
||
|
||
return this.rp('http://getbootstrap.com/css')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
})
|
||
|
||
it('proxies to the remote session', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.get('/assets/css/application.css')
|
||
.reply(200, 'html { color: #333 }', {
|
||
'Content-Type': 'text/css',
|
||
})
|
||
|
||
return this.rp('http://getbootstrap.com/assets/css/application.css')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('html { color: #333 }')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('localhost', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:6565')
|
||
})
|
||
|
||
it('makes requests to ipv6 when ipv4 fails', function (done) {
|
||
// make sure that this test times out relatively fast
|
||
// to ensure that requests are fast, the retrying functionality
|
||
// does not extend them, and that the server closes quickly
|
||
// due to the reduced keep alive timeout
|
||
this.timeout(1500)
|
||
|
||
return dns.lookup('localhost', { all: true }, (err, addresses) => {
|
||
if (err) {
|
||
return done(err)
|
||
}
|
||
|
||
// if we dont have multiple addresses aka ipv6 then
|
||
// just skip this test
|
||
if (!_.find(addresses, { family: 6 })) {
|
||
return done()
|
||
}
|
||
|
||
// create a server that is only bound to ipv6
|
||
// and ensure that it is found by localhost dns lookup
|
||
const server = http.createServer((req, res) => {
|
||
res.writeHead(200)
|
||
|
||
return res.end()
|
||
})
|
||
|
||
// reduce this down from 5000ms to 100ms
|
||
// so that our server closes much faster
|
||
server.keepAliveTimeout = 100
|
||
|
||
// start the server listening on ipv6 only
|
||
// for demo how to bind to localhost via ipv6 see project
|
||
// https://github.com/bahmutov/docker-ip6
|
||
return server.listen(6565, '::', () => {
|
||
return this.rp('http://localhost:6565/#/foo')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
return server.close(() => {
|
||
return done()
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
it('handles 204 no content status codes', function () {
|
||
nock('http://localhost:6565')
|
||
.get('/user/rooms')
|
||
.reply(204, '')
|
||
|
||
return this.rp('http://localhost:6565/user/rooms')
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(204)
|
||
|
||
expect(res.body).to.eq('')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('blocked hosts', () => {
|
||
beforeEach(function () {
|
||
return this.setup({
|
||
config: {
|
||
blockHosts: [
|
||
'*.google.com',
|
||
'shop.apple.com',
|
||
'cypress.io',
|
||
'localhost:6666',
|
||
'*adwords.com',
|
||
],
|
||
},
|
||
})
|
||
})
|
||
|
||
it('returns 503 and custom headers for all hosts', function () {
|
||
const expectedHeader = (res, val) => {
|
||
expect(res.headers['x-cypress-matched-blocked-host']).to.eq(val)
|
||
}
|
||
|
||
return Promise.all([
|
||
this.rp('https://mail.google.com/f'),
|
||
this.rp('http://shop.apple.com/o'),
|
||
this.rp('https://cypress.io'),
|
||
this.rp('https://localhost:6666/o'),
|
||
this.rp('https://some.adwords.com'),
|
||
])
|
||
.spread((...responses) => {
|
||
_.every(responses, (res) => {
|
||
expect(res.statusCode).to.eq(503)
|
||
|
||
expect(res.body).to.be.empty
|
||
})
|
||
|
||
expectedHeader(responses[0], '*.google.com')
|
||
expectedHeader(responses[1], 'shop.apple.com')
|
||
expectedHeader(responses[2], 'cypress.io')
|
||
expectedHeader(responses[3], 'localhost:6666')
|
||
|
||
return expectedHeader(responses[4], '*adwords.com')
|
||
})
|
||
})
|
||
})
|
||
|
||
context('client aborts', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:6565')
|
||
})
|
||
|
||
it('aborts the proxied request', function (done) {
|
||
getHugeJsFile()
|
||
.then((str) => {
|
||
const server = http.createServer((req, res) => {
|
||
// when the incoming message to our
|
||
// 3rd party server is aborted then
|
||
// we know we've juggled up the event
|
||
// properly
|
||
req.on('aborted', () => {
|
||
return server.close(() => {
|
||
return done()
|
||
})
|
||
})
|
||
|
||
res.writeHead(200, {
|
||
'Content-Type': 'application/javascript',
|
||
})
|
||
|
||
// write some bytes, causing
|
||
// the response event to fire
|
||
// on our request
|
||
return res.write(str.slice(0, 10000))
|
||
})
|
||
|
||
return server.listen(6565, () => {
|
||
const abort = () => {
|
||
return r.abort()
|
||
}
|
||
|
||
r = this.r({
|
||
url: 'http://localhost:6565/app.js',
|
||
})
|
||
// abort when we get the
|
||
// response headers
|
||
.on('response', abort)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('event source / server sent events / SSE', () => {
|
||
let onRequest = null
|
||
|
||
beforeEach(function () {
|
||
onRequest = function () {}
|
||
|
||
return this.setup('http://localhost:5959', {
|
||
config: {
|
||
responseTimeout: 50,
|
||
},
|
||
})
|
||
.then(() => {
|
||
this.srv = http.createServer((req, res) => {
|
||
onRequest(req, res)
|
||
|
||
this.sseStream = new SseStream(req)
|
||
|
||
return this.sseStream.pipe(res)
|
||
})
|
||
|
||
Promise.promisifyAll(this.srv)
|
||
|
||
return this.srv.listenAsync()
|
||
.then(() => {
|
||
this.port = this.srv.address().port
|
||
})
|
||
})
|
||
})
|
||
|
||
afterEach(function () {
|
||
return this.srv.closeAsync()
|
||
})
|
||
|
||
it('receives event source messages through the proxy', function (done) {
|
||
onRequest = function (req, res) {
|
||
// when the request is finally
|
||
// aborted then we know we've closed
|
||
// the connection correctly
|
||
const closed = new Promise((resolve) => {
|
||
return res.on('close', () => {
|
||
return resolve()
|
||
})
|
||
})
|
||
|
||
const aborted = new Promise((resolve) => {
|
||
return req.on('aborted', () => {
|
||
return resolve()
|
||
})
|
||
})
|
||
|
||
return Promise.join(aborted, closed)
|
||
.then(() => {
|
||
return done()
|
||
})
|
||
}
|
||
|
||
const es = new EventSource(`http://localhost:${this.port}/sse`, {
|
||
proxy: this.proxy,
|
||
})
|
||
|
||
es.onopen = () => {
|
||
return Promise
|
||
.delay(100)
|
||
.then(() => {
|
||
return this.sseStream.write({
|
||
data: 'hey',
|
||
})
|
||
})
|
||
}
|
||
|
||
es.onmessage = (m) => {
|
||
expect(m.data).to.eq('hey')
|
||
|
||
return es.close()
|
||
}
|
||
})
|
||
})
|
||
|
||
context('when body should be empty', function () {
|
||
this.timeout(10000) // TODO(tim): figure out why this is flaky now?
|
||
|
||
beforeEach(function (done) {
|
||
Fixtures.scaffoldProject('e2e')
|
||
.then(() => {
|
||
this.httpSrv = http.createServer((req, res) => {
|
||
const { query } = url.parse(req.url, true)
|
||
|
||
if (_.has(query, 'chunked')) {
|
||
res.setHeader('tranfer-encoding', 'chunked')
|
||
} else {
|
||
res.setHeader('content-length', '0')
|
||
}
|
||
|
||
res.writeHead(Number(query.status), {
|
||
'x-foo': 'bar',
|
||
})
|
||
|
||
return res.end()
|
||
})
|
||
|
||
return this.httpSrv.listen(() => {
|
||
this.port = this.httpSrv.address().port
|
||
|
||
return this.setup(`http://localhost:${this.port}`)
|
||
.then(_.ary(done, 0))
|
||
})
|
||
})
|
||
})
|
||
|
||
afterEach(function () {
|
||
return this.httpSrv.close()
|
||
})
|
||
|
||
return [204, 304].forEach((status) => {
|
||
it(`passes through a ${status} response immediately`, function () {
|
||
return this.rp({
|
||
url: `http://localhost:${this.port}/?status=${status}`,
|
||
timeout: 1000,
|
||
})
|
||
.then((res) => {
|
||
expect(res.headers['x-foo']).to.eq('bar')
|
||
|
||
expect(res.statusCode).to.eq(status)
|
||
})
|
||
})
|
||
|
||
it(`passes through a ${status} response with chunked encoding immediately`, function () {
|
||
return this.rp({
|
||
url: `http://localhost:${this.port}/?status=${status}&chunked`,
|
||
timeout: 1000,
|
||
})
|
||
.then((res) => {
|
||
expect(res.headers['x-foo']).to.eq('bar')
|
||
|
||
expect(res.statusCode).to.eq(status)
|
||
})
|
||
})
|
||
})
|
||
})
|
||
})
|
||
|
||
context('POST *', () => {
|
||
beforeEach(function () {
|
||
return this.setup('http://localhost:8000')
|
||
})
|
||
|
||
it('processes POST + redirect on remote proxy', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.post('/login', {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
})
|
||
.reply(302, undefined, {
|
||
'Location': '/dashboard',
|
||
})
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/login',
|
||
form: {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
},
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
|
||
expect(res.headers['location']).to.match(/dashboard/)
|
||
})
|
||
})
|
||
|
||
// 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.remoteStates.current().origin)
|
||
.post('/login', {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
})
|
||
.reply(302, undefined, {
|
||
'Location': '/dashboard',
|
||
})
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/login',
|
||
form: {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
},
|
||
headers: {
|
||
'Cookie': '__cypress.initial=true',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(302)
|
||
expect(res.headers['location']).to.match(/dashboard/)
|
||
|
||
expect(res.headers['set-cookie']).to.match(/initial=true/)
|
||
})
|
||
})
|
||
|
||
it('does not alter request headers', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.matchHeader('x-csrf-token', 'abc-123')
|
||
.post('/login', {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
})
|
||
.reply(200, 'OK')
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/login',
|
||
form: {
|
||
username: 'brian@cypress.io',
|
||
password: 'foobar',
|
||
},
|
||
headers: {
|
||
'X-CSRF-Token': 'abc-123',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
|
||
expect(res.body).to.eq('OK')
|
||
})
|
||
})
|
||
|
||
it('does not fail on a big cookie', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.post('/login')
|
||
.reply(200)
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/login',
|
||
json: true,
|
||
body: {
|
||
'query': { 'bool': { 'must': [{ 'filtered': { 'filter': { 'term': { 'brand_id': 3 } } } }, { 'filtered': { 'filter': { 'term': { 'is_live': true } } } }, { 'filtered': { 'filter': { 'bool': { 'should': [{ 'term': { 'subcategory_id': 25 } }, { 'term': { 'subcategory_id': 21 } }] } } } }] } }, 'aggs': { 'colors.raw3': { 'filter': {}, 'aggs': { 'colors.raw': { 'terms': { 'field': 'colors.raw', 'size': 50 } }, 'colors.raw_count': { 'cardinality': { 'field': 'colors.raw' } } } }, 'patterns.raw4': { 'filter': {}, 'aggs': { 'patterns.raw': { 'terms': { 'field': 'patterns.raw', 'size': 50 } }, 'patterns.raw_count': { 'cardinality': { 'field': 'patterns.raw' } } } }, 'custom_filters_a.raw5': { 'filter': {}, 'aggs': { 'custom_filters_a.raw': { 'terms': { 'field': 'custom_filters_a.raw', 'size': 50 } }, 'custom_filters_a.raw_count': { 'cardinality': { 'field': 'custom_filters_a.raw' } } } }, 'custom_filters_b.raw6': { 'filter': {}, 'aggs': { 'custom_filters_b.raw': { 'terms': { 'field': 'custom_filters_b.raw', 'size': 50 } }, 'custom_filters_b.raw_count': { 'cardinality': { 'field': 'custom_filters_b.raw' } } } } }, 'size': 100,
|
||
},
|
||
headers: {
|
||
'Cookie': '_treebook_session=MmxpQUFjak1mZXN1L21FSnY2dzhJaWFPa0kxaXR2MFVMMTFKQnhFMTY0ZGhVZDJLV3BPUm9xT3BWREVDOFExTFBSaDEzY2htQTVVZ0dpZjg0VmF1SmRINlVSZ2ZFeFpzd0s3Yk4wakRxQS9TdW11N3ZURnIvbHAvQ2NTVjZWY0VqdFNQZTFHV09xclZnM0lDNE1JUzZzN3BWamF0dXRSM09uRFZZeWMwd01ESkdWUzY2MFE2QkY0cStIQnBwNGk0V3hhWkNVd0QwamtMeDJheWxxb04wZVkwRzJmdmVLZXJsR3M5UFAyK0Y3ST0tLTBFWmJwWktQaThrWnN6dUZVaVBGckE9PQ%3D%3D--a07f8cd3fc4db9a0c52676e274e71546a9f047fb; _my-site_session=NVp4akFGelljaFZJR3A4US9ES1ZBcTZ4K2ZqNklkUzNKZUVGU3RuQk1HSGZxenU1Z2VDOHBvTmdwL0x3c0E0TGEvTmZDWVR5Y205ZkVOZWwrYkdaWEV2V3k0T1h0elN1NjlsSWo1dFZqVG9nNy9MNWp6enoyZmdYSzYwb3d4WVlvMG9qVXlOTnNNSm1VYlpSSWc3bkFnPT0tLWhQcnVqQ0NQMGF6dVE0SCtRQk1sdmc9PQ%3D%3D--2d25be12c8415195314fe3990cad281c14022d9d; _first_app_session=ajBJcFpRa2dSR29MaTd5UUFnTU45dVZzd2tKeUY2enpuR1cyNFB2bVRCQ201RTh5bXAvaTdlN2NuRm1WeEJOZlkyTWtIRXhpZm1HTlZMM0hPeHVzWUkvQWNhUmNUcUNRRzlDbW8yMlA4SjFZSjhYQ1I4V0FNU3k2Um1mOHlQNU94SkdrT1ZmZS9PZHB6WDVGN0s4cGNnPT0tLTA0aE5hQStvbXQ5T1VXN0UrT3MxN0E9PQ%3D%3D--1daec80639465389f0e5437193e633eeb7bbfca9; _pet_store_session=N055S0M0azJlUlFJWGdDN01MQ3Z4aXFCWHNFU2ttZUp0SDJyL3BPeTVOdzBYWGROb1F1UWx2eG80ZjlobDF2QWJ4Rk5uVFliSWtMSEltZ2RUVDNZUVFLd2VYS3JTQldRbkNnMVdReVY0V1FyWjBNdTVjSk9SQUJNZ0JmMitEcG9JQnh0WVErY2lZaWo4WGJFeXpKUElBPT0tLUZDL2k3bUlKQUl4aGV3ZGEvQXRCaUE9PQ%3D%3D--a658fa7995dec5c7ec3e041d5dc5f52e4f7be0ee; _adventures_session=MDZDeDBDQmRJRFdpYzk5SWFEa0txd0xBVmQyei9xTUhQZ3VlZjNRcUY2c0VsWkZaNUo2SktlNGRvcFNrdDlqL0wzRnA4eVdpMTdMUjJlcHJuMkVWU05SdmNRa29QQU5HWlhxajRZcWNSY1lqR1RDZjhsQ3loUzMvd1M5V3ZCck5iMHRkQXgzcFAwRlBOMFFja2dURDFBPT0tLWMyMHZvcnV6OUIvS0tkeFFpTVA5Qnc9PQ%3D%3D--04a940aa32a208e9bfb6b422481e658f57411132; _classroom_session=NFRSY05TaHZZdkVMYkRZdXh5djQxSmxYY2xIaHJDamI3bGJuRjRxbDZCcWxCMVRxOVpkdWV2MnQ0ajBBNDdOb0ZtTUZPUkFLNmJ0SHFVUlJCZ3FUSjBvSUkxTzZpL1h6ZWhsVlJ6Z2xGRjhWamlVSHJ3NG1vZ3IxSC9PaUxkNVA0Q2x6ZDRTaXJxcGlvc0tsazdkRHFBPT0tLTZ1Z0hxcU16cmZoRTR1VHBqN3RkbXc9PQ%3D%3D--cea92642cc4ec52a81ad6892db90e0e8b8236069; _boo_scaffold_session=am5oQVhMN2ZQQlJBb2VUdDNtdUVic0NMeFlhWVJOa0Y5SEhERjdpUmd2aVZicnY0Z1VsL0hTWVNudTF0dm1SQWNKZHhzZmpJQi9FK1JZVXZwU3Q2RHNObmFWcVg5T0thYmUrY0k2YTJaQjMzUVp3MlRNenk3a2JxczdWZmF6MUxEaW1EejlxeWpUNjFack4rM3N1bm5yZlFKT3BTYnIvTTNNaG5haXZrMEFZWUp2cHBRQnNXbldLUkRZZlBuRDlWdWRldTJqYmh6K0lWWGRyVGpOUERtbFBQUFVHS1pOR3RtMmRuYUxLbjUvYXQra3UwcUxkK3BHRzFRR2RqMWVGay0tQU9DdlFMNEluVVJPTUNlbDh3L1Y0Zz09--c3e2745720ea2080dadaac45223d36ed15fd7fb4; _db_explore_session=RW5NUDlSaG5SVFF4Zms4YmRwQmhZaitrckhRVXN0VzlKS3E3M3Fxa0xwQmZGcDY1amlVOG5DRnpPQ3hLSVBoUFZ5b3lKWGd2Z0JZWnptMEREcU1DUE04SkRMN1RUbVUvUDFaTkNFcUpqQkZ0NmxjTkhrZHZlM1pibmIyTGQxZ3UzNVpvTUlDSnNhcUxBcHRJaVJqdENRPT0tLTBXWVhqK3lwcTVONzVLRVhnZldmQkE9PQ%3D%3D--10c607bc41831a8b4e80689d051a5cfc19872cc1; _furniture_store_session=cm5lTW9XR0ptc2lCSVJNY0c3dW1oVFlJdG9OaGh1OENhdjgyMW5Heis0RlB3ZkNPR2ZZUmdiV1VlbGY3NmdmdHU0cERBZGtpWGFUeHo5cHM4K1lKa053cEhrV2VaR3Q5RVYzZms0VVh2aFBrd2N6U3hzeWhZTEhnVG1qYldxNFZOemVTMGRkQmZQSEFHWEJZZWNQWS9BPT0tLWk1R0d0SW1pZVEyckRGK2R1Vzh4M0E9PQ%3D%3D--99c9ed2f5a43af5bc018d930b6210b50bf973a79; _travel_session=VEcxVlB4NTg0SGNHdjdOekZndW5Jc1FJY2VJd3NTaDhVVk4vNnVuekpWUVE2ekVhK25YVWVUemhOUnl2S3pVWTc3U0trTTI2cUVNakwyd254M3RMT0J2L0dheTdRUVpGY1FRd0FTT3pSaGpsK3kreVJxNVdMbWFsY3pWSVdMUU5mRzRzQnIzRzFNMS8zQThZQStLTE9nPT0tLVNMK2hsYjVpRmZNMngwUFI1QU0zemc9PQ%3D%3D--19113054b718f1b9c733427c9a3e110b60187f4f; _doctors_office_session=RGxTSWhxVm45am0rSGZVb25jRnBVdVRvcHIxbTR3dVZWdjYvYklGY3BVa2FnYm5sRXlNUlp2TmN2Q3R3NndxWnFFdStPT2ZieTVKaE1SMUpFM0ZUaFhUb3ZhbkxDbDlrZk9lUVh5dlRCY0hhQ1NVNUVKSjhJSnhyTkFkWi9FUm1UZis1VEhVb2tXQW9Qb2MrQU5IMHNRPT0tLUMxdlpYMEVKV081L3QveFBHMS9scWc9PQ%3D%3D--7a4196ea6b5194f412be549a0f30925ec6bd118f; _recipe_app_session=OE1saTRQZ1pRQnNrdHNCNllyNld3cnVTcnhqUjh0Q2NZaSt3eWlLbjljUWxMdk5seW9SVnQwcmZ4VVBhdWRDUE5qM1hWSi9QaW9adnVFalJ6VjJFVFFhWm1LZFB1ZXlzRzR1UFpDSks0V01pdWFuWk04bExMeDdvYWFZRU9OQm5qQ2ZKcXRLYUdiZmwvWTYwUCs4bnJBPT0tLVdtYkh4cmFJeHQrZXdoazFGSUo2aGc9PQ%3D%3D--1649c7bd76e8944a0911d9e6a59d5a82aa1a0559; _ajax-sample_session=ZDM2NU8xZTRocmxFUlR5ZjZhMGxUSkgzdmFOcVVrQ1ZUN1ZNMFliRGNuRFQ2SkNvV1pVdUlLRWVoY2dodWxQRjNiek84Y0dhdk44dkN6Sy9naDRsQjFRY2FVMXYzcXc3TkVzY28wMnp4OFdiWnBFMFEvNUltK1ZYUUd2THRMblpLVmJHOFIvMEVQaWpMV2dqamI1ZFBnPT0tLTJ4RURLbjVEYkZaSGpvdDk1RU9sNEE9PQ%3D%3D--f13942ae013f1252c609762922b21b8a233b36ed; _stripe_test_session=RnRoMngvN0IwVjBENElrY0w1eStwOGYxdDgxKzNBbStYaHErMkxRQk9xUGFMenRqU3h3UFVNSGNoKzNWSXZJMVZHMXYxcUoyeEpBMVRQVk9CQktOampnaFdYdUJzYTFjSTJJYlI5SkpzTVdPSVJTNU5GaGR2UVJqa3NNaW1UM1l2N2YxUWVKeEtzOUtneldlVjdNcm1BPT0tLWEyaVVValdCSlJhcmRJUmNXVGNqR3c9PQ%3D%3D--66f51d7d80652f09a3d2c56f94a8bd6380d4972b; _wunderground_session=SFViSXNZeVFMYkp0MXNOWkZiSW5BMG94QzlqOFBIZnBSOGY2S3B4RkIybzFZdFBQeUlZTlAwMjVNRWJqckRRc2R6V1pweWZhdmpqcTgrNWliM3Izbk4xaWo4d05Tc1YvMyt4L2tuNTlIVjlUZUpTZEwxcE0xUXRTTjAwQjI3eVpSM2c4MlRnY21jUEpIcGo4SjNnY0pRPT0tLTc3TlMrZjFJT1U2S1cySm9DN0RDNGc9PQ%3D%3D--accf1fa6ea2a345286abe82acd0e882d9f4f2c40; _ecommerce_app_session=S1JlZ3BYTDZraDdZU3RZUmpCMHR2aTRGaUpvL1BJN0p6MjRqM0krbXplRFdjemhOMnQ0ckp1ZERzTUp2eWQ5Zit2bEVCOGZLRU9pOHJCRHp1Z0Z1UUZtRlVEVnBhaFBielBjckNMVmlXR0dxS1ZabjNKUFdBcVBUR1JSUlUxYWFSemNNem50TzFxam9aU1BTRXpvQkNrWHpMMVdvUEM2MjdWbHh3NVdYU2QxM1ZwMmFWM3RZdjVlSWFnd1o5OWxtb1lMenJiVStKUnRRcHEzdHlSTGUvdDlCcnFXWDF5TTBmdXFPN0VsUHJxVk50dTREeHkzZG9PdERZV2l6WXJIOEpyVnRjU0FVOW9XeWpldzdZVFBXM0xFTktjQWFMbXpuWHlLay9pK0ZhSVZ6dUVsVXJleDYwR090QjdmaTIyb0E5ODh6cWVHaTE2SHFCZ2JrcWFaMmtWTlM0K0lVejNUZVZkQThGVTN4N3VBPS0ta1E2clFzb1VRQS91WEFOYjl2NGdJUT09--c47816c90bd08c466f3f8f60f4db07eb40807b08; _Top5_session=dndqMXRCdFVxbERXWCs4UStiQ1gwc0daWE9aOW1wRndWTHVjSk1kNXR1T2xoOUNwbk1ndGR0T1lqZXBkSUVuN2VPRU05RnNxcGx1aUg2RjJya1dWaUNTekZ4RzZCb0VvdEFNYmhaUHNjZXhmWDFWM3EzK0lVUm4yazhoaXZLZlpxSldqUC9GN1NMNm54NHpyRUpTK0tBPT0tLW5QQldSb2VVVWJ2V2syRDhXVUs1TlE9PQ%3D%3D--3f5773b8063cfd8dc61e917501485c86e625a4a6; _recruiting_session=cnlTQ2h6YWZKZFVPb1Q1cklSVngvK3hTK0M5ckhJUmhheE83QStTRThCeGg0dGpaNGJCMVRrUGFiQXphQm5Pb2FZbGNkV095bGdNNjlMUmRCTmhSa2duNzRzZEppc2JBT0VoSnZIVWlCMVJtWXZmZktJQWgvM3paM2ZKbmZCdWd4MGw0SHF3OGt6b0xJaXhXTzVGSGxaL2ZBNkNPZ0dGL0ZPMklkNXVKQXRTdFY4Y0J4bGx1eTZYWW9QQlpPZ2JsLS02b3NpWmZibGJGd3JhWEg0TUJzYUpBPT0%3D--722ac1ecda4ef81214e52effeef8fe14317c2bd3; _marta_near_me_session=N2MreUZINElveVZMN2NyNkUrUkduZVZBZHdHMVRCcHE1TE02ZXlKeGdJd21iYUlDeGl4aGkwWUNiODZPRVNPTTlGd1ZlRWlJZjZTUHVzZ25qWHdtaTg1NjBkR3hKQ2hjSWUvTElQY0t1azN3UmpJbXQ2T0xiTlpCOVphTkVxcjlrNVVGNXhNcTZaY3dVY0JEcmtUb0FkeWRVQVdtWm5sU3l4NG1tbWQ0Z2lSYXpNSUsvNGt1V0IwV2NQV1RjVEZ5NWpWUG0rQWR4N1FTSzlxcUFLRW1LOFVEY1VzaXd2K0x4OHJnRVV6OVdlNTI0ZFJGUytqbUNmMmI0SWJsVWpnRWhWTTNWRnUzYksvazNTK01Zd3drTWc9PS0tVWxaSG1FeGl5Z2JrcUxXUUl3aDhJUT09--80376da7d66242e7d73d7e1e598b115d22ec59d6; _sassy_session=MTBuOGpOaVlWbDc3NEl5U0t5U0o3OHk3N0VOc3FNbVdnU2xEajBqTGtaQm5sMStQT3E0bTlLZmpRYy9zY2xKOVgrK1hhcVhSc2UvalVWYm9jS2RlMlhMTCtOK1JETFZQRUtnTUxRSmlrOHFNU0l4SmxmSTRYcUIrSHJ4cEJBbUNZMXBCS3NBL3hoYmhJY2xOZlJyaW13PT0tLW5ZOTBjUS92MjJ3bXlNWlRwb2wvZFE9PQ%3D%3D--62667911aa0f694c2215d10622c4a2e6d4d41007; _gf-hackathon_session=Z0xBUTAreU1EM1h4NFluL3BsMUhicE9wa1BtUHdQRWRic2NpOWh3NjIyNnduamRsZGh4UDhFcW83MDhXenhhQ2ZsNloxKzZsa3RxQXcycVVtcUw4Tnh4UVRwTVVqUWJiOExtSnQwcHJFb1BnM3d4eTVjejgza3pPTFV2OEwwWTZ2UE04cmVwc3IvN21wRks3c0NWK1A1dEpYdmVsK1NlTGt4b2JKRkpSOE1Db2oxajE0aHQ4YS8wYXVLNDM0OTBkcXlQUzJUVFlUeFc5dUxuRzNtaVU4YWZUSVBKQVUwM1dkRmFJcUpiR0JlST0tLVJEMzZNUWpmU0hqcFEzbVAyLzlDVFE9PQ%3D%3D--7bd822b0ba783a644ba067a9bc664450c8d8c7bb; _Library_session=eXhPTWVQLzFWcVlzcCtIMVJReDdaSzBLZXFsa3NJeFpzb21RdUYyb1d1OHJGaWdhMmVxV1l2aDFkUHZ3dUhLalRFenN6QWpPVDFnSTFKQUJpaWE2MHJZOXVtaWVGSU95SnVQeE13ZmdoV0FMYlYvSDR1NVRQMld5NTgxYTRnbS9VekZlT1VuNkl0TVVwSDdWMDNsbFZ3PT0tLVpocjJmTjZBSDRueTEwc3VMWHZ5Rmc9PQ%3D%3D--7470bc2a23b85bab792f4a9d2ef7e75438503948; _fittery-project_session=NVdHZ2Y4c3h0VFJDQkQ1d1lmTFlNVnZyeml1RzVoN1NBMnh4OTFCYmdYQXQyS0oyT1M5R2R2WFRSNStmdEhWMVpmcWJJYTR2a1RwNnJ3b0RnTWpKZTE3amtPQVhYeERUVWdVbC9pbjF0U2g4T1FFQzJDb2pjZGFDZnhFUkJqOUtuN05jSG1WSVRaZm4wQUgxNjZNWllnPT0tLXhONHdmQkp6T2RvbEVyL09POU8xK2c9PQ%3D%3D--559898f1567aac047c39765abe134e9c9b5081c7; _rubix-3_0-rails_session=ckZBdUFhOCsvbjBxWGk4SGZyQkJkZGFvdG9ReG9wRVBkTVBrRzljN1d5WXMxbzBJakNXZ01KamdGZTdpbTNiTUZPNktObGppbzZndEdkaE84S1pRU3RubEpsMm1VTndiNlVmYk9oWmN6dHczc3RWWUMwUmg2a1JWN1gzK2lmR3Y0aHFBUEJjTU9qOFo3MlZmSkh1dkVRPT0tLVZuN3FJQnhuZER6UVlmaXUvWm5yaGc9PQ%3D%3D--beae7a3f4cd8c9179965f81f4da275b27c9fd20c; _mainmarket_session=OGNILytyTzFxR1RRd1I3dnJva2V1UWMrYzBBckRkRC9nWmxxWC9MdXhUcUMzV0QxbVFnUVAzdWpXU2l1VFByODNDdWRpUzYyVHVwVTFlamNIaGFiSk9kTkZWSy9kWHpqa0xneC9FeVNOZjNYbDhMVU1UT0dYOElua0NaMTV3dFRoV1RHaGRIS3dMcElTZ0p6K1QxYnVrWlNTRXJMenlkVUZ5aWtnWkpRSFZndEpFMHVNR3gxcFRGYVhKaGVUT3BvVEdKMGRRUmFERlQxUDYxbHRJc29OamZJWVM5UUJSeXZzMDVzaUxzN3JYcWswYXZHb0YxZTFuNlhxYlBleEs4ZFBvekkrOXFOYU9QeUcrUHowZ3FpUG0zaE5NamwvOUppZSs1VWV3NXczbi85M2pGWG5ENnlvd1lCcGc5bEpUTTZBeUkyM2Vyd09Vb0dqSjd6Q2x5Z1g4MllzZzV3TkU2ejZ4ek5lVmtGaHlIcGU5NWlXMlNLazc2N0JvUHorT2o0MURRQVFUbHZLMFBraHAxTWRYZGZmT2gzMUx0aDdkdDNESFdMV1hLTUJZQS9XS3RsSGc5U3FQSzJaUmRjdDBjMi0tQlgyVzJITjVPbURqN2gxK1d5dmdQZz09--121f584b91d060b407193488b5341fc4919bfa28; _blog_app_session=VU0xeHFmL29hRTdBTWl6WExCZ2JGbHFwL2s1Qnh0QXpBajlsU2ZseHpDdjl3TzhHZzl0NHhVSFVLUktqRTRxejlYTXVFMW9rRllZSTJ3NTlUcHZHYkdhaFFObG43bkN5VkgwbmdzRWY0c21ZT3RJM0lFcXpCMHh1YjRqakpMdFFPZzc4b1dXLzRsQWc5QXJPMTdZNmwzajBOeG5kRHduYzdObFZSUDNMakFHUXNmU3k3MFdCYkRncU94cE0zcEVyOEN6dER2aXYwUGo2Mm44NW1Ub29sSzJ2RUxGVmV4V05oM2JUUHVjV1hhRT0tLUM4YUM3MUpPeWtDSEYxMTU1aStyR3c9PQ%3D%3D--f30b07b16552349bfb6d11fbae3afd1cbac32bcf; _todo_list_session=eER5TjRjMWdEL0p4eVY1SHFlcUdCYzhVNldhRk5qclVEWTJCSVFjdkNZcXFvT053VXRpU1NKdG9qb3dFdGdpMUwwcmxXbmJIMXVrbDBxSFVjZTNXcHBIbDRkTXhGaTRmTm90YXZ4d2plSWtuWWlwZGdMSkVMRkliSS8wajhVejJ1MC83Q0h3d2NuaTdmMC9WT3o4NTVnPT0tLWtmUlh5SzFiQ2llZHk0am15d3dzeVE9PQ%3D%3D--5a16fdce0a2c0afe942f70c3203334df3a64d2e8; _tts-reddit_session=WkxWcERrRUJFNURkTGxvR3h5VHpPa2FhWXJhc0RrUElIOFVuT1ZhQ3FGSXhta0ZnVm1GNlVUdnJmOUxIOE9aTUhUcS9hMGY0bFl2K1dyUEYvTlpaNlNjTjV4OHBvWW1YdzM0SFozUkZKQTd5YktMeHI3dXRDV21hN3piaGhyalh6b1EzSExmb0Vod1FRYkhPeU1meFVGaUhPVVM5cllPV3dRT2dsQlM4Ty9vT3VRSkdmcTBsZlFhNXBrYU1CWWlMUE5FSmZDam5DRjlUOGdOMjlHOW9TdVVSYlR4MVlFdWZ2b0dzYTJFajhsMD0tLWY5a1o3U1krKzRUK3pBNmc0bEsyS2c9PQ%3D%3D--458a23790e533b457ebbb80e9dd5bdf76e0de77a; _tweeter_session=ZFpCZHR2dklyd0ozUzJ4ZDBCWjhGZVJ1bFlvNGo0bmdIdkltalN4aDNmY1RsbmlhczR4cThsa3JJUGlFZ0JxcHFiUSsxOU5LbDhVdmtsb2ttWndpUWVMcXYzNEpEbXRpZ3JyOERrQytxMmoxT0FZd1orNDdQOEFGNFFqWllyWmJIdHE1RFF5aVY5MndoOGVmOUpMZTZFbEdJTjk2QzhUQWZXOEJNOCtXbWphM2QwS2NxRkhOUFl6ZTIrMDd6RHFJd2QyTDJkVEh5SmhqSnZvVjZZUllFbU5vOExXQmFGM2ppV2JXSXBXRUo3ZVY5ZG5ycEo0dnRnb0kxeitUT1JNWmhTZnNIMDVvbjV4emxQUDF5b2x2Njk3djREZmNza0NudDB4ZW0yay8xaytYS2QrU0cySzJ0ZzEvUUdhcEZwanBoN3o0OWpsaHVmYTc0dUY3U0R6VDN3PT0tLUE0Zy9mS0VmcGp2bXNDSVJ4UGQ2ZkE9PQ%3D%3D--e1ca4a8f11b7d639e0235fec0644adf6e4576888; _angular_rails_reddit_session=RVQ2anNFSE1qK3VGYjFyMzdTTnRXc1ZDM25JNkNqT25OT1ZTK3RWUXptUWtwSHZwVE1vOE5tUWJML2NKUDZVemRYcThicUpYYzJxejJab3RPanRNR21BSTZVM2NBVFRZeE94OE9QVzVhUTFFdUJ6WmljbTN4eFVQV1VKMkVyY3RjYlE5dkRyZVArelFNSTV5UWdqVTJBPT0tLTN4dUlHaVZXcW1wZFNER1JyOFBxWlE9PQ%3D%3D--67046be80e6a383c4ecf3d47bfb8b5d58f5ed7d1; _fittery_session=b1FVdGtyVTN5Sy9MOWdHTEJzU0RxYjBEeERteC9tTTd3ekFOZE02eXV3VHNTM29XV0F4dVhUb1ppTzNmdHZKZGRUSWVpU0lYTm1vQ045TFFCWDhKZldQTFJ1cWpvTkFiZlBIQnVuL0I4SGhIODA0bFNwK1diOVJXVnZ0MzNaekovcEN3dDI2cHdjN3lUSnVlVzdoYjY5a04wYXJiQk41aTc5TGVleExwZkZzdExGTDJrejJtTktiL0p4K3FreENBYnlPTW1WWXFSbEZMWWtidFdzMldDSDVyaG1acEM0OVhxREVlMUg0S0xWcGV2Q0dxTzNaODZWWmhvWWFrNW55SEJ3ZmtEMjR1YlpYSk5uV1Flem4wUjV4U0hSSUtyQlF0QUcvQkc5TGtBWUl1UFRNNEhzLzgrVlg0RmxMcGw3QW42UDVmUjVqQ1hPd0V1K1M1aXFFb0NnPT0tLUw1K0dHZXh6b3pPMDBNT1YwZFEyVFE9PQ%3D%3D--527a62a03427f1ed30709c0a8a591cadb7a26cea; _ladies-of-tts_session=OHEwdWNLVFJyRU8yQTEyVjZyNUp6YzdyUjNJSlVreU15dktVRk1QNlEzQ0xRcUFTUUx3M0hlTEZlUnE5MkNadmZwaWtqTUp0SVRQaGFZcHZ1TDh3am5wZ3J3MnBMaHVFOG5oeDZXTDBRb2VHcmVKZS9ORUpHSmFmcHg0V0dWKzdESGRkZnpZUGVoM3lvQU02VVBaalBnPT0tLVRPbTNQSkY3VUczUUk5UnQ1OUtZNkE9PQ%3D%3D--f48ce24828b13200003ee27dcc8717925dcfc9ce; _three-good-things_session=RWZ0NS92QVN2dWoxWkpzVk1RNEN6UmlXMlFhSnYyc0pXd1l1Q2NzMGFQM1BmMkJlM1gzMG9LeTFKY3I0L2NoUzJwRk1PZnprQUx6U3duUStYYkFBT055WkVRVVIxdlVkalFUaVNBaEdqSW9EM1ZSMUR5d0pUbDgxcUJ5cnZ5cDY5YkFhUlJYTStzZ0JINENTbnFjK2tGU1IxZEg2TWlnUGtuUjlvRXozbnVrcFdEZFlOQWdTREFVTCtOTHRhUW1MZDFWMXVSSDZwdXNmQWlHRlk1Q3ZsWmJWbjMwb0NjRDhNQk90L0YyY1RRST0tLVY2ZnZRemluWWNkY2ZyNTJONEpWNmc9PQ%3D%3D--0e7bf64b6d71d668d845f98f35c1f94d239aefc3; _my_site_session=MFE2NWZydk1Fa05PRHpwbWdDNzl3TXJZZHFEa01pdEFSQUdsSWpzN0F0dGx6cklacFZzV2hsaTdITWpubU5PUDdYU1ZZWkZEOUpWb1pIbDUyeDVHR0ZiNDZHU0U4K2V4QnU0K2pRTmdpdWNlUHFvTnZMQ3g3MHBDSEhjV2VKOWRoTEVFUm9NRWdIUzZ1YjFIaE9aVVlnPT0tLWpjVUVuaHJyM0JoWm1GUit0WmFWL2c9PQ%3D%3D--d7711f08ae193bd0528e10198728887b9eedc426; _rails-starter-kit_session=NmRveUJiZ0RMTTlRR003L1RrUE5lem5oMmRjdG5xMmtZcDRlUTFWRjBXNmNPMkFvT1RqSFBPd1pBQVBHSHRLSmg3Qk53WXZ3Ykp6bkQvT2lqa2dyQU5ZYXlJcXBnaEdabzhTSEJteHl5K1lBNzU2R1BMNGx6WGpFa1dOTVpOUS9mZ09LZWJYZlQwMjhRL2xiWFBnVU9BPT0tLUthVEdXNDlQZDREQTd0MWVkU093SUE9PQ%3D%3D--41cea213452b8234f53415d44c98bf55997633be; step-1=60; step-2=140; step-3=0; step-4=13; step-5=30; step-6=26; step-7=26; step-8=0; current-step=9; step-9=0; sid=111; __cypress.initial=false; __cypress.remoteHost=http://localhost:8000; best_fit=14 1/2, 34-35, Traditional',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(200)
|
||
})
|
||
})
|
||
|
||
// Set custom status message/reason phrase from http response
|
||
// https://github.com/cypress-io/cypress/issues/16973
|
||
it('set custom status message when reason phrase is set', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.post('/companies/validate', {
|
||
payload: { name: 'Brian' },
|
||
})
|
||
.reply(400, function (uri, requestBody) {
|
||
this.req.response.statusMessage = 'This is the custom status message'
|
||
|
||
return {
|
||
status: 400,
|
||
message: 'This is the reply body',
|
||
}
|
||
})
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/companies/validate',
|
||
json: true,
|
||
body: {
|
||
payload: { name: 'Brian' },
|
||
},
|
||
headers: {
|
||
'x-cypress-status': '400',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(400)
|
||
expect(res.statusMessage).to.eq('This is the custom status message')
|
||
})
|
||
})
|
||
|
||
// use default status message/reason phrase correspond to status code, from http response
|
||
// https://github.com/cypress-io/cypress/issues/16973
|
||
it('uses default status message when reason phrase is not set', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.post('/companies/validate', {
|
||
payload: { name: 'Brian' },
|
||
})
|
||
.reply(400)
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/companies/validate',
|
||
json: true,
|
||
body: {
|
||
payload: { name: 'Brian' },
|
||
},
|
||
headers: {
|
||
'x-cypress-status': '400',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(400)
|
||
expect(res.statusMessage).to.eq('Bad Request')
|
||
})
|
||
})
|
||
|
||
it('hands back 201 status codes', function () {
|
||
nock(this.server.remoteStates.current().origin)
|
||
.post('/companies/validate', {
|
||
payload: { name: 'Brian' },
|
||
})
|
||
.reply(201)
|
||
|
||
return this.rp({
|
||
method: 'POST',
|
||
url: 'http://localhost:8000/companies/validate',
|
||
json: true,
|
||
body: {
|
||
payload: { name: 'Brian' },
|
||
},
|
||
headers: {
|
||
'Cookie': '__cypress.initial=false',
|
||
},
|
||
})
|
||
.then((res) => {
|
||
expect(res.statusCode).to.eq(201)
|
||
})
|
||
})
|
||
})
|
||
})
|