Merge branch 'develop' into chore/merge_develop

This commit is contained in:
Bill Glesias
2023-07-25 12:28:35 -04:00
42 changed files with 928 additions and 672 deletions

View File

@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.
07-19-23
07-25-23

View File

@@ -30,6 +30,7 @@ mainBuildFilters: &mainBuildFilters
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'update-v8-snapshot-cache-on-develop'
- 'publish-binary'
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -50,6 +51,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'publish-binary', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -130,12 +132,24 @@ executors:
DISABLE_SNAPSHOT_REQUIRE: 1
commands:
# This command inserts SHOULD_PERSIST_ARTIFACTS into BASH_ENV. This way, we can define the variable in one place and use it in multiple steps.
# Run this command in a job before you want to use the SHOULD_PERSIST_ARTIFACTS variable.
setup_should_persist_artifacts:
steps:
- run:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "publish-binary" && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command
verify_should_persist_artifacts:
steps:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "ryanm/feat/change-test-isolation-logic" ]]; then
if [[ -z "$SHOULD_PERSIST_ARTIFACTS" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -670,14 +684,21 @@ commands:
path: ~/.npm/_logs
post-install-comment:
parameters:
package_url_path:
type: string
default: npm-package-url.json
binary_url_path:
type: string
default: binary-url.json
description: Post GitHub comment with a blurb on how to install pre-release version
steps:
- run:
name: Post pre-release install comment
command: |
node scripts/add-install-comment.js \
--npm npm-package-url.json \
--binary binary-url.json
--npm << parameters.package_url_path >> \
--binary << parameters.binary_url_path >>
verify-mocha-results:
description: Double-check that Mocha tests ran as expected.
@@ -715,7 +736,7 @@ commands:
cd ~/cypress/..
# install some deps for get-next-version
npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12
npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12 minimist@1.2.5
NEXT_VERSION=$(node ./cypress/scripts/get-next-version.js)
cd -
@@ -1025,6 +1046,33 @@ commands:
command: node ./scripts/wait-on-circle-jobs.js --job-names="<<parameters.job-names>>"
build-binary:
steps:
- run:
name: Build the Cypress binary
no_output_timeout: "45m"
command: |
source ./scripts/ensure-node.sh
node --version
if [[ `node ./scripts/get-platform-key.js` == 'linux-arm64' ]]; then
# these are missing on Circle and there is no way to pre-install them on Arm
sudo apt-get update
sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
DISABLE_SNAPSHOT_REQUIRE=1 yarn binary-build --version $(node ./scripts/get-next-version.js) --createTar
else
yarn binary-build --version $(node ./scripts/get-next-version.js) --createTar
fi
- store_artifacts:
path: cypress-dist.tgz
check-if-binary-exists:
steps:
- run:
name: Check if binary exists, exit if it does
command: |
source ./scripts/ensure-node.sh
yarn check-binary-on-cdn --version $(node ./scripts/get-next-version.js) --type binary --file cypress.zip
build-and-package-binary:
steps:
- run:
name: Check environment variables before code sign (if on Mac/Windows)
@@ -1057,9 +1105,6 @@ commands:
fi
- run:
name: Build the Cypress binary
environment:
DEBUG: electron-builder,electron-osx-sign*
# notarization on Mac can take a while
no_output_timeout: "45m"
command: |
source ./scripts/ensure-node.sh
@@ -1072,6 +1117,23 @@ commands:
else
yarn binary-build --version $(node ./scripts/get-next-version.js)
fi
- run:
name: Package the Cypress binary
environment:
DEBUG: electron-builder,electron-osx-sign*,electron-notarize*
# notarization on Mac can take a while
no_output_timeout: "45m"
command: |
source ./scripts/ensure-node.sh
node --version
if [[ `node ./scripts/get-platform-key.js` == 'linux-arm64' ]]; then
# these are missing on Circle and there is no way to pre-install them on Arm
sudo apt-get update
sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
DISABLE_SNAPSHOT_REQUIRE=1 yarn binary-package --version $(node ./scripts/get-next-version.js)
else
yarn binary-package --version $(node ./scripts/get-next-version.js)
fi
- run:
name: Zip the binary
command: |
@@ -1091,6 +1153,19 @@ commands:
paths:
- cypress/cypress.zip
trigger-publish-binary-pipeline:
steps:
- run:
name: "Trigger publish-binary pipeline"
command: |
source ./scripts/ensure-node.sh
echo $SHOULD_PERSIST_ARTIFACTS
node ./scripts/binary/trigger-publish-binary-pipeline.js
- persist_to_workspace:
root: ~/
paths:
- triggered_pipeline.json
build-cypress-npm-package:
parameters:
executor:
@@ -2003,13 +2078,64 @@ jobs:
resource_class: << parameters.resource_class >>
steps:
- restore_cached_workspace
- build-binary
- check-if-binary-exists
- build-and-package-binary
- build-cypress-npm-package:
executor: << parameters.executor >>
- setup_should_persist_artifacts
- verify_should_persist_artifacts
- upload-build-artifacts
- post-install-comment
create-and-trigger-packaging-artifacts:
<<: *defaults
parameters:
<<: *defaultsParameters
resource_class:
type: string
default: xlarge
resource_class: << parameters.resource_class >>
steps:
- restore_cached_workspace
- check-if-binary-exists
- setup_should_persist_artifacts
- build-binary
- trigger-publish-binary-pipeline
get-published-artifacts:
<<: *defaults
parameters:
<<: *defaultsParameters
resource_class:
type: string
default: large
resource_class: << parameters.resource_class >>
steps:
- restore_cached_workspace
- run:
name: Check pipeline info
command: cat ~/triggered_pipeline.json
- setup_should_persist_artifacts
- run:
name: Download binary artifacts
command: |
source ./scripts/ensure-node.sh
node ./scripts/binary/get-published-artifacts.js --pipelineInfo ~/triggered_pipeline.json
- persist_to_workspace:
root: ~/
paths:
- cypress/cypress.zip
- cypress/cypress.tgz
- verify_should_persist_artifacts
- persist_to_workspace:
root: ~/
paths:
- cypress/binary-url.json
- cypress/npm-package-url.json
- post-install-comment:
package_url_path: ~/cypress/npm-package-url.json
binary_url_path: ~/cypress/binary-url.json
test-kitchensink:
<<: *defaults
parameters:
@@ -2672,13 +2798,23 @@ linux-x64-workflow: &linux-x64-workflow
- run-vite-dev-server-integration-tests
- v8-integration-tests
- create-build-artifacts:
- create-and-trigger-packaging-artifacts:
context:
- test-runner:upload
- test-runner:commit-status-checks
- test-runner:build-binary
- publish-binary
requires:
- build
- wait-for-binary-publish:
type: approval
requires:
- create-and-trigger-packaging-artifacts
- get-published-artifacts:
context:
- publish-binary
- test-runner:commit-status-checks
requires:
- wait-for-binary-publish
# various testing scenarios, like building full binary
# and testing it on a real project
- test-against-staging:
@@ -2696,66 +2832,66 @@ linux-x64-workflow: &linux-x64-workflow
- build
- test-npm-module-on-minimum-node-version:
requires:
- create-build-artifacts
- get-published-artifacts
- test-types-cypress-and-jest:
requires:
- create-build-artifacts
- get-published-artifacts
- test-full-typescript-project:
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-kitchensink:
requires:
- create-build-artifacts
- get-published-artifacts
- test-npm-module-and-verify-binary:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-staging:
context: test-runner:record-tests
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-kitchensink-chrome:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-recipes-firefox:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-recipes-chrome:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-recipes:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-kitchensink-firefox:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-todomvc-firefox:
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-against-cypress-realworld-app:
context: test-runner:cypress-record-key
<<: *mainBuildFilters
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-as-specific-user:
name: "test binary as a non-root user"
executor: non-root-docker-user
requires:
- create-build-artifacts
- get-published-artifacts
- test-binary-as-specific-user:
name: "test binary as a root user"
requires:
- create-build-artifacts
- get-published-artifacts
- binary-system-tests:
requires:
- create-build-artifacts
- get-published-artifacts
- system-tests-node-modules-install
linux-arm64-workflow: &linux-arm64-workflow
@@ -2773,17 +2909,34 @@ linux-arm64-workflow: &linux-arm64-workflow
requires:
- linux-arm64-node-modules-install
- create-build-artifacts:
name: linux-arm64-create-build-artifacts
- create-and-trigger-packaging-artifacts:
name: linux-arm64-create-and-trigger-packaging-artifacts
context:
- test-runner:upload
- test-runner:commit-status-checks
- test-runner:build-binary
- publish-binary
executor: linux-arm64
resource_class: arm.medium
requires:
- linux-arm64-build
- wait-for-binary-publish:
name: linux-arm64-wait-for-binary-publish
type: approval
requires:
- linux-arm64-create-and-trigger-packaging-artifacts
- get-published-artifacts:
name: linux-arm64-get-published-artifacts
context:
- publish-binary
- test-runner:commit-status-checks
executor: linux-arm64
resource_class: arm.medium
requires:
- linux-arm64-wait-for-binary-publish
- v8-integration-tests:
name: linux-arm64-v8-integration-tests
executor: linux-arm64
@@ -2959,6 +3112,7 @@ windows-workflow: &windows-workflow
- test-runner:build-binary
requires:
- windows-build
- test-binary-against-kitchensink-chrome:
name: windows-test-binary-against-kitchensink-chrome
executor: windows

View File

@@ -84,6 +84,18 @@ jobs:
script: |
const script = require('./scripts/reports/triage_mitigation_kpis.js')
await script.getIssueMitigationMetrics(github, context, core, "${{ env.START_DATE }}", "${{ env.END_DATE }}", "${{ env.PROJECT_BOARD_NUMBER }}");
- name: Generate Feature Request KPIs
id: feature-metrics
uses: actions/github-script@v6
env:
START_DATE: ${{ github.event.inputs.start-date }}
END_DATE: ${{ github.event.inputs.end-date }}
PROJECT_BOARD_NUMBER: 9
with:
github-token: ${{ secrets.TRIAGE_BOARD_TOKEN }}
script: |
const script = require('./scripts/reports/triage_feature_requests_metrics.js')
await script.getFeatureRequestMetrics(github, context, core, "${{ env.START_DATE }}", "${{ env.END_DATE }}", "${{ env.PROJECT_BOARD_NUMBER }}");
- name: Generate KPI Report
id: generate-report
uses: actions/github-script@v6
@@ -95,5 +107,5 @@ jobs:
github-token: ${{ secrets.TRIAGE_BOARD_TOKEN }}
script: |
const script = require('./scripts/reports/generate_kpi_report.js')
await script.generateKPIReport(github, context, core, ${{ steps.non-mono-repo-open-closed-metrics.outputs.results }}, ${{ steps.mono-repo-open-closed-metrics.outputs.results }}, ${{ steps.triage-metrics.outputs.results }}, ${{ steps.mitigation-metrics.outputs.results }} );
await script.generateKPIReport(github, context, core, ${{ steps.non-mono-repo-open-closed-metrics.outputs.results }}, ${{ steps.mono-repo-open-closed-metrics.outputs.results }}, ${{ steps.triage-metrics.outputs.results }}, ${{ steps.mitigation-metrics.outputs.results }}, ${{ steps.feature-metrics.outputs.results }} );

View File

@@ -14,7 +14,14 @@ _Released 08/1/2023 (PENDING)_
## 12.17.2
_Released 07/18/2023 (PENDING)_
_Released 08/01/2023 (PENDING)_
**Performance:**
- Fixed an issue where unnecessary requests were being paused. No longer sends `X-Cypress-Is-XHR-Or-Fetch` header and infers resource type off of the server pre-request object. Fixes [#26620](https://github.com/cypress-io/cypress/issues/26620) and [#26622](https://github.com/cypress-io/cypress/issues/26622).
## 12.17.2
_Released 07/20/2023_
**Bugfixes:**

View File

@@ -33,7 +33,7 @@ The steps above:
The npm package requires a corresponding binary of the same version. In production, it will try to retrieve the binary from the Cypress CDN if it is not cached locally.
You can build the Cypress binary locally by running `yarn binary-build`. You can use Linux to build the Cypress binary (just like it is in CI) by running `yarn binary-build` inside of `yarn docker`.
You can build the Cypress binary locally by running `yarn binary-build`, then package the binary by running `yarn binary-package`. You can use Linux to build the Cypress binary (just like it is in CI) by running `yarn binary-build` and `yarn binary-package` inside of `yarn docker`.
If you're on macOS and building locally, you'll need a code-signing certificate in your keychain, which you can get by following the [instructions on Apple's website](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html#//apple_ref/doc/uid/TP40005929-CH4-SW30). Also, you'll also most likely want to skip notarization since it requires an Apple Developer Program account - set `SKIP_NOTARIZATION=1` when building locally to do this. [More info about code signing in CI](./code-signing.md).

View File

@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "12.17.1",
"version": "12.17.2",
"description": "Cypress is a next generation front end testing tool built for the modern web",
"private": true,
"scripts": {
@@ -12,6 +12,8 @@
"binary-release": "node ./scripts/binary.js release",
"binary-upload": "node ./scripts/binary.js upload",
"binary-zip": "node ./scripts/binary.js zip",
"binary-package": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node ./scripts/binary.js package",
"check-binary-on-cdn": "node ./scripts/binary.js checkIfBinaryExistsOnCdn",
"build": "yarn build-npm-modules && lerna run build --stream --no-bail --ignore create-cypress-tests --ignore cypress --ignore \"'@packages/{runner}'\" --ignore \"'@cypress/{angular,react,react18,vue,vue2,mount-utils,svelte}'\" && node ./cli/scripts/post-build.js && lerna run build --stream --scope create-cypress-tests",
"build-npm-modules": "lerna run build --scope cypress --scope @cypress/mount-utils --scope @cypress/react && lerna run build --scope \"'@cypress/{angular,react18,vue,vue2,svelte}'\"",
"build-prod": "lerna run build-prod-ui --stream && lerna run build-prod --stream --ignore create-cypress-tests && node ./cli/scripts/post-build.js && lerna run build-prod --stream --scope create-cypress-tests --scope",
@@ -73,6 +75,7 @@
"@cypress/request": "^2.88.11",
"@cypress/request-promise": "4.2.6",
"@electron/fuses": "1.6.1",
"@electron/notarize": "^2.1.0",
"@fellow/eslint-plugin-coffee": "0.4.13",
"@graphql-codegen/add": "3.1.0",
"@graphql-codegen/cli": "2.2.0",
@@ -144,7 +147,6 @@
"detect-port": "^1.3.0",
"electron": "21.0.0",
"electron-builder": "^22.13.1",
"electron-notarize": "^1.1.1",
"enzyme-adapter-react-16": "1.12.1",
"eslint": "7.22.0",
"eslint-plugin-cypress": "2.11.2",
@@ -200,6 +202,7 @@
"start-server-and-test": "1.10.8",
"stop-only": "3.0.1",
"strip-ansi": "6.0.0",
"tar": "6.1.15",
"term-to-html": "1.2.0",
"terminal-banner": "1.1.0",
"through": "2.3.8",

View File

@@ -89,7 +89,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -109,7 +109,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -129,7 +129,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -173,7 +173,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
@@ -218,7 +218,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -248,7 +248,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -276,7 +276,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: assertCredentialStatus,
})
})
@@ -327,7 +327,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
@@ -346,7 +346,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://localhost:3500/foo.bar.baz.json',
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
})
@@ -410,7 +410,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
@@ -450,7 +450,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: true,
})
})
@@ -502,7 +502,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://www.foobar.com:3500/test-request-credentials',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: withCredentials,
})
})
@@ -554,7 +554,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://app.foobar.com:3500/test-request',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: true,
})
})
@@ -574,7 +574,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout
cy.then(() => {
expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', {
url: 'http://localhost:3500/foo.bar.baz.json',
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: false,
})
})

