From fed0d269011d12e856b91c7ec9305f963fc7f627 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 11:11:58 +0000 Subject: [PATCH 1/2] chore(ci): split unit and e2e tests into separate jobs Reorganises tests to mirror apify/mcpc: - Move pure unit tests to `test/unit/` (`tools.js`) and the network/server-backed tests to `test/e2e/`. Shared helpers stay in `test/utils/`. SSL fixtures move alongside the e2e tests. - Add `test:unit` and `test:e2e` npm scripts; `npm test` still runs both suites under nyc with the `--insecure-http-parser` Node option that the e2e suite needs. - Split the Check workflow into a `unit` job that runs the matrix (Node.js 20/22/24) on ubuntu-24.04 with `fail-fast: false` plus lint on Node.js 24, and a single `e2e` job that runs on Node.js 24 with the `localhost-test` hosts entry. --- .github/workflows/check.yaml | 29 ++++++++++++--- package.json | 4 ++- test/README.md | 36 +++++++++++++++---- test/{ => e2e}/anonymize_proxy.js | 4 +-- test/{ => e2e}/anonymize_proxy_no_password.js | 2 +- test/{ => e2e}/ee-memory-leak.js | 2 +- test/{ => e2e}/http-agent.js | 4 +-- test/{ => e2e}/https-server.js | 2 +- test/{ => e2e}/https-stress-test.js | 4 +-- test/{ => e2e}/server.js | 6 ++-- test/{ => e2e}/socks.js | 2 +- test/{ => e2e}/ssl.crt | 0 test/{ => e2e}/ssl.key | 0 test/{ => e2e}/tcp_tunnel.js | 4 +-- test/{ => unit}/tools.js | 6 ++-- 15 files changed, 75 insertions(+), 30 deletions(-) rename test/{ => e2e}/anonymize_proxy.js (99%) rename test/{ => e2e}/anonymize_proxy_no_password.js (98%) rename test/{ => e2e}/ee-memory-leak.js (97%) rename test/{ => e2e}/http-agent.js (99%) rename test/{ => e2e}/https-server.js (99%) rename test/{ => e2e}/https-stress-test.js (98%) rename test/{ => e2e}/server.js (99%) rename test/{ => e2e}/socks.js (98%) rename test/{ => e2e}/ssl.crt (100%) rename test/{ => e2e}/ssl.key (100%) rename test/{ => e2e}/tcp_tunnel.js (98%) rename test/{ => unit}/tools.js (95%) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index fed9b57e..974a3bf7 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -7,12 +7,13 @@ on: workflow_call: jobs: - lint_and_test: - name: Lint & Test + unit: + name: Unit tests (Node.js ${{ matrix.node-version }}) if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: node-version: [20, 22, 24] @@ -33,8 +34,28 @@ jobs: if: ${{ matrix.node-version == 24 }} run: npm run lint + - name: Run unit tests + run: npm run test:unit + + e2e: + name: E2E tests + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Use Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Install Dependencies + run: npm install + - name: Add localhost-test to Linux hosts file run: sudo echo "127.0.0.1 localhost-test" | sudo tee -a /etc/hosts - - name: Run Tests - run: npm test + - name: Run E2E tests + run: npm run test:e2e diff --git a/package.json b/package.json index 2876b57e..472fe551 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,9 @@ "clean": "rimraf dist", "prepublishOnly": "npm run build", "local-proxy": "tsx test/utils/run_locally.js", - "test": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha", + "test": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha 'test/unit/**/*.js' 'test/e2e/**/*.js'", + "test:unit": "mocha 'test/unit/**/*.js'", + "test:e2e": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha 'test/e2e/**/*.js'", "test:docker": "docker build --tag proxy-chain-tests --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests", "test:docker:all": "bash scripts/test-docker-all.sh", "lint": "eslint .", diff --git a/test/README.md b/test/README.md index 99c1bead..b96f4b71 100644 --- a/test/README.md +++ b/test/README.md @@ -1,5 +1,15 @@ # Tests +The test suite is split into two directories: + +- `test/unit/` — pure unit tests over utility helpers (no network, no proxy + servers). Fast; runs in CI on every supported major Node.js version. +- `test/e2e/` — end-to-end tests that spin up real HTTP/HTTPS/SOCKS proxy + servers and target servers. Heavier; runs in CI on the latest Node.js + only. + +Shared helpers live in `test/utils/`. + ## Docker (recommended) Since Linux and macOS handle sockets differently, please run tests in a Docker container @@ -14,13 +24,13 @@ to have a consistent Linux environment for running tests. 2. Run a specific test file ```bash - npm run test:docker test/server.js + npm run test:docker test/e2e/server.js ``` 3. Run all `direct ipv6` test cases across all tests ```bash - npm run test:docker test/server.js -- --grep "direct ipv6" + npm run test:docker test/e2e/server.js -- --grep "direct ipv6" ``` Note: for test in Docker no changes in `/etc/hosts` needed. @@ -29,7 +39,7 @@ Note: for test in Docker no changes in `/etc/hosts` needed. ### Prerequisites -1. Node.js 18+ (see `.nvmrc` for exact version) +1. Node.js 20+ (see `.nvmrc` for exact version) 2. For MacOS with ARM CPUs install Rosetta (workaround for puppeteer) 3. Update `/etc/hosts` @@ -47,14 +57,26 @@ Note: for test in Docker no changes in `/etc/hosts` needed. ### Run tests -1. Run all tests +1. Run all tests (unit + e2e) ```bash - npm run test + npm test ``` -2. Run a specific test file +2. Run only unit tests + + ```bash + npm run test:unit + ``` + +3. Run only e2e tests + + ```bash + npm run test:e2e + ``` + +4. Run a specific test file ```bash - npm run test test/anonymize_proxy.js + npm test test/e2e/anonymize_proxy.js ``` diff --git a/test/anonymize_proxy.js b/test/e2e/anonymize_proxy.js similarity index 99% rename from test/anonymize_proxy.js rename to test/e2e/anonymize_proxy.js index b7d66ea2..b50ea7de 100644 --- a/test/anonymize_proxy.js +++ b/test/e2e/anonymize_proxy.js @@ -8,8 +8,8 @@ import basicAuthParser from 'basic-auth-parser'; import request from 'request'; import express from 'express'; -import { anonymizeProxy, closeAnonymizedProxy, listenConnectAnonymizedProxy } from '../src/index.js'; -import { expectThrowsAsync } from './utils/throws_async.js'; +import { anonymizeProxy, closeAnonymizedProxy, listenConnectAnonymizedProxy } from '../../src/index.js'; +import { expectThrowsAsync } from '../utils/throws_async.js'; let expressServer; let proxyServer; diff --git a/test/anonymize_proxy_no_password.js b/test/e2e/anonymize_proxy_no_password.js similarity index 98% rename from test/anonymize_proxy_no_password.js rename to test/e2e/anonymize_proxy_no_password.js index 710a1616..ee77dc0c 100644 --- a/test/anonymize_proxy_no_password.js +++ b/test/e2e/anonymize_proxy_no_password.js @@ -8,7 +8,7 @@ import basicAuthParser from 'basic-auth-parser'; import request from 'request'; import express from 'express'; -import { anonymizeProxy, closeAnonymizedProxy } from '../src/index.js'; +import { anonymizeProxy, closeAnonymizedProxy } from '../../src/index.js'; let expressServer; let proxyServer; diff --git a/test/ee-memory-leak.js b/test/e2e/ee-memory-leak.js similarity index 97% rename from test/ee-memory-leak.js rename to test/e2e/ee-memory-leak.js index 95c82b02..8f05c786 100644 --- a/test/ee-memory-leak.js +++ b/test/e2e/ee-memory-leak.js @@ -1,7 +1,7 @@ import net from 'node:net'; import http from 'node:http'; import { assert } from 'chai'; -import * as ProxyChain from '../src/index.js'; +import * as ProxyChain from '../../src/index.js'; describe('ProxyChain server', () => { let server; diff --git a/test/http-agent.js b/test/e2e/http-agent.js similarity index 99% rename from test/http-agent.js rename to test/e2e/http-agent.js index c3dec1a3..1e4209b3 100644 --- a/test/http-agent.js +++ b/test/e2e/http-agent.js @@ -7,8 +7,8 @@ import portastic from 'portastic'; import proxy from 'proxy'; import request from 'request'; -import { Server } from '../src/index.js'; -import { TargetServer } from './utils/target_server.js'; +import { Server } from '../../src/index.js'; +import { TargetServer } from '../utils/target_server.js'; const sslKey = fs.readFileSync(path.join(import.meta.dirname, 'ssl.key')); const sslCrt = fs.readFileSync(path.join(import.meta.dirname, 'ssl.crt')); diff --git a/test/https-server.js b/test/e2e/https-server.js similarity index 99% rename from test/https-server.js rename to test/e2e/https-server.js index cee193dc..7b4b78cd 100644 --- a/test/https-server.js +++ b/test/e2e/https-server.js @@ -4,7 +4,7 @@ import path from 'node:path'; import tls from 'node:tls'; import { expect } from 'chai'; import http from 'node:http'; -import { Server } from '../src/index.js'; +import { Server } from '../../src/index.js'; const sslKey = fs.readFileSync(path.join(import.meta.dirname, 'ssl.key')); const sslCrt = fs.readFileSync(path.join(import.meta.dirname, 'ssl.crt')); diff --git a/test/https-stress-test.js b/test/e2e/https-stress-test.js similarity index 98% rename from test/https-stress-test.js rename to test/e2e/https-stress-test.js index 5ce364b5..52398bc6 100644 --- a/test/https-stress-test.js +++ b/test/e2e/https-stress-test.js @@ -5,8 +5,8 @@ import tls from 'node:tls'; import util from 'node:util'; import request from 'request'; import { expect } from 'chai'; -import { Server } from '../src/index.js'; -import { TargetServer } from './utils/target_server.js'; +import { Server } from '../../src/index.js'; +import { TargetServer } from '../utils/target_server.js'; // Node.js 20+ enables HTTP keep-alive by default in the global agent, // which causes connection tracking issues in tests. Disable it. diff --git a/test/server.js b/test/e2e/server.js similarity index 99% rename from test/server.js rename to test/e2e/server.js index 7dff2d0f..5a5fb2a1 100644 --- a/test/server.js +++ b/test/e2e/server.js @@ -16,9 +16,9 @@ import request from 'request'; import WebSocket from 'faye-websocket'; import { gotScraping } from 'got-scraping'; -import { parseAuthorizationHeader } from '../src/utils/parse_authorization_header.js'; -import { Server, RequestError } from '../src/index.js'; -import { TargetServer } from './utils/target_server.js'; +import { parseAuthorizationHeader } from '../../src/utils/parse_authorization_header.js'; +import { Server, RequestError } from '../../src/index.js'; +import { TargetServer } from '../utils/target_server.js'; /* TODO - add following tests: diff --git a/test/socks.js b/test/e2e/socks.js similarity index 98% rename from test/socks.js rename to test/e2e/socks.js index bef39417..bc324ac1 100644 --- a/test/socks.js +++ b/test/e2e/socks.js @@ -2,7 +2,7 @@ import portastic from 'portastic'; import socksv5 from 'socksv5'; import { gotScraping } from 'got-scraping'; import { expect } from 'chai'; -import * as ProxyChain from '../src/index.js'; +import * as ProxyChain from '../../src/index.js'; describe('SOCKS protocol', () => { let socksServer; diff --git a/test/ssl.crt b/test/e2e/ssl.crt similarity index 100% rename from test/ssl.crt rename to test/e2e/ssl.crt diff --git a/test/ssl.key b/test/e2e/ssl.key similarity index 100% rename from test/ssl.key rename to test/e2e/ssl.key diff --git a/test/tcp_tunnel.js b/test/e2e/tcp_tunnel.js similarity index 98% rename from test/tcp_tunnel.js rename to test/e2e/tcp_tunnel.js index faadb1ee..6f38f99e 100644 --- a/test/tcp_tunnel.js +++ b/test/e2e/tcp_tunnel.js @@ -3,8 +3,8 @@ import { expect, assert } from 'chai'; import http from 'node:http'; import proxy from 'proxy'; -import { createTunnel, closeTunnel } from '../src/index.js'; -import { expectThrowsAsync } from './utils/throws_async.js'; +import { createTunnel, closeTunnel } from '../../src/index.js'; +import { expectThrowsAsync } from '../utils/throws_async.js'; const destroySocket = (socket) => new Promise((resolve) => { if (!socket || socket.destroyed) return resolve(); diff --git a/test/tools.js b/test/unit/tools.js similarity index 95% rename from test/tools.js rename to test/unit/tools.js index 60eae700..37bbab84 100644 --- a/test/tools.js +++ b/test/unit/tools.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; -import { redactUrl } from '../src/utils/redact_url.js'; -import { isHopByHopHeader } from '../src/utils/is_hop_by_hop_header.js'; -import { parseAuthorizationHeader } from '../src/utils/parse_authorization_header.js'; +import { redactUrl } from '../../src/utils/redact_url.js'; +import { isHopByHopHeader } from '../../src/utils/is_hop_by_hop_header.js'; +import { parseAuthorizationHeader } from '../../src/utils/parse_authorization_header.js'; describe('tools.redactUrl()', () => { it('works', () => { From 31d794df70d5b522e5fdd6275da497aded99f7db Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 10:36:42 +0000 Subject: [PATCH 2/2] chore(ci): extract lint into its own job Per review on #651: the unit job ran lint on Node 24 only, which made the job name misleading and coupled a lint failure to unit-test reporting on that one matrix entry. Lint now runs as a dedicated job alongside unit and e2e. --- .github/workflows/check.yaml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 974a3bf7..30bf51ed 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -7,6 +7,26 @@ on: workflow_call: jobs: + lint: + name: Lint + if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Use Node.js 24 + uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Install Dependencies + run: npm install + + - name: Lint code + run: npm run lint + unit: name: Unit tests (Node.js ${{ matrix.node-version }}) if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} @@ -29,11 +49,6 @@ jobs: - name: Install Dependencies run: npm install - # Lint only on the latest Node.js version to save time. - - name: Lint code - if: ${{ matrix.node-version == 24 }} - run: npm run lint - - name: Run unit tests run: npm run test:unit