@jskit-ai/uploads-runtime 0.1.1 → 0.1.2
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.descriptor.mjs +2 -6
- package/package.json +2 -3
- package/src/server/multipart/readSingleMultipartFile.js +92 -5
- package/src/server/providers/UploadsRuntimeServiceProvider.js +0 -2
- package/test/exportsContract.test.js +0 -1
- package/test/readSingleMultipartFile.test.js +129 -0
- package/src/server/multipart/readMultipartFiles.js +0 -111
- package/test/readMultipartFiles.test.js +0 -80
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/uploads-runtime",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.2",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Reusable upload runtime primitives for multipart parsing, policy validation, and blob storage.",
|
|
7
7
|
dependsOn: [
|
|
@@ -38,10 +38,6 @@ export default Object.freeze({
|
|
|
38
38
|
subpath: "./server/multipart/registerMultipartSupport",
|
|
39
39
|
summary: "Exports Fastify multipart registration helper."
|
|
40
40
|
},
|
|
41
|
-
{
|
|
42
|
-
subpath: "./server/multipart/readMultipartFiles",
|
|
43
|
-
summary: "Exports normalized multipart file parsing helpers."
|
|
44
|
-
},
|
|
45
41
|
{
|
|
46
42
|
subpath: "./server/multipart/readSingleMultipartFile",
|
|
47
43
|
summary: "Exports a convenience helper for single-file multipart uploads."
|
|
@@ -75,7 +71,7 @@ export default Object.freeze({
|
|
|
75
71
|
dependencies: {
|
|
76
72
|
runtime: {
|
|
77
73
|
"@fastify/multipart": "^9.4.0",
|
|
78
|
-
"@jskit-ai/kernel": "0.1.
|
|
74
|
+
"@jskit-ai/kernel": "0.1.24"
|
|
79
75
|
},
|
|
80
76
|
dev: {}
|
|
81
77
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/uploads-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
"./shared": "./src/shared/index.js",
|
|
11
11
|
"./server/providers/UploadsRuntimeServiceProvider": "./src/server/providers/UploadsRuntimeServiceProvider.js",
|
|
12
12
|
"./server/multipart/registerMultipartSupport": "./src/server/multipart/registerMultipartSupport.js",
|
|
13
|
-
"./server/multipart/readMultipartFiles": "./src/server/multipart/readMultipartFiles.js",
|
|
14
13
|
"./server/multipart/readSingleMultipartFile": "./src/server/multipart/readSingleMultipartFile.js",
|
|
15
14
|
"./server/policy/uploadPolicy": "./src/server/policy/uploadPolicy.js",
|
|
16
15
|
"./server/storage/createUploadStorageService": "./src/server/storage/createUploadStorageService.js"
|
|
17
16
|
},
|
|
18
17
|
"dependencies": {
|
|
19
18
|
"@fastify/multipart": "^9.4.0",
|
|
20
|
-
"@jskit-ai/kernel": "0.1.
|
|
19
|
+
"@jskit-ai/kernel": "0.1.24"
|
|
21
20
|
}
|
|
22
21
|
}
|
|
@@ -1,11 +1,98 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createUploadFieldError } from "../policy/uploadPolicy.js";
|
|
2
|
+
|
|
3
|
+
function normalizeFieldName(value) {
|
|
4
|
+
return String(value || "").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolveFieldName(options = {}) {
|
|
8
|
+
const normalizedFieldName = normalizeFieldName(options.fieldName);
|
|
9
|
+
if (normalizedFieldName) {
|
|
10
|
+
return normalizedFieldName;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const fieldNames = Array.isArray(options.fieldNames) ? options.fieldNames : [];
|
|
14
|
+
if (fieldNames.length !== 1) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return normalizeFieldName(fieldNames[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveMaxBytes(value) {
|
|
22
|
+
const normalized = Number(value);
|
|
23
|
+
return Number.isInteger(normalized) && normalized > 0 ? normalized : 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatMaxSizeMb(maxBytes) {
|
|
27
|
+
return Math.floor(maxBytes / (1024 * 1024));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isMultipartError(error, code) {
|
|
31
|
+
return String(error?.code || "").trim().toUpperCase() === String(code || "").trim().toUpperCase();
|
|
32
|
+
}
|
|
2
33
|
|
|
3
34
|
async function readSingleMultipartFile(request, options = {}) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
35
|
+
if (!request || typeof request.file !== "function") {
|
|
36
|
+
throw new TypeError("readSingleMultipartFile requires a request with file().");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizedFieldName = resolveFieldName(options);
|
|
40
|
+
const normalizedFieldErrorKey =
|
|
41
|
+
normalizeFieldName(options.fieldErrorKey) || normalizedFieldName || "file";
|
|
42
|
+
const normalizedLabel = normalizeFieldName(options.label) || "File";
|
|
43
|
+
const normalizedMaxBytes = resolveMaxBytes(options.maxBytes);
|
|
44
|
+
|
|
45
|
+
let filePart = null;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
filePart = await request.file({
|
|
49
|
+
throwFileSizeLimit: true,
|
|
50
|
+
limits: {
|
|
51
|
+
files: 1,
|
|
52
|
+
...(normalizedMaxBytes > 0 ? { fileSize: normalizedMaxBytes } : {})
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isMultipartError(error, "FST_REQ_FILE_TOO_LARGE") && normalizedMaxBytes > 0) {
|
|
57
|
+
throw createUploadFieldError(
|
|
58
|
+
normalizedFieldErrorKey,
|
|
59
|
+
`${normalizedLabel} file is too large. Maximum allowed size is ${formatMaxSizeMb(normalizedMaxBytes)}MB.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isMultipartError(error, "FST_FILES_LIMIT")) {
|
|
64
|
+
throw createUploadFieldError(normalizedFieldErrorKey, `${normalizedLabel} upload allows only one file.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!filePart) {
|
|
71
|
+
if (options.required === true) {
|
|
72
|
+
throw createUploadFieldError(normalizedFieldErrorKey, `${normalizedLabel} file is required.`);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const actualFieldName = normalizeFieldName(filePart.fieldname);
|
|
78
|
+
if (normalizedFieldName && actualFieldName && actualFieldName !== normalizedFieldName) {
|
|
79
|
+
if (filePart.file && typeof filePart.file.resume === "function") {
|
|
80
|
+
filePart.file.resume();
|
|
81
|
+
}
|
|
82
|
+
throw createUploadFieldError(
|
|
83
|
+
normalizedFieldErrorKey,
|
|
84
|
+
`${normalizedLabel} field "${actualFieldName}" is not allowed.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
fieldName: actualFieldName,
|
|
90
|
+
fileName: normalizeFieldName(filePart.filename),
|
|
91
|
+
mimeType: normalizeFieldName(filePart.mimetype).toLowerCase(),
|
|
92
|
+
encoding: normalizeFieldName(filePart.encoding).toLowerCase(),
|
|
93
|
+
stream: filePart.file,
|
|
94
|
+
fields: filePart.fields && typeof filePart.fields === "object" ? filePart.fields : {}
|
|
7
95
|
});
|
|
8
|
-
return result.files[0] || null;
|
|
9
96
|
}
|
|
10
97
|
|
|
11
98
|
export { readSingleMultipartFile };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { registerMultipartSupport } from "../multipart/registerMultipartSupport.js";
|
|
2
|
-
import { readMultipartFiles } from "../multipart/readMultipartFiles.js";
|
|
3
2
|
import { readSingleMultipartFile } from "../multipart/readSingleMultipartFile.js";
|
|
4
3
|
import {
|
|
5
4
|
createUploadFieldError,
|
|
@@ -14,7 +13,6 @@ import {
|
|
|
14
13
|
|
|
15
14
|
const UPLOADS_RUNTIME_SERVER_API = Object.freeze({
|
|
16
15
|
registerMultipartSupport,
|
|
17
|
-
readMultipartFiles,
|
|
18
16
|
readSingleMultipartFile,
|
|
19
17
|
createUploadFieldError,
|
|
20
18
|
readUploadBuffer,
|
|
@@ -18,7 +18,6 @@ test("uploads-runtime exports are explicit and aligned with usage", () => {
|
|
|
18
18
|
"./shared",
|
|
19
19
|
"./server/providers/UploadsRuntimeServiceProvider",
|
|
20
20
|
"./server/multipart/registerMultipartSupport",
|
|
21
|
-
"./server/multipart/readMultipartFiles",
|
|
22
21
|
"./server/multipart/readSingleMultipartFile",
|
|
23
22
|
"./server/policy/uploadPolicy",
|
|
24
23
|
"./server/storage/createUploadStorageService"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { readSingleMultipartFile } from "../src/server/multipart/readSingleMultipartFile.js";
|
|
5
|
+
|
|
6
|
+
test("readSingleMultipartFile normalizes file parts and forwards size limits", async () => {
|
|
7
|
+
let receivedOptions = null;
|
|
8
|
+
const request = {
|
|
9
|
+
async file(options) {
|
|
10
|
+
receivedOptions = options;
|
|
11
|
+
return {
|
|
12
|
+
fieldname: "avatar",
|
|
13
|
+
filename: " face.png ",
|
|
14
|
+
mimetype: " IMAGE/PNG ",
|
|
15
|
+
encoding: " 7BIT ",
|
|
16
|
+
file: Readable.from([Buffer.from("png")]),
|
|
17
|
+
fields: {
|
|
18
|
+
uploadDimension: {
|
|
19
|
+
value: "256"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const filePart = await readSingleMultipartFile(request, {
|
|
27
|
+
fieldName: "avatar",
|
|
28
|
+
required: true,
|
|
29
|
+
label: "Avatar",
|
|
30
|
+
maxBytes: 5 * 1024 * 1024
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(receivedOptions, {
|
|
34
|
+
throwFileSizeLimit: true,
|
|
35
|
+
limits: {
|
|
36
|
+
files: 1,
|
|
37
|
+
fileSize: 5 * 1024 * 1024
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
assert.equal(filePart?.fieldName, "avatar");
|
|
41
|
+
assert.equal(filePart?.fileName, "face.png");
|
|
42
|
+
assert.equal(filePart?.mimeType, "image/png");
|
|
43
|
+
assert.equal(filePart?.encoding, "7bit");
|
|
44
|
+
assert.equal(filePart?.fields?.uploadDimension?.value, "256");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("readSingleMultipartFile rejects missing required files", async () => {
|
|
48
|
+
const request = {
|
|
49
|
+
async file() {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await assert.rejects(
|
|
55
|
+
() =>
|
|
56
|
+
readSingleMultipartFile(request, {
|
|
57
|
+
fieldName: "avatar",
|
|
58
|
+
required: true,
|
|
59
|
+
fieldErrorKey: "avatar",
|
|
60
|
+
label: "Avatar"
|
|
61
|
+
}),
|
|
62
|
+
(error) => {
|
|
63
|
+
assert.equal(error?.message, "Validation failed.");
|
|
64
|
+
assert.equal(error?.details?.fieldErrors?.avatar, "Avatar file is required.");
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("readSingleMultipartFile maps multipart size errors to validation errors", async () => {
|
|
71
|
+
const request = {
|
|
72
|
+
async file() {
|
|
73
|
+
const error = new Error("request file too large");
|
|
74
|
+
error.code = "FST_REQ_FILE_TOO_LARGE";
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
() =>
|
|
81
|
+
readSingleMultipartFile(request, {
|
|
82
|
+
fieldName: "avatar",
|
|
83
|
+
fieldErrorKey: "avatar",
|
|
84
|
+
label: "Avatar",
|
|
85
|
+
maxBytes: 5 * 1024 * 1024
|
|
86
|
+
}),
|
|
87
|
+
(error) => {
|
|
88
|
+
assert.equal(error?.message, "Validation failed.");
|
|
89
|
+
assert.equal(error?.details?.fieldErrors?.avatar, "Avatar file is too large. Maximum allowed size is 5MB.");
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("readSingleMultipartFile rejects unexpected field names", async () => {
|
|
96
|
+
let resumeCount = 0;
|
|
97
|
+
const request = {
|
|
98
|
+
async file() {
|
|
99
|
+
return {
|
|
100
|
+
fieldname: "document",
|
|
101
|
+
filename: "face.png",
|
|
102
|
+
mimetype: "image/png",
|
|
103
|
+
encoding: "7bit",
|
|
104
|
+
file: {
|
|
105
|
+
resume() {
|
|
106
|
+
resumeCount += 1;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
fields: {}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
await assert.rejects(
|
|
115
|
+
() =>
|
|
116
|
+
readSingleMultipartFile(request, {
|
|
117
|
+
fieldName: "avatar",
|
|
118
|
+
fieldErrorKey: "avatar",
|
|
119
|
+
label: "Avatar"
|
|
120
|
+
}),
|
|
121
|
+
(error) => {
|
|
122
|
+
assert.equal(error?.message, "Validation failed.");
|
|
123
|
+
assert.equal(error?.details?.fieldErrors?.avatar, 'Avatar field "document" is not allowed.');
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assert.equal(resumeCount, 1);
|
|
129
|
+
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { createUploadFieldError } from "../policy/uploadPolicy.js";
|
|
2
|
-
|
|
3
|
-
function normalizeFieldName(value) {
|
|
4
|
-
return String(value || "").trim();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function appendField(fields, fieldName, value) {
|
|
8
|
-
const normalizedFieldName = normalizeFieldName(fieldName);
|
|
9
|
-
if (!normalizedFieldName) {
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const normalizedValue = value == null ? "" : String(value);
|
|
14
|
-
if (!Object.hasOwn(fields, normalizedFieldName)) {
|
|
15
|
-
fields[normalizedFieldName] = {
|
|
16
|
-
value: normalizedValue,
|
|
17
|
-
values: [normalizedValue]
|
|
18
|
-
};
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
fields[normalizedFieldName].values.push(normalizedValue);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function toAllowedFieldSet(fieldNames = []) {
|
|
26
|
-
const normalizedFieldNames = Array.isArray(fieldNames)
|
|
27
|
-
? fieldNames.map(normalizeFieldName).filter(Boolean)
|
|
28
|
-
: [];
|
|
29
|
-
return normalizedFieldNames.length > 0 ? new Set(normalizedFieldNames) : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function readMultipartFiles(
|
|
33
|
-
request,
|
|
34
|
-
{
|
|
35
|
-
fieldNames = [],
|
|
36
|
-
maxFiles = Number.POSITIVE_INFINITY,
|
|
37
|
-
required = false,
|
|
38
|
-
fieldErrorKey = "",
|
|
39
|
-
label = "File"
|
|
40
|
-
} = {}
|
|
41
|
-
) {
|
|
42
|
-
if (!request || typeof request.parts !== "function") {
|
|
43
|
-
throw new TypeError("readMultipartFiles requires a request with parts().");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const allowedFieldNames = toAllowedFieldSet(fieldNames);
|
|
47
|
-
const normalizedFieldErrorKey =
|
|
48
|
-
normalizeFieldName(fieldErrorKey) ||
|
|
49
|
-
(allowedFieldNames && allowedFieldNames.size === 1 ? [...allowedFieldNames][0] : "file");
|
|
50
|
-
const normalizedMaxFiles =
|
|
51
|
-
Number.isInteger(maxFiles) && maxFiles > 0 ? maxFiles : Number.POSITIVE_INFINITY;
|
|
52
|
-
|
|
53
|
-
const fields = {};
|
|
54
|
-
const files = [];
|
|
55
|
-
|
|
56
|
-
for await (const part of request.parts()) {
|
|
57
|
-
if (part?.type === "field") {
|
|
58
|
-
appendField(fields, part.fieldname, part.value);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (part?.type !== "file") {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const fieldName = normalizeFieldName(part.fieldname);
|
|
67
|
-
if (allowedFieldNames && !allowedFieldNames.has(fieldName)) {
|
|
68
|
-
if (part.file && typeof part.file.resume === "function") {
|
|
69
|
-
part.file.resume();
|
|
70
|
-
}
|
|
71
|
-
throw createUploadFieldError(
|
|
72
|
-
normalizedFieldErrorKey,
|
|
73
|
-
`${label} field "${fieldName || "unknown"}" is not allowed.`
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (files.length >= normalizedMaxFiles) {
|
|
78
|
-
if (part.file && typeof part.file.resume === "function") {
|
|
79
|
-
part.file.resume();
|
|
80
|
-
}
|
|
81
|
-
throw createUploadFieldError(
|
|
82
|
-
normalizedFieldErrorKey,
|
|
83
|
-
normalizedMaxFiles === 1
|
|
84
|
-
? `${label} upload allows only one file.`
|
|
85
|
-
: `${label} upload allows at most ${normalizedMaxFiles} files.`
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
files.push(
|
|
90
|
-
Object.freeze({
|
|
91
|
-
fieldName,
|
|
92
|
-
fileName: String(part.filename || "").trim(),
|
|
93
|
-
mimeType: String(part.mimetype || "").trim().toLowerCase(),
|
|
94
|
-
encoding: String(part.encoding || "").trim().toLowerCase(),
|
|
95
|
-
stream: part.file,
|
|
96
|
-
fields
|
|
97
|
-
})
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (required && files.length === 0) {
|
|
102
|
-
throw createUploadFieldError(normalizedFieldErrorKey, `${label} file is required.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return Object.freeze({
|
|
106
|
-
fields: Object.freeze(fields),
|
|
107
|
-
files: Object.freeze(files)
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export { readMultipartFiles };
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { Readable } from "node:stream";
|
|
4
|
-
import { readMultipartFiles } from "../src/server/multipart/readMultipartFiles.js";
|
|
5
|
-
import { readSingleMultipartFile } from "../src/server/multipart/readSingleMultipartFile.js";
|
|
6
|
-
|
|
7
|
-
function createRequestStub(parts) {
|
|
8
|
-
return {
|
|
9
|
-
async *parts() {
|
|
10
|
-
for (const part of parts) {
|
|
11
|
-
yield part;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
test("readMultipartFiles normalizes fields and files", async () => {
|
|
18
|
-
const request = createRequestStub([
|
|
19
|
-
{ type: "field", fieldname: "uploadDimension", value: "256" },
|
|
20
|
-
{
|
|
21
|
-
type: "file",
|
|
22
|
-
fieldname: "avatar",
|
|
23
|
-
filename: "face.png",
|
|
24
|
-
mimetype: "image/png",
|
|
25
|
-
encoding: "7bit",
|
|
26
|
-
file: Readable.from([Buffer.from("png")])
|
|
27
|
-
}
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const result = await readMultipartFiles(request, {
|
|
31
|
-
fieldNames: ["avatar"],
|
|
32
|
-
required: true,
|
|
33
|
-
label: "Avatar"
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
assert.equal(result.files.length, 1);
|
|
37
|
-
assert.equal(result.files[0].fieldName, "avatar");
|
|
38
|
-
assert.equal(result.files[0].fileName, "face.png");
|
|
39
|
-
assert.equal(result.files[0].mimeType, "image/png");
|
|
40
|
-
assert.equal(result.fields.uploadDimension.value, "256");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("readSingleMultipartFile returns the first file and enforces maxFiles", async () => {
|
|
44
|
-
const request = createRequestStub([
|
|
45
|
-
{
|
|
46
|
-
type: "file",
|
|
47
|
-
fieldname: "avatar",
|
|
48
|
-
filename: "face.png",
|
|
49
|
-
mimetype: "image/png",
|
|
50
|
-
encoding: "7bit",
|
|
51
|
-
file: Readable.from([Buffer.from("png")])
|
|
52
|
-
}
|
|
53
|
-
]);
|
|
54
|
-
|
|
55
|
-
const file = await readSingleMultipartFile(request, {
|
|
56
|
-
fieldNames: ["avatar"],
|
|
57
|
-
required: true,
|
|
58
|
-
label: "Avatar"
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
assert.equal(file?.fileName, "face.png");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("readMultipartFiles rejects missing required files", async () => {
|
|
65
|
-
const request = createRequestStub([]);
|
|
66
|
-
|
|
67
|
-
await assert.rejects(
|
|
68
|
-
() =>
|
|
69
|
-
readMultipartFiles(request, {
|
|
70
|
-
fieldNames: ["avatar"],
|
|
71
|
-
required: true,
|
|
72
|
-
label: "Avatar"
|
|
73
|
-
}),
|
|
74
|
-
(error) => {
|
|
75
|
-
assert.equal(error?.message, "Validation failed.");
|
|
76
|
-
assert.equal(error?.details?.fieldErrors?.avatar, "Avatar file is required.");
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
});
|