Skip to content

Commit 6378cc9

Browse files
authored
[test] Use a busboy mock to have more control over field emission timing (#13)
* [test] Use a busboy mock to have more control over field emission timing * No need for `nextTick` and avoid IIFE * No need to make the methods async
1 parent 399bdd3 commit 6378cc9

3 files changed

Lines changed: 138 additions & 139 deletions

File tree

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"art": "0.10.1",
5454
"babel-plugin-syntax-hermes-parser": "^0.32.0",
5555
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
56-
"busboy": "^1.6.0",
5756
"chalk": "^3.0.0",
5857
"cli-table": "^0.3.1",
5958
"coffee-script": "^1.12.7",

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReplyNode-test.js

Lines changed: 138 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -12,86 +12,98 @@
1212

1313
let webpackServerMap;
1414
let ReactServerDOMServer;
15-
let Stream;
16-
let serverAct;
17-
let Busboy;
18-
let Request;
15+
let EventEmitter;
1916

2017
describe('ReactFlightDOMReplyNode', () => {
2118
beforeEach(() => {
2219
jest.resetModules();
2320

24-
serverAct = require('internal-test-utils').serverAct;
25-
2621
jest.mock('react', () => require('react/react.react-server'));
2722
jest.mock('react-server-dom-webpack/server', () =>
2823
require('react-server-dom-webpack/server.node'),
2924
);
3025
const WebpackMock = require('./utils/WebpackMock');
3126
webpackServerMap = WebpackMock.webpackServerMap;
3227
ReactServerDOMServer = require('react-server-dom-webpack/server.node');
33-
Stream = require('stream');
34-
Busboy = require('busboy');
35-
Request = require('undici').Request;
28+
EventEmitter = require('events');
3629
});
3730

38-
async function createBusboyStreamFromFormData(formData) {
39-
const request = new Request('http://localhost', {
40-
method: 'POST',
41-
body: formData,
42-
});
43-
const contentType = request.headers.get('content-type');
44-
const arrayBuffer = await request.arrayBuffer();
45-
const buffer = Buffer.from(arrayBuffer);
46-
const stream = new Stream.Readable();
47-
stream.push(buffer);
48-
stream.push(null);
49-
const busboy = Busboy({headers: {'content-type': contentType}});
50-
51-
return {busboy, stream};
31+
function createMockBusboy() {
32+
const emitter = new EventEmitter();
33+
let destroyed = false;
34+
35+
const busboy = {
36+
emitField(name, value) {
37+
if (!destroyed) {
38+
emitter.emit('field', name, value);
39+
}
40+
},
41+
finish() {
42+
if (!destroyed) {
43+
emitter.emit('finish');
44+
}
45+
},
46+
on(event, handler) {
47+
emitter.on(event, handler);
48+
return busboy;
49+
},
50+
destroy(error) {
51+
destroyed = true;
52+
if (error) {
53+
process.nextTick(() => emitter.emit('error', error));
54+
}
55+
},
56+
};
57+
58+
return busboy;
5259
}
5360

5461
describe('decodeReplyFromBusboy', () => {
5562
it('should error when a stream chunk references itself', async () => {
56-
const formData = new FormData();
57-
formData.append('0', '["$R0"]');
58-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
59-
60-
await expect(
61-
serverAct(() => {
62-
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
63-
busboy,
64-
webpackServerMap,
65-
);
66-
stream.pipe(busboy);
67-
return reply;
68-
}),
69-
).rejects.toThrow('Already initialized stream.');
63+
const busboy = createMockBusboy();
64+
65+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
66+
busboy,
67+
webpackServerMap,
68+
);
69+
70+
busboy.emitField('0', '["$R0"]');
71+
busboy.finish();
72+
73+
await expect(reply).rejects.toThrow('Already initialized stream.');
7074
});
7175

7276
it('should not hang when self-referencing promise is referenced', async () => {
73-
const formData = new FormData();
74-
formData.append('0', '{"a":"$1:"}');
75-
formData.append('1', '"$@1"');
76-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
77-
78-
await expect(
79-
serverAct(() => {
80-
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
81-
busboy,
82-
webpackServerMap,
83-
);
84-
stream.pipe(busboy);
85-
return reply;
86-
}),
87-
).rejects.toThrow('Invalid reference.');
77+
const busboy = createMockBusboy();
78+
79+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
80+
busboy,
81+
webpackServerMap,
82+
);
83+
84+
busboy.emitField('0', '{"a":"$1:"}');
85+
busboy.emitField('1', '"$@1"');
86+
busboy.finish();
87+
88+
await expect(reply).rejects.toThrow('Invalid reference.');
8889
});
8990

9091
it.failing('should not hang with direct forward references', async () => {
91-
const formData = new FormData();
9292
const n = 3; // TODO: Increase this to trigger the hanging.
93+
const busboy = createMockBusboy();
94+
95+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
96+
busboy,
97+
webpackServerMap,
98+
);
99+
100+
// Call .then synchronously before fields have been emitted.
101+
const promise = new Promise((resolve, reject) =>
102+
reply.then(resolve, reject),
103+
);
104+
93105
for (let i = 0; i < n; i++) {
94-
formData.append(
106+
busboy.emitField(
95107
String(i),
96108
JSON.stringify([
97109
`$${(i + 1).toString(16)}`,
@@ -100,63 +112,71 @@ describe('ReactFlightDOMReplyNode', () => {
100112
]),
101113
);
102114
}
103-
formData.append(String(n), JSON.stringify(''));
104-
105-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
106-
107-
await expect(
108-
// The hang won't reproduce if the callback for serverAct is not async.
109-
// Otherwise .then would be called on the ReactPromise returned from
110-
// decodeReplyFromBusboy and resolve synchronously.
111-
serverAct(async () => {
112-
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
113-
busboy,
114-
webpackServerMap,
115-
);
116-
stream.pipe(busboy);
117-
return reply;
118-
}),
119-
).rejects.toThrow('Maximum array nesting exceeded');
115+
busboy.emitField(String(n), JSON.stringify(''));
116+
busboy.finish();
117+
118+
await expect(promise).rejects.toThrow('Maximum array nesting exceeded');
119+
});
120+
121+
it('should be able to resolve cyclic references in root models', async () => {
122+
const busboy = createMockBusboy();
123+
124+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
125+
busboy,
126+
webpackServerMap,
127+
);
128+
129+
busboy.emitField('0', '["$1"]');
130+
busboy.emitField('1', '"$0"');
131+
busboy.finish();
132+
const result = await reply;
133+
134+
expect(result).toBeInstanceOf(Array);
135+
expect(result[0]).toBe(result);
120136
});
121137

122138
it.failing(
123-
'should be able to resolve cyclic references in root models',
139+
'should be able to resolve cyclic references in root models when calling .then synchronously',
124140
async () => {
125-
const formData = new FormData();
126-
formData.append('0', '["$1"]');
127-
formData.append('1', '"$0"');
128-
129-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
130-
131-
const result = await serverAct(async () => {
132-
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
133-
busboy,
134-
webpackServerMap,
135-
);
136-
stream.pipe(busboy);
137-
return reply;
138-
});
139-
140-
expect(result).toBeInstanceOf(Array);
141-
expect(result[0]).toBe(result);
141+
const busboy = createMockBusboy();
142+
143+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
144+
busboy,
145+
webpackServerMap,
146+
);
147+
148+
// Call .then synchronously before fields have been emitted.
149+
const promise = new Promise((resolve, reject) =>
150+
reply.then(result => {
151+
try {
152+
expect(result).toBeInstanceOf(Array);
153+
expect(result[0]).toBe(result);
154+
resolve();
155+
} catch (err) {
156+
reject(err);
157+
}
158+
}),
159+
);
160+
161+
busboy.emitField('0', '["$1"]');
162+
busboy.emitField('1', '"$0"');
163+
busboy.finish();
164+
await promise;
142165
},
143166
);
144167

