bxo 0.0.5-dev.53 → 0.0.5-dev.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/utils/index.ts +160 -27
- package/tests/integration/bxo.test.ts +75 -0
- package/tests/unit/utils.test.ts +172 -51
package/package.json
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -78,6 +78,116 @@ export function validateResponse(
|
|
|
78
78
|
return data;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Helper function to parse form keys with nested object and array notation (Axios-compatible)
|
|
82
|
+
function parseFormKey(key: string): {
|
|
83
|
+
baseKey: string;
|
|
84
|
+
path: string[];
|
|
85
|
+
isArray: boolean;
|
|
86
|
+
isJson: boolean;
|
|
87
|
+
hasIndexes: boolean;
|
|
88
|
+
} {
|
|
89
|
+
// Check for special endings like "{}" for JSON serialization FIRST
|
|
90
|
+
if (key.endsWith('{}')) {
|
|
91
|
+
const actualBaseKey = key.slice(0, -2);
|
|
92
|
+
return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const bracketMatch = key.match(/^([^\[]+)(\[.*\])*$/);
|
|
96
|
+
if (!bracketMatch) {
|
|
97
|
+
return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const baseKey = bracketMatch[1];
|
|
101
|
+
if (!baseKey) {
|
|
102
|
+
return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const bracketPart = key.slice(baseKey.length);
|
|
106
|
+
|
|
107
|
+
if (!bracketPart) {
|
|
108
|
+
return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for special endings like "{}" for JSON serialization
|
|
112
|
+
if (baseKey.endsWith('{}')) {
|
|
113
|
+
const actualBaseKey = baseKey.slice(0, -2);
|
|
114
|
+
return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If no bracket part, return simple key
|
|
118
|
+
if (!bracketPart) {
|
|
119
|
+
return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if this is an array notation (e.g., "recordIds[]")
|
|
123
|
+
if (bracketPart === '[]') {
|
|
124
|
+
return { baseKey, path: [], isArray: true, isJson: false, hasIndexes: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract all bracket contents
|
|
128
|
+
const path: string[] = [];
|
|
129
|
+
const bracketRegex = /\[([^\]]*)\]/g;
|
|
130
|
+
let match;
|
|
131
|
+
let hasIndexes = false;
|
|
132
|
+
|
|
133
|
+
while ((match = bracketRegex.exec(bracketPart)) !== null) {
|
|
134
|
+
if (match[1] !== undefined) {
|
|
135
|
+
path.push(match[1]);
|
|
136
|
+
// Check if this is a numeric index
|
|
137
|
+
if (/^\d+$/.test(match[1])) {
|
|
138
|
+
hasIndexes = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if the last path element is empty (indicating array without indexes)
|
|
144
|
+
const isArray = path.length > 0 && path[path.length - 1] === '';
|
|
145
|
+
|
|
146
|
+
return { baseKey, path, isArray, isJson: false, hasIndexes };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Helper function to set nested value in object (Axios-compatible)
|
|
150
|
+
function setNestedValue(obj: any, baseKey: string, path: string[], value: any, isArray: boolean = false): void {
|
|
151
|
+
if (!(baseKey in obj)) {
|
|
152
|
+
obj[baseKey] = isArray ? [] : {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let current = obj[baseKey];
|
|
156
|
+
|
|
157
|
+
// Navigate to the parent of the target location
|
|
158
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
159
|
+
const key = path[i];
|
|
160
|
+
if (key && (!(key in current) || typeof current[key] !== 'object')) {
|
|
161
|
+
// Check if next key is numeric (array index)
|
|
162
|
+
const nextKey = path[i + 1];
|
|
163
|
+
const isNextKeyNumeric = nextKey && /^\d+$/.test(nextKey);
|
|
164
|
+
current[key] = isNextKeyNumeric ? [] : {};
|
|
165
|
+
}
|
|
166
|
+
if (key) {
|
|
167
|
+
current = current[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set the final value
|
|
172
|
+
const lastKey = path[path.length - 1];
|
|
173
|
+
if (lastKey) {
|
|
174
|
+
if (/^\d+$/.test(lastKey)) {
|
|
175
|
+
// Numeric key - treat as array index
|
|
176
|
+
const index = parseInt(lastKey, 10);
|
|
177
|
+
if (Array.isArray(current)) {
|
|
178
|
+
current[index] = value;
|
|
179
|
+
} else {
|
|
180
|
+
// Convert to array if needed
|
|
181
|
+
const newArray = [];
|
|
182
|
+
newArray[index] = value;
|
|
183
|
+
current[lastKey] = newArray;
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
current[lastKey] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
81
191
|
// Parse request body based on content type
|
|
82
192
|
export async function parseRequestBody(request: Request): Promise<any> {
|
|
83
193
|
const contentType = request.headers.get('content-type');
|
|
@@ -90,25 +200,56 @@ export async function parseRequestBody(request: Request): Promise<any> {
|
|
|
90
200
|
}
|
|
91
201
|
} else if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
|
|
92
202
|
const formData = await request.formData();
|
|
93
|
-
// Convert FormData to a structured object
|
|
203
|
+
// Convert FormData to a structured object
|
|
94
204
|
const formBody: Record<string, any> = {};
|
|
95
205
|
|
|
96
206
|
for (const [key, value] of formData.entries()) {
|
|
97
207
|
if (value instanceof File) {
|
|
98
|
-
//
|
|
99
|
-
formBody[key] = {
|
|
100
|
-
type: 'file',
|
|
101
|
-
name: value.name,
|
|
102
|
-
size: value.size,
|
|
103
|
-
lastModified: value.lastModified,
|
|
104
|
-
file: value, // Keep the actual File object for access
|
|
105
|
-
// Add convenience properties
|
|
106
|
-
filename: value.name,
|
|
107
|
-
mimetype: value.type || 'application/octet-stream'
|
|
108
|
-
};
|
|
109
|
-
} else {
|
|
110
|
-
// Handle regular form fields
|
|
208
|
+
// Return File instances directly
|
|
111
209
|
formBody[key] = value;
|
|
210
|
+
} else {
|
|
211
|
+
// Parse the key to handle nested objects and arrays (Axios-compatible)
|
|
212
|
+
const parsedKey = parseFormKey(key);
|
|
213
|
+
|
|
214
|
+
if (parsedKey.isJson) {
|
|
215
|
+
// Handle JSON serialization (e.g., "obj{}")
|
|
216
|
+
if (typeof value === 'string') {
|
|
217
|
+
try {
|
|
218
|
+
formBody[parsedKey.baseKey] = JSON.parse(value);
|
|
219
|
+
} catch {
|
|
220
|
+
formBody[parsedKey.baseKey] = value;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
formBody[parsedKey.baseKey] = value;
|
|
224
|
+
}
|
|
225
|
+
} else if (parsedKey.isArray) {
|
|
226
|
+
// Handle array notation like "recordIds[]"
|
|
227
|
+
if (parsedKey.baseKey in formBody) {
|
|
228
|
+
if (Array.isArray(formBody[parsedKey.baseKey])) {
|
|
229
|
+
formBody[parsedKey.baseKey].push(value);
|
|
230
|
+
} else {
|
|
231
|
+
formBody[parsedKey.baseKey] = [formBody[parsedKey.baseKey], value];
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
formBody[parsedKey.baseKey] = [value];
|
|
235
|
+
}
|
|
236
|
+
} else if (parsedKey.path.length > 0) {
|
|
237
|
+
// Handle nested object notation like "test[new]", "test[hi][hi]", "arr[0]", "users[0][name]"
|
|
238
|
+
setNestedValue(formBody, parsedKey.baseKey, parsedKey.path, value, parsedKey.hasIndexes);
|
|
239
|
+
} else {
|
|
240
|
+
// Handle regular form fields - check if this key already exists
|
|
241
|
+
if (key in formBody) {
|
|
242
|
+
// If key already exists, convert to array or append to existing array
|
|
243
|
+
if (Array.isArray(formBody[key])) {
|
|
244
|
+
formBody[key].push(value);
|
|
245
|
+
} else {
|
|
246
|
+
formBody[key] = [formBody[key], value];
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// First occurrence of this key
|
|
250
|
+
formBody[key] = value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
112
253
|
}
|
|
113
254
|
}
|
|
114
255
|
|
|
@@ -186,21 +327,13 @@ export function createRedirectResponse(
|
|
|
186
327
|
}
|
|
187
328
|
|
|
188
329
|
// Check if a value is a file upload
|
|
189
|
-
export function isFileUpload(value: any): value is {
|
|
190
|
-
|
|
191
|
-
file: File;
|
|
192
|
-
name: string;
|
|
193
|
-
size: number;
|
|
194
|
-
lastModified: number;
|
|
195
|
-
filename: string;
|
|
196
|
-
mimetype: string;
|
|
197
|
-
} {
|
|
198
|
-
return value && typeof value === 'object' && value.type === 'file' && value.file instanceof File;
|
|
330
|
+
export function isFileUpload(value: any): value is File {
|
|
331
|
+
return value instanceof File;
|
|
199
332
|
}
|
|
200
333
|
|
|
201
334
|
// Extract File object from upload value
|
|
202
335
|
export function getFileFromUpload(value: any): File | null {
|
|
203
|
-
return isFileUpload(value) ? value
|
|
336
|
+
return isFileUpload(value) ? value : null;
|
|
204
337
|
}
|
|
205
338
|
|
|
206
339
|
// Get file metadata without the File object
|
|
@@ -209,7 +342,7 @@ export function getFileInfo(value: any): { name: string; size: number; mimetype:
|
|
|
209
342
|
return {
|
|
210
343
|
name: value.name,
|
|
211
344
|
size: value.size,
|
|
212
|
-
mimetype: value.
|
|
345
|
+
mimetype: value.type || 'application/octet-stream',
|
|
213
346
|
lastModified: value.lastModified
|
|
214
347
|
};
|
|
215
348
|
}
|
|
@@ -240,7 +373,7 @@ export function getFileUploads(formData: Record<string, any>): Record<string, Fi
|
|
|
240
373
|
const files: Record<string, File> = {};
|
|
241
374
|
for (const [key, value] of Object.entries(formData)) {
|
|
242
375
|
if (isFileUpload(value)) {
|
|
243
|
-
files[key] = value
|
|
376
|
+
files[key] = value;
|
|
244
377
|
}
|
|
245
378
|
}
|
|
246
379
|
return files;
|
|
@@ -196,6 +196,81 @@ describe('BXO Framework Integration', () => {
|
|
|
196
196
|
expect(data.formData.email).toBe('john@example.com');
|
|
197
197
|
expect(data.message).toBe('Form submitted');
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
it('should handle form data with arrays', async () => {
|
|
201
|
+
app.post('/api/records', (ctx) => {
|
|
202
|
+
return { formData: ctx.body, message: 'Records submitted' };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const formData = new FormData();
|
|
206
|
+
formData.append('app', 'zodula');
|
|
207
|
+
formData.append('model', 'zodula_User');
|
|
208
|
+
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
209
|
+
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
210
|
+
formData.append('fields', '*');
|
|
211
|
+
|
|
212
|
+
const response = await fetch(`${baseUrl}/api/records`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: formData
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const data = await response.json() as {
|
|
218
|
+
formData: {
|
|
219
|
+
app: string,
|
|
220
|
+
model: string,
|
|
221
|
+
recordIds: string[],
|
|
222
|
+
fields: string
|
|
223
|
+
},
|
|
224
|
+
message: string
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
expect(response.status).toBe(200);
|
|
228
|
+
expect(data.formData.app).toBe('zodula');
|
|
229
|
+
expect(data.formData.model).toBe('zodula_User');
|
|
230
|
+
expect(Array.isArray(data.formData.recordIds)).toBe(true);
|
|
231
|
+
expect(data.formData.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
232
|
+
expect(data.formData.fields).toBe('*');
|
|
233
|
+
expect(data.message).toBe('Records submitted');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle form data with nested objects', async () => {
|
|
237
|
+
app.post('/api/nested', (ctx) => {
|
|
238
|
+
return { formData: ctx.body, message: 'Nested data submitted' };
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const formData = new FormData();
|
|
242
|
+
formData.append('test[test]', 'test');
|
|
243
|
+
formData.append('test[new]', 'new');
|
|
244
|
+
formData.append('test[hi][hi]', 'hi');
|
|
245
|
+
|
|
246
|
+
const response = await fetch(`${baseUrl}/api/nested`, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: formData
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const data = await response.json() as {
|
|
252
|
+
formData: {
|
|
253
|
+
test: {
|
|
254
|
+
test: string,
|
|
255
|
+
new: string,
|
|
256
|
+
hi: {
|
|
257
|
+
hi: string
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
message: string
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
expect(response.status).toBe(200);
|
|
265
|
+
expect(data.formData.test).toEqual({
|
|
266
|
+
test: 'test',
|
|
267
|
+
new: 'new',
|
|
268
|
+
hi: {
|
|
269
|
+
hi: 'hi'
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
expect(data.message).toBe('Nested data submitted');
|
|
273
|
+
});
|
|
199
274
|
});
|
|
200
275
|
|
|
201
276
|
describe('Response Handling', () => {
|
package/tests/unit/utils.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parseCookies,
|
|
6
6
|
validateData,
|
|
7
7
|
validateResponse,
|
|
8
|
+
parseRequestBody,
|
|
8
9
|
cookiesToHeaders,
|
|
9
10
|
mergeHeadersWithCookies,
|
|
10
11
|
createRedirectResponse,
|
|
@@ -200,36 +201,178 @@ describe('Utility Functions', () => {
|
|
|
200
201
|
});
|
|
201
202
|
});
|
|
202
203
|
|
|
204
|
+
describe('parseRequestBody', () => {
|
|
205
|
+
it('should parse JSON body correctly', async () => {
|
|
206
|
+
const jsonData = { name: 'john', age: 25 };
|
|
207
|
+
const request = new Request('http://localhost/test', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(jsonData)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await parseRequestBody(request);
|
|
214
|
+
expect(result).toEqual(jsonData);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should parse form data with arrays correctly', async () => {
|
|
218
|
+
const formData = new FormData();
|
|
219
|
+
formData.append('app', 'zodula');
|
|
220
|
+
formData.append('model', 'zodula_User');
|
|
221
|
+
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
222
|
+
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
223
|
+
formData.append('fields', '*');
|
|
224
|
+
|
|
225
|
+
const request = new Request('http://localhost/test', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: formData
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = await parseRequestBody(request);
|
|
231
|
+
|
|
232
|
+
expect(result.app).toBe('zodula');
|
|
233
|
+
expect(result.model).toBe('zodula_User');
|
|
234
|
+
expect(Array.isArray(result.recordIds)).toBe(true);
|
|
235
|
+
expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
236
|
+
expect(result.fields).toBe('*');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle single form field correctly', async () => {
|
|
240
|
+
const formData = new FormData();
|
|
241
|
+
formData.append('name', 'john');
|
|
242
|
+
formData.append('email', 'john@example.com');
|
|
243
|
+
|
|
244
|
+
const request = new Request('http://localhost/test', {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
body: formData
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await parseRequestBody(request);
|
|
250
|
+
|
|
251
|
+
expect(result.name).toBe('john');
|
|
252
|
+
expect(result.email).toBe('john@example.com');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle file uploads in form data', async () => {
|
|
256
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
257
|
+
const formData = new FormData();
|
|
258
|
+
formData.append('name', 'john');
|
|
259
|
+
formData.append('file', file);
|
|
260
|
+
|
|
261
|
+
const request = new Request('http://localhost/test', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
body: formData
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const result = await parseRequestBody(request);
|
|
267
|
+
|
|
268
|
+
expect(result.name).toBe('john');
|
|
269
|
+
expect(result.file).toBeInstanceOf(File);
|
|
270
|
+
expect(result.file.name).toBe('test.txt');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should parse nested object form data correctly', async () => {
|
|
274
|
+
const formData = new FormData();
|
|
275
|
+
formData.append('test[test]', 'test');
|
|
276
|
+
formData.append('test[new]', 'new');
|
|
277
|
+
formData.append('test[hi][hi]', 'hi');
|
|
278
|
+
|
|
279
|
+
const request = new Request('http://localhost/test', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
body: formData
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await parseRequestBody(request);
|
|
285
|
+
|
|
286
|
+
expect(result.test).toEqual({
|
|
287
|
+
test: 'test',
|
|
288
|
+
new: 'new',
|
|
289
|
+
hi: {
|
|
290
|
+
hi: 'hi'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should parse Axios-compatible multipart data with arrays and objects', async () => {
|
|
296
|
+
const formData = new FormData();
|
|
297
|
+
formData.append('x', '1');
|
|
298
|
+
formData.append('arr[]', '1');
|
|
299
|
+
formData.append('arr[]', '2');
|
|
300
|
+
formData.append('arr[]', '3');
|
|
301
|
+
formData.append('arr2[0]', '1');
|
|
302
|
+
formData.append('arr2[1][0]', '2');
|
|
303
|
+
formData.append('arr2[2]', '3');
|
|
304
|
+
formData.append('users[0][name]', 'Peter');
|
|
305
|
+
formData.append('users[0][surname]', 'Griffin');
|
|
306
|
+
formData.append('users[1][name]', 'Thomas');
|
|
307
|
+
formData.append('users[1][surname]', 'Anderson');
|
|
308
|
+
formData.append('obj2{}', '[{"x":1}]');
|
|
309
|
+
|
|
310
|
+
const request = new Request('http://localhost/test', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
body: formData
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = await parseRequestBody(request);
|
|
316
|
+
|
|
317
|
+
expect(result.x).toBe('1');
|
|
318
|
+
expect(result.arr).toEqual(['1', '2', '3']);
|
|
319
|
+
expect(result.arr2).toEqual(['1', ['2'], '3']);
|
|
320
|
+
expect(result.users).toEqual([
|
|
321
|
+
{ name: 'Peter', surname: 'Griffin' },
|
|
322
|
+
{ name: 'Thomas', surname: 'Anderson' }
|
|
323
|
+
]);
|
|
324
|
+
expect(result.obj2).toEqual([{ x: 1 }]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should handle JSON serialization with special endings', async () => {
|
|
328
|
+
const formData = new FormData();
|
|
329
|
+
formData.append('myObj{}', '{"x": 1, "s": "foo"}');
|
|
330
|
+
formData.append('settings{}', '{"theme": "dark", "notifications": true}');
|
|
331
|
+
|
|
332
|
+
const request = new Request('http://localhost/test', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: formData
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = await parseRequestBody(request);
|
|
338
|
+
|
|
339
|
+
expect(result.myObj).toEqual({ x: 1, s: 'foo' });
|
|
340
|
+
expect(result.settings).toEqual({ theme: 'dark', notifications: true });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle mixed array indexing styles', async () => {
|
|
344
|
+
const formData = new FormData();
|
|
345
|
+
formData.append('tags[]', 'javascript');
|
|
346
|
+
formData.append('tags[]', 'typescript');
|
|
347
|
+
formData.append('scores[0]', '100');
|
|
348
|
+
formData.append('scores[1]', '95');
|
|
349
|
+
formData.append('scores[2]', '88');
|
|
350
|
+
|
|
351
|
+
const request = new Request('http://localhost/test', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: formData
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = await parseRequestBody(request);
|
|
357
|
+
|
|
358
|
+
expect(result.tags).toEqual(['javascript', 'typescript']);
|
|
359
|
+
expect(result.scores).toEqual(['100', '95', '88']);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
203
363
|
describe('File Upload Utilities', () => {
|
|
204
364
|
it('should identify file uploads correctly', () => {
|
|
205
|
-
const
|
|
206
|
-
type: 'file',
|
|
207
|
-
name: 'test.jpg',
|
|
208
|
-
size: 1024,
|
|
209
|
-
lastModified: 1234567890,
|
|
210
|
-
file: new File(['test'], 'test.jpg'),
|
|
211
|
-
filename: 'test.jpg',
|
|
212
|
-
mimetype: 'image/jpeg'
|
|
213
|
-
};
|
|
365
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
214
366
|
|
|
215
|
-
expect(isFileUpload(
|
|
367
|
+
expect(isFileUpload(file)).toBe(true);
|
|
216
368
|
expect(isFileUpload('not a file')).toBe(false);
|
|
217
369
|
expect(isFileUpload({ type: 'text' })).toBe(false);
|
|
218
370
|
});
|
|
219
371
|
|
|
220
372
|
it('should extract file from upload', () => {
|
|
221
|
-
const file = new File(['test'], 'test.jpg');
|
|
222
|
-
const fileUpload = {
|
|
223
|
-
type: 'file',
|
|
224
|
-
name: 'test.jpg',
|
|
225
|
-
size: 1024,
|
|
226
|
-
lastModified: 1234567890,
|
|
227
|
-
file,
|
|
228
|
-
filename: 'test.jpg',
|
|
229
|
-
mimetype: 'image/jpeg'
|
|
230
|
-
};
|
|
373
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
231
374
|
|
|
232
|
-
const result = getFileFromUpload(
|
|
375
|
+
const result = getFileFromUpload(file);
|
|
233
376
|
expect(result).toBe(file);
|
|
234
377
|
});
|
|
235
378
|
|
|
@@ -239,22 +382,14 @@ describe('Utility Functions', () => {
|
|
|
239
382
|
});
|
|
240
383
|
|
|
241
384
|
it('should get file info', () => {
|
|
242
|
-
const
|
|
243
|
-
type: 'file',
|
|
244
|
-
name: 'test.jpg',
|
|
245
|
-
size: 1024,
|
|
246
|
-
lastModified: 1234567890,
|
|
247
|
-
file: new File(['test'], 'test.jpg'),
|
|
248
|
-
filename: 'test.jpg',
|
|
249
|
-
mimetype: 'image/jpeg'
|
|
250
|
-
};
|
|
385
|
+
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
251
386
|
|
|
252
|
-
const result = getFileInfo(
|
|
387
|
+
const result = getFileInfo(file);
|
|
253
388
|
expect(result).toEqual({
|
|
254
389
|
name: 'test.jpg',
|
|
255
|
-
size:
|
|
390
|
+
size: 4,
|
|
256
391
|
mimetype: 'image/jpeg',
|
|
257
|
-
lastModified:
|
|
392
|
+
lastModified: expect.any(Number)
|
|
258
393
|
});
|
|
259
394
|
});
|
|
260
395
|
|
|
@@ -264,17 +399,10 @@ describe('Utility Functions', () => {
|
|
|
264
399
|
});
|
|
265
400
|
|
|
266
401
|
it('should get all file uploads from form data', () => {
|
|
402
|
+
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
267
403
|
const formData = {
|
|
268
404
|
name: 'john',
|
|
269
|
-
avatar:
|
|
270
|
-
type: 'file',
|
|
271
|
-
name: 'avatar.jpg',
|
|
272
|
-
size: 1024,
|
|
273
|
-
lastModified: 1234567890,
|
|
274
|
-
file: new File(['test'], 'avatar.jpg'),
|
|
275
|
-
filename: 'avatar.jpg',
|
|
276
|
-
mimetype: 'image/jpeg'
|
|
277
|
-
},
|
|
405
|
+
avatar: file,
|
|
278
406
|
email: 'john@example.com'
|
|
279
407
|
};
|
|
280
408
|
|
|
@@ -285,17 +413,10 @@ describe('Utility Functions', () => {
|
|
|
285
413
|
});
|
|
286
414
|
|
|
287
415
|
it('should get all non-file fields from form data', () => {
|
|
416
|
+
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
288
417
|
const formData = {
|
|
289
418
|
name: 'john',
|
|
290
|
-
avatar:
|
|
291
|
-
type: 'file',
|
|
292
|
-
name: 'avatar.jpg',
|
|
293
|
-
size: 1024,
|
|
294
|
-
lastModified: 1234567890,
|
|
295
|
-
file: new File(['test'], 'avatar.jpg'),
|
|
296
|
-
filename: 'avatar.jpg',
|
|
297
|
-
mimetype: 'image/jpeg'
|
|
298
|
-
},
|
|
419
|
+
avatar: file,
|
|
299
420
|
email: 'john@example.com'
|
|
300
421
|
};
|
|
301
422
|
|