Skip to content

Commit f456fbf

Browse files
committed
move test env to remix
1 parent ce1e036 commit f456fbf

File tree

1 file changed

+258
-1
lines changed
  • packages/remix/test/integration/test/server/utils

1 file changed

+258
-1
lines changed

packages/remix/test/integration/test/server/utils/helpers.ts

Lines changed: 258 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,264 @@
11
import * as http from 'http';
22
import { AddressInfo } from 'net';
3+
import * as path from 'path';
34
import { createRequestHandler } from '@remix-run/express';
5+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
6+
import * as Sentry from '@sentry/node';
7+
import type { EnvelopeItemType } from '@sentry/types';
8+
import { logger } from '@sentry/utils';
9+
import type { AxiosRequestConfig } from 'axios';
10+
import axios from 'axios';
411
import express from 'express';
5-
import { TestEnv } from '../../../../../../../dev-packages/node-integration-tests/utils';
12+
import type { Express } from 'express';
13+
import type { HttpTerminator } from 'http-terminator';
14+
import { createHttpTerminator } from 'http-terminator';
15+
import nock from 'nock';
616

717
export * from '../../../../../../../dev-packages/node-integration-tests/utils';
818

19+
type DataCollectorOptions = {
20+
// Optional custom URL
21+
url?: string;
22+
23+
// The expected amount of requests to the envelope endpoint.
24+
// If the amount of sent requests is lower than `count`, this function will not resolve.
25+
count?: number;
26+
27+
// The method of the request.
28+
method?: 'get' | 'post';
29+
30+
// Whether to stop the server after the requests have been intercepted
31+
endServer?: boolean;
32+
33+
// Type(s) of the envelopes to capture
34+
envelopeType?: EnvelopeItemType | EnvelopeItemType[];
35+
};
36+
37+
async function makeRequest(
38+
method: 'get' | 'post' = 'get',
39+
url: string,
40+
axiosConfig?: AxiosRequestConfig,
41+
): Promise<void> {
42+
try {
43+
if (method === 'get') {
44+
await axios.get(url, axiosConfig);
45+
} else {
46+
await axios.post(url, axiosConfig);
47+
}
48+
} catch (e) {
49+
// We sometimes expect the request to fail, but not the test.
50+
// So, we do nothing.
51+
logger.warn(e);
52+
}
53+
}
54+
55+
class TestEnv {
56+
private _axiosConfig: AxiosRequestConfig | undefined = undefined;
57+
private _terminator: HttpTerminator;
58+
59+
public constructor(public readonly server: http.Server, public readonly url: string) {
60+
this.server = server;
61+
this.url = url;
62+
this._terminator = createHttpTerminator({ server: this.server, gracefulTerminationTimeout: 0 });
63+
}
64+
65+
/**
66+
* Starts a test server and returns the TestEnv instance
67+
*
68+
* @param {string} testDir
69+
* @param {string} [serverPath]
70+
* @param {string} [scenarioPath]
71+
* @return {*} {Promise<string>}
72+
*/
73+
public static async init(testDir: string, serverPath?: string, scenarioPath?: string): Promise<TestEnv> {
74+
const defaultServerPath = path.resolve(process.cwd(), 'utils', 'defaults', 'server');
75+
76+
const [server, url] = await new Promise<[http.Server, string]>(resolve => {
77+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
78+
const app = require(serverPath || defaultServerPath).default as Express;
79+
80+
app.get('/test', (_req, res) => {
81+
try {
82+
require(scenarioPath || `${testDir}/scenario`);
83+
} finally {
84+
res.status(200).end();
85+
}
86+
});
87+
88+
const server = app.listen(0, () => {
89+
const url = `http://localhost:${(server.address() as AddressInfo).port}/test`;
90+
resolve([server, url]);
91+
});
92+
});
93+
94+
return new TestEnv(server, url);
95+
}
96+
97+
/**
98+
* Intercepts and extracts up to a number of requests containing Sentry envelopes.
99+
*
100+
* @param {DataCollectorOptions} options
101+
* @returns The intercepted envelopes.
102+
*/
103+
public async getMultipleEnvelopeRequest(options: DataCollectorOptions): Promise<Record<string, unknown>[][]> {
104+
const envelopeTypeArray =
105+
typeof options.envelopeType === 'string'
106+
? [options.envelopeType]
107+
: options.envelopeType || (['event'] as EnvelopeItemType[]);
108+
109+
const resProm = this.setupNock(
110+
options.count || 1,
111+
typeof options.endServer === 'undefined' ? true : options.endServer,
112+
envelopeTypeArray,
113+
);
114+
115+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
116+
makeRequest(options.method, options.url || this.url, this._axiosConfig);
117+
return resProm;
118+
}
119+
120+
/**
121+
* Intercepts and extracts a single request containing a Sentry envelope
122+
*
123+
* @param {DataCollectorOptions} options
124+
* @returns The extracted envelope.
125+
*/
126+
public async getEnvelopeRequest(options?: DataCollectorOptions): Promise<Array<Record<string, unknown>>> {
127+
const requests = await this.getMultipleEnvelopeRequest({ ...options, count: 1 });
128+
129+
if (!requests[0]) {
130+
throw new Error('No requests found');
131+
}
132+
133+
return requests[0];
134+
}
135+
136+
/**
137+
* Sends a get request to given URL, with optional headers. Returns the response.
138+
* Ends the server instance and flushes the Sentry event queue.
139+
*
140+
* @param {Record<string, string>} [headers]
141+
* @return {*} {Promise<any>}
142+
*/
143+
public async getAPIResponse(
144+
url?: string,
145+
headers: Record<string, string> = {},
146+
endServer: boolean = true,
147+
): Promise<unknown> {
148+
try {
149+
const { data } = await axios.get(url || this.url, {
150+
headers,
151+
// KeepAlive false to work around a Node 20 bug with ECONNRESET: https://github.com/axios/axios/issues/5929
152+
httpAgent: new http.Agent({ keepAlive: false }),
153+
});
154+
return data;
155+
} finally {
156+
await Sentry.flush();
157+
158+
if (endServer) {
159+
this.server.close();
160+
}
161+
}
162+
}
163+
164+
public async setupNock(
165+
count: number,
166+
endServer: boolean,
167+
envelopeType: EnvelopeItemType[],
168+
): Promise<Record<string, unknown>[][]> {
169+
return new Promise(resolve => {
170+
const envelopes: Record<string, unknown>[][] = [];
171+
const mock = nock('https://dsn.ingest.sentry.io')
172+
.persist()
173+
.post('/api/1337/envelope/', body => {
174+
const envelope = parseEnvelope(body);
175+
176+
if (envelopeType.includes(envelope[1]?.type as EnvelopeItemType)) {
177+
envelopes.push(envelope);
178+
} else {
179+
return false;
180+
}
181+
182+
if (count === envelopes.length) {
183+
nock.removeInterceptor(mock);
184+
185+
if (endServer) {
186+
// Cleaning nock only before the server is closed,
187+
// not to break tests that use simultaneous requests to the server.
188+
// Ex: Remix scope bleed tests.
189+
nock.cleanAll();
190+
191+
// Abort all pending requests to nock to prevent hanging / flakes.
192+
// See: https://github.com/nock/nock/issues/1118#issuecomment-544126948
193+
nock.abortPendingRequests();
194+
195+
this._closeServer()
196+
.catch(e => {
197+
logger.warn(e);
198+
})
199+
.finally(() => {
200+
resolve(envelopes);
201+
});
202+
} else {
203+
resolve(envelopes);
204+
}
205+
}
206+
207+
return true;
208+
});
209+
210+
mock
211+
.query(true) // accept any query params - used for sentry_key param
212+
.reply(200);
213+
});
214+
}
215+
216+
public setAxiosConfig(axiosConfig: AxiosRequestConfig): void {
217+
this._axiosConfig = axiosConfig;
218+
}
219+
220+
public async countEnvelopes(options: {
221+
url?: string;
222+
timeout?: number;
223+
envelopeType: EnvelopeItemType | EnvelopeItemType[];
224+
}): Promise<number> {
225+
return new Promise(resolve => {
226+
let reqCount = 0;
227+
228+
const mock = nock('https://dsn.ingest.sentry.io')
229+
.persist()
230+
.post('/api/1337/envelope/', body => {
231+
const envelope = parseEnvelope(body);
232+
233+
if (options.envelopeType.includes(envelope[1]?.type as EnvelopeItemType)) {
234+
reqCount++;
235+
return true;
236+
}
237+
238+
return false;
239+
});
240+
241+
setTimeout(
242+
() => {
243+
nock.removeInterceptor(mock);
244+
245+
nock.cleanAll();
246+
247+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
248+
this._closeServer().then(() => {
249+
resolve(reqCount);
250+
});
251+
},
252+
options.timeout || 1000,
253+
);
254+
});
255+
}
256+
257+
private _closeServer(): Promise<void> {
258+
return this._terminator.terminate();
259+
}
260+
}
261+
9262
export class RemixTestEnv extends TestEnv {
10263
private constructor(public readonly server: http.Server, public readonly url: string) {
11264
super(server, url);
@@ -27,3 +280,7 @@ export class RemixTestEnv extends TestEnv {
27280
return new RemixTestEnv(server, `http://localhost:${serverPort}`);
28281
}
29282
}
283+
284+
const parseEnvelope = (body: string): Array<Record<string, unknown>> => {
285+
return body.split('\n').map(e => JSON.parse(e));
286+
};

0 commit comments

Comments
 (0)