From 3c656270b38bbd61d034e8d137ec3b3bc26037d4 Mon Sep 17 00:00:00 2001 From: lohit Date: Sun, 7 Sep 2025 23:06:44 +0530 Subject: [PATCH] ca certs fixes and tests (#5429) Co-authored-by: Anoop M D --- .../actions/common/setup-node-deps/action.yml | 26 ++ .../linux/run-basic-ssl-cli-tests/action.yml | 36 +++ .../run-custom-ca-certs-cli-tests/action.yml | 33 +++ .../ssl/linux/run-ssl-e2e-tests/action.yml | 19 ++ .../ssl/linux/setup-ca-certs/action.yml | 26 ++ .../setup-feature-specific-deps/action.yml | 14 ++ .../macos/run-basic-ssl-cli-tests/action.yml | 36 +++ .../run-custom-ca-certs-cli-tests/action.yml | 33 +++ .../ssl/macos/run-ssl-e2e-tests/action.yml | 17 ++ .../ssl/macos/setup-ca-certs/action.yml | 26 ++ .../setup-feature-specific-deps/action.yml | 9 + .../run-basic-ssl-cli-tests/action.yml | 50 ++++ .../run-custom-ca-certs-cli-tests/action.yml | 47 ++++ .../ssl/windows/run-ssl-e2e-tests/action.yml | 17 ++ .../ssl/windows/setup-ca-certs/action.yml | 25 ++ .github/workflows/ssl-tests.yml | 91 +++++++ package.json | 3 +- .../src/runner/run-single-request.js | 21 +- .../src/ipc/network/cert-utils.js | 35 ++- .../src/store/last-opened-collections.js | 5 +- .../bruno-electron/src/utils/proxy-util.js | 34 +-- packages/bruno-requests/src/index.ts | 2 +- .../bruno-requests/src/network/ca-cert.ts | 165 +++++++++++++ packages/bruno-requests/src/network/index.ts | 4 +- packages/bruno-tests/.nvmrc | 2 +- playwright.config.ts | 13 +- playwright/electron.ts | 4 +- playwright/index.ts | 2 +- .../basic-ssl/collections/badssl/bruno.json | 6 + .../basic-ssl/collections/badssl/package.json | 5 + .../basic-ssl/collections/badssl/request.bru | 15 ++ .../collections/self-signed-badssl/bruno.json | 6 + .../self-signed-badssl/package.json | 5 + .../self-signed-badssl/request.bru | 15 ++ .../basic-ssl-success.spec.ts | 59 +++++ .../init-user-data/preferences.json | 16 ++ .../init-user-data/preferences.json | 16 ++ .../self-signed-rejected.spec.ts | 59 +++++ .../init-user-data/preferences.json | 16 ++ ...d-success-with-validation-disabled.spec.ts | 59 +++++ .../ssl/custom-ca-certs/collection/bruno.json | 6 + .../custom-ca-certs/collection/package.json | 5 + .../custom-ca-certs/collection/request.bru | 16 ++ tests/ssl/custom-ca-certs/server/.gitignore | 1 + .../custom-ca-certs/server/helpers/certs.js | 225 ++++++++++++++++++ .../server/helpers/platform.js | 60 +++++ tests/ssl/custom-ca-certs/server/index.js | 74 ++++++ tests/ssl/custom-ca-certs/server/readme.md | 106 +++++++++ .../server/scripts/generate-certs.js | 46 ++++ ...id-ca-cert-in-config-with-defaults.spec.ts | 57 +++++ .../init-user-data/preferences.json | 16 ++ .../custom-invalid-ca-cert-in-config.spec.ts | 57 +++++ .../init-user-data/preferences.json | 16 ++ ...id-ca-cert-in-config-with-defaults.spec.ts | 57 +++++ .../init-user-data/preferences.json | 16 ++ .../custom-valid-ca-cert-in-config.spec.ts | 57 +++++ .../init-user-data/preferences.json | 16 ++ 57 files changed, 1853 insertions(+), 50 deletions(-) create mode 100644 .github/actions/common/setup-node-deps/action.yml create mode 100644 .github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml create mode 100644 .github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml create mode 100644 .github/actions/ssl/linux/run-ssl-e2e-tests/action.yml create mode 100644 .github/actions/ssl/linux/setup-ca-certs/action.yml create mode 100644 .github/actions/ssl/linux/setup-feature-specific-deps/action.yml create mode 100644 .github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml create mode 100644 .github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml create mode 100644 .github/actions/ssl/macos/run-ssl-e2e-tests/action.yml create mode 100644 .github/actions/ssl/macos/setup-ca-certs/action.yml create mode 100644 .github/actions/ssl/macos/setup-feature-specific-deps/action.yml create mode 100644 .github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml create mode 100644 .github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml create mode 100644 .github/actions/ssl/windows/run-ssl-e2e-tests/action.yml create mode 100644 .github/actions/ssl/windows/setup-ca-certs/action.yml create mode 100644 .github/workflows/ssl-tests.yml create mode 100644 packages/bruno-requests/src/network/ca-cert.ts create mode 100644 tests/ssl/basic-ssl/collections/badssl/bruno.json create mode 100644 tests/ssl/basic-ssl/collections/badssl/package.json create mode 100644 tests/ssl/basic-ssl/collections/badssl/request.bru create mode 100644 tests/ssl/basic-ssl/collections/self-signed-badssl/bruno.json create mode 100644 tests/ssl/basic-ssl/collections/self-signed-badssl/package.json create mode 100644 tests/ssl/basic-ssl/collections/self-signed-badssl/request.bru create mode 100644 tests/ssl/basic-ssl/tests/basic-ssl-success/basic-ssl-success.spec.ts create mode 100644 tests/ssl/basic-ssl/tests/basic-ssl-success/init-user-data/preferences.json create mode 100644 tests/ssl/basic-ssl/tests/self-signed-rejected/init-user-data/preferences.json create mode 100644 tests/ssl/basic-ssl/tests/self-signed-rejected/self-signed-rejected.spec.ts create mode 100644 tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/init-user-data/preferences.json create mode 100644 tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/self-signed-success-with-validation-disabled.spec.ts create mode 100644 tests/ssl/custom-ca-certs/collection/bruno.json create mode 100644 tests/ssl/custom-ca-certs/collection/package.json create mode 100644 tests/ssl/custom-ca-certs/collection/request.bru create mode 100644 tests/ssl/custom-ca-certs/server/.gitignore create mode 100644 tests/ssl/custom-ca-certs/server/helpers/certs.js create mode 100644 tests/ssl/custom-ca-certs/server/helpers/platform.js create mode 100644 tests/ssl/custom-ca-certs/server/index.js create mode 100644 tests/ssl/custom-ca-certs/server/readme.md create mode 100644 tests/ssl/custom-ca-certs/server/scripts/generate-certs.js create mode 100644 tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/custom-invalid-ca-cert-in-config-with-defaults.spec.ts create mode 100644 tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/init-user-data/preferences.json create mode 100644 tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/custom-invalid-ca-cert-in-config.spec.ts create mode 100644 tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/init-user-data/preferences.json create mode 100644 tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/custom-valid-ca-cert-in-config-with-defaults.spec.ts create mode 100644 tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/init-user-data/preferences.json create mode 100644 tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/custom-valid-ca-cert-in-config.spec.ts create mode 100644 tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/init-user-data/preferences.json diff --git a/.github/actions/common/setup-node-deps/action.yml b/.github/actions/common/setup-node-deps/action.yml new file mode 100644 index 000000000..f99758768 --- /dev/null +++ b/.github/actions/common/setup-node-deps/action.yml @@ -0,0 +1,26 @@ +name: 'Setup Node Dependencies' +description: 'Install Node.js and npm dependencies' +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: v22.17.0 + cache: 'npm' + cache-dependency-path: './package-lock.json' + + - name: Install node dependencies + shell: bash + run: npm ci --legacy-peer-deps + + - name: Build libraries + shell: bash + run: | + npm run build:graphql-docs + npm run build:bruno-query + npm run build:bruno-common + npm run sandbox:bundle-libraries --workspace=packages/bruno-js + npm run build:bruno-converters + npm run build:bruno-requests + npm run build:bruno-filestore diff --git a/.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml b/.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml new file mode 100644 index 000000000..e0d8c04eb --- /dev/null +++ b/.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml @@ -0,0 +1,36 @@ +name: 'Run Basic SSL CLI Tests - Linux' +description: 'Run basic SSL CLI tests on Linux' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: bash + run: | + set -euo pipefail + + # navigate to basic SSL test collection directory + cd tests/ssl/basic-ssl/collections/badssl + + echo "basic ssl success" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 + + echo "with default/system ca certs" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 + + # navigate to self-signed SSL test collection directory + cd ../self-signed-badssl + + echo "self-signed ssl with validation disabled" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1 + + echo "self-signed ssl with default/system ca certs" + echo "request will error" + # should fail + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true + xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1 diff --git a/.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml b/.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml new file mode 100644 index 000000000..375a14228 --- /dev/null +++ b/.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml @@ -0,0 +1,33 @@ +name: 'Run Custom CA Certs CLI Tests - Linux' +description: 'Run custom CA certs CLI tests on Linux' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: bash + run: | + set -euo pipefail + + # navigate to CA certificates test collection directory + cd tests/ssl/custom-ca-certs/collection + + echo "custom valid ca cert" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 + + echo "custom valid ca cert with defaults" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 + + echo "custom invalid ca cert" + echo "request will error" + # should fail + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true + xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1 + + echo "custom invalid ca cert with defaults" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1 diff --git a/.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml b/.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml new file mode 100644 index 000000000..bd8c7949e --- /dev/null +++ b/.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml @@ -0,0 +1,19 @@ +name: 'Run SSL E2E Tests - Linux' +description: 'Run SSL E2E tests on Linux' +runs: + using: 'composite' + steps: + - name: Run E2E tests + shell: bash + run: | + set -euo pipefail + + xvfb-run npm run test:e2e:ssl + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-linux + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/ssl/linux/setup-ca-certs/action.yml b/.github/actions/ssl/linux/setup-ca-certs/action.yml new file mode 100644 index 000000000..9561ecf30 --- /dev/null +++ b/.github/actions/ssl/linux/setup-ca-certs/action.yml @@ -0,0 +1,26 @@ +name: 'Setup CA Certificates - Linux' +description: 'Setup CA certificates and start test server for custom CA certs tests on Linux' +runs: + using: 'composite' + steps: + - name: Setup CA certificates + shell: bash + run: | + set -euo pipefail + + cd tests/ssl/custom-ca-certs/server + + echo "running certificate setup" + node scripts/generate-certs.js + + - name: Start test server + shell: bash + run: | + set -euo pipefail + + cd tests/ssl/custom-ca-certs/server + + echo "starting server in background" + node index.js & + + echo "server started with PID: $!" diff --git a/.github/actions/ssl/linux/setup-feature-specific-deps/action.yml b/.github/actions/ssl/linux/setup-feature-specific-deps/action.yml new file mode 100644 index 000000000..297f842a6 --- /dev/null +++ b/.github/actions/ssl/linux/setup-feature-specific-deps/action.yml @@ -0,0 +1,14 @@ +name: 'Setup Custom CA Certs Feature Dependencies - Linux' +description: 'Setup feature-specific dependencies for custom CA certs tests on Linux' +runs: + using: 'composite' + steps: + - name: Install additional OS dependencies for custom CA certs + shell: bash + run: | + sudo apt-get --no-install-recommends install -y \ + libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \ + xvfb libxml2-utils + + sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox + sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox diff --git a/.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml b/.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml new file mode 100644 index 000000000..6b60fc7a8 --- /dev/null +++ b/.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml @@ -0,0 +1,36 @@ +name: 'Run Basic SSL CLI Tests - macOS' +description: 'Run basic SSL CLI tests on macOS' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: bash + run: | + set -euo pipefail + + # navigate to basic SSL test collection directory + cd tests/ssl/basic-ssl/collections/badssl + + echo "basic ssl success" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 + + echo "with default/system ca certs" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 + + # navigate to self-signed SSL test collection directory + cd ../self-signed-badssl + + echo "self-signed ssl with validation disabled" + # should pass + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1 + + echo "self-signed ssl with default/system ca certs" + echo "request will error" + # should fail + node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true + xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1 diff --git a/.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml b/.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml new file mode 100644 index 000000000..634dca1fc --- /dev/null +++ b/.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml @@ -0,0 +1,33 @@ +name: 'Run Custom CA Certs CLI Tests - macOS' +description: 'Run custom CA certs CLI tests on macOS' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: bash + run: | + set -euo pipefail + + # navigate to CA certificates test collection directory + cd tests/ssl/custom-ca-certs/collection + + echo "custom valid ca cert" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1 + + echo "custom valid ca cert with defaults" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1 + + echo "custom invalid ca cert" + echo "request will error" + # should fail + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true + xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1 + + echo "custom invalid ca cert with defaults" + # should pass + node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit + xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1 diff --git a/.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml b/.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml new file mode 100644 index 000000000..b3fea6368 --- /dev/null +++ b/.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml @@ -0,0 +1,17 @@ +name: 'Run SSL E2E Tests - macOS' +description: 'Run SSL E2E tests on macOS' +runs: + using: 'composite' + steps: + - name: Run E2E tests + shell: bash + run: | + npm run test:e2e:ssl + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-macos + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/ssl/macos/setup-ca-certs/action.yml b/.github/actions/ssl/macos/setup-ca-certs/action.yml new file mode 100644 index 000000000..89180e494 --- /dev/null +++ b/.github/actions/ssl/macos/setup-ca-certs/action.yml @@ -0,0 +1,26 @@ +name: 'Setup CA Certificates - macOS' +description: 'Setup CA certificates and start test server for custom CA certs tests on macOS' +runs: + using: 'composite' + steps: + - name: Setup CA certificates + shell: bash + run: | + set -euo pipefail + + cd tests/ssl/custom-ca-certs/server + + echo "running certificate setup" + node scripts/generate-certs.js + + - name: Start test server + shell: bash + run: | + set -euo pipefail + + cd tests/ssl/custom-ca-certs/server + + echo "starting server in background" + node index.js & + + echo "server started with PID: $!" diff --git a/.github/actions/ssl/macos/setup-feature-specific-deps/action.yml b/.github/actions/ssl/macos/setup-feature-specific-deps/action.yml new file mode 100644 index 000000000..676050509 --- /dev/null +++ b/.github/actions/ssl/macos/setup-feature-specific-deps/action.yml @@ -0,0 +1,9 @@ +name: 'Setup Custom CA Certs Feature Dependencies - macOS' +description: 'Setup feature-specific dependencies for custom CA certs tests on macOS' +runs: + using: 'composite' + steps: + - name: Install additional OS dependencies for custom CA certs + shell: bash + run: | + brew install libxml2 diff --git a/.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml b/.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml new file mode 100644 index 000000000..2d296b1f3 --- /dev/null +++ b/.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml @@ -0,0 +1,50 @@ +name: 'Run Basic SSL CLI Tests - Windows' +description: 'Run basic SSL CLI tests on Windows' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + # navigate to basic SSL test collection directory + Set-Location tests\ssl\basic-ssl\collections\badssl + + Write-Host "basic ssl success" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml1 = Get-Content junit1.xml + $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite } + $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount1 -ne 1) { exit 1 } + + Write-Host "with default/system ca certs" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml2 = Get-Content junit2.xml + $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite } + $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount2 -ne 1) { exit 1 } + + # navigate to self-signed SSL test collection directory + Set-Location ..\self-signed-badssl + + Write-Host "self-signed ssl with validation disabled" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml3 = Get-Content junit3.xml + $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite } + $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount3 -ne 1) { exit 1 } + + Write-Host "self-signed ssl with default/system ca certs" + Write-Host "request will error" + # should fail + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + # Ignore the exit code - we expect this to fail + [xml]$xml4 = Get-Content junit4.xml + $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite } + $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count + if ($errorCount4 -ne 1) { exit 1 } diff --git a/.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml b/.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml new file mode 100644 index 000000000..f35b7a65f --- /dev/null +++ b/.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml @@ -0,0 +1,47 @@ +name: 'Run Custom CA Certs CLI Tests - Windows' +description: 'Run custom CA certs CLI tests on Windows' +runs: + using: 'composite' + steps: + - name: Run CLI tests + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + # navigate to CA certificates test collection directory + Set-Location tests\ssl\custom-ca-certs\collection + + Write-Host "custom valid ca cert" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --cacert ..\server\certs\ca-cert.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml1 = Get-Content junit1.xml + $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite } + $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount1 -ne 1) { exit 1 } + + Write-Host "custom valid ca cert with defaults" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --cacert ..\server\certs\ca-cert.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml2 = Get-Content junit2.xml + $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite } + $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount2 -ne 1) { exit 1 } + + Write-Host "custom invalid ca cert" + Write-Host "request will error" + # should fail + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --cacert ..\server\certs\ca-key.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + # Ignore the exit code - we expect this to fail + [xml]$xml3 = Get-Content junit3.xml + $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite } + $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count + if ($errorCount3 -ne 1) { exit 1 } + + Write-Host "custom invalid ca cert with defaults" + # should pass + $process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --cacert ..\server\certs\ca-key.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul" + [xml]$xml4 = Get-Content junit4.xml + $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite } + $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count + if ($errorCount4 -ne 1) { exit 1 } diff --git a/.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml b/.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml new file mode 100644 index 000000000..41140d80d --- /dev/null +++ b/.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml @@ -0,0 +1,17 @@ +name: 'Run SSL E2E Tests - Windows' +description: 'Run SSL E2E tests on Windows' +runs: + using: 'composite' + steps: + - name: Run E2E tests + shell: pwsh + run: | + npm run test:e2e:ssl + + - name: Upload Playwright Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report-windows + path: playwright-report/ + retention-days: 30 diff --git a/.github/actions/ssl/windows/setup-ca-certs/action.yml b/.github/actions/ssl/windows/setup-ca-certs/action.yml new file mode 100644 index 000000000..182a91896 --- /dev/null +++ b/.github/actions/ssl/windows/setup-ca-certs/action.yml @@ -0,0 +1,25 @@ +name: 'Setup CA Certificates - Windows' +description: 'Setup CA certificates and start test server for custom CA certs tests on Windows' +runs: + using: 'composite' + steps: + - name: Setup CA certificates + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = "Stop" + + Set-Location tests\ssl\custom-ca-certs\server + + Write-Host "running certificate setup" + node scripts/generate-certs.js + + - name: Start test server + shell: pwsh + run: | + Set-StrictMode -Version Latest + + Set-Location tests\ssl\custom-ca-certs\server + + Write-Host "starting server in background" + Start-Process -FilePath "node" -ArgumentList "index.js" -PassThru -WindowStyle Hidden diff --git a/.github/workflows/ssl-tests.yml b/.github/workflows/ssl-tests.yml new file mode 100644 index 000000000..467a62ce9 --- /dev/null +++ b/.github/workflows/ssl-tests.yml @@ -0,0 +1,91 @@ +name: SSL Tests +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tests-for-linux: + name: SSL Tests - Linux + timeout-minutes: 60 + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Setup Feature Dependencies + uses: ./.github/actions/ssl/linux/setup-feature-specific-deps + + - name: Setup CA Certificates + uses: ./.github/actions/ssl/linux/setup-ca-certs + + - name: Run Basic SSL CLI Tests + uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests + + - name: Run Custom CA Certs CLI Tests + uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests + + - name: Run Custom CA Certs E2E Tests + uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests + + tests-for-macos: + name: SSL Tests - macOS + timeout-minutes: 60 + runs-on: macos-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Setup Feature Dependencies + uses: ./.github/actions/ssl/macos/setup-feature-specific-deps + + - name: Setup CA Certificates + uses: ./.github/actions/ssl/macos/setup-ca-certs + + - name: Run Basic SSL CLI Tests + uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests + + - name: Run Custom CA Certs CLI Tests + uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests + + - name: Run Custom CA Certs E2E Tests + uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests + + tests-for-windows: + name: SSL Tests - Windows + timeout-minutes: 60 + runs-on: windows-latest + permissions: + checks: write + pull-requests: write + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Setup Node Dependencies + uses: ./.github/actions/common/setup-node-deps + + - name: Setup CA Certificates + uses: ./.github/actions/ssl/windows/setup-ca-certs + + - name: Run Basic SSL CLI Tests + uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests + + - name: Run Custom CA Certs CLI Tests + uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests + + - name: Run Custom CA Certs E2E Tests + uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests diff --git a/package.json b/package.json index fe8dafdc9..695f0d3f5 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "build:electron:snap": "./scripts/build-electron.sh snap", "watch:common": "npm run watch --workspace=packages/bruno-common", "test:codegen": "node playwright/codegen.ts", - "test:e2e": "playwright test", + "test:e2e": "playwright test --project=default", + "test:e2e:ssl": "playwright test --project=ssl", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "lint": "node --max_old_space_size=4096 $(npx which eslint)" }, diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 9b9ca8c74..864e3a2fe 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -25,7 +25,7 @@ const { createFormData } = require('../utils/form-data'); const { getOAuth2Token } = require('./oauth2'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); -const { addDigestInterceptor } = require('@usebruno/requests'); +const { addDigestInterceptor, getCACertificates } = require('@usebruno/requests'); const { encodeUrl } = require('@usebruno/common').utils; const onConsoleLog = (type, args) => { @@ -151,21 +151,16 @@ const runSingleRequest = async function ( const insecure = get(options, 'insecure', false); const noproxy = get(options, 'noproxy', false); const httpsAgentRequestFields = {}; + if (insecure) { httpsAgentRequestFields['rejectUnauthorized'] = false; } else { - const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; - const caCert = caCertArray.find((el) => el); - if (caCert && caCert.length > 1) { - try { - let caCertBuffer = fs.readFileSync(caCert); - if (!options['ignoreTruststore']) { - caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates - } - httpsAgentRequestFields['ca'] = caCertBuffer; - } catch (err) { - console.log('Error reading CA cert file:' + caCert, err); - } + const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE]; + const caCertFilePath = caCertArray.find((el) => el); + let caCertificatesWithCertType = getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] }); + let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate); + if (caCertificates?.length > 0) { + httpsAgentRequestFields['ca'] = caCertificates; } } diff --git a/packages/bruno-electron/src/ipc/network/cert-utils.js b/packages/bruno-electron/src/ipc/network/cert-utils.js index ecd036fb0..cb1c83e12 100644 --- a/packages/bruno-electron/src/ipc/network/cert-utils.js +++ b/packages/bruno-electron/src/ipc/network/cert-utils.js @@ -1,7 +1,7 @@ -const fs = require('fs'); -const tls = require('tls'); +const fs = require('node:fs'); const path = require('path'); const { get } = require('lodash'); +const { getCACertificates } = require('@usebruno/requests'); const { preferencesUtil } = require('../../store/preferences'); const { getBrunoConfig } = require('../../store/bruno-config'); const { interpolateString } = require('./interpolate-string'); @@ -26,15 +26,28 @@ const getCertsAndProxyConfig = async ({ httpsAgentRequestFields['rejectUnauthorized'] = false; } - if (preferencesUtil.shouldUseCustomCaCertificate()) { - const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath(); - if (caCertFilePath) { - let caCertBuffer = fs.readFileSync(caCertFilePath); - if (preferencesUtil.shouldKeepDefaultCaCertificates()) { - caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates - } - httpsAgentRequestFields['ca'] = caCertBuffer; - } + let caCertFilePath = preferencesUtil.shouldUseCustomCaCertificate() && preferencesUtil.getCustomCaCertificateFilePath(); + let caCertificatesWithCertType = getCACertificates({ + caCertFilePath, + shouldKeepDefaultCerts: preferencesUtil.shouldKeepDefaultCaCertificates() + }); + + let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate); + let caCertificateDetails = caCertificatesWithCertType.reduce((details, certificateData) => { + // get the count for each certificate type + details[certificateData.type] += 1; + return details; + }, { + custom: 0, + bundled: 0, + system: 0, + extra: 0 + }); + + // configure HTTPS agent with aggregated CA certificates + if (caCertificates?.length > 0) { + httpsAgentRequestFields['caCertificateDetails'] = caCertificateDetails; + httpsAgentRequestFields['ca'] = caCertificates; } const brunoConfig = getBrunoConfig(collectionUid); diff --git a/packages/bruno-electron/src/store/last-opened-collections.js b/packages/bruno-electron/src/store/last-opened-collections.js index 72452eef3..8705e9dfd 100644 --- a/packages/bruno-electron/src/store/last-opened-collections.js +++ b/packages/bruno-electron/src/store/last-opened-collections.js @@ -1,3 +1,4 @@ +const path = require('node:path'); const _ = require('lodash'); const Store = require('electron-store'); const { isDirectory } = require('../utils/filesystem'); @@ -12,7 +13,9 @@ class LastOpenedCollections { } getAll() { - return this.store.get('lastOpenedCollections') || []; + let collections = this.store.get('lastOpenedCollections') || []; + collections = collections.map(collection => path.resolve(collection)); + return collections; } add(collectionPath) { diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js index f611ea1f6..cb151a81a 100644 --- a/packages/bruno-electron/src/utils/proxy-util.js +++ b/packages/bruno-electron/src/utils/proxy-util.js @@ -1,5 +1,5 @@ const parseUrl = require('url').parse; -const https = require('https'); +const https = require('node:https'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { interpolateString } = require('../ipc/network/interpolate-string'); const { SocksProxyAgent } = require('socks-proxy-agent'); @@ -87,6 +87,10 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent { function createTimelineAgentClass(BaseAgentClass) { return class extends BaseAgentClass { constructor(options, timeline) { + + let caCertificateDetails = options.caCertificateDetails || {}; + delete options.caCertificateDetails; + // For proxy agents, the first argument is the proxy URI and the second is options if (options?.proxy) { const { proxy: proxyUri, ...agentOptions } = options; @@ -118,7 +122,7 @@ function createTimelineAgentClass(BaseAgentClass) { const tlsOptions = { ...options, rejectUnauthorized: options.rejectUnauthorized ?? true, - }; + }; super(tlsOptions); this.timeline = Array.isArray(timeline) ? timeline : []; this.alpnProtocols = options.ALPNProtocols || ['h2', 'http/1.1']; @@ -131,6 +135,8 @@ function createTimelineAgentClass(BaseAgentClass) { message: `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`, }); } + + this.caCertificateDetails = caCertificateDetails; } @@ -146,20 +152,16 @@ function createTimelineAgentClass(BaseAgentClass) { }); } - // Log CAfile and CApath (if possible) - if (this.caProvided) { - this.timeline.push({ - timestamp: new Date(), - type: 'tls', - message: `CA certificates provided`, - }); - } else { - this.timeline.push({ - timestamp: new Date(), - type: 'tls', - message: `Using system default CA certificates`, - }); - } + const bundledCerts = this.caCertificateDetails.bundled || 0; + const systemCerts = this.caCertificateDetails.system || 0; + const extraCerts = this.caCertificateDetails.extra || 0; + const customCerts = this.caCertificateDetails.custom || 0; + + this.timeline.push({ + timestamp: new Date(), + type: 'tls', + message: `CA Certificates: ${bundledCerts} bundled, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`, + }); // Log "Trying host:port..." this.timeline.push({ diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 82d596828..3d6079c84 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -2,6 +2,6 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; export { GrpcClient, generateGrpcSampleMessage } from './grpc'; export { default as cookies } from './cookies'; -export * as network from './network'; +export { getCACertificates } from './network'; export * as scripting from './scripting'; \ No newline at end of file diff --git a/packages/bruno-requests/src/network/ca-cert.ts b/packages/bruno-requests/src/network/ca-cert.ts new file mode 100644 index 000000000..b08a60916 --- /dev/null +++ b/packages/bruno-requests/src/network/ca-cert.ts @@ -0,0 +1,165 @@ +import * as fs from 'node:fs'; +import { spawnSync } from 'node:child_process'; + +type T_CACertSource = 'bundled' | 'system' | 'extra' + +type T_CACertificateData = { + type: T_CACertSource | 'custom'; + certificate: string +} + +/** + * Safely executes tls.getCACertificates in a separate Node.js process + * Returns empty array if the process fails or exits + */ +const safeTlsGetCACertificates = (certType: T_CACertSource): string[] => { + try { + + // adding seperate script for each cert type + // to make sure no unexpected code can be included in the script + + const getBundledCACertificatesScript = ` + const tls = require('node:tls'); + try { + const result = tls.getCACertificates('bundled'); + console.log(JSON.stringify(result || [])); + } catch (error) { + console.log('[]'); + } + `; + + const getSystemCACertificatesScript = ` + const tls = require('node:tls'); + try { + const result = tls.getCACertificates('system'); + console.log(JSON.stringify(result || [])); + } catch (error) { + console.log('[]'); + } + `; + + const getExtraCACertificatesScript = ` + const tls = require('node:tls'); + try { + const result = tls.getCACertificates('extra'); + console.log(JSON.stringify(result || [])); + } catch (error) { + console.log('[]'); + } + `; + + // bundled + let script = getBundledCACertificatesScript; + + // system + if (certType === 'system') script = getSystemCACertificatesScript; + + // extra + if (certType === 'extra') script = getExtraCACertificatesScript; + + const result = spawnSync('node', ['-e', script], { + encoding: 'utf8', + timeout: 5000, // 5 second timeout + stdio: 'pipe', + maxBuffer: 1024 * 1024 * 50 + }); + + if (result.error || result.status !== 0) { + return []; + } + + const output = result.stdout.trim(); + + return JSON.parse(output); + } catch (error) { + // Return empty array if child process fails + return []; + } +}; + +/** + * retrieves default CA certificates from multiple sources using Node.js TLS API + * + * this function aggregates CA certificates from three sources: + * - 'bundled': mozilla CA certificates bundled with Node.js (same as tls.rootCertificates) + * - 'system': CA certificates from the system's trusted certificate store + * - 'extra': additional CA certificates loaded from `NODE_EXTRA_CA_CERTS` environment variable + * + * @returns {string[]} Array of PEM-encoded CA certificate strings + * @see https://nodejs.org/docs/latest-v22.x/api/tls.html#tlsgetcacertificatestype + */ +const getCerts = (sources: T_CACertSource[] = ['bundled', 'system', 'extra']): T_CACertificateData[] => { + let certificates: T_CACertificateData[] = []; + + // iterate through different certificate store types to build comprehensive CA list + (sources).forEach(certType => { + try { + // get certificates from specific store type + const certList = safeTlsGetCACertificates(certType); + + if (certList && Array.isArray(certList)) { + // filter out empty/invalid certificates to ensure we only include valid data + const validCertificates = certList.filter(cert => cert && cert.trim()); + const validCertificatesWithCertType = validCertificates.map(certificate => ({ + type: certType, + certificate + })); + certificates.push(...validCertificatesWithCertType); + } + } catch (err) { + console.warn(`Failed to load ${certType} CA certificates:`, (err as Error).message); + } + }); + + return certificates; +}; + +const getCACertificates = ({ caCertFilePath, shouldKeepDefaultCerts = true }: { caCertFilePath: string, shouldKeepDefaultCerts: boolean }) : T_CACertificateData[] => { + // CA certificate configuration + try { + let caCertificates: T_CACertificateData[] = []; + + // handle user-provided custom CA certificate file with optional default certificates + if (caCertFilePath) { + + // validate custom CA certificate file + if (fs.existsSync(caCertFilePath)) { + try { + const customCert = fs.readFileSync(caCertFilePath, 'utf8'); + if (customCert && customCert.trim()) { + caCertificates.push({ + type: 'custom', + certificate: customCert.trim() + }); + } + } catch (err) { + console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err as Error).message); + throw new Error(`Unable to load custom CA certificate: ${(err as Error).message}`); + } + } + + // optionally augment custom CA with default certificates + if (shouldKeepDefaultCerts) { + const defaultCertificates = getCerts(['bundled', 'system', 'extra']); + if (defaultCertificates?.length > 0) { + caCertificates.push(...defaultCertificates); + } + } + } else { + // use default CA certificates when no custom configuration is specified + const defaultCertificates = getCerts(['bundled', 'system', 'extra']); + if (defaultCertificates?.length > 0) { + caCertificates.push(...defaultCertificates); + } + } + + return caCertificates; + } catch (err) { + console.error('Error configuring CA certificates:', (err as Error).message); + throw err; // Re-throw certificate loading errors as they're critical + } +} + +export { + getCACertificates +}; \ No newline at end of file diff --git a/packages/bruno-requests/src/network/index.ts b/packages/bruno-requests/src/network/index.ts index 7d72cb7d1..98f3bfceb 100644 --- a/packages/bruno-requests/src/network/index.ts +++ b/packages/bruno-requests/src/network/index.ts @@ -1 +1,3 @@ -export { makeAxiosInstance } from './axios-instance'; \ No newline at end of file +export { makeAxiosInstance } from './axios-instance'; + +export { getCACertificates } from './ca-cert'; \ No newline at end of file diff --git a/packages/bruno-tests/.nvmrc b/packages/bruno-tests/.nvmrc index 9a2a0e219..6edc5a20f 100644 --- a/packages/bruno-tests/.nvmrc +++ b/packages/bruno-tests/.nvmrc @@ -1 +1 @@ -v20 +v22.17.0 diff --git a/playwright.config.ts b/playwright.config.ts index b47ff84f0..c4cf7f6df 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig } from '@playwright/test'; const reporter: any[] = [['list'], ['html']]; @@ -7,7 +7,6 @@ if (process.env.CI) { } export default defineConfig({ - testDir: './tests', fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, @@ -20,7 +19,15 @@ export default defineConfig({ projects: [ { - name: 'Bruno Electron App' + name: 'default', + testDir: './tests', + testIgnore: [ + 'ssl/**' // custom CA certificate tests require separate server setup and certificate generation + ] + }, + { + name: 'ssl', + testDir: './tests/ssl' } ], diff --git a/playwright/electron.ts b/playwright/electron.ts index 4363f46e0..89f73f25d 100644 --- a/playwright/electron.ts +++ b/playwright/electron.ts @@ -4,7 +4,9 @@ const { _electron: electron } = require('playwright'); const electronAppPath = path.join(__dirname, '../packages/bruno-electron'); exports.startApp = async () => { - const app = await electron.launch({ args: [electronAppPath] }); + const app = await electron.launch({ + args: [electronAppPath] + }); const context = await app.context(); app.process().stdout.on('data', (data) => { diff --git a/playwright/index.ts b/playwright/index.ts index a17809da4..bca567793 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -48,7 +48,7 @@ export const test = baseTest.extend< if (initUserDataPath) { const replacements = { - projectRoot: path.join(__dirname, '..') + projectRoot: path.posix.join(__dirname, '..') }; for (const file of await fs.promises.readdir(initUserDataPath)) { diff --git a/tests/ssl/basic-ssl/collections/badssl/bruno.json b/tests/ssl/basic-ssl/collections/badssl/bruno.json new file mode 100644 index 000000000..9bdf26e2a --- /dev/null +++ b/tests/ssl/basic-ssl/collections/badssl/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "badssl", + "type": "collection", + "ignore": ["node_modules", ".git"] +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/collections/badssl/package.json b/tests/ssl/basic-ssl/collections/badssl/package.json new file mode 100644 index 000000000..d342945f3 --- /dev/null +++ b/tests/ssl/basic-ssl/collections/badssl/package.json @@ -0,0 +1,5 @@ +{ + "name": "badssl", + "version": "1.0.0", + "description": "Bruno test collection for basic ssl testing" +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/collections/badssl/request.bru b/tests/ssl/basic-ssl/collections/badssl/request.bru new file mode 100644 index 000000000..eaee54f14 --- /dev/null +++ b/tests/ssl/basic-ssl/collections/badssl/request.bru @@ -0,0 +1,15 @@ +meta { + name: request + type: http + seq: 6 +} + +get { + url: https://www.badssl.com + body: none + auth: inherit +} + +assert { + res.status: eq 200 +} diff --git a/tests/ssl/basic-ssl/collections/self-signed-badssl/bruno.json b/tests/ssl/basic-ssl/collections/self-signed-badssl/bruno.json new file mode 100644 index 000000000..c35adf766 --- /dev/null +++ b/tests/ssl/basic-ssl/collections/self-signed-badssl/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "self-signed-badssl", + "type": "collection", + "ignore": ["node_modules", ".git"] +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/collections/self-signed-badssl/package.json b/tests/ssl/basic-ssl/collections/self-signed-badssl/package.json new file mode 100644 index 000000000..3b40105e8 --- /dev/null +++ b/tests/ssl/basic-ssl/collections/self-signed-badssl/package.json @@ -0,0 +1,5 @@ +{ + "name": "self-signed-badssl", + "version": "1.0.0", + "description": "Bruno test collection for basic ssl testing" +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/collections/self-signed-badssl/request.bru b/tests/ssl/basic-ssl/collections/self-signed-badssl/request.bru new file mode 100644 index 000000000..53b472b5d --- /dev/null +++ b/tests/ssl/basic-ssl/collections/self-signed-badssl/request.bru @@ -0,0 +1,15 @@ +meta { + name: request + type: http + seq: 6 +} + +get { + url: https://self-signed.badssl.com + body: none + auth: inherit +} + +assert { + res.status: eq 200 +} diff --git a/tests/ssl/basic-ssl/tests/basic-ssl-success/basic-ssl-success.spec.ts b/tests/ssl/basic-ssl/tests/basic-ssl-success/basic-ssl-success.spec.ts new file mode 100644 index 000000000..00159174c --- /dev/null +++ b/tests/ssl/basic-ssl/tests/basic-ssl-success/basic-ssl-success.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('basic ssl success', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('badssl').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/basic-ssl/tests/basic-ssl-success/init-user-data/preferences.json b/tests/ssl/basic-ssl/tests/basic-ssl-success/init-user-data/preferences.json new file mode 100644 index 000000000..6552b27b8 --- /dev/null +++ b/tests/ssl/basic-ssl/tests/basic-ssl-success/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/basic-ssl/collections/badssl"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": false, + "filePath": "" + }, + "keepDefaultCaCertificates": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/tests/self-signed-rejected/init-user-data/preferences.json b/tests/ssl/basic-ssl/tests/self-signed-rejected/init-user-data/preferences.json new file mode 100644 index 000000000..52696e791 --- /dev/null +++ b/tests/ssl/basic-ssl/tests/self-signed-rejected/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/basic-ssl/collections/self-signed-badssl"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": false, + "filePath": "" + }, + "keepDefaultCaCertificates": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/tests/self-signed-rejected/self-signed-rejected.spec.ts b/tests/ssl/basic-ssl/tests/self-signed-rejected/self-signed-rejected.spec.ts new file mode 100644 index 000000000..f2165121b --- /dev/null +++ b/tests/ssl/basic-ssl/tests/self-signed-rejected/self-signed-rejected.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('self signed rejected', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('self-signed-badssl').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(0); + await expect(parseInt(failed)).toBe(1); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(0); + await expect(parseInt(failed)).toBe(1); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/init-user-data/preferences.json b/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/init-user-data/preferences.json new file mode 100644 index 000000000..a03d40903 --- /dev/null +++ b/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/basic-ssl/collections/self-signed-badssl"], + "preferences": { + "request": { + "sslVerification": false, + "customCaCertificate": { + "enabled": false, + "filePath": "" + }, + "keepDefaultCaCertificates": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/self-signed-success-with-validation-disabled.spec.ts b/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/self-signed-success-with-validation-disabled.spec.ts new file mode 100644 index 000000000..d8edf5a0a --- /dev/null +++ b/tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/self-signed-success-with-validation-disabled.spec.ts @@ -0,0 +1,59 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('self signed success with validation disabled', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('self-signed-badssl').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/collection/bruno.json b/tests/ssl/custom-ca-certs/collection/bruno.json new file mode 100644 index 000000000..09f421e5f --- /dev/null +++ b/tests/ssl/custom-ca-certs/collection/bruno.json @@ -0,0 +1,6 @@ +{ + "version": "1", + "name": "custom-ca-certs", + "type": "collection", + "ignore": ["node_modules", ".git"] +} \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/collection/package.json b/tests/ssl/custom-ca-certs/collection/package.json new file mode 100644 index 000000000..c7809059c --- /dev/null +++ b/tests/ssl/custom-ca-certs/collection/package.json @@ -0,0 +1,5 @@ +{ + "name": "custom-ca-certs", + "version": "1.0.0", + "description": "Bruno test collection for CA certificates and HTTPS server testing" +} \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/collection/request.bru b/tests/ssl/custom-ca-certs/collection/request.bru new file mode 100644 index 000000000..aecdea35e --- /dev/null +++ b/tests/ssl/custom-ca-certs/collection/request.bru @@ -0,0 +1,16 @@ +meta { + name: request + type: http + seq: 6 +} + +get { + url: https://localhost:8090 + body: none + auth: inherit +} + +assert { + res.status: eq 200 + res.body: eq helloworld +} diff --git a/tests/ssl/custom-ca-certs/server/.gitignore b/tests/ssl/custom-ca-certs/server/.gitignore new file mode 100644 index 000000000..1503cc8a5 --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/.gitignore @@ -0,0 +1 @@ +certs \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/server/helpers/certs.js b/tests/ssl/custom-ca-certs/server/helpers/certs.js new file mode 100644 index 000000000..259a4f25e --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/helpers/certs.js @@ -0,0 +1,225 @@ +const { execCommand, execCommandSilent, detectPlatform } = require('./platform'); +const fs = require('node:fs'); +const path = require('node:path'); + +function createCertsDir(certsDir) { + if (fs.existsSync(certsDir)) { + fs.rmSync(certsDir, { recursive: true, force: true }); + } + fs.mkdirSync(certsDir, { recursive: true }); +} + +function generateCertificates(certsDir) { + execCommand('openssl version'); + + // Generate CA private key + execCommand('openssl genrsa -out ca-key.pem 4096', certsDir); + + // Create CA configuration file with proper CA extensions and subject (LibreSSL/OpenSSL compatible) + const caConfigContent = `[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +prompt = no + +[req_distinguished_name] +C = US +ST = Dev +L = Local +O = Local Dev CA +CN = Local Dev CA + +[v3_ca] +basicConstraints = critical, CA:TRUE +keyUsage = critical, keyCertSign, cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always`; + + fs.writeFileSync(path.join(certsDir, 'ca.conf'), caConfigContent); + + // Generate CA certificate with proper CA extensions using config file (no -subj needed) + execCommand('openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days 3650 -config ca.conf', certsDir); + + // Generate server private key and CSR + execCommand('openssl genrsa -out localhost-key.pem 4096', certsDir); + + // Create server CSR configuration file + const serverCsrConfigContent = `[req] +distinguished_name = req_distinguished_name +prompt = no + +[req_distinguished_name] +C = US +ST = Dev +L = Local +O = Local Dev +CN = localhost`; + + fs.writeFileSync(path.join(certsDir, 'localhost-csr.conf'), serverCsrConfigContent); + execCommand('openssl req -new -key localhost-key.pem -out localhost.csr -config localhost-csr.conf', certsDir); + + // Create server certificate configuration file (LibreSSL/OpenSSL compatible) + const serverConfigContent = `[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = Country Name +ST = State or Province Name +L = Locality Name +O = Organization Name +CN = Common Name + +[v3_req] +keyUsage = critical, keyEncipherment, dataEncipherment, digitalSignature +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +basicConstraints = critical, CA:FALSE +authorityKeyIdentifier = keyid:always,issuer:always + +[alt_names] +DNS.1 = localhost +DNS.2 = localhost.localdomain +IP.1 = 127.0.0.1 +IP.2 = ::1 +IP.3 = ::ffff:127.0.0.1`; + + fs.writeFileSync(path.join(certsDir, 'localhost.conf'), serverConfigContent); + execCommand('openssl x509 -req -in localhost.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out localhost-cert.pem -days 730 -extensions v3_req -extfile localhost.conf', certsDir); + + const platform = detectPlatform(); + if (platform === 'windows') { + execCommand('openssl x509 -in ca-cert.pem -outform DER -out ca-cert.der', certsDir); + execCommand('openssl pkcs12 -export -out localhost.p12 -inkey localhost-key.pem -in localhost-cert.pem -certfile ca-cert.pem -password pass:', certsDir); + execCommand('openssl x509 -in localhost-cert.pem -outform DER -out localhost-cert.der', certsDir); + } + + if (platform !== 'windows') { + execCommand('chmod 600 ca-key.pem localhost-key.pem', certsDir); + execCommand('chmod 644 ca-cert.pem localhost-cert.pem', certsDir); + } + + ['localhost.csr', 'localhost.conf', 'localhost-csr.conf', 'ca.conf', 'ca-cert.srl'].forEach(file => { + const filePath = path.join(certsDir, file); + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + }); + + // Validate certificate chain + validateCertificateChain(certsDir); +} + +function validateCertificateChain(certsDir) { + try { + // Verify CA certificate is valid and has proper CA extensions + const caVerifyOutput = execCommandSilent('openssl x509 -in ca-cert.pem -text -noout', certsDir).toString(); + + if (!caVerifyOutput.includes('CA:TRUE')) { + throw new Error('CA certificate missing basicConstraints=CA:TRUE'); + } + + if (!caVerifyOutput.includes('Certificate Sign')) { + throw new Error('CA certificate missing keyCertSign in keyUsage'); + } + + // Verify server certificate is valid and signed by CA + const serverVerifyOutput = execCommandSilent('openssl x509 -in localhost-cert.pem -text -noout', certsDir).toString(); + + if (!serverVerifyOutput.includes('CA:FALSE')) { + throw new Error('Server certificate should have basicConstraints=CA:FALSE'); + } + + if (!serverVerifyOutput.includes('TLS Web Server Authentication')) { + throw new Error('Server certificate missing serverAuth in extendedKeyUsage'); + } + + // Verify certificate chain + execCommandSilent('openssl verify -CAfile ca-cert.pem localhost-cert.pem', certsDir); + + console.log('✅ Certificate chain validation passed'); + } catch (error) { + console.error('❌ Certificate validation failed:', error.message); + throw new Error(`Certificate validation failed: ${error.message}`); + } +} + +function addCAToTruststore(certsDir) { + const platform = detectPlatform(); + + switch (platform) { + case 'macos': { + const macCertPath = path.join(certsDir, 'ca-cert.pem'); + execCommand(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${macCertPath}"`); + break; + } + + case 'linux': { + const linuxCertPath = path.join(certsDir, 'ca-cert.pem'); + execCommand(`sudo cp "${linuxCertPath}" /usr/local/share/ca-certificates/bruno-ca.crt`); + execCommand('sudo update-ca-certificates'); + break; + } + + case 'windows': { + const winCertPath = path.join(certsDir, 'ca-cert.der'); + + // Escape backslashes for PowerShell + const psPath = winCertPath.replace(/\\/g, '\\\\'); + + // PowerShell .NET method (works reliably in CI) + const psCommand = [ + `$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('${psPath}');`, + `$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`, + `$store.Open('ReadWrite');`, + `$store.Add($cert);`, + `$store.Close();`, + // Verify cert was added by checking if it exists in LocalMachine\Root + `$verifyStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`, + `$verifyStore.Open('ReadOnly');`, + `$found = $verifyStore.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint };`, + `$verifyStore.Close();`, + `if (-not $found) { throw 'Certificate was not added to LocalMachine\Root' };` + ].join(' '); + + execCommand(`powershell -Command "${psCommand}"`); + break; + } + + default: + throw new Error(`Unsupported platform: ${platform}`); + } +} + +function verifyCertificates(certsDir) { + const platform = detectPlatform(); + // Core PEM files required for all platforms + const requiredFiles = ['ca-cert.pem', 'ca-key.pem', 'localhost-cert.pem', 'localhost-key.pem']; + + // Verify required PEM files exist + for (const file of requiredFiles) { + const filePath = path.join(certsDir, file); + if (!fs.existsSync(filePath)) { + throw new Error(`missing certificate file: ${file}`); + } + } + + // Check Windows-specific files but don't require them (they're optional fallbacks) + if (platform === 'windows') { + const windowsFiles = ['ca-cert.der', 'localhost.p12', 'localhost-cert.der']; + for (const file of windowsFiles) { + const filePath = path.join(certsDir, file); + if (fs.existsSync(filePath)) { + console.log(`✅ Windows certificate file available: ${file}`); + } else { + console.log(`⚠️ Windows certificate file missing (but not required): ${file}`); + } + } + } +} + +module.exports = { + createCertsDir, + generateCertificates, + addCAToTruststore, + verifyCertificates +}; + diff --git a/tests/ssl/custom-ca-certs/server/helpers/platform.js b/tests/ssl/custom-ca-certs/server/helpers/platform.js new file mode 100644 index 000000000..b1c88882e --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/helpers/platform.js @@ -0,0 +1,60 @@ +const { execSync } = require('node:child_process'); +const os = require('node:os'); + +function execCommand(command, cwd = process.cwd()) { + return execSync(command, { + cwd, + stdio: 'inherit', + timeout: 30000 + }); +} + +function execCommandSilent(command, cwd = process.cwd()) { + return execSync(command, { + cwd, + stdio: 'pipe', + timeout: 30000 + }); +} + +function detectPlatform() { + const platform = os.platform(); + switch (platform) { + case 'darwin': return 'macos'; + case 'linux': return 'linux'; + case 'win32': return 'windows'; + default: throw new Error(`Unsupported platform: ${platform}`); + } +} + +function killProcessOnPort(port) { + const platform = detectPlatform(); + + try { + switch (platform) { + case 'macos': + execCommand(`lsof -ti :${port} | xargs kill -9`); + break; + case 'linux': + execCommand(`lsof -ti :${port} | xargs kill -9`); + break; + case 'windows': + const result = execCommandSilent(`netstat -ano | findstr :${port}`); + const lines = result.toString().split('\n'); + for (const line of lines) { + const match = line.trim().match(/\s+(\d+)$/); + if (match) { + execCommandSilent(`taskkill /F /PID ${match[1]}`); + } + } + break; + } + } catch (error) {} +} + +module.exports = { + execCommand, + execCommandSilent, + detectPlatform, + killProcessOnPort +}; \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/server/index.js b/tests/ssl/custom-ca-certs/server/index.js new file mode 100644 index 000000000..4f22a7a7d --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/index.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +const path = require('node:path'); +const fs = require('node:fs'); +const https = require('node:https'); +const { killProcessOnPort } = require('./helpers/platform'); + +function createServer(certsDir, port = 8090) { + const serverOptions = { + key: fs.readFileSync(path.join(certsDir, 'localhost-key.pem')), + cert: fs.readFileSync(path.join(certsDir, 'localhost-cert.pem')), + ca: fs.readFileSync(path.join(certsDir, 'ca-cert.pem')) + } + + const server = https.createServer(serverOptions, (req, res) => { + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + res.end('helloworld'); + }); + + return new Promise((resolve, reject) => { + server.listen(port, (error) => { + if (error) { + reject(error); + } else { + resolve(server); + } + }); + }); +} + +function shutdownServer(server, cleanup) { + const shutdown = (signal) => { + console.log(`🛑 Received ${signal}, shutting down`); + + if (cleanup) cleanup(); + + if (server) { + server.close(() => process.exit(0)); + } else { + process.exit(0); + } + }; + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); +} + +async function startServer() { + const certsDir = path.join(__dirname, 'certs'); + const port = 8090; + + console.log('🚀 Starting HTTPS test server'); + + try { + killProcessOnPort(port); + + console.log(`🌐 Creating server on port ${port}`); + const server = await createServer(certsDir, port); + + shutdownServer(server, () => { + console.log('✨ Server cleanup completed'); + }); + + } catch (error) { + console.error('❌ Server startup failed:', error.message); + process.exit(1); + } +} + +if (require.main === module) { + startServer(); +} + +module.exports = { startServer }; \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/server/readme.md b/tests/ssl/custom-ca-certs/server/readme.md new file mode 100644 index 000000000..4a3757e96 --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/readme.md @@ -0,0 +1,106 @@ +# CA Certificates Test Server + +A Node.js HTTPS test server with self-signed certificate generation for testing SSL/TLS connections in Bruno. + +## Overview + +This server provides two main functionalities: +1. **Certificate Generation** - Creates a complete CA certificate chain for testing +2. **HTTPS Server** - Runs a secure server using the generated certificates + +## Usage + +### 1. Generate Certificates + +Generate the required CA certificates and add them to your system's truststore: + +```bash +node scripts/generate-certs.js +``` + +This will: +- Create a `certs/` directory +- Generate CA certificate, server certificate, and private keys +- Verify the certificate chain +- Add the CA certificate to your system's truststore (macOS/Linux/Windows) + +**Generated Files:** +- `certs/ca-cert.pem` - Certificate Authority certificate +- `certs/ca-key.pem` - CA private key +- `certs/localhost-cert.pem` - Server certificate for localhost +- `certs/localhost-key.pem` - Server private key + +**Windows-Specific Files (automatically generated on Windows):** +- `certs/ca-cert.der` - CA certificate in DER format (for Windows certificate store) +- `certs/localhost.p12` - PKCS#12 bundle containing server certificate and key +- `certs/localhost-cert.der` - Server certificate in DER format + +### Certificate Installation Details + +The certificate generation script automatically adds the CA certificate to your system's truststore: + +**macOS:** Uses `security add-trusted-cert` to add the CA to the System keychain +**Linux:** Copies the CA certificate to `/usr/local/share/ca-certificates/` and runs `update-ca-certificates` +**Windows:** Uses PowerShell to add the CA certificate to the LocalMachine\Root certificate store + +> **Note:** On Windows, the script requires Administrator privileges to install certificates to the machine-wide certificate store. If you encounter permission issues, run your terminal as Administrator. + +### 2. Run HTTPS Server + +Start the HTTPS server on port 8090: + +```bash +node index.js +``` + +The server will: +- Load certificates from the `certs/` directory +- Start an HTTPS server on `https://localhost:8090` +- Serve a simple "helloworld" response +- Handle graceful shutdown on SIGINT/SIGTERM + +## Testing + +Once the server is running, you can test SSL connections: + +### Unix/Linux/macOS +```bash +# Test with curl +curl https://localhost:8090 + +# Test certificate verification +openssl s_client -connect localhost:8090 -CAfile certs/ca-cert.pem +``` + +### Windows +```powershell +# Test with curl (if available) +curl https://localhost:8090 + +# Test with PowerShell Invoke-WebRequest +Invoke-WebRequest -Uri https://localhost:8090 + +# Test certificate verification with OpenSSL +openssl s_client -connect localhost:8090 -CAfile certs/ca-cert.pem + +# Verify certificate is installed in Windows certificate store +Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*Local Dev CA*" } + +# Test with .NET WebClient (alternative method) +$client = New-Object System.Net.WebClient +$client.DownloadString("https://localhost:8090") +``` + +## File Structure + +``` +server/ +├── index.js # Main HTTPS server +├── scripts/ +│ └── generate-certs.js # Certificate generation script +├── helpers/ +│ ├── certs.js # Certificate management utilities +│ └── platform.js # Platform-specific utilities +├── certs/ # Generated certificates (created by script) +└── readme.md # This file +``` diff --git a/tests/ssl/custom-ca-certs/server/scripts/generate-certs.js b/tests/ssl/custom-ca-certs/server/scripts/generate-certs.js new file mode 100644 index 000000000..f470f77b8 --- /dev/null +++ b/tests/ssl/custom-ca-certs/server/scripts/generate-certs.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +const path = require('node:path'); +const { + createCertsDir, + generateCertificates, + addCAToTruststore, + verifyCertificates +} = require('../helpers/certs'); + +/** + * Setup CA certificates for testing server + */ +async function setup() { + console.log('🔧 Setting up CA certificates for test server'); + + const certsDir = path.join(__dirname, '..', 'certs'); + + try { + console.log('📁 Creating certificates directory'); + createCertsDir(certsDir); + + console.log('🔐 Generating certificates'); + generateCertificates(certsDir); + + console.log('✅ Verifying certificates'); + verifyCertificates(certsDir); + + console.log('🛡️ Adding CA to truststore'); + addCAToTruststore(certsDir); + + console.log('🎉 CA certificate setup completed successfully'); + return true; + } catch (error) { + console.error('❌ Generate certs failed:', error.message); + throw error; + } +} + +if (require.main === module) { + setup() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +} + +module.exports = { setup }; diff --git a/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/custom-invalid-ca-cert-in-config-with-defaults.spec.ts b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/custom-invalid-ca-cert-in-config-with-defaults.spec.ts new file mode 100644 index 000000000..5ff1e72db --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/custom-invalid-ca-cert-in-config-with-defaults.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('custom invalid ca cert added to the config and keep default ca certs', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('custom-ca-certs').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/init-user-data/preferences.json b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/init-user-data/preferences.json new file mode 100644 index 000000000..452b90b10 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/custom-ca-certs/collection"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": true, + "filePath": "{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-key.pem" + }, + "keepDefaultCaCertificates": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/custom-invalid-ca-cert-in-config.spec.ts b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/custom-invalid-ca-cert-in-config.spec.ts new file mode 100644 index 000000000..9d89e0bc4 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/custom-invalid-ca-cert-in-config.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('custom invalid ca cert added to the config and NO default ca certs', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('custom-ca-certs').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(0); + await expect(parseInt(failed)).toBe(1); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(0); + await expect(parseInt(failed)).toBe(1); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/init-user-data/preferences.json b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/init-user-data/preferences.json new file mode 100644 index 000000000..a1f944aa7 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/custom-ca-certs/collection"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": true, + "filePath": "{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-key.pem" + }, + "keepDefaultCaCertificates": { + "enabled": false + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/custom-valid-ca-cert-in-config-with-defaults.spec.ts b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/custom-valid-ca-cert-in-config-with-defaults.spec.ts new file mode 100644 index 000000000..97db83b44 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/custom-valid-ca-cert-in-config-with-defaults.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('custom valid ca cert added to the config and keep default ca certs', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('custom-ca-certs').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/init-user-data/preferences.json b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/init-user-data/preferences.json new file mode 100644 index 000000000..b252fec6b --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/custom-ca-certs/collection"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": true, + "filePath": "{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem" + }, + "keepDefaultCaCertificates": { + "enabled": true + } + } + } +} \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/custom-valid-ca-cert-in-config.spec.ts b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/custom-valid-ca-cert-in-config.spec.ts new file mode 100644 index 000000000..35a7776b2 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/custom-valid-ca-cert-in-config.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '../../../../../playwright'; + +test.describe.serial('custom valid ca cert added to the config and NO default ca certs', () => { + test('developer mode', async ({ pageWithUserData: page }) => { + + // init dev mode + await page.getByText('custom-ca-certs').click(); + await page.getByLabel('Developer Mode(use only if').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); + + test('safe mode', async ({ pageWithUserData: page }) => { + + // init safe mode + await page.getByText('Developer Mode').click(); + await page.getByLabel('Safe Mode').check(); + await page.getByRole('button', { name: 'Save' }).click(); + + test.setTimeout(2 * 60 * 1000); + await page.locator('.collection-actions').hover(); + await page.locator('.collection-actions .icon').click(); + await page.getByText('Run', { exact: true }).click(); + await page.getByRole('button', { name: 'Run Collection' }).click(); + await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 }); + + const result = await page.getByText('Total Requests: ').innerText(); + const matches = result.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/); + if (!matches) { + throw new Error('Could not parse test results'); + } + const [totalRequests, passed, failed, skipped] = matches.slice(1); + await expect(parseInt(totalRequests)).toBe(1); + await expect(parseInt(passed)).toBe(1); + await expect(parseInt(failed)).toBe(0); + await expect(parseInt(skipped)).toBe(0); + await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed)); + }); +}); \ No newline at end of file diff --git a/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/init-user-data/preferences.json b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/init-user-data/preferences.json new file mode 100644 index 000000000..33608d333 --- /dev/null +++ b/tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/init-user-data/preferences.json @@ -0,0 +1,16 @@ +{ + "maximized": true, + "lastOpenedCollections": ["{{projectRoot}}/tests/ssl/custom-ca-certs/collection"], + "preferences": { + "request": { + "sslVerification": true, + "customCaCertificate": { + "enabled": true, + "filePath": "{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem" + }, + "keepDefaultCaCertificates": { + "enabled": false + } + } + } +} \ No newline at end of file