@uploadista/server 0.0.13-beta.4 → 0.0.13
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/dist/auth/index.d.cts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +1 -1
- package/dist/{auth-BqArZeGK.mjs → auth-D2lKhlzK.mjs} +1 -1
- package/dist/{auth-BqArZeGK.mjs.map → auth-D2lKhlzK.mjs.map} +1 -1
- package/dist/{index-50KlDIjc.d.cts → index-BXLtlr98.d.mts} +1 -1
- package/dist/{index-50KlDIjc.d.cts.map → index-BXLtlr98.d.mts.map} +1 -1
- package/dist/{index--Lny6VJP.d.mts → index-mMP18lsw.d.cts} +1 -1
- package/dist/{index--Lny6VJP.d.mts.map → index-mMP18lsw.d.cts.map} +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +13 -11
- package/src/core/plugin-types.ts +5 -3
- package/src/plugins-typing.ts +1 -0
- package/{src/__tests__ → tests}/backward-compatibility.test.ts +23 -23
- package/{src → tests}/cache.test.ts +2 -2
- package/tests/core/http-handlers/flow-handlers.test.ts +495 -0
- package/tests/core/http-handlers/upload-handlers.test.ts +657 -0
- package/{src/core/__tests__ → tests/core}/plugin-validation.test.ts +59 -26
- package/tests/core/websocket-handlers/websocket-handlers.test.ts +659 -0
- package/{src → tests}/service.test.ts +2 -2
- package/type-tests/plugin-types.test-d.ts +56 -25
- package/vitest.config.ts +39 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HTTP Upload Handlers
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Chunked upload processing
|
|
6
|
+
* - Multipart upload handling
|
|
7
|
+
* - Upload progress tracking
|
|
8
|
+
* - HTTP request/response handling
|
|
9
|
+
* - Error handling and status codes
|
|
10
|
+
* - Upload metadata validation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { it } from "@effect/vitest";
|
|
14
|
+
import { Effect } from "effect";
|
|
15
|
+
import { describe, expect } from "vitest";
|
|
16
|
+
|
|
17
|
+
describe("HTTP Upload Handlers", () => {
|
|
18
|
+
describe("Chunked Upload Processing", () => {
|
|
19
|
+
it.effect("should process single chunk upload", () =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
// Mock upload service
|
|
22
|
+
const mockUploadService = {
|
|
23
|
+
processChunk: (
|
|
24
|
+
uploadId: string,
|
|
25
|
+
chunkData: Uint8Array,
|
|
26
|
+
chunkIndex: number,
|
|
27
|
+
) =>
|
|
28
|
+
Effect.succeed({
|
|
29
|
+
uploadId,
|
|
30
|
+
chunkIndex,
|
|
31
|
+
bytesReceived: chunkData.length,
|
|
32
|
+
status: "in-progress" as const,
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const chunkData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
37
|
+
const result = yield* mockUploadService.processChunk(
|
|
38
|
+
"upload-123",
|
|
39
|
+
chunkData,
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(result.uploadId).toBe("upload-123");
|
|
44
|
+
expect(result.chunkIndex).toBe(0);
|
|
45
|
+
expect(result.bytesReceived).toBe(5);
|
|
46
|
+
expect(result.status).toBe("in-progress");
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
it.effect("should process multiple chunks in sequence", () =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const chunks: Array<{ index: number; size: number }> = [];
|
|
53
|
+
|
|
54
|
+
const mockUploadService = {
|
|
55
|
+
processChunk: (
|
|
56
|
+
uploadId: string,
|
|
57
|
+
chunkData: Uint8Array,
|
|
58
|
+
chunkIndex: number,
|
|
59
|
+
) =>
|
|
60
|
+
Effect.sync(() => {
|
|
61
|
+
chunks.push({ index: chunkIndex, size: chunkData.length });
|
|
62
|
+
return {
|
|
63
|
+
uploadId,
|
|
64
|
+
chunkIndex,
|
|
65
|
+
bytesReceived: chunkData.length,
|
|
66
|
+
status:
|
|
67
|
+
chunkIndex === 2
|
|
68
|
+
? ("completed" as const)
|
|
69
|
+
: ("in-progress" as const),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Process 3 chunks
|
|
75
|
+
yield* mockUploadService.processChunk(
|
|
76
|
+
"upload-123",
|
|
77
|
+
new Uint8Array(1024),
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
yield* mockUploadService.processChunk(
|
|
81
|
+
"upload-123",
|
|
82
|
+
new Uint8Array(1024),
|
|
83
|
+
1,
|
|
84
|
+
);
|
|
85
|
+
const finalResult = yield* mockUploadService.processChunk(
|
|
86
|
+
"upload-123",
|
|
87
|
+
new Uint8Array(512),
|
|
88
|
+
2,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(chunks).toHaveLength(3);
|
|
92
|
+
expect(chunks[0]?.index).toBe(0);
|
|
93
|
+
expect(chunks[1]?.index).toBe(1);
|
|
94
|
+
expect(chunks[2]?.index).toBe(2);
|
|
95
|
+
expect(finalResult.status).toBe("completed");
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
it.effect("should handle chunk upload errors", () =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
const mockUploadService = {
|
|
102
|
+
processChunk: (
|
|
103
|
+
uploadId: string,
|
|
104
|
+
chunkData: Uint8Array,
|
|
105
|
+
chunkIndex: number,
|
|
106
|
+
) =>
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
if (chunkIndex === 1) {
|
|
109
|
+
return yield* Effect.fail(new Error("Chunk processing failed"));
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
uploadId,
|
|
113
|
+
chunkIndex,
|
|
114
|
+
bytesReceived: chunkData.length,
|
|
115
|
+
status: "in-progress" as const,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// First chunk succeeds
|
|
121
|
+
const result1 = yield* mockUploadService.processChunk(
|
|
122
|
+
"upload-123",
|
|
123
|
+
new Uint8Array(1024),
|
|
124
|
+
0,
|
|
125
|
+
);
|
|
126
|
+
expect(result1.status).toBe("in-progress");
|
|
127
|
+
|
|
128
|
+
// Second chunk fails
|
|
129
|
+
const result2 = yield* Effect.either(
|
|
130
|
+
mockUploadService.processChunk("upload-123", new Uint8Array(1024), 1),
|
|
131
|
+
);
|
|
132
|
+
expect(result2._tag).toBe("Left");
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it.effect("should validate chunk size limits", () =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
const MAX_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
|
139
|
+
|
|
140
|
+
const mockUploadService = {
|
|
141
|
+
processChunk: (
|
|
142
|
+
uploadId: string,
|
|
143
|
+
chunkData: Uint8Array,
|
|
144
|
+
chunkIndex: number,
|
|
145
|
+
) =>
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
if (chunkData.length > MAX_CHUNK_SIZE) {
|
|
148
|
+
return yield* Effect.fail(
|
|
149
|
+
new Error("Chunk size exceeds maximum"),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
uploadId,
|
|
154
|
+
chunkIndex,
|
|
155
|
+
bytesReceived: chunkData.length,
|
|
156
|
+
status: "in-progress" as const,
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Valid chunk size
|
|
162
|
+
const result1 = yield* mockUploadService.processChunk(
|
|
163
|
+
"upload-123",
|
|
164
|
+
new Uint8Array(1024 * 1024),
|
|
165
|
+
0,
|
|
166
|
+
);
|
|
167
|
+
expect(result1.status).toBe("in-progress");
|
|
168
|
+
|
|
169
|
+
// Oversized chunk
|
|
170
|
+
const result2 = yield* Effect.either(
|
|
171
|
+
mockUploadService.processChunk(
|
|
172
|
+
"upload-123",
|
|
173
|
+
new Uint8Array(6 * 1024 * 1024),
|
|
174
|
+
1,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
expect(result2._tag).toBe("Left");
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
it.effect("should track total bytes received across chunks", () =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
let totalBytesReceived = 0;
|
|
184
|
+
|
|
185
|
+
const mockUploadService = {
|
|
186
|
+
processChunk: (
|
|
187
|
+
uploadId: string,
|
|
188
|
+
chunkData: Uint8Array,
|
|
189
|
+
chunkIndex: number,
|
|
190
|
+
) =>
|
|
191
|
+
Effect.sync(() => {
|
|
192
|
+
totalBytesReceived += chunkData.length;
|
|
193
|
+
return {
|
|
194
|
+
uploadId,
|
|
195
|
+
chunkIndex,
|
|
196
|
+
bytesReceived: chunkData.length,
|
|
197
|
+
totalBytesReceived,
|
|
198
|
+
status: "in-progress" as const,
|
|
199
|
+
};
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
yield* mockUploadService.processChunk(
|
|
204
|
+
"upload-123",
|
|
205
|
+
new Uint8Array(1024),
|
|
206
|
+
0,
|
|
207
|
+
);
|
|
208
|
+
yield* mockUploadService.processChunk(
|
|
209
|
+
"upload-123",
|
|
210
|
+
new Uint8Array(2048),
|
|
211
|
+
1,
|
|
212
|
+
);
|
|
213
|
+
yield* mockUploadService.processChunk(
|
|
214
|
+
"upload-123",
|
|
215
|
+
new Uint8Array(512),
|
|
216
|
+
2,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(totalBytesReceived).toBe(3584); // 1024 + 2048 + 512
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("Multipart Upload Handling", () => {
|
|
225
|
+
it.effect("should initiate multipart upload", () =>
|
|
226
|
+
Effect.gen(function* () {
|
|
227
|
+
const mockUploadService = {
|
|
228
|
+
initiateMultipartUpload: (
|
|
229
|
+
fileName: string,
|
|
230
|
+
fileSize: number,
|
|
231
|
+
contentType: string,
|
|
232
|
+
) =>
|
|
233
|
+
Effect.succeed({
|
|
234
|
+
uploadId: "multipart-123",
|
|
235
|
+
fileName,
|
|
236
|
+
fileSize,
|
|
237
|
+
contentType,
|
|
238
|
+
status: "initiated" as const,
|
|
239
|
+
parts: [] as Array<{ partNumber: number; etag: string }>,
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const result = yield* mockUploadService.initiateMultipartUpload(
|
|
244
|
+
"large-file.bin",
|
|
245
|
+
100 * 1024 * 1024, // 100MB
|
|
246
|
+
"application/octet-stream",
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(result.uploadId).toBe("multipart-123");
|
|
250
|
+
expect(result.fileName).toBe("large-file.bin");
|
|
251
|
+
expect(result.fileSize).toBe(100 * 1024 * 1024);
|
|
252
|
+
expect(result.status).toBe("initiated");
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
it.effect("should upload multipart parts", () =>
|
|
257
|
+
Effect.gen(function* () {
|
|
258
|
+
const uploadedParts: Array<{ partNumber: number; size: number }> = [];
|
|
259
|
+
|
|
260
|
+
const mockUploadService = {
|
|
261
|
+
uploadPart: (
|
|
262
|
+
uploadId: string,
|
|
263
|
+
partNumber: number,
|
|
264
|
+
data: Uint8Array,
|
|
265
|
+
) =>
|
|
266
|
+
Effect.sync(() => {
|
|
267
|
+
uploadedParts.push({ partNumber, size: data.length });
|
|
268
|
+
return {
|
|
269
|
+
uploadId,
|
|
270
|
+
partNumber,
|
|
271
|
+
etag: `etag-${partNumber}`,
|
|
272
|
+
size: data.length,
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Upload 3 parts
|
|
278
|
+
yield* mockUploadService.uploadPart(
|
|
279
|
+
"multipart-123",
|
|
280
|
+
1,
|
|
281
|
+
new Uint8Array(5 * 1024 * 1024),
|
|
282
|
+
);
|
|
283
|
+
yield* mockUploadService.uploadPart(
|
|
284
|
+
"multipart-123",
|
|
285
|
+
2,
|
|
286
|
+
new Uint8Array(5 * 1024 * 1024),
|
|
287
|
+
);
|
|
288
|
+
yield* mockUploadService.uploadPart(
|
|
289
|
+
"multipart-123",
|
|
290
|
+
3,
|
|
291
|
+
new Uint8Array(2 * 1024 * 1024),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
expect(uploadedParts).toHaveLength(3);
|
|
295
|
+
expect(uploadedParts[0]?.partNumber).toBe(1);
|
|
296
|
+
expect(uploadedParts[1]?.partNumber).toBe(2);
|
|
297
|
+
expect(uploadedParts[2]?.partNumber).toBe(3);
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
it.effect("should complete multipart upload", () =>
|
|
302
|
+
Effect.gen(function* () {
|
|
303
|
+
const mockUploadService = {
|
|
304
|
+
completeMultipartUpload: (
|
|
305
|
+
uploadId: string,
|
|
306
|
+
parts: Array<{ partNumber: number; etag: string }>,
|
|
307
|
+
) =>
|
|
308
|
+
Effect.succeed({
|
|
309
|
+
uploadId,
|
|
310
|
+
status: "completed" as const,
|
|
311
|
+
totalParts: parts.length,
|
|
312
|
+
location: `https://storage.example.com/files/${uploadId}`,
|
|
313
|
+
}),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const parts = [
|
|
317
|
+
{ partNumber: 1, etag: "etag-1" },
|
|
318
|
+
{ partNumber: 2, etag: "etag-2" },
|
|
319
|
+
{ partNumber: 3, etag: "etag-3" },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const result = yield* mockUploadService.completeMultipartUpload(
|
|
323
|
+
"multipart-123",
|
|
324
|
+
parts,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(result.uploadId).toBe("multipart-123");
|
|
328
|
+
expect(result.status).toBe("completed");
|
|
329
|
+
expect(result.totalParts).toBe(3);
|
|
330
|
+
expect(result.location).toContain("multipart-123");
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
it.effect("should abort multipart upload", () =>
|
|
335
|
+
Effect.gen(function* () {
|
|
336
|
+
const mockUploadService = {
|
|
337
|
+
abortMultipartUpload: (uploadId: string, reason: string) =>
|
|
338
|
+
Effect.succeed({
|
|
339
|
+
uploadId,
|
|
340
|
+
status: "aborted" as const,
|
|
341
|
+
reason,
|
|
342
|
+
timestamp: Date.now(),
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const result = yield* mockUploadService.abortMultipartUpload(
|
|
347
|
+
"multipart-123",
|
|
348
|
+
"User cancelled upload",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(result.uploadId).toBe("multipart-123");
|
|
352
|
+
expect(result.status).toBe("aborted");
|
|
353
|
+
expect(result.reason).toBe("User cancelled upload");
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
it.effect("should handle multipart upload errors", () =>
|
|
358
|
+
Effect.gen(function* () {
|
|
359
|
+
const mockUploadService = {
|
|
360
|
+
uploadPart: (
|
|
361
|
+
uploadId: string,
|
|
362
|
+
partNumber: number,
|
|
363
|
+
data: Uint8Array,
|
|
364
|
+
) =>
|
|
365
|
+
Effect.gen(function* () {
|
|
366
|
+
if (partNumber === 2) {
|
|
367
|
+
return yield* Effect.fail(new Error("Part 2 upload failed"));
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
uploadId,
|
|
371
|
+
partNumber,
|
|
372
|
+
etag: `etag-${partNumber}`,
|
|
373
|
+
size: data.length,
|
|
374
|
+
};
|
|
375
|
+
}),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Part 1 succeeds
|
|
379
|
+
const result1 = yield* mockUploadService.uploadPart(
|
|
380
|
+
"multipart-123",
|
|
381
|
+
1,
|
|
382
|
+
new Uint8Array(5 * 1024 * 1024),
|
|
383
|
+
);
|
|
384
|
+
expect(result1.partNumber).toBe(1);
|
|
385
|
+
|
|
386
|
+
// Part 2 fails
|
|
387
|
+
const result2 = yield* Effect.either(
|
|
388
|
+
mockUploadService.uploadPart(
|
|
389
|
+
"multipart-123",
|
|
390
|
+
2,
|
|
391
|
+
new Uint8Array(5 * 1024 * 1024),
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
expect(result2._tag).toBe("Left");
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("Upload Progress Tracking", () => {
|
|
400
|
+
it.effect("should report upload progress", () =>
|
|
401
|
+
Effect.gen(function* () {
|
|
402
|
+
const progressEvents: Array<{
|
|
403
|
+
bytesUploaded: number;
|
|
404
|
+
percentage: number;
|
|
405
|
+
}> = [];
|
|
406
|
+
|
|
407
|
+
const mockUploadService = {
|
|
408
|
+
trackProgress: (
|
|
409
|
+
uploadId: string,
|
|
410
|
+
bytesUploaded: number,
|
|
411
|
+
totalBytes: number,
|
|
412
|
+
) =>
|
|
413
|
+
Effect.sync(() => {
|
|
414
|
+
const percentage = Math.round((bytesUploaded / totalBytes) * 100);
|
|
415
|
+
progressEvents.push({ bytesUploaded, percentage });
|
|
416
|
+
return {
|
|
417
|
+
uploadId,
|
|
418
|
+
bytesUploaded,
|
|
419
|
+
totalBytes,
|
|
420
|
+
percentage,
|
|
421
|
+
};
|
|
422
|
+
}),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const totalBytes = 10 * 1024 * 1024; // 10MB
|
|
426
|
+
|
|
427
|
+
yield* mockUploadService.trackProgress(
|
|
428
|
+
"upload-123",
|
|
429
|
+
2 * 1024 * 1024,
|
|
430
|
+
totalBytes,
|
|
431
|
+
); // 20%
|
|
432
|
+
yield* mockUploadService.trackProgress(
|
|
433
|
+
"upload-123",
|
|
434
|
+
5 * 1024 * 1024,
|
|
435
|
+
totalBytes,
|
|
436
|
+
); // 50%
|
|
437
|
+
yield* mockUploadService.trackProgress(
|
|
438
|
+
"upload-123",
|
|
439
|
+
8 * 1024 * 1024,
|
|
440
|
+
totalBytes,
|
|
441
|
+
); // 80%
|
|
442
|
+
yield* mockUploadService.trackProgress(
|
|
443
|
+
"upload-123",
|
|
444
|
+
10 * 1024 * 1024,
|
|
445
|
+
totalBytes,
|
|
446
|
+
); // 100%
|
|
447
|
+
|
|
448
|
+
expect(progressEvents).toHaveLength(4);
|
|
449
|
+
expect(progressEvents[0]?.percentage).toBe(20);
|
|
450
|
+
expect(progressEvents[1]?.percentage).toBe(50);
|
|
451
|
+
expect(progressEvents[2]?.percentage).toBe(80);
|
|
452
|
+
expect(progressEvents[3]?.percentage).toBe(100);
|
|
453
|
+
}),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
it.effect("should calculate upload speed", () =>
|
|
457
|
+
Effect.gen(function* () {
|
|
458
|
+
const mockUploadService = {
|
|
459
|
+
calculateSpeed: (bytesUploaded: number, elapsedSeconds: number) =>
|
|
460
|
+
Effect.succeed({
|
|
461
|
+
bytesPerSecond: Math.floor(bytesUploaded / elapsedSeconds),
|
|
462
|
+
mbps: parseFloat(
|
|
463
|
+
((bytesUploaded / elapsedSeconds / 1024 / 1024) * 8).toFixed(2),
|
|
464
|
+
),
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// 5MB in 2 seconds
|
|
469
|
+
const result = yield* mockUploadService.calculateSpeed(
|
|
470
|
+
5 * 1024 * 1024,
|
|
471
|
+
2,
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(result.bytesPerSecond).toBeGreaterThan(0);
|
|
475
|
+
expect(result.mbps).toBeGreaterThan(0);
|
|
476
|
+
}),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
it.effect("should estimate time remaining", () =>
|
|
480
|
+
Effect.gen(function* () {
|
|
481
|
+
const mockUploadService = {
|
|
482
|
+
estimateTimeRemaining: (
|
|
483
|
+
bytesUploaded: number,
|
|
484
|
+
totalBytes: number,
|
|
485
|
+
bytesPerSecond: number,
|
|
486
|
+
) =>
|
|
487
|
+
Effect.succeed({
|
|
488
|
+
bytesRemaining: totalBytes - bytesUploaded,
|
|
489
|
+
secondsRemaining: Math.ceil(
|
|
490
|
+
(totalBytes - bytesUploaded) / bytesPerSecond,
|
|
491
|
+
),
|
|
492
|
+
}),
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// 3MB uploaded out of 10MB, at 1MB/s
|
|
496
|
+
const result = yield* mockUploadService.estimateTimeRemaining(
|
|
497
|
+
3 * 1024 * 1024,
|
|
498
|
+
10 * 1024 * 1024,
|
|
499
|
+
1024 * 1024,
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
expect(result.bytesRemaining).toBe(7 * 1024 * 1024);
|
|
503
|
+
expect(result.secondsRemaining).toBe(7);
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("HTTP Status Codes", () => {
|
|
509
|
+
it.effect("should return 200 OK for successful upload", () =>
|
|
510
|
+
Effect.gen(function* () {
|
|
511
|
+
const mockHandler = {
|
|
512
|
+
handleUpload: () =>
|
|
513
|
+
Effect.succeed({
|
|
514
|
+
statusCode: 200,
|
|
515
|
+
body: { success: true, uploadId: "upload-123" },
|
|
516
|
+
}),
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const result = yield* mockHandler.handleUpload();
|
|
520
|
+
expect(result.statusCode).toBe(200);
|
|
521
|
+
expect(result.body.success).toBe(true);
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
it.effect("should return 400 Bad Request for invalid input", () =>
|
|
526
|
+
Effect.gen(function* () {
|
|
527
|
+
const mockHandler = {
|
|
528
|
+
handleUpload: (fileSize: number) =>
|
|
529
|
+
Effect.succeed(
|
|
530
|
+
fileSize <= 0
|
|
531
|
+
? {
|
|
532
|
+
statusCode: 400,
|
|
533
|
+
body: { error: "Invalid file size" },
|
|
534
|
+
}
|
|
535
|
+
: {
|
|
536
|
+
statusCode: 200,
|
|
537
|
+
body: { success: true },
|
|
538
|
+
},
|
|
539
|
+
),
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = yield* mockHandler.handleUpload(0);
|
|
543
|
+
expect(result.statusCode).toBe(400);
|
|
544
|
+
expect(result.body.error).toBe("Invalid file size");
|
|
545
|
+
}),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
it.effect("should return 413 Payload Too Large for oversized uploads", () =>
|
|
549
|
+
Effect.gen(function* () {
|
|
550
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
551
|
+
|
|
552
|
+
const mockHandler = {
|
|
553
|
+
handleUpload: (fileSize: number) =>
|
|
554
|
+
Effect.succeed(
|
|
555
|
+
fileSize > MAX_FILE_SIZE
|
|
556
|
+
? {
|
|
557
|
+
statusCode: 413,
|
|
558
|
+
body: { error: "File too large" },
|
|
559
|
+
}
|
|
560
|
+
: {
|
|
561
|
+
statusCode: 200,
|
|
562
|
+
body: { success: true },
|
|
563
|
+
},
|
|
564
|
+
),
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const result = yield* mockHandler.handleUpload(200 * 1024 * 1024);
|
|
568
|
+
expect(result.statusCode).toBe(413);
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
it.effect(
|
|
573
|
+
"should return 500 Internal Server Error for server failures",
|
|
574
|
+
() =>
|
|
575
|
+
Effect.gen(function* () {
|
|
576
|
+
const mockHandler = {
|
|
577
|
+
handleUpload: () =>
|
|
578
|
+
Effect.succeed({
|
|
579
|
+
statusCode: 500,
|
|
580
|
+
body: { error: "Internal server error" },
|
|
581
|
+
}),
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const result = yield* mockHandler.handleUpload();
|
|
585
|
+
expect(result.statusCode).toBe(500);
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe("Upload Metadata Validation", () => {
|
|
591
|
+
it.effect("should validate required metadata fields", () =>
|
|
592
|
+
Effect.gen(function* () {
|
|
593
|
+
const mockValidator = {
|
|
594
|
+
validateMetadata: (metadata: Record<string, unknown>) =>
|
|
595
|
+
Effect.gen(function* () {
|
|
596
|
+
const requiredFields = ["fileName", "fileSize", "contentType"];
|
|
597
|
+
const missingFields = requiredFields.filter(
|
|
598
|
+
(field) => !(field in metadata),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if (missingFields.length > 0) {
|
|
602
|
+
return yield* Effect.fail(
|
|
603
|
+
new Error(
|
|
604
|
+
`Missing required fields: ${missingFields.join(", ")}`,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { valid: true };
|
|
610
|
+
}),
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Valid metadata
|
|
614
|
+
const result1 = yield* mockValidator.validateMetadata({
|
|
615
|
+
fileName: "test.txt",
|
|
616
|
+
fileSize: 1024,
|
|
617
|
+
contentType: "text/plain",
|
|
618
|
+
});
|
|
619
|
+
expect(result1.valid).toBe(true);
|
|
620
|
+
|
|
621
|
+
// Missing fields
|
|
622
|
+
const result2 = yield* Effect.either(
|
|
623
|
+
mockValidator.validateMetadata({
|
|
624
|
+
fileName: "test.txt",
|
|
625
|
+
}),
|
|
626
|
+
);
|
|
627
|
+
expect(result2._tag).toBe("Left");
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
it.effect("should validate file type restrictions", () =>
|
|
632
|
+
Effect.gen(function* () {
|
|
633
|
+
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
|
|
634
|
+
|
|
635
|
+
const mockValidator = {
|
|
636
|
+
validateFileType: (contentType: string) =>
|
|
637
|
+
Effect.gen(function* () {
|
|
638
|
+
if (!allowedTypes.includes(contentType)) {
|
|
639
|
+
return yield* Effect.fail(new Error("File type not allowed"));
|
|
640
|
+
}
|
|
641
|
+
return { valid: true };
|
|
642
|
+
}),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// Allowed type
|
|
646
|
+
const result1 = yield* mockValidator.validateFileType("image/jpeg");
|
|
647
|
+
expect(result1.valid).toBe(true);
|
|
648
|
+
|
|
649
|
+
// Disallowed type
|
|
650
|
+
const result2 = yield* Effect.either(
|
|
651
|
+
mockValidator.validateFileType("application/pdf"),
|
|
652
|
+
);
|
|
653
|
+
expect(result2._tag).toBe("Left");
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
});
|