Skip to content

Commit 29d597f

Browse files
authored
test(node): Test proxy server (#10156)
This PR adds an integration that tests the `proxy` transport option. To do this I've added a `.withMockSentryServer()` feature to the test runner. When optionally enabled, a basic express server is started which listens with an envelope endpoint. Test scenarios can then use the `SENTRY_DSN` environment variable to set the correct DSN. ### Why a Sentry server when we have this new stdout transport for testing? We need to test the transport and assosiated things like the proxy option which can't use stdout parsing. ### Why use the stdout transport at all if we can just use this server? Resources when running tests in parallel? Because they use the same test runner, it's relatively simple to migrate tests between the two, so once we have a full test suite we can test this further.
1 parent 66333e6 commit 29d597f

File tree

6 files changed

+201
-63
lines changed

6 files changed

+201
-63
lines changed

dev-packages/node-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"mysql": "^2.18.1",
4545
"nock": "^13.1.0",
4646
"pg": "^8.7.3",
47+
"proxy": "^2.1.1",
4748
"yargs": "^16.2.0"
4849
},
4950
"config": {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const http = require('http');
2+
const Sentry = require('@sentry/node');
3+
const { createProxy } = require('proxy');
4+
5+
const proxy = createProxy(http.createServer());
6+
proxy.listen(0, () => {
7+
const proxyPort = proxy.address().port;
8+
9+
Sentry.init({
10+
dsn: process.env.SENTRY_DSN,
11+
debug: true,
12+
transportOptions: {
13+
proxy: `http://localhost:${proxyPort}`,
14+
},
15+
});
16+
17+
Sentry.captureMessage('Hello, via proxy!');
18+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createRunner } from '../../utils/runner';
2+
3+
test('proxies sentry requests', done => {
4+
createRunner(__dirname, 'basic.js')
5+
.withMockSentryServer()
6+
.ignore('session')
7+
.expect({
8+
event: {
9+
message: 'Hello, via proxy!',
10+
},
11+
})
12+
.start(done);
13+
});

dev-packages/node-integration-tests/utils/runner.ts

Lines changed: 96 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
33
import { join } from 'path';
44
import type { Envelope, EnvelopeItemType, Event, SerializedSession } from '@sentry/types';
55
import axios from 'axios';
6+
import { createBasicSentryServer } from './server';
67

78
export function assertSentryEvent(actual: Event, expected: Event): void {
89
expect(actual).toMatchObject({
@@ -37,6 +38,18 @@ export function cleanupChildProcesses(): void {
3738
}
3839
}
3940

41+
/** Promise only resolves when fn returns true */
42+
async function waitFor(fn: () => boolean, timeout = 10_000): Promise<void> {
43+
let remaining = timeout;
44+
while (fn() === false) {
45+
await new Promise<void>(resolve => setTimeout(resolve, 100));
46+
remaining -= 100;
47+
if (remaining < 0) {
48+
throw new Error('Timed out waiting for server port');
49+
}
50+
}
51+
}
52+
4053
type Expected =
4154
| {
4255
event: Partial<Event> | ((event: Event) => void);
@@ -48,15 +61,15 @@ type Expected =
4861
session: Partial<SerializedSession> | ((event: SerializedSession) => void);
4962
};
5063

51-
/** */
64+
/** Creates a test runner */
5265
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
5366
export function createRunner(...paths: string[]) {
5467
const testPath = join(...paths);
5568

5669
const expectedEnvelopes: Expected[] = [];
5770
const flags: string[] = [];
5871
const ignored: EnvelopeItemType[] = [];
59-
let hasExited = false;
72+
let withSentryServer = false;
6073

6174
if (testPath.endsWith('.ts')) {
6275
flags.push('-r', 'ts-node/register');
@@ -71,71 +84,36 @@ export function createRunner(...paths: string[]) {
7184
flags.push(...args);
7285
return this;
7386
},
87+
withMockSentryServer: function () {
88+
withSentryServer = true;
89+
return this;
90+
},
7491
ignore: function (...types: EnvelopeItemType[]) {
7592
ignored.push(...types);
7693
return this;
7794
},
7895
start: function (done?: (e?: unknown) => void) {
7996
const expectedEnvelopeCount = expectedEnvelopes.length;
80-
let envelopeCount = 0;
81-
let serverPort: number | undefined;
82-
83-
const child = spawn('node', [...flags, testPath]);
8497

85-
CHILD_PROCESSES.add(child);
86-
87-
child.on('close', () => {
88-
hasExited = true;
89-
});
90-
91-
// Pass error to done to end the test quickly
92-
child.on('error', e => {
93-
done?.(e);
94-
});
98+
let envelopeCount = 0;
99+
let scenarioServerPort: number | undefined;
100+
let hasExited = false;
101+
let child: ReturnType<typeof spawn> | undefined;
95102

96-
async function waitForServerPort(timeout = 10_000): Promise<void> {
97-
let remaining = timeout;
98-
while (serverPort === undefined) {
99-
await new Promise<void>(resolve => setTimeout(resolve, 100));
100-
remaining -= 100;
101-
if (remaining < 0) {
102-
throw new Error('Timed out waiting for server port');
103-
}
104-
}
103+
function complete(error?: Error): void {
104+
child?.kill();
105+
done?.(error);
105106
}
106107

107108
/** Called after each expect callback to check if we're complete */
108109
function expectCallbackCalled(): void {
109110
envelopeCount++;
110111
if (envelopeCount === expectedEnvelopeCount) {
111-
child.kill();
112-
done?.();
112+
complete();
113113
}
114114
}
115115

116-
function tryParseLine(line: string): void {
117-
// Lines can have leading '[something] [{' which we need to remove
118-
const cleanedLine = line.replace(/^.*?] \[{"/, '[{"');
119-
120-
// See if we have a port message
121-
if (cleanedLine.startsWith('{"port":')) {
122-
const { port } = JSON.parse(cleanedLine) as { port: number };
123-
serverPort = port;
124-
return;
125-
}
126-
127-
// Skip any lines that don't start with envelope JSON
128-
if (!cleanedLine.startsWith('[{')) {
129-
return;
130-
}
131-
132-
let envelope: Envelope | undefined;
133-
try {
134-
envelope = JSON.parse(cleanedLine) as Envelope;
135-
} catch (_) {
136-
return;
137-
}
138-
116+
function newEnvelope(envelope: Envelope): void {
139117
for (const item of envelope[1]) {
140118
const envelopeItemType = item[0].type;
141119

@@ -190,22 +168,77 @@ export function createRunner(...paths: string[]) {
190168
expectCallbackCalled();
191169
}
192170
} catch (e) {
193-
done?.(e);
171+
complete(e as Error);
194172
}
195173
}
196174
}
197175

198-
let buffer = Buffer.alloc(0);
199-
child.stdout.on('data', (data: Buffer) => {
200-
// This is horribly memory inefficient but it's only for tests
201-
buffer = Buffer.concat([buffer, data]);
176+
const serverStartup: Promise<number | undefined> = withSentryServer
177+
? createBasicSentryServer(newEnvelope)
178+
: Promise.resolve(undefined);
179+
180+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
181+
serverStartup.then(mockServerPort => {
182+
const env = mockServerPort
183+
? { ...process.env, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` }
184+
: process.env;
185+
186+
// eslint-disable-next-line no-console
187+
if (process.env.DEBUG) console.log('starting scenario', testPath, flags, env.SENTRY_DSN);
188+
189+
child = spawn('node', [...flags, testPath], { env });
190+
191+
CHILD_PROCESSES.add(child);
192+
193+
child.on('close', () => {
194+
hasExited = true;
195+
});
196+
197+
// Pass error to done to end the test quickly
198+
child.on('error', e => {
199+
// eslint-disable-next-line no-console
200+
if (process.env.DEBUG) console.log('scenario error', e);
201+
complete(e);
202+
});
202203

203-
let splitIndex = -1;
204-
while ((splitIndex = buffer.indexOf(0xa)) >= 0) {
205-
const line = buffer.subarray(0, splitIndex).toString();
206-
buffer = Buffer.from(buffer.subarray(splitIndex + 1));
207-
tryParseLine(line);
204+
function tryParseEnvelopeFromStdoutLine(line: string): void {
205+
// Lines can have leading '[something] [{' which we need to remove
206+
const cleanedLine = line.replace(/^.*?] \[{"/, '[{"');
207+
208+
// See if we have a port message
209+
if (cleanedLine.startsWith('{"port":')) {
210+
const { port } = JSON.parse(cleanedLine) as { port: number };
211+
scenarioServerPort = port;
212+
return;
213+
}
214+
215+
// Skip any lines that don't start with envelope JSON
216+
if (!cleanedLine.startsWith('[{')) {
217+
return;
218+
}
219+
220+
try {
221+
const envelope = JSON.parse(cleanedLine) as Envelope;
222+
newEnvelope(envelope);
223+
} catch (_) {
224+
//
225+
}
208226
}
227+
228+
let buffer = Buffer.alloc(0);
229+
child.stdout.on('data', (data: Buffer) => {
230+
// This is horribly memory inefficient but it's only for tests
231+
buffer = Buffer.concat([buffer, data]);
232+
233+
let splitIndex = -1;
234+
while ((splitIndex = buffer.indexOf(0xa)) >= 0) {
235+
const line = buffer.subarray(0, splitIndex).toString();
236+
buffer = Buffer.from(buffer.subarray(splitIndex + 1));
237+
// eslint-disable-next-line no-console
238+
if (process.env.DEBUG) console.log('line', line);
239+
tryParseEnvelopeFromStdoutLine(line);
240+
}
241+
});
209242
});
210243

211244
return {
@@ -218,13 +251,13 @@ export function createRunner(...paths: string[]) {
218251
headers: Record<string, string> = {},
219252
): Promise<T | undefined> {
220253
try {
221-
await waitForServerPort();
254+
await waitFor(() => scenarioServerPort !== undefined);
222255
} catch (e) {
223-
done?.(e);
256+
complete(e as Error);
224257
return undefined;
225258
}
226259

227-
const url = `http://localhost:${serverPort}${path}`;
260+
const url = `http://localhost:${scenarioServerPort}${path}`;
228261
if (method === 'get') {
229262
return (await axios.get(url, { headers })).data;
230263
} else {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { AddressInfo } from 'net';
2+
import { TextDecoder, TextEncoder } from 'util';
3+
import type { Envelope } from '@sentry/types';
4+
import { parseEnvelope } from '@sentry/utils';
5+
import express from 'express';
6+
7+
/**
8+
* Creates a basic Sentry server that accepts POST to the envelope endpoint
9+
*
10+
* This does no checks on the envelope, it just calls the callback if it managed to parse an envelope from the raw POST
11+
* body data.
12+
*/
13+
export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Promise<number> {
14+
const app = express();
15+
app.use(express.raw({ type: () => true, inflate: true, limit: '100mb' }));
16+
app.post('/api/:id/envelope/', (req, res) => {
17+
try {
18+
const env = parseEnvelope(req.body as Buffer, new TextEncoder(), new TextDecoder());
19+
onEnvelope(env);
20+
} catch (e) {
21+
// eslint-disable-next-line no-console
22+
console.error(e);
23+
}
24+
25+
res.status(200).send();
26+
});
27+
28+
return new Promise(resolve => {
29+
const server = app.listen(0, () => {
30+
const address = server.address() as AddressInfo;
31+
resolve(address.port);
32+
});
33+
});
34+
}

yarn.lock

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8046,6 +8046,16 @@ argparse@^2.0.1:
80468046
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
80478047
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
80488048

8049+
args@^5.0.3:
8050+
version "5.0.3"
8051+
resolved "https://registry.yarnpkg.com/args/-/args-5.0.3.tgz#943256db85021a85684be2f0882f25d796278702"
8052+
integrity sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==
8053+
dependencies:
8054+
camelcase "5.0.0"
8055+
chalk "2.4.2"
8056+
leven "2.1.0"
8057+
mri "1.1.4"
8058+
80498059
80508060
version "0.0.2"
80518061
resolved "https://registry.yarnpkg.com/argv/-/argv-0.0.2.tgz#ecbd16f8949b157183711b1bda334f37840185ab"
@@ -9423,6 +9433,11 @@ base@^0.11.1:
94239433
mixin-deep "^1.2.0"
94249434
pascalcase "^0.1.1"
94259435

9436+
9437+
version "0.0.2-1"
9438+
resolved "https://registry.yarnpkg.com/basic-auth-parser/-/basic-auth-parser-0.0.2-1.tgz#f1ea575979b27af6a411921d6ff8793d9117347f"
9439+
integrity sha512-GFj8iVxo9onSU6BnnQvVwqvxh60UcSHJEDnIk3z4B6iOjsKSmqe+ibW0Rsz7YO7IE1HG3D3tqCNIidP46SZVdQ==
9440+
94269441
basic-auth@~2.0.1:
94279442
version "2.0.1"
94289443
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
@@ -10698,6 +10713,11 @@ camelcase-keys@^6.2.2:
1069810713
map-obj "^4.0.0"
1069910714
quick-lru "^4.0.1"
1070010715

10716+
10717+
version "5.0.0"
10718+
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
10719+
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
10720+
1070110721
[email protected], camelcase@^5.0.0, camelcase@^5.3.1:
1070210722
version "5.3.1"
1070310723
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -20019,6 +20039,11 @@ less@^4.1.0:
2001920039
needle "^3.1.0"
2002020040
source-map "~0.6.0"
2002120041

20042+
20043+
version "2.1.0"
20044+
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
20045+
integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==
20046+
2002220047
leven@^3.1.0:
2002320048
version "3.1.0"
2002420049
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -22136,6 +22161,11 @@ move-concurrently@^1.0.1:
2213622161
rimraf "^2.5.4"
2213722162
run-queue "^1.0.3"
2213822163

22164+
22165+
version "1.1.4"
22166+
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a"
22167+
integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==
22168+
2213922169
mri@^1.1.0:
2214022170
version "1.2.0"
2214122171
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@@ -25888,6 +25918,15 @@ proxy-from-env@^1.1.0:
2588825918
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
2588925919
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
2589025920

25921+
proxy@^2.1.1:
25922+
version "2.1.1"
25923+
resolved "https://registry.yarnpkg.com/proxy/-/proxy-2.1.1.tgz#45f9b307508ffcae12bdc71678d44a4ab79cbf8b"
25924+
integrity sha512-nLgd7zdUAOpB3ZO/xCkU8gy74UER7P0aihU8DkUsDS5ZoFwVCX7u8dy+cv5tVK8UaB/yminU1GiLWE26TKPYpg==
25925+
dependencies:
25926+
args "^5.0.3"
25927+
basic-auth-parser "0.0.2-1"
25928+
debug "^4.3.4"
25929+
2589125930
prr@~1.0.1:
2589225931
version "1.0.1"
2589325932
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"

0 commit comments

Comments
 (0)