145168
it('should be able to resolve cyclic references in arrays', async () => {
146-
const formData = new FormData();
147-
formData.append('0', '["$1"]');
148-
formData.append('1', '["$0"]');
169+
const busboy = createMockBusboy();
149170

150-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
171+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
172+
busboy,
173+
webpackServerMap,
174+
);
151175

152-
const result = await serverAct(async () => {
153-
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
154-
busboy,
155-
webpackServerMap,
156-
);
157-
stream.pipe(busboy);
158-
return reply;
159-
});
176+
busboy.emitField('0', '["$1"]');
177+
busboy.emitField('1', '["$0"]');
178+
busboy.finish();
179+
const result = await reply;
160180

161181
expect(result).toBeInstanceOf(Array);
162182
expect(result[0]).toBeInstanceOf(Array);
@@ -167,19 +187,15 @@ describe('ReactFlightDOMReplyNode', () => {
167187
it.failing(
168188
'should be able to resolve cyclic references in arrays when calling .then synchronously',
169189
async () => {
170-
const formData = new FormData();
171-
formData.append('0', '["$1"]');
172-
formData.append('1', '["$0"]');
173-
174-
const {busboy, stream} = await createBusboyStreamFromFormData(formData);
190+
const busboy = createMockBusboy();
175191

176192
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
177193
busboy,
178194
webpackServerMap,
179195
);
180-
stream.pipe(busboy);
181196

182-
await new Promise((resolve, reject) =>
197+
// Call .then synchronously before fields have been emitted.
198+
const promise = new Promise((resolve, reject) =>
183199
reply.then(result => {
184200
try {
185201
expect(result).toBeInstanceOf(Array);
@@ -192,35 +208,31 @@ describe('ReactFlightDOMReplyNode', () => {
192208
}
193209
}),
194210
);
211+
212+
busboy.emitField('0', '["$1"]');
213+
busboy.emitField('1', '["$0"]');
214+
busboy.finish();
215+
await promise;
195216
},
196217
);
197218

198219
it('cannot parse __proto__ into an object property through references', async () => {
199-
const busboyStream = new (require('node:events').EventEmitter)();
220+
const busboy = createMockBusboy();
200221

201-
const pendingResult = ReactServerDOMServer.decodeReplyFromBusboy(
202-
busboyStream,
222+
const reply = ReactServerDOMServer.decodeReplyFromBusboy(
223+
busboy,
203224
webpackServerMap,
204225
);
205226

227+
// Call .then synchronously before fields have been emitted.
206228
function ignore() {}
207-
pendingResult.then(ignore, ignore);
208-
209-
await serverAct(async () => {
210-
busboyStream.emit('field', '0', '"$@1"');
211-
});
212-
213-
await serverAct(async () => {});
214-
215-
await serverAct(async () => {
216-
busboyStream.emit('field', '1', '{"proto": true, "__proto__": "bad"}');
217-
});
229+
reply.then(ignore, ignore);
218230

219-
const result = await pendingResult;
231+
busboy.emitField('0', '"$@1"');
232+
busboy.emitField('1', '{"proto": true, "__proto__": "bad"}');
233+
busboy.finish();
234+
const result = await reply;
220235

221-
await serverAct(async () => {
222-
busboyStream.emit('finish');
223-
});
224236
// eslint-disable-next-line no-proto
225237
expect(result.__proto__).toBe(Object.prototype);
226238
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);

yarn.lock

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6074,13 +6074,6 @@ [email protected]:
60746074
mv "~2"
60756075
safe-json-stringify "~1"
60766076

6077-
busboy@^1.6.0:
6078-
version "1.6.0"
6079-
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
6080-
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
6081-
dependencies:
6082-
streamsearch "^1.1.0"
6083-
60846077
60856078
version "3.0.0"
60866079
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -16062,11 +16055,6 @@ stream-shift@^1.0.0:
1606216055
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
1606316056
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
1606416057

16065-
streamsearch@^1.1.0:
16066-
version "1.1.0"
16067-
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
16068-
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
16069-
1607016058
strict-uri-encode@^1.0.0:
1607116059
version "1.1.0"
1607216060
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"

0 commit comments

Comments
 (0)