@uploadista/data-store-s3 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/observability.d.ts +45 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +155 -0
- package/dist/s3-store-old.d.ts +51 -0
- package/dist/s3-store-old.d.ts.map +1 -0
- package/dist/s3-store-old.js +765 -0
- package/dist/s3-store.d.ts +9 -0
- package/dist/s3-store.d.ts.map +1 -0
- package/dist/s3-store.js +666 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts +44 -0
- package/dist/services/__mocks__/s3-client-mock.service.d.ts.map +1 -0
- package/dist/services/__mocks__/s3-client-mock.service.js +379 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/s3-client.service.d.ts +68 -0
- package/dist/services/s3-client.service.d.ts.map +1 -0
- package/dist/services/s3-client.service.js +209 -0
- package/dist/test-observability.d.ts +6 -0
- package/dist/test-observability.d.ts.map +1 -0
- package/dist/test-observability.js +62 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/calculations.d.ts +7 -0
- package/dist/utils/calculations.d.ts.map +1 -0
- package/dist/utils/calculations.js +41 -0
- package/dist/utils/error-handling.d.ts +7 -0
- package/dist/utils/error-handling.d.ts.map +1 -0
- package/dist/utils/error-handling.js +29 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/stream-adapter.d.ts +14 -0
- package/dist/utils/stream-adapter.d.ts.map +1 -0
- package/dist/utils/stream-adapter.js +41 -0
- package/package.json +36 -0
- package/src/__tests__/integration/s3-store.integration.test.ts +548 -0
- package/src/__tests__/multipart-logic.test.ts +395 -0
- package/src/__tests__/s3-store.edge-cases.test.ts +681 -0
- package/src/__tests__/s3-store.performance.test.ts +622 -0
- package/src/__tests__/s3-store.test.ts +662 -0
- package/src/__tests__/utils/performance-helpers.ts +459 -0
- package/src/__tests__/utils/test-data-generator.ts +331 -0
- package/src/__tests__/utils/test-setup.ts +256 -0
- package/src/index.ts +1 -0
- package/src/s3-store.ts +1059 -0
- package/src/services/__mocks__/s3-client-mock.service.ts +604 -0
- package/src/services/index.ts +1 -0
- package/src/services/s3-client.service.ts +359 -0
- package/src/types.ts +96 -0
- package/src/utils/calculations.ts +61 -0
- package/src/utils/error-handling.ts +52 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/stream-adapter.ts +50 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
calcOffsetFromParts,
|
|
4
|
+
calcOptimalPartSize,
|
|
5
|
+
getExpirationDate,
|
|
6
|
+
partKey,
|
|
7
|
+
shouldUseExpirationTags,
|
|
8
|
+
} from "../utils/calculations";
|
|
9
|
+
|
|
10
|
+
describe("Multipart Upload Logic", () => {
|
|
11
|
+
describe("calcOffsetFromParts", () => {
|
|
12
|
+
it("should calculate offset from empty parts list", () => {
|
|
13
|
+
expect(calcOffsetFromParts([])).toBe(0);
|
|
14
|
+
expect(calcOffsetFromParts(undefined)).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should calculate offset from single part", () => {
|
|
18
|
+
const parts = [{ Size: 1024 }];
|
|
19
|
+
expect(calcOffsetFromParts(parts)).toBe(1024);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should calculate offset from multiple parts", () => {
|
|
23
|
+
const parts = [{ Size: 1024 }, { Size: 2048 }, { Size: 512 }];
|
|
24
|
+
expect(calcOffsetFromParts(parts)).toBe(3584);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should handle parts with undefined sizes", () => {
|
|
28
|
+
const parts = [{ Size: 1024 }, { Size: undefined }, { Size: 512 }];
|
|
29
|
+
expect(calcOffsetFromParts(parts)).toBe(1536);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should handle parts with mixed sizes", () => {
|
|
33
|
+
const parts = [
|
|
34
|
+
{ Size: 5 * 1024 * 1024 }, // 5MB
|
|
35
|
+
{ Size: 8 * 1024 * 1024 }, // 8MB
|
|
36
|
+
{ Size: 3 * 1024 * 1024 }, // 3MB
|
|
37
|
+
];
|
|
38
|
+
expect(calcOffsetFromParts(parts)).toBe(16 * 1024 * 1024); // 16MB
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("calcOptimalPartSize", () => {
|
|
43
|
+
const minPartSize = 5 * 1024 * 1024; // 5MB
|
|
44
|
+
const preferredPartSize = 8 * 1024 * 1024; // 8MB
|
|
45
|
+
const maxMultipartParts = 10000;
|
|
46
|
+
|
|
47
|
+
it("should handle undefined file size", () => {
|
|
48
|
+
const partSize = calcOptimalPartSize(
|
|
49
|
+
undefined,
|
|
50
|
+
preferredPartSize,
|
|
51
|
+
minPartSize,
|
|
52
|
+
maxMultipartParts,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Should use a size that works with the maximum upload size
|
|
56
|
+
expect(partSize).toBeGreaterThanOrEqual(minPartSize);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should use file size for small files", () => {
|
|
60
|
+
const fileSize = 1 * 1024 * 1024; // 1MB
|
|
61
|
+
const partSize = calcOptimalPartSize(
|
|
62
|
+
fileSize,
|
|
63
|
+
preferredPartSize,
|
|
64
|
+
minPartSize,
|
|
65
|
+
maxMultipartParts,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// For small files, should align to reasonable boundary but stay small
|
|
69
|
+
expect(partSize).toBeLessThanOrEqual(preferredPartSize);
|
|
70
|
+
expect(partSize % 1024).toBe(0); // Should be aligned to 1KB boundaries
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should use preferred part size for medium files", () => {
|
|
74
|
+
const fileSize = 50 * 1024 * 1024; // 50MB
|
|
75
|
+
const partSize = calcOptimalPartSize(
|
|
76
|
+
fileSize,
|
|
77
|
+
preferredPartSize,
|
|
78
|
+
minPartSize,
|
|
79
|
+
maxMultipartParts,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(partSize).toBe(preferredPartSize);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should adjust part size for very large files", () => {
|
|
86
|
+
const fileSize = 100 * 1024 * 1024 * 1024; // 100GB
|
|
87
|
+
const partSize = calcOptimalPartSize(
|
|
88
|
+
fileSize,
|
|
89
|
+
preferredPartSize,
|
|
90
|
+
minPartSize,
|
|
91
|
+
maxMultipartParts,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Should be larger than preferred to stay within part limits
|
|
95
|
+
expect(partSize).toBeGreaterThan(preferredPartSize);
|
|
96
|
+
|
|
97
|
+
// Should not exceed part count limit
|
|
98
|
+
const estimatedParts = Math.ceil(fileSize / partSize);
|
|
99
|
+
expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should respect minimum part size for multipart uploads", () => {
|
|
103
|
+
const fileSize = 20 * 1024 * 1024; // 20MB
|
|
104
|
+
const smallPreferredSize = 1 * 1024 * 1024; // 1MB (below S3 minimum)
|
|
105
|
+
|
|
106
|
+
const partSize = calcOptimalPartSize(
|
|
107
|
+
fileSize,
|
|
108
|
+
smallPreferredSize,
|
|
109
|
+
minPartSize,
|
|
110
|
+
maxMultipartParts,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(partSize).toBeGreaterThanOrEqual(minPartSize);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle edge case at part limit boundary", () => {
|
|
117
|
+
// File size that would require exactly max parts with preferred size
|
|
118
|
+
const fileSize = preferredPartSize * maxMultipartParts;
|
|
119
|
+
|
|
120
|
+
const partSize = calcOptimalPartSize(
|
|
121
|
+
fileSize,
|
|
122
|
+
preferredPartSize,
|
|
123
|
+
minPartSize,
|
|
124
|
+
maxMultipartParts,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(partSize).toBe(preferredPartSize);
|
|
128
|
+
|
|
129
|
+
const estimatedParts = Math.ceil(fileSize / partSize);
|
|
130
|
+
expect(estimatedParts).toBe(maxMultipartParts);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should handle file size just over part limit boundary", () => {
|
|
134
|
+
// File size that would require one more than max parts with preferred size
|
|
135
|
+
const fileSize = preferredPartSize * maxMultipartParts + 1;
|
|
136
|
+
|
|
137
|
+
const partSize = calcOptimalPartSize(
|
|
138
|
+
fileSize,
|
|
139
|
+
preferredPartSize,
|
|
140
|
+
minPartSize,
|
|
141
|
+
maxMultipartParts,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(partSize).toBeGreaterThan(preferredPartSize);
|
|
145
|
+
|
|
146
|
+
const estimatedParts = Math.ceil(fileSize / partSize);
|
|
147
|
+
expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should align part sizes to 1KB boundaries", () => {
|
|
151
|
+
const fileSizes = [
|
|
152
|
+
13 * 1024 * 1024 + 500, // 13.5MB + some bytes
|
|
153
|
+
25 * 1024 * 1024 + 777, // 25MB + some bytes
|
|
154
|
+
100 * 1024 * 1024 + 123, // 100MB + some bytes
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
fileSizes.forEach((fileSize) => {
|
|
158
|
+
const partSize = calcOptimalPartSize(
|
|
159
|
+
fileSize,
|
|
160
|
+
preferredPartSize,
|
|
161
|
+
minPartSize,
|
|
162
|
+
maxMultipartParts,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(partSize % 1024).toBe(0); // Should be aligned to 1KB
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should handle maximum upload size", () => {
|
|
170
|
+
const maxUploadSize = 5 * 1024 * 1024 * 1024 * 1024; // 5TB
|
|
171
|
+
|
|
172
|
+
const partSize = calcOptimalPartSize(
|
|
173
|
+
maxUploadSize,
|
|
174
|
+
preferredPartSize,
|
|
175
|
+
minPartSize,
|
|
176
|
+
maxMultipartParts,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(partSize).toBeGreaterThanOrEqual(minPartSize);
|
|
180
|
+
|
|
181
|
+
const estimatedParts = Math.ceil(maxUploadSize / partSize);
|
|
182
|
+
expect(estimatedParts).toBeLessThanOrEqual(maxMultipartParts);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("partKey", () => {
|
|
187
|
+
it("should generate consistent part keys", () => {
|
|
188
|
+
expect(partKey("test-upload-id")).toBe("test-upload-id.part");
|
|
189
|
+
expect(partKey("upload-123")).toBe("upload-123.part");
|
|
190
|
+
expect(partKey("")).toBe(".part");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should handle special characters in upload IDs", () => {
|
|
194
|
+
expect(partKey("upload-with-dashes")).toBe("upload-with-dashes.part");
|
|
195
|
+
expect(partKey("upload_with_underscores")).toBe(
|
|
196
|
+
"upload_with_underscores.part",
|
|
197
|
+
);
|
|
198
|
+
expect(partKey("upload.with.dots")).toBe("upload.with.dots.part");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("shouldUseExpirationTags", () => {
|
|
203
|
+
it("should return false when expiration is disabled", () => {
|
|
204
|
+
expect(shouldUseExpirationTags(0, true)).toBe(false);
|
|
205
|
+
expect(shouldUseExpirationTags(0, false)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should return false when tags are disabled", () => {
|
|
209
|
+
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
|
210
|
+
expect(shouldUseExpirationTags(oneWeek, false)).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should return true when both expiration and tags are enabled", () => {
|
|
214
|
+
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
|
215
|
+
expect(shouldUseExpirationTags(oneWeek, true)).toBe(true);
|
|
216
|
+
|
|
217
|
+
const oneDay = 24 * 60 * 60 * 1000;
|
|
218
|
+
expect(shouldUseExpirationTags(oneDay, true)).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("getExpirationDate", () => {
|
|
223
|
+
it("should calculate expiration date correctly", () => {
|
|
224
|
+
const createdAt = "2023-01-01T00:00:00.000Z";
|
|
225
|
+
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
|
226
|
+
|
|
227
|
+
const expirationDate = getExpirationDate(createdAt, oneWeek);
|
|
228
|
+
|
|
229
|
+
expect(expirationDate.getTime()).toBe(
|
|
230
|
+
new Date(createdAt).getTime() + oneWeek,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should handle different date formats", () => {
|
|
235
|
+
const formats = [
|
|
236
|
+
"2023-01-01T00:00:00.000Z",
|
|
237
|
+
"2023-01-01T12:30:45.123Z",
|
|
238
|
+
"2023-12-31T23:59:59.999Z",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const oneDay = 24 * 60 * 60 * 1000;
|
|
242
|
+
|
|
243
|
+
formats.forEach((dateStr) => {
|
|
244
|
+
const originalDate = new Date(dateStr);
|
|
245
|
+
const expirationDate = getExpirationDate(dateStr, oneDay);
|
|
246
|
+
|
|
247
|
+
expect(expirationDate.getTime()).toBe(originalDate.getTime() + oneDay);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should handle different expiration periods", () => {
|
|
252
|
+
const createdAt = "2023-06-15T10:30:00.000Z";
|
|
253
|
+
const baseTime = new Date(createdAt).getTime();
|
|
254
|
+
|
|
255
|
+
const periods = {
|
|
256
|
+
oneHour: 60 * 60 * 1000,
|
|
257
|
+
oneDay: 24 * 60 * 60 * 1000,
|
|
258
|
+
oneWeek: 7 * 24 * 60 * 60 * 1000,
|
|
259
|
+
oneMonth: 30 * 24 * 60 * 60 * 1000,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
Object.entries(periods).forEach(([_name, period]) => {
|
|
263
|
+
const expirationDate = getExpirationDate(createdAt, period);
|
|
264
|
+
expect(expirationDate.getTime()).toBe(baseTime + period);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("should handle zero expiration period", () => {
|
|
269
|
+
const createdAt = "2023-01-01T00:00:00.000Z";
|
|
270
|
+
const expirationDate = getExpirationDate(createdAt, 0);
|
|
271
|
+
|
|
272
|
+
expect(expirationDate.getTime()).toBe(new Date(createdAt).getTime());
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle very large expiration periods", () => {
|
|
276
|
+
const createdAt = "2023-01-01T00:00:00.000Z";
|
|
277
|
+
const oneYear = 365 * 24 * 60 * 60 * 1000;
|
|
278
|
+
|
|
279
|
+
const expirationDate = getExpirationDate(createdAt, oneYear);
|
|
280
|
+
|
|
281
|
+
expect(expirationDate.getTime()).toBe(
|
|
282
|
+
new Date(createdAt).getTime() + oneYear,
|
|
283
|
+
);
|
|
284
|
+
expect(expirationDate.getFullYear()).toBe(2024);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("Real-world scenarios", () => {
|
|
289
|
+
const minPartSize = 5 * 1024 * 1024; // 5MB
|
|
290
|
+
const preferredPartSize = 8 * 1024 * 1024; // 8MB
|
|
291
|
+
const maxMultipartParts = 10000;
|
|
292
|
+
|
|
293
|
+
it("should handle typical video file upload (1GB)", () => {
|
|
294
|
+
const fileSize = 1024 * 1024 * 1024; // 1GB
|
|
295
|
+
const partSize = calcOptimalPartSize(
|
|
296
|
+
fileSize,
|
|
297
|
+
preferredPartSize,
|
|
298
|
+
minPartSize,
|
|
299
|
+
maxMultipartParts,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(partSize).toBe(preferredPartSize); // Should use preferred 8MB
|
|
303
|
+
|
|
304
|
+
const parts = Math.ceil(fileSize / partSize);
|
|
305
|
+
expect(parts).toBeLessThanOrEqual(maxMultipartParts);
|
|
306
|
+
expect(parts).toBe(128); // 1GB / 8MB = 128 parts
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should handle large backup file (500GB)", () => {
|
|
310
|
+
const fileSize = 500 * 1024 * 1024 * 1024; // 500GB
|
|
311
|
+
const partSize = calcOptimalPartSize(
|
|
312
|
+
fileSize,
|
|
313
|
+
preferredPartSize,
|
|
314
|
+
minPartSize,
|
|
315
|
+
maxMultipartParts,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Should be larger than preferred to stay within part limits
|
|
319
|
+
expect(partSize).toBeGreaterThan(preferredPartSize);
|
|
320
|
+
|
|
321
|
+
const parts = Math.ceil(fileSize / partSize);
|
|
322
|
+
expect(parts).toBeLessThanOrEqual(maxMultipartParts);
|
|
323
|
+
|
|
324
|
+
// Calculate expected part size
|
|
325
|
+
const expectedMinPartSize = Math.ceil(fileSize / maxMultipartParts);
|
|
326
|
+
expect(partSize).toBeGreaterThanOrEqual(expectedMinPartSize);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should handle edge case: exactly 5GB file with 5MB parts", () => {
|
|
330
|
+
const fileSize = 5 * 1024 * 1024 * 1024; // 5GB
|
|
331
|
+
const smallPreferredSize = 5 * 1024 * 1024; // 5MB
|
|
332
|
+
|
|
333
|
+
const partSize = calcOptimalPartSize(
|
|
334
|
+
fileSize,
|
|
335
|
+
smallPreferredSize,
|
|
336
|
+
minPartSize,
|
|
337
|
+
maxMultipartParts,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(partSize).toBe(smallPreferredSize); // Should use 5MB
|
|
341
|
+
|
|
342
|
+
const parts = Math.ceil(fileSize / partSize);
|
|
343
|
+
expect(parts).toBe(1024); // 5GB / 5MB = 1024 parts
|
|
344
|
+
expect(parts).toBeLessThanOrEqual(maxMultipartParts);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should optimize for files that exceed part count limits", () => {
|
|
348
|
+
// File that would require 20,000 parts with 8MB parts
|
|
349
|
+
const fileSize = 20000 * preferredPartSize;
|
|
350
|
+
|
|
351
|
+
const partSize = calcOptimalPartSize(
|
|
352
|
+
fileSize,
|
|
353
|
+
preferredPartSize,
|
|
354
|
+
minPartSize,
|
|
355
|
+
maxMultipartParts,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Should be larger than preferred to fit within limit
|
|
359
|
+
expect(partSize).toBeGreaterThan(preferredPartSize);
|
|
360
|
+
|
|
361
|
+
const parts = Math.ceil(fileSize / partSize);
|
|
362
|
+
expect(parts).toBeLessThanOrEqual(maxMultipartParts);
|
|
363
|
+
|
|
364
|
+
// Should be close to the limit but not exceed it
|
|
365
|
+
expect(parts).toBeGreaterThan(maxMultipartParts * 0.9); // At least 90% of limit used
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("should handle small files efficiently", () => {
|
|
369
|
+
const smallSizes = [
|
|
370
|
+
1024, // 1KB
|
|
371
|
+
100 * 1024, // 100KB
|
|
372
|
+
1024 * 1024, // 1MB
|
|
373
|
+
4.9 * 1024 * 1024, // 4.9MB (just under S3 minimum)
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
smallSizes.forEach((fileSize) => {
|
|
377
|
+
const partSize = calcOptimalPartSize(
|
|
378
|
+
fileSize,
|
|
379
|
+
preferredPartSize,
|
|
380
|
+
minPartSize,
|
|
381
|
+
maxMultipartParts,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (fileSize < minPartSize) {
|
|
385
|
+
// For small files, part size should be small but aligned
|
|
386
|
+
expect(partSize).toBeLessThan(minPartSize);
|
|
387
|
+
expect(partSize % 1024).toBe(0); // 1KB aligned
|
|
388
|
+
} else {
|
|
389
|
+
// For files at or above minimum, should respect minimum
|
|
390
|
+
expect(partSize).toBeGreaterThanOrEqual(minPartSize);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|