Skip to content

Commit

Permalink
Add NordVPN E2E tests (#144)
Browse files Browse the repository at this point in the history
Runs a simple end to end test against NordVPN servers,
for `https-proxy-agent` and `socks-proxy-agent`.
  • Loading branch information
TooTallNate committed Apr 29, 2023
1 parent de70a45 commit 4b3e591
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 7 deletions.
22 changes: 21 additions & 1 deletion .github/workflows/test.yml
Expand Up @@ -26,7 +26,6 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test

lint:
Expand All @@ -46,3 +45,24 @@ jobs:
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint

e2e:
name: E2E

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 7.32.2
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm test-e2e
env:
NORDVPN_USERNAME: ${{ secrets.NORDVPN_USERNAME }}
NORDVPN_PASSWORD: ${{ secrets.NORDVPN_PASSWORD }}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -5,6 +5,7 @@
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"test-e2e": "turbo run test-e2e",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
Expand Down
5 changes: 3 additions & 2 deletions packages/agent-base/src/helpers.ts
Expand Up @@ -12,14 +12,15 @@ export async function toBuffer(stream: Readable): Promise<Buffer> {
return Buffer.concat(chunks, length);
}

export async function json(stream: Readable): Promise<Record<string, string>> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function json(stream: Readable): Promise<any> {
const buf = await toBuffer(stream);
return JSON.parse(buf.toString('utf8'));
}

export function req(
url: string | URL,
opts: https.RequestOptions
opts: https.RequestOptions = {}
): Promise<http.IncomingMessage> {
return new Promise((resolve, reject) => {
const href = typeof url === 'string' ? url : url.href;
Expand Down
5 changes: 4 additions & 1 deletion packages/https-proxy-agent/package.json
Expand Up @@ -9,7 +9,8 @@
],
"scripts": {
"build": "tsc",
"test": "jest --env node --verbose --bail",
"test": "jest --env node --verbose --bail test/test.ts",
"test-e2e": "jest --env node --verbose --bail test/e2e.test.ts",
"lint": "eslint src --ext .js,.ts",
"prepublishOnly": "npm run build"
},
Expand All @@ -33,10 +34,12 @@
"debug": "4"
},
"devDependencies": {
"@types/async-retry": "^1.4.5",
"@types/debug": "4",
"@types/jest": "^29.5.1",
"@types/node": "^14.18.43",
"async-listen": "^2.1.0",
"async-retry": "^1.3.3",
"jest": "^29.5.0",
"proxy": "workspace:*",
"ts-jest": "^29.1.0",
Expand Down
84 changes: 84 additions & 0 deletions packages/https-proxy-agent/test/e2e.test.ts
@@ -0,0 +1,84 @@
import retry from 'async-retry';
import { req, json } from 'agent-base';
import { HttpsProxyAgent } from '../src';

interface NordVPNServer {
name: string;
domain: string;
flag: string;
features: { [key: string]: boolean };
}

jest.setTimeout(30000);

const findNordVpnServer = () =>
retry(
async (): Promise<NordVPNServer> => {
const res = await req('https://nordvpn.com/api/server');
if (res.statusCode !== 200) {
res.socket.destroy();
throw new Error(`Status code: ${res.statusCode}`);
}
const body = await json(res);
const servers = (body as NordVPNServer[]).filter(
(s) => s.features.proxy_ssl
);
if (servers.length === 0) {
throw new Error(
'Could not find `https` proxy server from NordVPN'
);
}
const server = servers[Math.floor(Math.random() * servers.length)];
return server;
},
{
retries: 5,
onRetry(err, attempt) {
console.log(
`Failed to get NordVPN servers. Retrying… (attempt #${attempt}, ${err.message})`
);
},
}
);

async function getRealIP(): Promise<string> {
const res = await req('https://dump.n8.io');
const body = await json(res);
return body.request.headers['x-real-ip'];
}

describe('HttpsProxyAgent', () => {
it('should work over NordVPN proxy', async () => {
const { NORDVPN_USERNAME, NORDVPN_PASSWORD } = process.env;
if (!NORDVPN_USERNAME) {
throw new Error('`NORDVPN_USERNAME` env var is not defined');
}
if (!NORDVPN_PASSWORD) {
throw new Error('`NORDVPN_PASSWORD` env var is not defined');
}

const [realIp, server] = await Promise.all([
getRealIP(),
findNordVpnServer(),
]);
console.log(
`Using NordVPN HTTPS proxy server: ${server.name} (${server.domain})`
);

const username = encodeURIComponent(NORDVPN_USERNAME);
const password = encodeURIComponent(NORDVPN_PASSWORD);

// NordVPN runs their HTTPS proxy servers on port 89
// https://www.reddit.com/r/nordvpn/comments/hvz48h/nordvpn_https_proxy/
const agent = new HttpsProxyAgent(
`https://${username}:${password}@${server.domain}:89`
);

const res = await req('https://dump.n8.io', { agent });
const body = await json(res);
expect(body.request.headers['x-real-ip']).not.toEqual(realIp);
expect(body.request.headers['x-vercel-ip-country']).toEqual(
server.flag
);
});
});
2 changes: 1 addition & 1 deletion packages/https-proxy-agent/test/tsconfig.json
@@ -1,4 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["test.ts"]
"include": ["*.ts"]
}
5 changes: 5 additions & 0 deletions packages/socks-proxy-agent/jest.config.js
@@ -0,0 +1,5 @@
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
9 changes: 8 additions & 1 deletion packages/socks-proxy-agent/package.json
Expand Up @@ -114,12 +114,18 @@
"socks": "^2.7.1"
},
"devDependencies": {
"@types/async-retry": "^1.4.5",
"@types/debug": "^4.1.7",
"@types/jest": "^29.5.1",
"@types/node": "^14.18.43",
"async-retry": "^1.3.3",
"cacheable-lookup": "^6.1.0",
"dns2": "^2.1.0",
"jest": "^29.5.0",
"mocha": "^9.2.2",
"proxy": "workspace:*",
"socksv5": "github:TooTallNate/socksv5#fix/dstSock-close-event",
"ts-jest": "^29.1.0",
"tsconfig": "workspace:*",
"typescript": "^5.0.4"
},
Expand All @@ -128,7 +134,8 @@
},
"scripts": {
"build": "tsc",
"test": "mocha --reporter spec",
"test": "mocha --reporter spec test/test.js",
"test-e2e": "jest --env node --verbose --bail test/e2e.test.ts",
"lint": "eslint . --ext .ts",
"prepublishOnly": "npm run build"
},
Expand Down
82 changes: 82 additions & 0 deletions packages/socks-proxy-agent/test/e2e.test.ts
@@ -0,0 +1,82 @@
import retry from 'async-retry';
import { req, json } from 'agent-base';
import { SocksProxyAgent } from '../src';

