main
js 149 lines 5.18 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 let webpackServerMap;
14 let busboy;
15 let ReactServerDOMServer;
16 let ReactServerDOMClient;
17
18 describe('ReactFlightDOMReplyNode', () => {
19 beforeEach(() => {
20 jest.resetModules();
21 // Simulate the condition resolution
22 jest.mock('react', () => require('react/react.react-server'));
23 jest.mock('react-server-dom-webpack/server', () =>
24 require('react-server-dom-webpack/server.node'),
25 );
26 const WebpackMock = require('./utils/WebpackMock');
27 webpackServerMap = WebpackMock.webpackServerMap;
28 ReactServerDOMServer = require('react-server-dom-webpack/server.node');
29 jest.resetModules();
30 ReactServerDOMClient = require('react-server-dom-webpack/client.node');
31
32 busboy = require('busboy');
33 });
34
35 // Writes the body to busboy as a multipart stream. Blob entries become
36 // `filename`-bearing parts so busboy emits them as 'file' events (with
37 // streamed data) rather than 'field' events.
38 async function pipeBodyToBusboy(bb, body, boundary) {
39 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
40 for (const [name, value] of body) {
41 if (typeof value === 'string') {
42 bb.write(
43 `--${boundary}\r\n` +
44 `Content-Disposition: form-data; name="${name}"\r\n` +
45 `\r\n` +
46 `${value}\r\n`,
47 );
48 } else {
49 const filename =
50 typeof value.name === 'string' && value.name !== ''
51 ? value.name
52 : 'blob';
53 const mimeType =
54 typeof value.type === 'string' && value.type !== ''
55 ? value.type
56 : 'application/octet-stream';
57 const buffer = Buffer.from(await value.arrayBuffer());
58 bb.write(
59 `--${boundary}\r\n` +
60 `Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n` +
61 `Content-Type: ${mimeType}\r\n` +
62 `\r\n`,
63 );
64 bb.write(buffer);
65 bb.write('\r\n');
66 }
67 }
68 bb.end(`--${boundary}--\r\n`);
69 }
70
71 // FormData iterates entries in insertion order per spec, so a referenced
72 // FormData must round-trip with its entry order intact even when files
73 // and text fields are interleaved in the payload.
74 it('preserves entry order when referenced FormDatas interleave files and text', async () => {
75 const a = new FormData();
76 a.append('text_a', 'value_a');
77 a.append('file_a', new Blob(['content_a'], {type: 'text/plain'}), 'a.txt');
78 const b = new FormData();
79 b.append('text_b', 'value_b');
80 b.append('file_b', new Blob(['content_b'], {type: 'text/plain'}), 'b.txt');
81
82 const body = await ReactServerDOMClient.encodeReply([a, b]);
83 const boundary = 'boundary';
84 const bb = busboy({
85 headers: {
86 'content-type': `multipart/form-data; boundary=${boundary}`,
87 },
88 });
89 const reply = ReactServerDOMServer.decodeReplyFromBusboy(
90 bb,
91 webpackServerMap,
92 );
93 await pipeBodyToBusboy(bb, body, boundary);
94
95 const result = await reply;
96 expect(result).toHaveLength(2);
97 const [decodedA, decodedB] = result;
98
99 const aEntries = Array.from(decodedA.entries());
100 expect(aEntries.map(([k]) => k)).toEqual(['text_a', 'file_a']);
101 expect(aEntries[0][1]).toBe('value_a');
102 expect(aEntries[1][1]).toBeInstanceOf(File);
103 expect(aEntries[1][1].name).toBe('a.txt');
104
105 const bEntries = Array.from(decodedB.entries());
106 expect(bEntries.map(([k]) => k)).toEqual(['text_b', 'file_b']);
107 expect(bEntries[0][1]).toBe('value_b');
108 expect(bEntries[1][1]).toBeInstanceOf(File);
109 expect(bEntries[1][1].name).toBe('b.txt');
110 });
111
112 // Every entry of a referenced FormData must be present in the decoded
113 // FormData regardless of where files appear in its iteration order.
114 it('does not drop entries when referenced FormDatas iterate files before text', async () => {
115 const a = new FormData();
116 a.append('file_a', new Blob(['content_a'], {type: 'text/plain'}), 'a.txt');
117 a.append('text_a', 'value_a');
118 const b = new FormData();
119 b.append('file_b', new Blob(['content_b'], {type: 'text/plain'}), 'b.txt');
120 b.append('text_b', 'value_b');
121
122 const body = await ReactServerDOMClient.encodeReply([a, b]);
123 const boundary = 'boundary';
124 const bb = busboy({
125 headers: {
126 'content-type': `multipart/form-data; boundary=${boundary}`,
127 },
128 });
129 const reply = ReactServerDOMServer.decodeReplyFromBusboy(
130 bb,
131 webpackServerMap,
132 );
133 await pipeBodyToBusboy(bb, body, boundary);
134
135 const result = await reply;
136 expect(result).toHaveLength(2);
137 const [decodedA, decodedB] = result;
138
139 const aKeys = Array.from(decodedA.keys()).sort();
140 expect(aKeys).toEqual(['file_a', 'text_a']);
141 expect(decodedA.get('text_a')).toBe('value_a');
142 expect(decodedA.get('file_a')).toBeInstanceOf(File);
143
144 const bKeys = Array.from(decodedB.keys()).sort();
145 expect(bKeys).toEqual(['file_b', 'text_b']);
146 expect(decodedB.get('text_b')).toBe('value_b');
147 expect(decodedB.get('file_b')).toBeInstanceOf(File);
148 });
149 });