View File

@@ -63,43 +63,24 @@ const connect = function (host, path, extraOpts) {
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
browser.webRequest.onBeforeSendHeaders.addListener((details) => {
const requestModifications = {
requestHeaders: [
...(details.requestHeaders || []),
/**
* Unlike CDP, the web extensions onBeforeSendHeaders resourceType cannot discern the difference
* between fetch or xhr resource types, but classifies both as 'xmlhttprequest'. Because of this,
* we set X-Cypress-Is-XHR-Or-Fetch to true if the request is made with 'xhr' or 'fetch' so the
* middleware doesn't incorrectly assume which request type is being sent
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType
*/
...(details.type === 'xmlhttprequest' ? [{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'true',
}] : []),
],
}
if (
// parentFrameId: 0 means the parent is the top-level, so if it isn't
// 0, it's nested inside the AUT and can't be the AUT itself
details.parentFrameId !== 0
// isn't an iframe
|| details.type !== 'sub_frame'
// is the spec frame, not the AUT
|| details.url.includes('__cypress')
) return requestModifications
) return
return {
requestHeaders: [
...requestModifications.requestHeaders,
...details.requestHeaders,
{
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
},
],
}
}, { urls: ['<all_urls>'] }, ['blocking', 'requestHeaders'])
}, { urls: ['<all_urls>'], types: ['sub_frame'] }, ['blocking', 'requestHeaders'])
})
const fail = (id, err) => {

View File

@@ -285,7 +285,7 @@ describe('app/background', () => {
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('does not add header if it is a nested frame', async function () {
@@ -299,22 +299,7 @@ describe('app/background', () => {
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
})
it('does not add header if it is not a sub frame request', async function () {
const details = {
parentFrameId: 0,
type: 'stylesheet',
}
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('does not add header if it is a spec frame request', async function () {
@@ -329,7 +314,7 @@ describe('app/background', () => {
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({ requestHeaders: [] })
expect(result).to.be.undefined
})
it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () {
@@ -361,60 +346,6 @@ describe('app/background', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to request if the resourceType is "xmlhttprequest"', async function () {
const details = {
parentFrameId: 0,
type: 'xmlhttprequest',
url: 'http://localhost:3000/index.html',
requestHeaders: [
{ name: 'X-Foo', value: 'Bar' },
],
}
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.deep.equal({
requestHeaders: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'true',
},
],
})
})
it('does not append X-Cypress-Is-XHR-Or-Fetch header to request if the resourceType is not an "xmlhttprequest"', async function () {
const details = {
parentFrameId: 0,
type: 'sub_frame',
url: 'http://localhost:3000/index.html',
requestHeaders: [
{ name: 'X-Foo', value: 'Bar' },
],
}
sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')
await this.connect()
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)
expect(result).to.not.deep.equal({
requestHeaders: [
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'true',
},
],
})
})
it('does not add before-headers listener if in non-Firefox browser', async function () {
browser.runtime.getBrowserInfo = undefined

View File

@@ -24,7 +24,7 @@ import type { Readable } from 'stream'
import type { Request, Response } from 'express'
import type { RemoteStates } from '@packages/server/lib/remote_states'
import type { CookieJar, SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
import type { RequestedWithAndCredentialManager } from '@packages/server/lib/util/requestedWithAndCredentialManager'
import type { ResourceTypeAndCredentialManager } from '@packages/server/lib/util/resourceTypeAndCredentialManager'
function getRandomColorFn () {
return chalk.hex(`#${Number(
@@ -82,7 +82,7 @@ export type ServerCtx = Readonly<{
getFileServerToken: () => string | undefined
getCookieJar: () => CookieJar
remoteStates: RemoteStates
requestedWithAndCredentialManager: RequestedWithAndCredentialManager
resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
getRenderedHTMLOrigins: Http['getRenderedHTMLOrigins']
netStubbingState: NetStubbingState
middleware: HttpMiddlewareStacks
@@ -258,7 +258,7 @@ export class Http {
request: any
socket: CyServer.Socket
serverBus: EventEmitter
requestedWithAndCredentialManager: RequestedWithAndCredentialManager
resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
renderedHTMLOrigins: {[key: string]: boolean} = {}
autUrl?: string
getCookieJar: () => CookieJar
@@ -275,7 +275,7 @@ export class Http {
this.socket = opts.socket
this.request = opts.request
this.serverBus = opts.serverBus
this.requestedWithAndCredentialManager = opts.requestedWithAndCredentialManager
this.resourceTypeAndCredentialManager = opts.resourceTypeAndCredentialManager
this.getCookieJar = opts.getCookieJar
if (typeof opts.middleware === 'undefined') {
@@ -303,7 +303,7 @@ export class Http {
netStubbingState: this.netStubbingState,
socket: this.socket,
serverBus: this.serverBus,
requestedWithAndCredentialManager: this.requestedWithAndCredentialManager,
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
getCookieJar: this.getCookieJar,
simulatedCookies: [],
debug: (formatter, ...args) => {

View File

@@ -31,7 +31,6 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'extract:cypress:metadata:headers', parentSpan: this.reqMiddlewareSpan, isVerbose })
this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame']
const requestIsXhrOrFetch = this.req.headers['x-cypress-is-xhr-or-fetch']
span?.setAttributes({
isAUTFrame: this.req.isAUTFrame,
@@ -41,34 +40,6 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
delete this.req.headers['x-cypress-is-aut-frame']
}
if (this.req.headers['x-cypress-is-xhr-or-fetch']) {
this.debug(`found x-cypress-is-xhr-or-fetch header. Deleting x-cypress-is-xhr-or-fetch header.`)
delete this.req.headers['x-cypress-is-xhr-or-fetch']
}
if (!doesTopNeedToBeSimulated(this) ||
// this should be unreachable, as the x-cypress-is-xhr-or-fetch header is only attached if
// the resource type is 'xhr' or 'fetch or 'true' (in the case of electron|extension).
// This is only needed for defensive purposes.
(requestIsXhrOrFetch !== 'true' && requestIsXhrOrFetch !== 'xhr' && requestIsXhrOrFetch !== 'fetch')) {
this.next()
return
}
this.debug(`looking up credentials for ${this.req.proxiedUrl}`)
const { requestedWith, credentialStatus } = this.requestedWithAndCredentialManager.get(this.req.proxiedUrl, requestIsXhrOrFetch !== 'true' ? requestIsXhrOrFetch : undefined)
this.debug(`credentials calculated for ${requestedWith}:${credentialStatus}`)
this.req.requestedWith = requestedWith
this.req.credentialsLevel = credentialStatus
span?.setAttributes({
calculatedResourceType: this.req.resourceType,
credentialsLevel: credentialStatus,
})
span?.end()
this.next()
}
@@ -103,74 +74,6 @@ const MaybeSimulateSecHeaders: RequestMiddleware = function () {
this.next()
}
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
const doesTopNeedSimulation = doesTopNeedToBeSimulated(this)
span?.setAttributes({
doesTopNeedToBeSimulated: doesTopNeedSimulation,
resourceType: this.req.resourceType,
})
if (!doesTopNeedSimulation) {
span?.end()
return this.next()
}
// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
const currentAUTUrl = this.getAUTUrl()
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.requestedWith, this.req.credentialsLevel, this.req.isAUTFrame)
span?.setAttributes({
currentAUTUrl,
shouldCookiesBeAttachedToRequest,
})
this.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
if (!shouldCookiesBeAttachedToRequest) {
span?.end()
return this.next()
}
const sameSiteContext = getSameSiteContext(
currentAUTUrl,
this.req.proxiedUrl,
this.req.isAUTFrame,
)
span?.setAttributes({
sameSiteContext,
currentAUTUrl,
isAUTFrame: this.req.isAUTFrame,
})
const applicableCookiesInCookieJar = this.getCookieJar().getCookies(this.req.proxiedUrl, sameSiteContext)
const cookiesOnRequest = (this.req.headers['cookie'] || '').split('; ')
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
this.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
this.debug('add cookies to request from header: %s', addedCookiesFromHeader)
// if the cookie header is empty (i.e. ''), set it to undefined for expected behavior
this.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
span?.setAttributes({
existingCookiesInJar,
addedCookiesFromHeader,
cookieHeader: this.req.headers['cookie'],
})
this.debug('cookies being sent with request: %s', this.req.headers['cookie'])
span?.end()
this.next()
}
const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
const span = telemetry.startSpan({ name: 'correlate:prerequest', parentSpan: this.reqMiddlewareSpan, isVerbose })
@@ -231,6 +134,93 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
}))
}
const CalculateCredentialLevelIfApplicable: RequestMiddleware = function () {
if (!doesTopNeedToBeSimulated(this) ||
(this.req.resourceType !== undefined && this.req.resourceType !== 'xhr' && this.req.resourceType !== 'fetch')) {
this.next()
return
}
this.debug(`looking up credentials for ${this.req.proxiedUrl}`)
const { credentialStatus, resourceType } = this.resourceTypeAndCredentialManager.get(this.req.proxiedUrl, this.req.resourceType)
this.debug(`credentials calculated for ${resourceType}:${credentialStatus}`)
// if for some reason the resourceType is not set by the prerequest, have a fallback in place
this.req.resourceType = !this.req.resourceType ? resourceType : this.req.resourceType
this.req.credentialsLevel = credentialStatus
this.next()
}
const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
const span = telemetry.startSpan({ name: 'maybe:attach:cross:origin:cookies', parentSpan: this.reqMiddlewareSpan, isVerbose })
const doesTopNeedSimulation = doesTopNeedToBeSimulated(this)
span?.setAttributes({
doesTopNeedToBeSimulated: doesTopNeedSimulation,
resourceType: this.req.resourceType,
})
if (!doesTopNeedSimulation) {
span?.end()
return this.next()
}
// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
const currentAUTUrl = this.getAUTUrl()
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.resourceType, this.req.credentialsLevel, this.req.isAUTFrame)
span?.setAttributes({
currentAUTUrl,
shouldCookiesBeAttachedToRequest,
})
this.debug(`should cookies be attached to request?: ${shouldCookiesBeAttachedToRequest}`)
if (!shouldCookiesBeAttachedToRequest) {
span?.end()
return this.next()
}
const sameSiteContext = getSameSiteContext(
currentAUTUrl,
this.req.proxiedUrl,
this.req.isAUTFrame,
)
span?.setAttributes({
sameSiteContext,
currentAUTUrl,
isAUTFrame: this.req.isAUTFrame,
})
const applicableCookiesInCookieJar = this.getCookieJar().getCookies(this.req.proxiedUrl, sameSiteContext)
const cookiesOnRequest = (this.req.headers['cookie'] || '').split('; ')
const existingCookiesInJar = applicableCookiesInCookieJar.join('; ')
const addedCookiesFromHeader = cookiesOnRequest.join('; ')
this.debug('existing cookies on request from cookie jar: %s', existingCookiesInJar)
this.debug('add cookies to request from header: %s', addedCookiesFromHeader)
// if the cookie header is empty (i.e. ''), set it to undefined for expected behavior
this.req.headers['cookie'] = addCookieJarCookiesToRequest(applicableCookiesInCookieJar, cookiesOnRequest) || undefined
span?.setAttributes({
existingCookiesInJar,
addedCookiesFromHeader,
cookieHeader: this.req.headers['cookie'],
})
this.debug('cookies being sent with request: %s', this.req.headers['cookie'])
span?.end()
this.next()
}
function shouldLog (req: CypressIncomingRequest) {
// 1. Any matching `cy.intercept()` should cause `req` to be logged by default, unless `log: false` is passed explicitly.
if (req.matchingRoutes?.length) {
@@ -525,9 +515,10 @@ export default {
LogRequest,
ExtractCypressMetadataHeaders,
MaybeSimulateSecHeaders,
CorrelateBrowserPreRequest,
CalculateCredentialLevelIfApplicable,
MaybeAttachCrossOriginCookies,
MaybeEndRequestWithBufferedResponse,
CorrelateBrowserPreRequest,
SetMatchingRoutes,
SendToDriver,
InterceptRequest,

View File

@@ -580,7 +580,7 @@ const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
url: this.req.proxiedUrl,
isAUTFrame: this.req.isAUTFrame,
doesTopNeedSimulating,
requestedWith: this.req.requestedWith,
resourceType: this.req.resourceType,
credentialLevel: this.req.credentialsLevel,
},
})

View File

@@ -4,7 +4,8 @@ import { URL } from 'url'
import { cors } from '@packages/network'
import { urlOriginsMatch, urlSameSiteMatch } from '@packages/network/lib/cors'
import { SerializableAutomationCookie, Cookie, CookieJar, toughCookieToAutomationCookie } from '@packages/server/lib/util/cookies'
import type { RequestCredentialLevel, RequestedWithHeader } from '../../types'
import type { RequestCredentialLevel } from '../../types'
import type { ResourceType } from 'cypress/types/net-stubbing'
type SiteContext = 'same-origin' | 'same-site' | 'cross-site'
@@ -12,7 +13,7 @@ interface RequestDetails {
url: string
isAUTFrame: boolean
doesTopNeedSimulating: boolean
requestedWith?: RequestedWithHeader
resourceType?: ResourceType
credentialLevel?: RequestCredentialLevel
}
@@ -23,18 +24,18 @@ interface RequestDetails {
* which is critical for lax cookies
* @param {string} requestUrl - the url of the request
* @param {string} AUTUrl - The current url of the app under test
* @param {requestedWith} [requestedWith] -
* @param {resourceType} [resourceType] - the request resourceType
* @param {RequestCredentialLevel} [credentialLevel] - The credentialLevel of the request. For `fetch` this is `omit|same-origin|include` (defaults to same-origin)
* and for `XmlHttpRequest` it is `true|false` (defaults to false)
* @param {isAutFrame} [boolean] - whether or not the request is from the AUT Iframe or not
* @returns {boolean}
*/
export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | undefined, requestedWith?: RequestedWithHeader, credentialLevel?: RequestCredentialLevel, isAutFrame?: boolean): boolean => {
export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | undefined, resourceType?: ResourceType, credentialLevel?: RequestCredentialLevel, isAutFrame?: boolean): boolean => {
if (!AUTUrl) return false
const siteContext = calculateSiteContext(requestUrl, AUTUrl)
switch (requestedWith) {
switch (resourceType) {
case 'fetch':
// never attach cookies regardless of siteContext if omit is optioned
if (credentialLevel === 'omit') {
@@ -59,7 +60,7 @@ export const shouldAttachAndSetCookies = (requestUrl: string, AUTUrl: string | u
return false
default:
// if we cannot determine a resource level, we likely should store the cookie as it is a navigation or another event as long as the context is same-origin
// if we cannot determine a resource level or it isn't applicable,, we likely should store the cookie as it is a navigation or another event as long as the context is same-origin
if (siteContext === 'same-origin' || isAutFrame) {
return true
}
@@ -220,7 +221,9 @@ export class CookiesHelper {
// cross site cookies cannot set lax/strict cookies in the browser for xhr/fetch requests (but ok with navigation/document requests)
// NOTE: This is allowable in firefox as the default cookie behavior is no_restriction (none). However, this shouldn't
// impact what is happening in the server-side cookie jar as Set-Cookie is still called and firefox will allow it to be set in the browser
if (this.request.requestedWith && this.siteContext === 'cross-site' && toughCookie.sameSite !== 'none') {
const isXhrOrFetchRequest = this.request.resourceType === 'fetch' || this.request.resourceType === 'xhr'
if (isXhrOrFetchRequest && this.siteContext === 'cross-site' && toughCookie.sameSite !== 'none') {
this.debug(`cannot set cookie with SameSite=${toughCookie.sameSite} when site context is ${this.siteContext}`)
return
@@ -228,10 +231,10 @@ export class CookiesHelper {
// don't set the cookie in our own cookie jar if the cookie would otherwise fail being set in the browser if the AUT Url
// was actually top. This prevents cookies from being applied to our cookie jar when they shouldn't, preventing possible security implications.
const shouldSetCookieGivenSiteContext = shouldAttachAndSetCookies(this.request.url, this.currentAUTUrl, this.request.requestedWith, this.request.credentialLevel, this.request.isAUTFrame)
const shouldSetCookieGivenSiteContext = shouldAttachAndSetCookies(this.request.url, this.currentAUTUrl, this.request.resourceType, this.request.credentialLevel, this.request.isAUTFrame)
if (!shouldSetCookieGivenSiteContext) {
this.debug(`not setting cookie for ${this.request.url} with simulated top ${ this.currentAUTUrl} for ${ this.request.requestedWith}:${this.request.credentialLevel}, cookie: ${toughCookie}`)
this.debug(`not setting cookie for ${this.request.url} with simulated top ${ this.currentAUTUrl} for ${ this.request.resourceType}:${this.request.credentialLevel}, cookie: ${toughCookie}`)
return
}

View File

@@ -15,7 +15,6 @@ export type CypressIncomingRequest = Request & {
responseTimeout?: number
followRedirect?: boolean
isAUTFrame: boolean
requestedWith?: RequestedWithHeader
credentialsLevel?: RequestCredentialLevel
/**
* Resource type from browserPreRequest. Copied to req so intercept matching can work.
@@ -27,8 +26,6 @@ export type CypressIncomingRequest = Request & {
matchingRoutes?: BackendRoute[]
}
export type RequestedWithHeader = 'fetch' | 'xhr' | 'true'
export type RequestCredentialLevel = 'same-origin' | 'include' | 'omit' | boolean
export type CypressWantsInjection = 'full' | 'fullCrossOrigin' | 'partial' | false

View File

@@ -49,10 +49,10 @@ context('network stubbing', () => {
request: new Request(),
getRenderedHTMLOrigins: () => ({}),
serverBus: new EventEmitter(),
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get () {
return {
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: 'same-origin',
}
},

View File

@@ -15,9 +15,10 @@ describe('http/request-middleware', () => {
'LogRequest',
'ExtractCypressMetadataHeaders',
'MaybeSimulateSecHeaders',
'CorrelateBrowserPreRequest',
'CalculateCredentialLevelIfApplicable',
'MaybeAttachCrossOriginCookies',
'MaybeEndRequestWithBufferedResponse',
'CorrelateBrowserPreRequest',
'SetMatchingRoutes',
'SendToDriver',
'InterceptRequest',
@@ -77,58 +78,16 @@ describe('http/request-middleware', () => {
expect(ctx.req.isAUTFrame).to.be.false
})
})
})
it('removes x-cypress-is-xhr-or-fetch header when it exists', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(true),
},
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
off: (event, listener) => {},
},
}
describe('CalculateCredentialLevelIfApplicable', () => {
const { CalculateCredentialLevelIfApplicable } = RequestMiddleware
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-xhr-or-fetch']).not.to.exist
})
})
it('removes x-cypress-is-xhr-or-fetch header when it does not exist', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
req: {
headers: {},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
off: (event, listener) => {},
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-xhr-or-fetch']).not.to.exist
})
})
it('does not set requestedWith or credentialLevel on the request if top does NOT need to be simulated', async () => {
it('does not set credentialLevel on the request if top does NOT need to be simulated', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns(undefined),
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
resourceType: 'xhr',
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -136,23 +95,20 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).not.to.exist
expect(ctx.req.credentialsLevel).not.to.exist
})
})
it('does not set requestedWith or credentialLevel on the request if x-cypress-is-xhr-or-fetch has invalid values', async () => {
it('does not set credentialLevel on the request if resourceType has invalid value', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
req: {
headers: {
'x-cypress-is-xhr-or-fetch': 'sub_frame',
},
resourceType: 'document',
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -160,28 +116,25 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).not.to.exist
expect(ctx.req.credentialsLevel).not.to.exist
})
})
// CDP can determine whether or not the request is xhr | fetch, but the extension or electron cannot
it('provides requestedWithAndCredentialManager with requestedWith if able to determine from header (xhr)', async () => {
it('provides resourceTypeAndCredentialManager with resourceType if able to determine from prerequest (xhr)', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({}),
},
req: {
resourceType: 'xhr',
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'xhr',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -189,27 +142,25 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.requestedWithAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `xhr`)
expect(ctx.resourceTypeAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `xhr`)
})
})
// CDP can determine whether or not the request is xhr | fetch, but the extension or electron cannot
it('provides requestedWithAndCredentialManager with requestedWith if able to determine from header (fetch)', async () => {
it('provides resourceTypeAndCredentialManager with resourceType if able to determine from prerequest (fetch)', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({}),
},
req: {
resourceType: 'fetch',
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'fetch',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -217,29 +168,27 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.requestedWithAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `fetch`)
expect(ctx.resourceTypeAndCredentialManager.get).to.have.been.calledWith('http://localhost:8080', `fetch`)
})
})
it('sets the requestedWith and credentialsLevel on the request from whatever is returned by requestedWithAndCredentialManager if conditions apply', async () => {
it('sets the resourceType and credentialsLevel on the request from whatever is returned by resourceTypeAndCredentialManager if conditions apply, assuming resourceType does NOT exist on the request', async () => {
const ctx = {
getAUTUrl: sinon.stub().returns('http://localhost:8080'),
remoteStates: {
isPrimarySuperDomainOrigin: sinon.stub().returns(false),
},
requestedWithAndCredentialManager: {
resourceTypeAndCredentialManager: {
get: sinon.stub().returns({
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: 'same-origin',
}),
},
req: {
resourceType: undefined,
proxiedUrl: 'http://localhost:8080',
headers: {
'x-cypress-is-xhr-or-fetch': 'true',
},
} as Partial<CypressIncomingRequest>,
res: {
on: (event, listener) => {},
@@ -247,9 +196,9 @@ describe('http/request-middleware', () => {
},
}
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
await testMiddleware([CalculateCredentialLevelIfApplicable], ctx)
.then(() => {
expect(ctx.req.requestedWith).to.equal('fetch')
expect(ctx.req.resourceType).to.equal('fetch')
expect(ctx.req.credentialsLevel).to.equal('same-origin')
})
})
@@ -373,7 +322,7 @@ describe('http/request-middleware', () => {
it('is a noop if cookies do NOT need to be attached to request', async () => {
const ctx = await getContext(['request=cookie'], ['jar=cookie'], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'fetch'
ctx.req.resourceType = 'fetch'
ctx.req.credentialsLevel = 'omit'
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -384,7 +333,7 @@ describe('http/request-middleware', () => {
it(`allows setting cookies on request if resource type cannot be determined, but comes from the AUT frame (likely in the case of documents or redirects)`, async function () {
const ctx = await getContext([], ['jar=cookie'], 'http://foobar.com/index.html', 'http://app.foobar.com/index.html')
ctx.req.requestedWith = undefined
ctx.req.resourceType = undefined
ctx.req.credentialsLevel = undefined
ctx.req.isAUTFrame = true
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -395,7 +344,7 @@ describe('http/request-middleware', () => {
it(`otherwise, does not allow setting cookies if request type cannot be determined and is not from the AUT and is cross-origin`, async function () {
const ctx = await getContext([], ['jar=cookie'], 'http://foobar.com/index.html', 'http://app.foobar.com/index.html')
ctx.req.requestedWith = undefined
ctx.req.resourceType = undefined
ctx.req.credentialsLevel = undefined
ctx.req.isAUTFrame = false
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -406,7 +355,7 @@ describe('http/request-middleware', () => {
it('sets the cookie header to undefined if no cookies exist on the request, none in the jar, but cookies should be attached', async () => {
const ctx = await getContext([], [], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'xhr'
ctx.req.resourceType = 'xhr'
ctx.req.credentialsLevel = true
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)
@@ -417,7 +366,7 @@ describe('http/request-middleware', () => {
it('prepends cookie jar cookies to request', async () => {
const ctx = await getContext(['request=cookie'], ['jar=cookie'], 'http://foobar.com', 'http://app.foobar.com')
ctx.req.requestedWith = 'fetch'
ctx.req.resourceType = 'fetch'
ctx.req.credentialsLevel = 'include'
await testMiddleware([MaybeAttachCrossOriginCookies], ctx)

View File

@@ -1104,7 +1104,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1159,7 +1159,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1213,7 +1213,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'omit',
proxiedUrl: 'https://www.foobar.com/test-request',
},
@@ -1269,7 +1269,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'include',
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1322,7 +1322,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1376,7 +1376,7 @@ describe('http/response-middleware', function () {
},
req: {
// a same-site request that has the ability to set first-party cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://app.foobar.com/test-request',
},
@@ -1415,7 +1415,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: 'include',
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1466,7 +1466,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'fetch',
resourceType: 'fetch',
credentialsLevel: credentialLevel,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1500,7 +1500,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1550,7 +1550,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: false,
proxiedUrl: 'https://www.barbaz.com/test-request',
},
@@ -1584,7 +1584,7 @@ describe('http/response-middleware', function () {
},
req: {
// a cross-site request that has the ability to set cookies in the browser
requestedWith: 'xhr',
resourceType: 'xhr',
credentialsLevel: true,
proxiedUrl: 'https://www.barbaz.com/test-request',
},

View File

@@ -33,10 +33,10 @@ export const patchFetch = (window) => {
credentials = credentials || 'same-origin'
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the requestedWith in the proxy
// if the option isn't set, we can imply the default as we know the resourceType in the proxy
await requestSentWithCredentials({
url,
requestedWith: 'fetch',
resourceType: 'fetch',
credentialStatus: credentials,
})
} finally {

View File

@@ -57,10 +57,10 @@ export const postMessagePromise = <T>({ event, data = {}, timeout }: {event: str
/**
* Returns a promise from the backend request for the 'request:sent:with:credentials' event.
* @param args - an object containing a url, requestedWith and Credential status.
* @param args - an object containing a url, resourceType and Credential status.
* @returns A Promise or null depending on the url parameter.
*/
export const requestSentWithCredentials = <T>(args: {url?: string, requestedWith: 'xhr' | 'fetch', credentialStatus: string | boolean}): Promise<T> | undefined => {
export const requestSentWithCredentials = <T>(args: {url?: string, resourceType: 'xhr' | 'fetch', credentialStatus: string | boolean}): Promise<T> | undefined => {
if (args.url) {
// If cypress is enabled on the window use that, otherwise use post message to call out to the primary cypress instance.
// cypress may be found on the window if this is either the primary cypress instance or if a spec bridge has already been created for this spec bridge.

View File

@@ -20,10 +20,10 @@ export const patchXmlHttpRequest = (window: Window) => {
window.XMLHttpRequest.prototype.send = async function (...args) {
try {
// if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
// if the option isn't set, we can imply the default as we know the "requestedWith" in the proxy
// if the option isn't set, we can imply the default as we know the "resourceType" in the proxy
await requestSentWithCredentials({
url: this._url,
requestedWith: 'xhr',
resourceType: 'xhr',
credentialStatus: this.withCredentials,
})
} finally {

View File

@@ -305,19 +305,19 @@ export class CdpAutomation implements CDPClient {
})
}
private _continueRequest = (client, params, headers?) => {
private _continueRequest = (client, params, header?) => {
const details: Protocol.Fetch.ContinueRequestRequest = {
requestId: params.requestId,
}
if (headers && headers.length) {
if (header) {
// headers are received as an object but need to be an array
// to modify them
const currentHeaders = _.map(params.request.headers, (value, name) => ({ name, value }))
details.headers = [
...currentHeaders,
...headers,
header,
]
}
@@ -358,46 +358,26 @@ export class CdpAutomation implements CDPClient {
_handlePausedRequests = async (client) => {
// NOTE: only supported in chromium based browsers
await client.send('Fetch.enable')
await client.send('Fetch.enable', {
// only enable request pausing for documents to determine the AUT iframe
patterns: [{
resourceType: 'Document',
}],
})
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
client.on('Fetch.requestPaused', async (params: Protocol.Fetch.RequestPausedEvent) => {
const addedHeaders: {
name: string
value: string
}[] = []
if (await this._isAUTFrame(params.frameId)) {
debugVerbose('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url)
/**
* Unlike the the web extension or Electrons's onBeforeSendHeaders, CDP can discern the difference
* between fetch or xhr resource types. Because of this, we set X-Cypress-Is-XHR-Or-Fetch to either
* 'xhr' or 'fetch' with CDP so the middleware can assume correct defaults in case credential/resourceTypes
* are not sent to the server.
* @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
*/
if (params.resourceType === 'XHR' || params.resourceType === 'Fetch') {
debugVerbose('add X-Cypress-Is-XHR-Or-Fetch header to: %s', params.request.url)
addedHeaders.push({
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: params.resourceType.toLowerCase(),
return this._continueRequest(client, params, {
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
})
}
if (
// is a script, stylesheet, image, etc
params.resourceType !== 'Document'
|| !(await this._isAUTFrame(params.frameId))
) {
return this._continueRequest(client, params, addedHeaders)
}
debugVerbose('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url)
addedHeaders.push({
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
})
return this._continueRequest(client, params, addedHeaders)
return this._continueRequest(client, params)
})
}

View File

@@ -33,7 +33,7 @@ import type { FoundSpec, ProtocolManagerShape } from '@packages/types'
import type { Server as WebSocketServer } from 'ws'
import { RemoteStates } from './remote_states'
import { cookieJar, SerializableAutomationCookie } from './util/cookies'
import { requestedWithAndCredentialManager, RequestedWithAndCredentialManager } from './util/requestedWithAndCredentialManager'
import { resourceTypeAndCredentialManager, ResourceTypeAndCredentialManager } from './util/resourceTypeAndCredentialManager'
const debug = Debug('cypress:server:server-base')
@@ -118,7 +118,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
protected request: Request
protected isListening: boolean
protected socketAllowed: SocketAllowed
protected requestedWithAndCredentialManager: RequestedWithAndCredentialManager
protected resourceTypeAndCredentialManager: ResourceTypeAndCredentialManager
protected _fileServer: FileServer | null
protected _baseUrl: string | null
protected _server?: DestroyableHttpServer
@@ -149,7 +149,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
}
})
this.requestedWithAndCredentialManager = requestedWithAndCredentialManager
this.resourceTypeAndCredentialManager = resourceTypeAndCredentialManager
}
ensureProp = ensureProp
@@ -197,7 +197,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.socket.toDriver('cross:origin:cookies', cookies)
})
this.socket.localBus.on('request:sent:with:credentials', this.requestedWithAndCredentialManager.set)
this.socket.localBus.on('request:sent:with:credentials', this.resourceTypeAndCredentialManager.set)
}
abstract createServer (
@@ -238,7 +238,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.createNetworkProxy({
config,
remoteStates: this._remoteStates,
requestedWithAndCredentialManager: this.requestedWithAndCredentialManager,
resourceTypeAndCredentialManager: this.resourceTypeAndCredentialManager,
shouldCorrelatePreRequests,
})
@@ -332,7 +332,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
return e
}
createNetworkProxy ({ config, remoteStates, requestedWithAndCredentialManager, shouldCorrelatePreRequests }) {
createNetworkProxy ({ config, remoteStates, resourceTypeAndCredentialManager, shouldCorrelatePreRequests }) {
const getFileServerToken = () => {
return this._fileServer?.token
}
@@ -349,7 +349,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
netStubbingState: this.netStubbingState,
request: this.request,
serverBus: this._eventBus,
requestedWithAndCredentialManager,
resourceTypeAndCredentialManager,
})
}
@@ -363,7 +363,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.networkProxy.reset()
this.netStubbingState.reset()
this._remoteStates.reset()
this.requestedWithAndCredentialManager.clear()
this.resourceTypeAndCredentialManager.clear()
}
const io = this.socket.startListening(this.server, automation, config, options)
@@ -498,7 +498,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
reset () {
this._networkProxy?.reset()
this.requestedWithAndCredentialManager.clear()
this.resourceTypeAndCredentialManager.clear()
const baseUrl = this._baseUrl ?? '<root>'
return this._remoteStates.set(baseUrl)

View File

@@ -1,9 +1,10 @@
import md5 from 'md5'
import Debug from 'debug'
import type { RequestCredentialLevel, RequestedWithHeader } from '@packages/proxy'
import type { RequestCredentialLevel } from '@packages/proxy'
import type { ResourceType } from '@packages/net-stubbing'
type AppliedCredentialByUrlAndResourceMap = Map<string, Array<{
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
}>>
@@ -16,16 +17,16 @@ const hashUrl = (url: string): string => {
// leverage a singleton Map throughout the server to prevent clashes with this context bindings
const _appliedCredentialByUrlAndResourceMap: AppliedCredentialByUrlAndResourceMap = new Map()
class RequestedWithAndCredentialManagerClass {
get (url: string, optionalRequestedWith?: RequestedWithHeader): {
requestedWith: RequestedWithHeader
class ResourceTypeAndCredentialManagerClass {
get (url: string, optionalResourceType?: ResourceType): {
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
} {
const hashKey = hashUrl(url)
debug(`credentials request received for request url ${url}, hashKey ${hashKey}`)
let value: {
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
} | undefined
@@ -37,12 +38,12 @@ class RequestedWithAndCredentialManagerClass {
debug(`credential value found ${value}`)
}
// if value is undefined for any reason, apply defaults and assume xhr if no optionalRequestedWith
// optionalRequestedWith should be provided with CDP based browsers, so at least we have a fallback that is more accurate
// if value is undefined for any reason, apply defaults and assume xhr if no optionalResourceType
// optionalResourceType should be provided by the prerequest resourceType, so at least we have a fallback that is more accurate
if (value === undefined) {
value = {
requestedWith: optionalRequestedWith || 'xhr',
credentialStatus: optionalRequestedWith === 'fetch' ? 'same-origin' : false,
resourceType: optionalResourceType || 'xhr',
credentialStatus: optionalResourceType === 'fetch' ? 'same-origin' : false,
}
}
@@ -50,27 +51,27 @@ class RequestedWithAndCredentialManagerClass {
}
set ({ url,
requestedWith,
resourceType,
credentialStatus,
}: {
url: string
requestedWith: RequestedWithHeader
resourceType: ResourceType
credentialStatus: RequestCredentialLevel
}) {
const hashKey = hashUrl(url)
debug(`credentials request stored for request url ${url}, requestedWith ${requestedWith}, hashKey ${hashKey}`)
debug(`credentials request stored for request url ${url}, resourceType ${resourceType}, hashKey ${hashKey}`)
let urlHashValue = _appliedCredentialByUrlAndResourceMap.get(hashKey)
if (!urlHashValue) {
_appliedCredentialByUrlAndResourceMap.set(hashKey, [{
requestedWith,
resourceType,
credentialStatus,
}])
} else {
urlHashValue.push({
requestedWith,
resourceType,
credentialStatus,
})
}
@@ -82,7 +83,7 @@ class RequestedWithAndCredentialManagerClass {
}
// export as a singleton
export const requestedWithAndCredentialManager = new RequestedWithAndCredentialManagerClass()
export const resourceTypeAndCredentialManager = new ResourceTypeAndCredentialManagerClass()
// export but only as a type. We do NOT want others to create instances of the class as it is intended to be a singleton
export type RequestedWithAndCredentialManager = RequestedWithAndCredentialManagerClass
export type ResourceTypeAndCredentialManager = ResourceTypeAndCredentialManagerClass

View File

@@ -399,10 +399,14 @@ describe('lib/browsers/chrome', () => {
this.pageCriClient.send.withArgs('Page.getFrameTree').resolves(frameTree)
})
it('sends Fetch.enable', async function () {
it('sends Fetch.enable only for Document ResourceType', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', {
patterns: [{
resourceType: 'Document',
}],
})
})
it('does not add header when not a document', async function () {
@@ -413,9 +417,7 @@ describe('lib/browsers/chrome', () => {
resourceType: 'Script',
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
})
expect(this.pageCriClient.send).not.to.be.calledWith('Fetch.continueRequest')
})
it('does not add header when it is a spec frame request', async function () {
@@ -469,70 +471,6 @@ describe('lib/browsers/chrome', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to fetch request', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)
this.pageCriClient.on.withArgs('Page.frameAttached').yield()
await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'Fetch',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'fetch',
},
],
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to xhr request', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)
this.pageCriClient.on.withArgs('Page.frameAttached').yield()
await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'XHR',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'xhr',
},
],
})
})
it('gets frame tree on Page.frameAttached', async function () {
await chrome.open('chrome', 'http://', openOpts, this.automation)

View File

@@ -370,10 +370,14 @@ describe('lib/browsers/electron', () => {
this.pageCriClient.send.withArgs('Page.getFrameTree').resolves(frameTree)
})
it('sends Fetch.enable', async function () {
it('sends Fetch.enable only for Document ResourceType', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', {
patterns: [{
resourceType: 'Document',
}],
})
})
it('does not add header when not a document', async function () {
@@ -384,9 +388,7 @@ describe('lib/browsers/electron', () => {
resourceType: 'Script',
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
})
expect(this.pageCriClient.send).not.to.be.calledWith('Fetch.continueRequest')
})
it('does not add header when it is a spec frame request', async function () {
@@ -440,70 +442,6 @@ describe('lib/browsers/electron', () => {
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to fetch request', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
this.pageCriClient.on.withArgs('Page.frameAttached').yield()
await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'Fetch',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'fetch',
},
],
})
})
it('appends X-Cypress-Is-XHR-Or-Fetch header to xhr request', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)
this.pageCriClient.on.withArgs('Page.frameAttached').yield()
await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'XHR',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})
expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Is-XHR-Or-Fetch',
value: 'xhr',
},
],
})
})
it('gets frame tree on Page.frameAttached', async function () {
await electron._launch(this.win, this.url, this.automation, this.options)

View File

@@ -1,86 +0,0 @@
import { expect } from 'chai'
import { requestedWithAndCredentialManager } from '../../../lib/util/requestedWithAndCredentialManager'
context('requestedWithAndCredentialManager Singleton', () => {
beforeEach(() => {
requestedWithAndCredentialManager.clear()
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'xhr',
credentialStatus: true,
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com%2Ftest-request-2',
requestedWith: 'fetch',
credentialStatus: 'same-origin',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request-2',
requestedWith: 'fetch',
credentialStatus: 'include',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'fetch',
credentialStatus: 'omit',
})
requestedWithAndCredentialManager.set({
url: 'www.foobar.com/test-request',
requestedWith: 'fetch',
credentialStatus: 'include',
})
})
it('gets the first record out of the queue matching the absolute url and removes it', () => {
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: true,
})
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'omit',
})
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'include',
})
// the default as no other records should exist in the map for this URL
expect(requestedWithAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
})
it('can locate a record hash even when the URL is encoded', () => {
expect(requestedWithAndCredentialManager.get('www.foobar.com%2Ftest-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: true,
})
})
it('applies defaults if a record cannot be found without a requestedWith', () => {
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
})
it('applies defaults if a record cannot be found with a requestedWith', () => {
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request', 'xhr')).to.deep.equal({
requestedWith: 'xhr',
credentialStatus: false,
})
expect(requestedWithAndCredentialManager.get('www.barbaz.com/test-request', 'fetch')).to.deep.equal({
requestedWith: 'fetch',
credentialStatus: 'same-origin',
})
})
})

View File

@@ -0,0 +1,86 @@
import { expect } from 'chai'
import { resourceTypeAndCredentialManager } from '../../../lib/util/resourceTypeAndCredentialManager'
context('resourceTypeAndCredentialManager Singleton', () => {
beforeEach(() => {
resourceTypeAndCredentialManager.clear()
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'xhr',
credentialStatus: true,
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com%2Ftest-request-2',
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request-2',
resourceType: 'fetch',
credentialStatus: 'include',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'fetch',
credentialStatus: 'omit',
})
resourceTypeAndCredentialManager.set({
url: 'www.foobar.com/test-request',
resourceType: 'fetch',
credentialStatus: 'include',
})
})
it('gets the first record out of the queue matching the absolute url and removes it', () => {
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: true,
})
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'omit',
})
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'include',
})
// the default as no other records should exist in the map for this URL
expect(resourceTypeAndCredentialManager.get('www.foobar.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
})
it('can locate a record hash even when the URL is encoded', () => {
expect(resourceTypeAndCredentialManager.get('www.foobar.com%2Ftest-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: true,
})
})
it('applies defaults if a record cannot be found without a resourceType', () => {
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
})
it('applies defaults if a record cannot be found with a resourceType', () => {
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request', 'xhr')).to.deep.equal({
resourceType: 'xhr',
credentialStatus: false,
})
expect(resourceTypeAndCredentialManager.get('www.barbaz.com/test-request', 'fetch')).to.deep.equal({
resourceType: 'fetch',
credentialStatus: 'same-origin',
})
})
})

View File

@@ -3,7 +3,7 @@
// "afterSign": "./scripts/after-sign-hook.js"
const fs = require('fs')
const path = require('path')
let electron_notarize = require('electron-notarize')
let electron_notarize = require('@electron/notarize')
module.exports = async function (params) {
// Only notarize the app on Mac OS.
@@ -40,12 +40,17 @@ module.exports = async function (params) {
throw new Error('Missing Apple password for notarization: NOTARIZE_APP_PASSWORD')
}
if (!process.env.NOTARIZE_APP_TEAM_ID) {
throw new Error('Missing Apple team id for notarization: NOTARIZE_APP_TEAM_ID')
}
try {
await electron_notarize.notarize({
appBundleId: appId,
appPath,
appleId: process.env.NOTARIZE_APP_APPLE_ID,
appleIdPassword: process.env.NOTARIZE_APP_PASSWORD,
teamId: process.env.NOTARIZE_APP_TEAM_ID,
})
} catch (error) {
console.error('could not notarize application')

View File

@@ -1,4 +1,4 @@
require('@packages/ts/register')
require('../packages/ts/register')
const command = process.argv[2]

View File

@@ -4,10 +4,11 @@ import path from 'path'
import _ from 'lodash'
import del from 'del'
import chalk from 'chalk'
import electron from '@packages/electron'
import electron from '../../packages/electron'
import la from 'lazy-ass'
import { promisify } from 'util'
import glob from 'glob'
import tar from 'tar'
import * as packages from './util/packages'
import * as meta from './meta'
@@ -25,6 +26,8 @@ const globAsync = promisify(glob)
const CY_ROOT_DIR = path.join(__dirname, '..', '..')
const jsonRoot = fs.readJSONSync(path.join(CY_ROOT_DIR, 'package.json'))
const log = function (msg) {
const time = new Date()
const timeStamp = time.toLocaleTimeString()
@@ -37,6 +40,7 @@ interface BuildCypressAppOpts {
version: string
skipSigning?: boolean
keepBuild?: boolean
createTar?: boolean
}
/**
@@ -74,7 +78,7 @@ async function checkMaxPathLength () {
// For debugging the flow without rebuilding each time
export async function buildCypressApp (options: BuildCypressAppOpts) {
const { platform, version, skipSigning = false, keepBuild = false } = options
const { platform, version, keepBuild = false, createTar } = options
log('#checkPlatform')
if (platform !== os.platform()) {
@@ -109,8 +113,6 @@ export async function buildCypressApp (options: BuildCypressAppOpts) {
await packages.copyAllToDist(DIST_DIR)
fs.copySync(path.join(CY_ROOT_DIR, 'patches'), path.join(DIST_DIR, 'patches'))
const jsonRoot = fs.readJSONSync(path.join(CY_ROOT_DIR, 'package.json'))
const packageJsonContents = _.omit(jsonRoot, [
'devDependencies',
'lint-staged',
@@ -200,11 +202,21 @@ require('./packages/server/index.js')
log('#transformSymlinkRequires')
await transformRequires(meta.distDir())
// optionally create a tar of the `cypress-build` directory. This is used in CI.
if (createTar) {
log('#create tar from dist dir')
await tar.c({ file: 'cypress-dist.tgz', gzip: true, cwd: os.tmpdir() }, ['cypress-build'])
}
log(`#testDistVersion ${meta.distDir()}`)
await testDistVersion(meta.distDir(), version)
log('#testStaticAssets')
await testStaticAssets(meta.distDir())
}
export async function packageElectronApp (options: BuildCypressAppOpts) {
const { platform, version, skipSigning = false } = options
log('#removeCyAndBinFolders')
await del([
@@ -229,6 +241,8 @@ require('./packages/server/index.js')
// to learn how to get the right Mac certificate for signing and notarizing
// the built Test Runner application
const electronVersion = electron.getElectronVersion()
const appFolder = meta.distDir()
const outputFolder = meta.buildRootDir()

View File

@@ -0,0 +1,163 @@
const fs = require('fs-extra')
const rp = require('@cypress/request-promise')
const util = require('util')
const exec = require('child_process').exec
const minimist = require('minimist')
const chalk = require('chalk')
const execPromise = util.promisify(exec)
const artifactJobName = 'publish-binary'
const urlPaths = [
'~/cypress/binary-url.json',
'~/cypress/npm-package-url.json',
]
const archivePaths = [
'~/cypress/cypress.zip',
'~/cypress/cypress.tgz',
]
function getRequestOptions (url) {
return {
method: 'GET',
url,
headers: { 'Circle-Token': process.env.CIRCLE_TOKEN },
}
}
function getPipelineId (pipelineInfoFilePath) {
const data = fs.readFileSync(pipelineInfoFilePath)
const parsedPipelineId = JSON.parse(data).id
if (!parsedPipelineId) {
throw new Error(`error retrieving pipeline id from ${pipelineInfoFilePath}`)
}
return parsedPipelineId
}
async function getWorkflows (pipelineId) {
const response = await rp(getRequestOptions(`https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`))
const parsed = JSON.parse(response)
if (parsed.items.length === 0) {
throw new Error(`did not find any workflows in pipeline ${pipelineId}`)
}
if (parsed.items.length > 1) {
console.log(parsed.items)
throw new Error(`expected pipeline ${pipelineId} to only have one workflow, but it had many`)
}
return parsed.items
}
async function getWorkflowJobs (workflowId) {
const response = await rp(getRequestOptions(`https://circleci.com/api/v2/workflow/${workflowId}/job`))
const parsed = JSON.parse(response)
if (parsed.items.length === 0) {
throw new Error(`did not find any jobs in workflow ${workflowId}`)
}
return parsed.items
}
async function getJobArtifacts (jobNumber) {
const response = await rp(getRequestOptions(`https://circleci.com/api/v2/project/github/cypress-io/cypress-publish-binary/${jobNumber}/artifacts`))
const parsed = JSON.parse(response)
if (parsed.items.length === 0) {
throw new Error(`did not find any artifacts for job ${jobNumber}`)
}
return parsed.items
}
async function downloadArtifact (url, path) {
try {
console.log(`Downloading artifact from ${chalk.cyan(url)} \n to path ${chalk.cyan(path)}...`)
await execPromise(`curl -L --url ${url} --header 'Circle-Token: ${process.env.CIRCLE_TOKEN}' --header 'content-type: application/json' -o ${path}`)
} catch (error) {
throw new Error(`failed to fetch artifact from URL ${url}: ${error}`)
}
}
async function run (args) {
const options = minimist(args)
const pipelineInfoFilePath = options.pipelineInfo
if (!pipelineInfoFilePath) {
throw new Error('--pipelineInfo must be provided as a parameter')
}
console.log(`Parsing pipeline info from ${chalk.cyan(pipelineInfoFilePath)}...`)
const pipelineId = module.exports.getPipelineId(pipelineInfoFilePath)
console.log(`Getting workflows from pipeline ${chalk.cyan(pipelineId)}...`)
const workflows = await module.exports.getWorkflows(pipelineId)
const workflow = workflows[0]
if (workflow.status !== 'success') {
console.error(chalk.red(`\nThe ${chalk.cyan(workflow.name)} workflow that we triggered in the ${chalk.cyan('cypress-publish-binary')} project did not succeed.\n
Status: ${chalk.red(workflow.status)} \n
Check the workflow logs to see why it failed
${chalk.cyan.underline(`https://app.circleci.com/pipelines/workflows/${workflow.id}`)}
`))
process.exitCode = 1
return
}
console.log(`Getting jobs from workflow ${chalk.cyan(workflow.name)}...`)
const jobs = await module.exports.getWorkflowJobs(workflow.id)
const job = jobs.find((job) => job.name === artifactJobName)
if (!job) {
throw new Error(`unable to find job in workflow ${workflow.name} named ${artifactJobName}`)
}
const artifacts = await module.exports.getJobArtifacts(job.job_number)
let artifactPaths
if (process.env.SHOULD_PERSIST_ARTIFACTS) {
artifactPaths = [...urlPaths, ...archivePaths]
} else {
// If we didn't persist the artifacts to the registry, then we only want the build artifacts, no URLs.
artifactPaths = [...archivePaths]
}
const filteredArtifacts = artifacts.filter((artifact) => artifactPaths.includes(artifact.path))
await Promise.all(filteredArtifacts.map(({ url, path }) => {
return module.exports.downloadArtifact(url, path)
}))
console.log('Artifacts successfully downloaded ✅')
}
module.exports = {
getPipelineId,
getWorkflows,
getWorkflowJobs,
getJobArtifacts,
downloadArtifact,
run,
}
if (!module.parent) {
run(process.argv)
}

View File

@@ -22,6 +22,7 @@ const upload = require('./upload')
const uploadUtils = require('./util/upload')
const { uploadArtifactToS3 } = require('./upload-build-artifact')
const { moveBinaries } = require('./move-binaries')
const { exec } = require('child_process')
const success = (str) => {
return console.log(chalk.bgGreen(` ${chalk.black(str)} `))
@@ -197,6 +198,22 @@ const deploy = {
})
},
package (options) {
console.log('#package')
if (options == null) {
options = this.parseOptions(process.argv)
}
debug('parsed build options %o', options)
return askMissingOptions(['version', 'platform'])(options)
.then(() => {
console.log('packaging binary: platform %s version %s', options.platform, options.version)
return build.packageElectronApp(options)
})
},
zip (options) {
console.log('#zip')
if (!options) {
@@ -302,6 +319,31 @@ const deploy = {
})
})
},
async checkIfBinaryExistsOnCdn (args = process.argv) {
console.log('#checkIfBinaryExistsOnCdn')
const url = await uploadArtifactToS3([...args, '--dry-run', 'true'])
console.log(`Checking if ${url} exists...`)
const binaryExists = await rp.head(url)
.then(() => true)
.catch(() => false)
if (binaryExists) {
console.log('A binary was already built for this operating system and commit hash. Skipping binary build process...')
exec('circleci-agent step halt', (_, __, stdout) => {
console.log(stdout)
})
return
}
console.log('Binary does not yet exist. Continuing to build binary...')
return binaryExists
},
}
module.exports = _.bindAll(deploy, _.functions(deploy))

View File

@@ -0,0 +1,50 @@
const fs = require('fs-extra')
const os = require('os')
const path = require('path')
const fetch = require('node-fetch')
const { getNextVersionForBinary } = require('../get-next-version')
;(async () => {
const pipelineInfoFilePath = path.join(os.homedir(), 'triggered_pipeline.json')
const { nextVersion } = await getNextVersionForBinary()
function getArtifactUrl (fileName) {
return `https://output.circle-artifacts.com/output/job/${process.env.CIRCLE_WORKFLOW_JOB_ID}/artifacts/${process.env.CIRCLE_NODE_INDEX}/${fileName}`
}
const body = JSON.stringify({
parameters: {
temp_dir: os.tmpdir(),
sha: process.env.CIRCLE_SHA1,
job_name: process.env.CIRCLE_JOB,
binary_artifact_url: getArtifactUrl('cypress-dist.tgz'),
built_source_artifact_url: getArtifactUrl('cypress-built-source.tgz'),
triggered_workflow_id: process.env.CIRCLE_WORKFLOW_ID,
triggered_job_url: process.env.CIRCLE_BUILD_URL,
branch: process.env.CIRCLE_BRANCH,
should_persist_artifacts: Boolean(process.env.SHOULD_PERSIST_ARTIFACTS),
binary_version: nextVersion,
},
})
try {
console.log('Triggering new pipeline in cypress-publish-binary project...')
const response = await fetch('https://circleci.com/api/v2/project/github/cypress-io/cypress-publish-binary/pipeline', { method: 'POST', headers: { 'Circle-Token': process.env.CIRCLE_TOKEN, 'content-type': 'application/json' }, body })
const pipeline = await response.json()
console.log(pipeline)
console.log(`Triggered pipeline: https://app.circleci.com/pipelines/github/cypress-io/cypress-publish-binary/${pipeline.number}`)
try {
console.log(`Saving pipeline info in ${pipelineInfoFilePath} ...`)
await fs.writeFile(path.resolve(pipelineInfoFilePath), JSON.stringify(pipeline))
} catch (error) {
throw new Error(`error writing triggered pipeline info ${error}`)
}
} catch (error) {
throw new Error(`error triggering new pipeline ${error}`)
}
})()

View File

@@ -87,7 +87,7 @@ const validateOptions = (options) => {
}
const uploadArtifactToS3 = function (args = []) {
const supportedOptions = ['type', 'version', 'file', 'hash', 'platform']
const supportedOptions = ['type', 'version', 'file', 'hash', 'platform', 'dry-run']
let options = minimist(args, {
string: supportedOptions,
})
@@ -99,13 +99,17 @@ const uploadArtifactToS3 = function (args = []) {
const uploadPath = getUploadPath(options)
const cdnUrl = getCDN(uploadPath)
if (options['dry-run']) {
return new Promise((resolve) => resolve(cdnUrl))
}
return upload.toS3({ file: options.file, uploadPath })
.then(() => {
return setChecksum(options.file, uploadPath)
})
.then(() => {
const cdnUrl = getCDN(uploadPath)
if (options.type === 'binary') {
console.log('Binary can be downloaded using URL')
console.log(cdnUrl)

View File

@@ -2,6 +2,7 @@ const path = require('path')
const semver = require('semver')
const bumpCb = require('conventional-recommended-bump')
const { promisify } = require('util')
const minimist = require('minimist')
const checkedInBinaryVersion = require('../package.json').version
const { changeCatagories } = require('./semantic-commits/change-categories')
@@ -102,9 +103,11 @@ if (require.main !== module) {
(async () => {
process.chdir(path.join(__dirname, '..'))
const { nextVersion } = await getNextVersionForBinary()
const args = minimist(process.argv.slice(2))
if (process.argv.includes('--npm') && checkedInBinaryVersion !== nextVersion) {
const nextVersion = args.nextVersion || (await getNextVersionForBinary()).nextVersion
if (args.npm && checkedInBinaryVersion !== nextVersion) {
const cmd = `npm --no-git-tag-version version ${nextVersion}`
console.log(`Running '${cmd}'...`)

View File

@@ -0,0 +1,58 @@
const getPublishedArtifactsModule = require('../../binary/get-published-artifacts')
const sinon = require('sinon')
const { expect } = require('chai')
const mockArtifacts = [
{ url: '/', path: '~/cypress/binary-url.json' },
{ url: '/', path: '~/cypress/npm-package-url.json' },
{ url: '/', path: '~/cypress/cypress.zip' },
{ url: '/', path: '~/cypress/cypress.tgz' },
]
describe('get-published-artifacts', () => {
afterEach(() => {
sinon.reset()
})
it('downloads artifacts', async () => {
process.env.SHOULD_PERSIST_ARTIFACTS = true
const getPipelineIdStub = sinon.stub(getPublishedArtifactsModule, 'getPipelineId').returns('abc123')
const getWorkflowsStub = sinon.stub(getPublishedArtifactsModule, 'getWorkflows').returns([{ id: 'my-workflow', name: 'linux-x64', status: 'success' }])
const getWorkflowJobsStub = sinon.stub(getPublishedArtifactsModule, 'getWorkflowJobs').returns([{ name: 'publish-binary', job_number: 2 }])
const getJobArtifactsStub = sinon.stub(getPublishedArtifactsModule, 'getJobArtifacts').returns(mockArtifacts)
const downloadArtifactStub = sinon.stub(getPublishedArtifactsModule, 'downloadArtifact')
await getPublishedArtifactsModule.run(['--pipelineInfo', 'foo'])
expect(getPipelineIdStub).to.have.been.calledWith('foo')
expect(getWorkflowsStub).to.have.been.calledWith('abc123')
expect(getWorkflowJobsStub).to.have.been.calledWith('my-workflow')
expect(getJobArtifactsStub).to.have.been.calledWith(2)
expect(downloadArtifactStub.getCalls()).to.have.length(4)
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/binary-url.json')
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/npm-package-url.json')
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/cypress.zip')
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/cypress.tgz')
})
it('URLs are not fetched if SHOULD_PERSIST_ARTIFACTS is false', async () => {
process.env.SHOULD_PERSIST_ARTIFACTS = ''
const getPipelineIdStub = sinon.stub(getPublishedArtifactsModule, 'getPipelineId').returns('abc123')
const getWorkflowsStub = sinon.stub(getPublishedArtifactsModule, 'getWorkflows').returns([{ id: 'my-workflow', name: 'linux-x64', status: 'success' }])
const getWorkflowJobsStub = sinon.stub(getPublishedArtifactsModule, 'getWorkflowJobs').returns([{ name: 'publish-binary', job_number: 2 }])
const getJobArtifactsStub = sinon.stub(getPublishedArtifactsModule, 'getJobArtifacts').returns(mockArtifacts)
const downloadArtifactStub = sinon.stub(getPublishedArtifactsModule, 'downloadArtifact')
await getPublishedArtifactsModule.run(['--pipelineInfo', 'foo'])
expect(getPipelineIdStub).to.have.been.calledWith('foo')
expect(getWorkflowsStub).to.have.been.calledWith('abc123')
expect(getWorkflowJobsStub).to.have.been.calledWith('my-workflow')
expect(getJobArtifactsStub).to.have.been.calledWith(2)
expect(downloadArtifactStub.getCalls()).to.have.length(2)
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/cypress.zip')
expect(downloadArtifactStub).to.have.been.calledWith('/', '~/cypress/cypress.tgz')
})
})

View File

@@ -93,6 +93,29 @@ describe('upload-release-artifact', () => {
expect(() => uploadArtifactToS3(['--type', 'npm-package', '--version', '1.0.0'])).to.throw()
})
it('does not call s3 methods and returns url when --dry-run is passed', async () => {
uploadUtils.formHashFromEnvironment.returns('hash')
uploadUtils.getUploadNameByOsAndArch.returns('darwin-x64')
const binaryArgs = ['--file', 'my.zip', '--type', 'binary', '--version', '1.0.0', '--dry-run', 'true']
const binaryUrl = await uploadArtifactToS3(binaryArgs)
expect(uploadUtils.formHashFromEnvironment).to.have.calledOnce
expect(uploadUtils.getUploadNameByOsAndArch).to.have.calledOnce
expect(upload.toS3).not.to.have.been.called
expect(binaryUrl).to.equal('https://cdn.cypress.io/beta/binary/1.0.0/darwin-x64/hash/cypress.zip')
const packageArgs = ['--file', 'cypress.tgz', '--type', 'npm-package', '--version', '1.0.0', '--dry-run', 'true']
const packageUrl = await uploadArtifactToS3(packageArgs)
expect(uploadUtils.formHashFromEnvironment).to.have.calledTwice
expect(uploadUtils.getUploadNameByOsAndArch).to.have.calledTwice
expect(upload.toS3).not.to.have.been.called
expect(packageUrl).to.equal('https://cdn.cypress.io/beta/npm/1.0.0/darwin-x64/hash/cypress.tgz')
})
it('uploads binary to s3 and saves url to json', () => {
uploadUtils.formHashFromEnvironment.returns('hash')
uploadUtils.getUploadNameByOsAndArch.returns('darwin-x64')

View File

@@ -114,7 +114,7 @@ const checkBuiltBinary = async () => {
try {
await fs.stat(path.join(__dirname, '..', '..', 'cypress.zip'))
} catch (err) {
throw new Error('Expected built cypress.zip at project root. Run `yarn binary-build` and `yarn binary-zip`.')
throw new Error('Expected built cypress.zip at project root. Run `yarn binary-build`, `yarn binary-package`, and `yarn binary-zip`.')
}
try {

View File

@@ -4742,7 +4742,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",

View File

@@ -4449,7 +4449,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",

View File

@@ -4449,7 +4449,7 @@
"./packages/server/lib/util/print-run.ts",
"./packages/server/lib/util/proxy.ts",
"./packages/server/lib/util/random.js",
"./packages/server/lib/util/requestedWithAndCredentialManager.ts",
"./packages/server/lib/util/resourceTypeAndCredentialManager.ts",
"./packages/server/lib/util/server_destroy.ts",
"./packages/server/lib/util/shell.js",
"./packages/server/lib/util/socket_allowed.ts",

View File

@@ -2516,6 +2516,15 @@
global-agent "^3.0.0"
global-tunnel-ng "^2.7.1"
"@electron/notarize@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.1.0.tgz#76aaec10c8687225e8d0a427cc9df67611c46ff3"
integrity sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==
dependencies:
debug "^4.1.1"
fs-extra "^9.0.1"
promise-retry "^2.0.1"
"@electron/rebuild@3.2.10":
version "3.2.10"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.2.10.tgz#adc9443179709d4e4b93a68fac6a08b9a3b9e5e6"
@@ -28120,6 +28129,18 @@ tar@6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@6.1.15, tar@^6.0.2, tar@^6.0.5, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2:
version "6.1.15"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69"
integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tar@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
@@ -28129,18 +28150,6 @@ tar@^2.2.2:
fstream "^1.0.12"
inherits "2"
tar@^6.0.2, tar@^6.0.5, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2:
version "6.1.14"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.14.tgz#e87926bec1cfe7c9e783a77a79f3e81c1cfa3b66"
integrity sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^5.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
tcomb-validation@^3.3.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65"