interface NordVPNServer {
name: string;
domain: string;
flag: string;
features: { [key: string]: boolean };
}

jest.setTimeout(30000);

const findNordVpnServer = () =>
retry(
async (): Promise<NordVPNServer> => {
const res = await req('https://nordvpn.com/api/server');
if (res.statusCode !== 200) {
res.socket.destroy();
throw new Error(`Status code: ${res.statusCode}`);
}
const body = await json(res);
const servers = (body as NordVPNServer[]).filter(
(s) => s.features.socks
);
if (servers.length === 0) {
throw new Error(
'Could not find `socks` proxy server from NordVPN'
);
}
const server = servers[Math.floor(Math.random() * servers.length)];
return server;
},
{
retries: 5,
onRetry(err, attempt) {
console.log(
`Failed to get NordVPN servers. Retrying… (attempt #${attempt}, ${err.message})`
);
},
}
);

async function getRealIP(): Promise<string> {
const res = await req('https://dump.n8.io');
const body = await json(res);
return body.request.headers['x-real-ip'];
}

describe('SocksProxyAgent', () => {
it('should work over NordVPN proxy', async () => {
const { NORDVPN_USERNAME, NORDVPN_PASSWORD } = process.env;
if (!NORDVPN_USERNAME) {
throw new Error('`NORDVPN_USERNAME` env var is not defined');
}
if (!NORDVPN_PASSWORD) {
throw new Error('`NORDVPN_PASSWORD` env var is not defined');
}

const [realIp, server] = await Promise.all([
getRealIP(),
findNordVpnServer(),
]);
console.log(
`Using NordVPN SOCKS proxy server: ${server.name} (${server.domain})`
);

const username = encodeURIComponent(NORDVPN_USERNAME);
const password = encodeURIComponent(NORDVPN_PASSWORD);

const agent = new SocksProxyAgent(
`socks://${username}:${password}@${server.domain}`
);

const res = await req('https://dump.n8.io', { agent });
const body = await json(res);
expect(body.request.headers['x-real-ip']).not.toEqual(realIp);
expect(body.request.headers['x-vercel-ip-country']).toEqual(
server.flag
);
});
});
4 changes: 4 additions & 0 deletions packages/socks-proxy-agent/test/tsconfig.json
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["*.ts"]
}

0 comments on commit 4b3e591

Please sign in to comment.