Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@
"proxying",
"rawbody",
"restream",
"selfsigned",
"snyk",
"streamify",
"trivago",
"tseslint",
"typicode",
"unstub",
"vhosted",
"websockets",
"xfwd"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"open": "11.0.0",
"oxfmt": "0.46.0",
"pkg-pr-new": "0.0.66",
"selfsigned": "5.5.0",
"supertest": "7.2.2",
"typescript": "6.0.3",
"typescript-eslint": "8.59.0",
Expand Down
157 changes: 157 additions & 0 deletions test/e2e/http2-server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { createSecureServer, type Http2SecureServer } from 'node:http2';

import type { Mockttp } from 'mockttp';
import { generate } from 'selfsigned';
import request from 'supertest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { createApp, createProxyMiddleware } from './test-kit.js';

describe('E2E http2', () => {
// Self-signed certs are used in this test; allow local TLS connections for this spec only.
beforeEach(() => vi.stubEnv('NODE_TLS_REJECT_UNAUTHORIZED', '0'));
afterEach(() => vi.unstubAllEnvs());

describe('E2E http2 to http1', () => {
let mockTargetServer: Mockttp;
let http2ProxyServer: Http2SecureServer | undefined;
let proxyServerCertPem: string;

async function closeServer(server: Http2SecureServer) {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
}

beforeEach(async () => {
const cert = await generate([{ name: 'commonName', value: 'localhost' }]);
proxyServerCertPem = cert.cert;

// Keep this dynamic to avoid a Vitest module-load ordering issue that breaks cert generation.
const { getLocal } = await import('mockttp');
mockTargetServer = getLocal();
await mockTargetServer.start();

const app = createApp(
createProxyMiddleware({
target: mockTargetServer.url,
pathFilter: '/api',
}),
);

http2ProxyServer = createSecureServer(
{
key: cert.private,
cert: cert.cert,
// Express + supertest use the HTTP/1.1 request/response API in this e2e flow.
allowHTTP1: true,
},
(req, res) => app(req as never, res as never),
);
});

afterEach(async () => {
await mockTargetServer.stop();

const server = http2ProxyServer;
if (!server?.listening) {
return;
}

await closeServer(server);
});

it('proxies requests through a secure HTTP/2 server to a mocked target', async () => {
await mockTargetServer.forGet('/api/hello').thenReply(200, 'hello from mocked target');

expect(http2ProxyServer).toBeDefined();
const response = await request(http2ProxyServer!)
.get('/api/hello')
.ca(proxyServerCertPem)
.expect(200);

expect(response.text).toBe('hello from mocked target');
});
});

describe('E2E http2 to http2', () => {
let mockTargetServer: Mockttp;
let http2ProxyServer: Http2SecureServer | undefined;
let proxyServerCertPem: string;

async function closeServer(server: Http2SecureServer) {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
}

beforeEach(async () => {
const cert = await generate([{ name: 'commonName', value: 'localhost' }]);
proxyServerCertPem = cert.cert;

// Keep this dynamic to avoid a Vitest module-load ordering issue that breaks cert generation.
const { getLocal } = await import('mockttp');
mockTargetServer = getLocal({ http2: true });
await mockTargetServer.start();

const app = createApp(
createProxyMiddleware({
target: mockTargetServer.url,
pathFilter: '/api',
}),
);

http2ProxyServer = createSecureServer(
{
key: cert.private,
cert: cert.cert,
// Express + supertest use the HTTP/1.1 request/response API in this e2e flow.
allowHTTP1: true,
},
(req, res) => app(req as never, res as never),
);
});

afterEach(async () => {
await mockTargetServer.stop();

const server = http2ProxyServer;
if (!server?.listening) {
return;
}

await closeServer(server);
});

it('proxies requests through a secure HTTP/2 server to an HTTP/2 mocked target', async () => {
await mockTargetServer.forGet('/api/hello').thenReply(200, 'hello from http2 target');

expect(http2ProxyServer).toBeDefined();
const response = await request(http2ProxyServer!)
.get('/api/hello')
.ca(proxyServerCertPem)
.expect(200);

expect(response.text).toBe('hello from http2 target');
});

it('handles multiple concurrent HTTP/2 requests (multiplexing)', async () => {
await mockTargetServer.forGet('/api/request1').thenReply(200, 'response 1');
await mockTargetServer.forGet('/api/request2').thenReply(200, 'response 2');
await mockTargetServer.forGet('/api/request3').thenReply(200, 'response 3');

expect(http2ProxyServer).toBeDefined();

// Send multiple concurrent requests to test HTTP/2 multiplexing
const [response1, response2, response3] = await Promise.all([
request(http2ProxyServer!).get('/api/request1').expect(200),
request(http2ProxyServer!).get('/api/request2').expect(200),
request(http2ProxyServer!).get('/api/request3').expect(200),
]);

expect(response1.text).toBe('response 1');
expect(response2.text).toBe('response 2');
expect(response3.text).toBe('response 3');
});
});
});
Loading
Loading