mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-21 14:41:00 -06:00
Merge branch 'develop' into chore/merge_develop
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Bump this version to force CI to re-create the cache from scratch.
|
||||
|
||||
07-19-23
|
||||
07-25-23
|
||||
@@ -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
|
||||
|
||||
14
.github/workflows/report_weekly_app_kpis.yml
vendored
14
.github/workflows/report_weekly_app_kpis.yml
vendored
@@ -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 }} );
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require('@packages/ts/register')
|
||||
require('../packages/ts/register')
|
||||
|
||||
const command = process.argv[2]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
163
scripts/binary/get-published-artifacts.js
Normal file
163
scripts/binary/get-published-artifacts.js
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
50
scripts/binary/trigger-publish-binary-pipeline.js
Normal file
50
scripts/binary/trigger-publish-binary-pipeline.js
Normal 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}`)
|
||||
}
|
||||
})()
|
||||
@@ -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)
|
||||
|
||||
@@ -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}'...`)
|
||||
|
||||
58
scripts/unit/binary/get-published-artifacts-spec.js
Normal file
58
scripts/unit/binary/get-published-artifacts-spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
33
yarn.lock
33
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user