@aj-archipelago/cortex 1.3.11 → 1.3.14

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.
Files changed (29) hide show
  1. package/helper-apps/cortex-file-handler/.env.test +7 -0
  2. package/helper-apps/cortex-file-handler/.env.test.azure +6 -0
  3. package/helper-apps/cortex-file-handler/.env.test.gcs +9 -0
  4. package/helper-apps/cortex-file-handler/blobHandler.js +313 -204
  5. package/helper-apps/cortex-file-handler/constants.js +107 -0
  6. package/helper-apps/cortex-file-handler/docHelper.js +4 -1
  7. package/helper-apps/cortex-file-handler/fileChunker.js +170 -109
  8. package/helper-apps/cortex-file-handler/helper.js +82 -16
  9. package/helper-apps/cortex-file-handler/index.js +226 -146
  10. package/helper-apps/cortex-file-handler/localFileHandler.js +21 -3
  11. package/helper-apps/cortex-file-handler/package-lock.json +2622 -51
  12. package/helper-apps/cortex-file-handler/package.json +25 -4
  13. package/helper-apps/cortex-file-handler/redis.js +9 -18
  14. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +22 -0
  15. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +49 -0
  16. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +34 -0
  17. package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +49 -0
  18. package/helper-apps/cortex-file-handler/start.js +39 -4
  19. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +292 -0
  20. package/helper-apps/cortex-file-handler/tests/docHelper.test.js +148 -0
  21. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +311 -0
  22. package/helper-apps/cortex-file-handler/tests/start.test.js +930 -0
  23. package/package.json +1 -1
  24. package/pathways/system/entity/sys_entity_continue.js +1 -1
  25. package/pathways/system/entity/sys_entity_start.js +1 -0
  26. package/pathways/system/entity/sys_generator_video_vision.js +2 -1
  27. package/pathways/system/entity/sys_router_tool.js +6 -4
  28. package/server/plugins/openAiWhisperPlugin.js +9 -13
  29. package/server/plugins/replicateApiPlugin.js +54 -2
@@ -0,0 +1,930 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import test from 'ava';
3
+ import axios from 'axios';
4
+ // eslint-disable-next-line import/no-extraneous-dependencies
5
+ import FormData from 'form-data';
6
+ import { port, publicFolder, ipAddress } from '../start.js';
7
+ import { v4 as uuidv4 } from 'uuid';
8
+
9
+ // Add these helper functions at the top after imports
10
+ const baseUrl = `http://localhost:${port}/api/CortexFileHandler`;
11
+
12
+ // Helper function to determine if Azure is configured
13
+ function isAzureConfigured() {
14
+ return process.env.AZURE_STORAGE_CONNECTION_STRING &&
15
+ process.env.AZURE_STORAGE_CONNECTION_STRING !== 'UseDevelopmentStorage=true';
16
+ }
17
+
18
+ // Helper function to convert URLs for testing
19
+ function convertToLocalUrl(url) {
20
+ // If it's an Azurite URL (contains 127.0.0.1:10000), use it as is
21
+ if (url.includes('127.0.0.1:10000')) {
22
+ return url;
23
+ }
24
+ // For local storage URLs, convert any IP:port to localhost:port
25
+ const urlObj = new URL(url);
26
+ return url.replace(urlObj.host, `localhost:${port}`);
27
+ }
28
+
29
+ // Helper function to clean up uploaded files
30
+ async function cleanupUploadedFile(t, url) {
31
+ // Convert URL to use localhost
32
+ url = convertToLocalUrl(url);
33
+ const folderName = getFolderNameFromUrl(url);
34
+
35
+ // Delete the file
36
+ const deleteResponse = await axios.delete(`${baseUrl}?operation=delete&requestId=${folderName}`);
37
+ t.is(deleteResponse.status, 200, 'Delete should succeed');
38
+ t.true(Array.isArray(deleteResponse.data.body), 'Delete response should be an array');
39
+ t.true(deleteResponse.data.body.length > 0, 'Should have deleted at least one file');
40
+
41
+ // Verify file is gone
42
+ const verifyResponse = await axios.get(url, {
43
+ validateStatus: status => true,
44
+ timeout: 5000
45
+ });
46
+ t.is(verifyResponse.status, 404, 'File should not exist after deletion');
47
+ }
48
+
49
+ // Helper function to get folder name from URL
50
+ function getFolderNameFromUrl(url) {
51
+ const urlObj = new URL(url);
52
+ const parts = urlObj.pathname.split('/');
53
+ // For Azure URLs (contains 127.0.0.1:10000), folder name is at index 3
54
+ if (url.includes('127.0.0.1:10000')) {
55
+ return parts[3].split('_')[0];
56
+ }
57
+ // For local storage URLs, folder name is at index 2
58
+ return parts[2].split('_')[0];
59
+ }
60
+
61
+ // Helper function to upload files
62
+ async function uploadFile(file, requestId, hash = null) {
63
+ const form = new FormData();
64
+ form.append('file', file);
65
+ if (requestId) form.append('requestId', requestId);
66
+ if (hash) form.append('hash', hash);
67
+
68
+ const response = await axios.post(baseUrl, form, {
69
+ headers: {
70
+ ...form.getHeaders(),
71
+ 'Content-Type': 'multipart/form-data'
72
+ },
73
+ validateStatus: status => true,
74
+ timeout: 5000
75
+ });
76
+
77
+ if (response.data?.url) {
78
+ response.data.url = convertToLocalUrl(response.data.url);
79
+ }
80
+
81
+ return response;
82
+ }
83
+
84
+ // Ensure server is ready before tests
85
+ test.before(async t => {
86
+ // Wait for server to be ready
87
+ await new Promise(resolve => setTimeout(resolve, 1000));
88
+
89
+ // Verify server is responding
90
+ try {
91
+ await axios.get(`http://localhost:${port}/files`);
92
+ } catch (error) {
93
+ // 404 is fine, it means server is running but directory is empty
94
+ if (error.response?.status !== 404) {
95
+ throw new Error('Server not ready');
96
+ }
97
+ }
98
+ });
99
+
100
+ // Configuration Tests
101
+ test('should have valid server configuration', t => {
102
+ t.truthy(port, 'Port should be defined');
103
+ t.truthy(publicFolder, 'Public folder should be defined');
104
+ t.truthy(ipAddress, 'IP address should be defined');
105
+ });
106
+
107
+ // Parameter Validation Tests
108
+ test.serial('should validate required parameters on CortexFileHandler endpoint', async t => {
109
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
110
+ validateStatus: status => true,
111
+ timeout: 5000
112
+ });
113
+
114
+ t.is(response.status, 400, 'Should return 400 for missing parameters');
115
+ t.is(
116
+ response.data,
117
+ 'Please pass a uri and requestId on the query string or in the request body',
118
+ 'Should return proper error message'
119
+ );
120
+ });
121
+
122
+ test.serial('should validate required parameters on MediaFileChunker legacy endpoint', async t => {
123
+ const response = await axios.get(`http://localhost:${port}/api/MediaFileChunker`, {
124
+ validateStatus: status => true,
125
+ timeout: 5000
126
+ });
127
+
128
+ t.is(response.status, 400, 'Should return 400 for missing parameters');
129
+ t.is(
130
+ response.data,
131
+ 'Please pass a uri and requestId on the query string or in the request body',
132
+ 'Should return proper error message'
133
+ );
134
+ });
135
+
136
+ // Static Files Tests
137
+ test.serial('should serve static files from public directory', async t => {
138
+ try {
139
+ const response = await axios.get(`http://localhost:${port}/files`, {
140
+ timeout: 5000,
141
+ validateStatus: status => status === 200 || status === 404
142
+ });
143
+
144
+ t.true(
145
+ response.status === 200 || response.status === 404,
146
+ 'Should respond with 200 or 404 for static files'
147
+ );
148
+ } catch (error) {
149
+ t.fail(`Failed to connect to files endpoint: ${error.message}`);
150
+ }
151
+ });
152
+
153
+ // Hash Operation Tests
154
+ test.serial('should handle non-existent hash check', async t => {
155
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
156
+ params: {
157
+ hash: 'nonexistent-hash',
158
+ checkHash: true
159
+ },
160
+ validateStatus: status => true,
161
+ timeout: 5000
162
+ });
163
+
164
+ t.is(response.status, 404, 'Should return 404 for non-existent hash');
165
+ t.is(response.data, 'Hash nonexistent-hash not found', 'Should return proper error message');
166
+ });
167
+
168
+ test.serial('should handle hash clearing for non-existent hash', async t => {
169
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
170
+ params: {
171
+ hash: 'nonexistent-hash',
172
+ clearHash: true
173
+ },
174
+ validateStatus: status => true,
175
+ timeout: 5000
176
+ });
177
+
178
+ t.is(response.status, 404, 'Should return 404 for non-existent hash');
179
+ t.is(response.data, 'Hash nonexistent-hash not found', 'Should return proper message');
180
+ });
181
+
182
+ test.serial('should handle hash operations without hash parameter', async t => {
183
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
184
+ params: {
185
+ checkHash: true
186
+ },
187
+ validateStatus: status => true,
188
+ timeout: 5000
189
+ });
190
+
191
+ t.is(response.status, 400, 'Should return 400 for missing hash');
192
+ t.is(
193
+ response.data,
194
+ 'Please pass a uri and requestId on the query string or in the request body',
195
+ 'Should return proper error message'
196
+ );
197
+ });
198
+
199
+ // URL Validation Tests
200
+ test.serial('should reject invalid URLs', async t => {
201
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
202
+ params: {
203
+ uri: 'not-a-valid-url',
204
+ requestId: 'test-request'
205
+ },
206
+ validateStatus: status => true,
207
+ timeout: 5000
208
+ });
209
+
210
+ t.is(response.status, 500, 'Should return 500 for invalid URL');
211
+ t.true(response.data.includes('Invalid URL'), 'Should indicate invalid URL in error message');
212
+ });
213
+
214
+ test.serial('should reject unsupported protocols', async t => {
215
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
216
+ params: {
217
+ uri: 'ftp://example.com/test.mp3',
218
+ requestId: 'test-request'
219
+ },
220
+ validateStatus: status => true,
221
+ timeout: 5000
222
+ });
223
+
224
+ t.is(response.status, 500, 'Should return 500 for unsupported protocol');
225
+ t.true(
226
+ response.data.includes('Error processing media file'),
227
+ 'Should indicate error processing media file'
228
+ );
229
+ });
230
+
231
+ // Remote File Operation Tests
232
+ test.serial('should validate remote file URL format', async t => {
233
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
234
+ params: {
235
+ fetch: 'not-a-valid-url'
236
+ },
237
+ validateStatus: status => true,
238
+ timeout: 5000
239
+ });
240
+
241
+ t.is(response.status, 400, 'Should return 400 for invalid remote URL');
242
+ t.is(response.data, 'Invalid or inaccessible URL', 'Should return proper error message');
243
+ });
244
+
245
+ test.serial('should handle restore operation with invalid URL', async t => {
246
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
247
+ params: {
248
+ restore: 'not-a-valid-url'
249
+ },
250
+ validateStatus: status => true,
251
+ timeout: 5000
252
+ });
253
+
254
+ t.is(response.status, 400, 'Should return 400 for invalid restore URL');
255
+ t.is(response.data, 'Invalid or inaccessible URL', 'Should return proper error message');
256
+ });
257
+
258
+ test.serial('should handle load operation with invalid URL', async t => {
259
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
260
+ params: {
261
+ load: 'not-a-valid-url'
262
+ },
263
+ validateStatus: status => true,
264
+ timeout: 5000
265
+ });
266
+
267
+ t.is(response.status, 400, 'Should return 400 for invalid load URL');
268
+ t.is(response.data, 'Invalid or inaccessible URL', 'Should return proper error message');
269
+ });
270
+
271
+ // Delete Operation Tests
272
+ test.serial('should validate requestId for delete operation', async t => {
273
+ const response = await axios.delete(`http://localhost:${port}/api/CortexFileHandler`, {
274
+ validateStatus: status => true,
275
+ timeout: 5000
276
+ });
277
+
278
+ t.is(response.status, 400, 'Should return 400 for missing requestId');
279
+ t.is(
280
+ response.data,
281
+ 'Please pass a requestId on the query string',
282
+ 'Should return proper error message'
283
+ );
284
+ });
285
+
286
+ test.serial('should handle delete with valid requestId', async t => {
287
+ const testRequestId = 'test-delete-request';
288
+ const testContent = 'test content';
289
+ const form = new FormData();
290
+ form.append('file', Buffer.from(testContent), 'test.txt');
291
+
292
+ // Upload a file first
293
+ const uploadResponse = await axios.post(baseUrl, form, {
294
+ headers: form.getHeaders(),
295
+ validateStatus: status => true,
296
+ timeout: 5000
297
+ });
298
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
299
+
300
+ // Extract the folder name from the URL
301
+ const url = uploadResponse.data.url;
302
+ const folderName = getFolderNameFromUrl(url);
303
+
304
+ // Delete the file
305
+ const deleteResponse = await axios.delete(`${baseUrl}?operation=delete&requestId=${folderName}`);
306
+ t.is(deleteResponse.status, 200, 'Delete should succeed');
307
+ t.true(Array.isArray(deleteResponse.data.body), 'Response should be an array of deleted files');
308
+ t.true(deleteResponse.data.body.length > 0, 'Should have deleted at least one file');
309
+ t.true(deleteResponse.data.body[0].includes(folderName), 'Deleted file should contain folder name');
310
+ });
311
+
312
+ test.serial('should handle delete with non-existent requestId', async t => {
313
+ const response = await axios.delete(`http://localhost:${port}/api/CortexFileHandler`, {
314
+ params: {
315
+ requestId: 'nonexistent-request'
316
+ },
317
+ validateStatus: status => true,
318
+ timeout: 30000
319
+ });
320
+
321
+ t.is(response.status, 200, 'Should return 200 even for non-existent requestId');
322
+ t.deepEqual(response.data.body, [], 'Should return empty array for non-existent requestId');
323
+ });
324
+
325
+ test('should handle delete with invalid requestId', async t => {
326
+ const response = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
327
+ params: {
328
+ requestId: 'nonexistent-request',
329
+ operation: 'delete'
330
+ },
331
+ timeout: 5000
332
+ });
333
+ t.is(response.status, 200, 'Should return 200 for delete with invalid requestId');
334
+ t.true(Array.isArray(response.data.body), 'Response should be an array');
335
+ t.is(response.data.body.length, 0, 'Response should be empty array for non-existent requestId');
336
+ });
337
+
338
+ // POST Operation Tests
339
+ test('should handle empty POST request', async t => {
340
+ const form = new FormData();
341
+ try {
342
+ await axios.post(
343
+ `http://localhost:${port}/api/CortexFileHandler`,
344
+ form,
345
+ {
346
+ headers: form.getHeaders(),
347
+ timeout: 5000
348
+ }
349
+ );
350
+ t.fail('Should have thrown error');
351
+ } catch (error) {
352
+ t.is(error.response.status, 400, 'Should return 400 for empty POST request');
353
+ t.is(error.response.data, 'No file provided in request', 'Should return proper error message');
354
+ }
355
+ });
356
+
357
+ // Upload Tests
358
+ test.serial('should handle successful file upload with hash', async t => {
359
+ const form = new FormData();
360
+ const testHash = 'test-hash-123';
361
+ const testContent = 'test content';
362
+ form.append('file', Buffer.from(testContent), 'test.txt');
363
+ form.append('hash', testHash);
364
+
365
+ // Upload file with hash
366
+ const uploadResponse = await axios.post(
367
+ `http://localhost:${port}/api/CortexFileHandler`,
368
+ form,
369
+ {
370
+ headers: {
371
+ ...form.getHeaders(),
372
+ 'Content-Type': 'multipart/form-data'
373
+ },
374
+ validateStatus: status => true,
375
+ timeout: 5000
376
+ }
377
+ );
378
+
379
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
380
+ t.truthy(uploadResponse.data.url, 'Response should contain file URL');
381
+
382
+ // Wait a bit for Redis to be updated
383
+ await new Promise(resolve => setTimeout(resolve, 1000));
384
+
385
+ // Verify hash exists and returns the file info
386
+ const hashCheckResponse = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
387
+ params: {
388
+ hash: testHash,
389
+ checkHash: true
390
+ },
391
+ validateStatus: status => true,
392
+ timeout: 5000
393
+ });
394
+
395
+ t.is(hashCheckResponse.status, 404, 'Hash check should return 404 for new hash');
396
+ t.is(hashCheckResponse.data, `Hash ${testHash} not found`, 'Should indicate hash not found');
397
+
398
+ await cleanupUploadedFile(t, uploadResponse.data.url);
399
+ });
400
+
401
+ test.serial('should handle hash clearing', async t => {
402
+ const testHash = 'test-hash-to-clear';
403
+ const form = new FormData();
404
+ form.append('file', Buffer.from('test content'), 'test.txt');
405
+ form.append('hash', testHash);
406
+
407
+ // First upload a file with the hash
408
+ const uploadResponse = await axios.post(
409
+ `http://localhost:${port}/api/CortexFileHandler`,
410
+ form,
411
+ {
412
+ headers: {
413
+ ...form.getHeaders(),
414
+ 'Content-Type': 'multipart/form-data'
415
+ },
416
+ validateStatus: status => true,
417
+ timeout: 5000
418
+ }
419
+ );
420
+
421
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
422
+ t.truthy(uploadResponse.data.url, 'Response should contain file URL');
423
+
424
+ // Wait a bit for Redis to be updated
425
+ await new Promise(resolve => setTimeout(resolve, 1000));
426
+
427
+ // Clear the hash
428
+ const clearResponse = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
429
+ params: {
430
+ hash: testHash,
431
+ clearHash: true
432
+ },
433
+ validateStatus: status => true,
434
+ timeout: 5000
435
+ });
436
+
437
+ t.is(clearResponse.status, 404, 'Hash clearing should return 404 for new hash');
438
+ t.is(clearResponse.data, `Hash ${testHash} not found`, 'Should indicate hash not found');
439
+
440
+ // Verify hash no longer exists
441
+ const verifyResponse = await axios.get(`http://localhost:${port}/api/CortexFileHandler`, {
442
+ params: {
443
+ hash: testHash,
444
+ checkHash: true
445
+ },
446
+ validateStatus: status => true,
447
+ timeout: 5000
448
+ });
449
+
450
+ t.is(verifyResponse.status, 404, 'Hash should not exist');
451
+ t.is(verifyResponse.data, `Hash ${testHash} not found`, 'Should indicate hash not found');
452
+
453
+ // Clean up the uploaded file
454
+ await cleanupUploadedFile(t, uploadResponse.data.url);
455
+ });
456
+
457
+ test.serial('should handle file upload without hash', async t => {
458
+ const form = new FormData();
459
+ form.append('file', Buffer.from('test content'), 'test.txt');
460
+
461
+ const response = await axios.post(
462
+ `http://localhost:${port}/api/CortexFileHandler`,
463
+ form,
464
+ {
465
+ headers: {
466
+ ...form.getHeaders(),
467
+ 'Content-Type': 'multipart/form-data'
468
+ },
469
+ validateStatus: status => true,
470
+ timeout: 5000
471
+ }
472
+ );
473
+
474
+ t.is(response.status, 200, 'Upload should succeed');
475
+ t.truthy(response.data.url, 'Response should contain file URL');
476
+
477
+ await cleanupUploadedFile(t, response.data.url);
478
+ });
479
+
480
+ test.serial('should handle upload with empty file', async t => {
481
+ const form = new FormData();
482
+ // Empty file
483
+ form.append('file', Buffer.from(''), 'empty.txt');
484
+
485
+ const response = await axios.post(
486
+ `http://localhost:${port}/api/CortexFileHandler`,
487
+ form,
488
+ {
489
+ headers: {
490
+ ...form.getHeaders(),
491
+ 'Content-Type': 'multipart/form-data'
492
+ },
493
+ validateStatus: status => true,
494
+ timeout: 5000
495
+ }
496
+ );
497
+
498
+ t.is(response.status, 200, 'Should accept empty file');
499
+ t.truthy(response.data.url, 'Should return URL for empty file');
500
+
501
+ await cleanupUploadedFile(t, response.data.url);
502
+ });
503
+
504
+ test.serial('should handle complete upload-request-delete-verify sequence', async t => {
505
+ const testContent = 'test content for sequence';
506
+ const testHash = 'test-sequence-hash';
507
+ const form = new FormData();
508
+ form.append('file', Buffer.from(testContent), 'sequence-test.txt');
509
+ form.append('hash', testHash);
510
+
511
+ // Upload file with hash
512
+ const uploadResponse = await axios.post(baseUrl, form, {
513
+ headers: form.getHeaders(),
514
+ validateStatus: status => true,
515
+ timeout: 5000
516
+ });
517
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
518
+ t.truthy(uploadResponse.data.url, 'Response should contain URL');
519
+
520
+ await cleanupUploadedFile(t, uploadResponse.data.url);
521
+
522
+ // Verify hash is gone by trying to get the file URL
523
+ const hashCheckResponse = await axios.get(`${baseUrl}`, {
524
+ params: {
525
+ hash: testHash,
526
+ checkHash: true
527
+ },
528
+ validateStatus: status => true
529
+ });
530
+ t.is(hashCheckResponse.status, 404, 'Hash should not exist after deletion');
531
+ });
532
+
533
+ test.serial('should handle multiple file uploads with unique hashes', async t => {
534
+ const uploadedFiles = [];
535
+
536
+ // Upload 10 files
537
+ for (let i = 0; i < 10; i++) {
538
+ const content = `test content for file ${i}`;
539
+ const form = new FormData();
540
+ form.append('file', Buffer.from(content), `file-${i}.txt`);
541
+
542
+ const uploadResponse = await axios.post(baseUrl, form, {
543
+ headers: form.getHeaders(),
544
+ validateStatus: status => true,
545
+ timeout: 5000
546
+ });
547
+ t.is(uploadResponse.status, 200, `Upload should succeed for file ${i}`);
548
+
549
+ const url = uploadResponse.data.url;
550
+ t.truthy(url, `Response should contain URL for file ${i}`);
551
+
552
+ uploadedFiles.push({
553
+ url: convertToLocalUrl(url),
554
+ content
555
+ });
556
+
557
+ // Small delay between uploads
558
+ await new Promise(resolve => setTimeout(resolve, 100));
559
+ }
560
+
561
+ // Verify files are stored and can be fetched
562
+ for (const file of uploadedFiles) {
563
+ const fileResponse = await axios.get(file.url, {
564
+ validateStatus: status => true,
565
+ timeout: 5000
566
+ });
567
+ t.is(fileResponse.status, 200, `File should be accessible at ${file.url}`);
568
+ t.is(fileResponse.data, file.content, `File content should match original content`);
569
+ }
570
+
571
+ // Clean up all files
572
+ for (const file of uploadedFiles) {
573
+ await cleanupUploadedFile(t, file.url);
574
+ }
575
+ });
576
+
577
+ // Example of a hash-specific test that only runs with Azure
578
+ test.serial('should handle hash reuse with Azure storage', async t => {
579
+ if (!isAzureConfigured()) {
580
+ t.pass('Skipping hash test - Azure not configured');
581
+ return;
582
+ }
583
+
584
+ const testHash = 'test-hash-reuse';
585
+ const testContent = 'test content for hash reuse';
586
+ const form = new FormData();
587
+ form.append('file', Buffer.from(testContent), 'test.txt');
588
+ form.append('hash', testHash);
589
+
590
+ // First upload
591
+ const upload1 = await axios.post(baseUrl, form, {
592
+ headers: form.getHeaders(),
593
+ validateStatus: status => true,
594
+ timeout: 5000
595
+ });
596
+ t.is(upload1.status, 200, 'First upload should succeed');
597
+ const originalUrl = upload1.data.url;
598
+
599
+ // Check hash exists and returns the correct URL
600
+ const hashCheck1 = await axios.get(baseUrl, {
601
+ params: { hash: testHash, checkHash: true },
602
+ validateStatus: status => true
603
+ });
604
+ t.is(hashCheck1.status, 200, 'Hash should exist after first upload');
605
+ t.truthy(hashCheck1.data.url, 'Hash check should return URL');
606
+ t.is(hashCheck1.data.url, originalUrl, 'Hash check should return original upload URL');
607
+
608
+ // Verify file is accessible via URL from hash check
609
+ const fileResponse = await axios.get(convertToLocalUrl(hashCheck1.data.url), {
610
+ validateStatus: status => true,
611
+ timeout: 5000
612
+ });
613
+ t.is(fileResponse.status, 200, 'File should be accessible');
614
+ t.is(fileResponse.data, testContent, 'File content should match original');
615
+
616
+ // Second upload with same hash
617
+ const upload2 = await axios.post(baseUrl, form, {
618
+ headers: form.getHeaders(),
619
+ validateStatus: status => true,
620
+ timeout: 5000
621
+ });
622
+ t.is(upload2.status, 200, 'Second upload should succeed');
623
+ t.is(upload2.data.url, originalUrl, 'URLs should match for same hash');
624
+
625
+ // Verify file is still accessible after second upload
626
+ const fileResponse2 = await axios.get(convertToLocalUrl(upload2.data.url), {
627
+ validateStatus: status => true,
628
+ timeout: 5000
629
+ });
630
+ t.is(fileResponse2.status, 200, 'File should still be accessible');
631
+ t.is(fileResponse2.data, testContent, 'File content should still match original');
632
+
633
+ // Clean up
634
+ await cleanupUploadedFile(t, originalUrl);
635
+
636
+ // Verify hash is now gone
637
+ const hashCheckAfterDelete = await axios.get(baseUrl, {
638
+ params: { hash: testHash, checkHash: true },
639
+ validateStatus: status => true
640
+ });
641
+ t.is(hashCheckAfterDelete.status, 404, 'Hash should be gone after file deletion');
642
+ });
643
+
644
+ // Helper to check if GCS is configured
645
+ function isGCSConfigured() {
646
+ return process.env.GCP_SERVICE_ACCOUNT_KEY && process.env.STORAGE_EMULATOR_HOST;
647
+ }
648
+
649
+ // Helper function to check if file exists in fake GCS
650
+ async function checkGCSFile(gcsUrl) {
651
+ // Convert gs:// URL to bucket and object path
652
+ const [, , bucket, ...objectParts] = gcsUrl.split('/');
653
+ const object = objectParts.join('/');
654
+
655
+ // Query fake-gcs-server
656
+ const response = await axios.get(`http://localhost:4443/storage/v1/b/${bucket}/o/${encodeURIComponent(object)}`, {
657
+ validateStatus: status => true
658
+ });
659
+ return response.status === 200;
660
+ }
661
+
662
+ // Helper function to verify file exists in both storages
663
+ async function verifyFileInBothStorages(t, uploadResponse) {
664
+ // Verify Azure URL is accessible
665
+ const azureResponse = await axios.get(convertToLocalUrl(uploadResponse.data.url), {
666
+ validateStatus: status => true,
667
+ timeout: 5000
668
+ });
669
+ t.is(azureResponse.status, 200, 'File should be accessible in Azure');
670
+
671
+ if (isGCSConfigured()) {
672
+ // Verify GCS URL exists and is in correct format
673
+ t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
674
+ t.true(uploadResponse.data.gcs.startsWith('gs://'), 'GCS URL should use gs:// protocol');
675
+
676
+ // Check if file exists in fake GCS
677
+ const exists = await checkGCSFile(uploadResponse.data.gcs);
678
+ t.true(exists, 'File should exist in GCS');
679
+ }
680
+ }
681
+
682
+ // Helper function to verify file is deleted from both storages
683
+ async function verifyFileDeletedFromBothStorages(t, uploadResponse) {
684
+ // Verify Azure URL is no longer accessible
685
+ const azureResponse = await axios.get(convertToLocalUrl(uploadResponse.data.url), {
686
+ validateStatus: status => true,
687
+ timeout: 5000
688
+ });
689
+ t.is(azureResponse.status, 404, 'File should not be accessible in Azure');
690
+
691
+ if (isGCSConfigured()) {
692
+ // Verify file is also deleted from GCS
693
+ const exists = await checkGCSFile(uploadResponse.data.gcs);
694
+ t.false(exists, 'File should not exist in GCS');
695
+ }
696
+ }
697
+
698
+ test.serial('should handle dual storage upload and cleanup when GCS configured', async t => {
699
+ if (!isGCSConfigured()) {
700
+ t.pass('Skipping test - GCS not configured');
701
+ return;
702
+ }
703
+
704
+ const requestId = uuidv4();
705
+ const testContent = 'test content for dual storage';
706
+ const form = new FormData();
707
+ form.append('file', Buffer.from(testContent), 'dual-test.txt');
708
+ form.append('requestId', requestId);
709
+
710
+ // Upload file
711
+ const uploadResponse = await uploadFile(Buffer.from(testContent), requestId);
712
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
713
+ t.truthy(uploadResponse.data.url, 'Response should contain Azure URL');
714
+ t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
715
+ t.true(uploadResponse.data.gcs.startsWith('gs://'), 'GCS URL should use gs:// protocol');
716
+
717
+ // Verify file exists in both storages
718
+ await verifyFileInBothStorages(t, uploadResponse);
719
+
720
+ // Get the folder name (requestId) from the URL
721
+ const fileRequestId = getFolderNameFromUrl(uploadResponse.data.url);
722
+
723
+ // Delete file using the correct requestId
724
+ const deleteResponse = await axios.delete(`${baseUrl}?operation=delete&requestId=${fileRequestId}`);
725
+ t.is(deleteResponse.status, 200, 'Delete should succeed');
726
+
727
+ // Verify file is deleted from both storages
728
+ await verifyFileDeletedFromBothStorages(t, uploadResponse);
729
+ });
730
+
731
+ test.serial('should handle GCS URL format and accessibility', async t => {
732
+ if (!isGCSConfigured()) {
733
+ t.pass('Skipping test - GCS not configured');
734
+ return;
735
+ }
736
+
737
+ const requestId = uuidv4();
738
+ const testContent = 'test content for GCS URL verification';
739
+ const form = new FormData();
740
+ form.append('file', Buffer.from(testContent), 'gcs-url-test.txt');
741
+
742
+ // Upload with explicit GCS preference
743
+ const uploadResponse = await axios.post(`http://localhost:${port}/api/CortexFileHandler`, form, {
744
+ params: {
745
+ operation: 'upload',
746
+ requestId,
747
+ useGCS: true
748
+ },
749
+ headers: form.getHeaders()
750
+ });
751
+
752
+ t.is(uploadResponse.status, 200, 'Upload should succeed');
753
+ t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
754
+ t.true(uploadResponse.data.gcs.startsWith('gs://'), 'GCS URL should use gs:// protocol');
755
+
756
+ // Verify content is accessible via normal URL since we can't directly access gs:// URLs
757
+ const fileResponse = await axios.get(uploadResponse.data.url);
758
+ t.is(fileResponse.status, 200, 'File should be accessible');
759
+ t.is(fileResponse.data, testContent, 'Content should match original');
760
+
761
+ // Clean up
762
+ await cleanupUploadedFile(t, uploadResponse.data.url);
763
+ });
764
+
765
+ // Legacy MediaFileChunker Tests
766
+ test.serial('should handle file upload through legacy MediaFileChunker endpoint', async t => {
767
+ const form = new FormData();
768
+ form.append('file', Buffer.from('test content'), 'test.txt');
769
+
770
+ const response = await axios.post(
771
+ `http://localhost:${port}/api/MediaFileChunker`,
772
+ form,
773
+ {
774
+ headers: {
775
+ ...form.getHeaders(),
776
+ 'Content-Type': 'multipart/form-data'
777
+ },
778
+ validateStatus: status => true,
779
+ timeout: 5000
780
+ }
781
+ );
782
+
783
+ t.is(response.status, 200, 'Upload through legacy endpoint should succeed');
784
+ t.truthy(response.data.url, 'Response should contain file URL');
785
+
786
+ await cleanupUploadedFile(t, response.data.url);
787
+ });
788
+
789
+ test.serial('should handle hash operations through legacy MediaFileChunker endpoint', async t => {
790
+ const testHash = 'test-hash-legacy';
791
+ const form = new FormData();
792
+ form.append('file', Buffer.from('test content'), 'test.txt');
793
+ form.append('hash', testHash);
794
+
795
+ // Upload file with hash through legacy endpoint
796
+ const uploadResponse = await axios.post(
797
+ `http://localhost:${port}/api/MediaFileChunker`,
798
+ form,
799
+ {
800
+ headers: {
801
+ ...form.getHeaders(),
802
+ 'Content-Type': 'multipart/form-data'
803
+ },
804
+ validateStatus: status => true,
805
+ timeout: 5000
806
+ }
807
+ );
808
+
809
+ t.is(uploadResponse.status, 200, 'Upload should succeed through legacy endpoint');
810
+ t.truthy(uploadResponse.data.url, 'Response should contain file URL');
811
+
812
+ // Wait a bit for Redis to be updated
813
+ await new Promise(resolve => setTimeout(resolve, 1000));
814
+
815
+ // Check hash through legacy endpoint
816
+ const hashCheckResponse = await axios.get(`http://localhost:${port}/api/MediaFileChunker`, {
817
+ params: {
818
+ hash: testHash,
819
+ checkHash: true
820
+ },
821
+ validateStatus: status => true,
822
+ timeout: 5000
823
+ });
824
+
825
+ t.is(hashCheckResponse.status, 404, 'Hash check should return 404 for new hash');
826
+ t.is(hashCheckResponse.data, `Hash ${testHash} not found`, 'Should indicate hash not found');
827
+
828
+ await cleanupUploadedFile(t, uploadResponse.data.url);
829
+ });
830
+
831
+ test.serial('should handle delete operation through legacy MediaFileChunker endpoint', async t => {
832
+ const testRequestId = 'test-delete-request-legacy';
833
+ const testContent = 'test content';
834
+ const form = new FormData();
835
+ form.append('file', Buffer.from(testContent), 'test.txt');
836
+
837
+ // Upload a file first through legacy endpoint
838
+ const uploadResponse = await axios.post(
839
+ `http://localhost:${port}/api/MediaFileChunker`,
840
+ form,
841
+ {
842
+ headers: form.getHeaders(),
843
+ validateStatus: status => true,
844
+ timeout: 5000
845
+ }
846
+ );
847
+ t.is(uploadResponse.status, 200, 'Upload should succeed through legacy endpoint');
848
+
849
+ // Extract the folder name from the URL
850
+ const url = uploadResponse.data.url;
851
+ const folderName = getFolderNameFromUrl(url);
852
+
853
+ // Delete the file through legacy endpoint
854
+ const deleteResponse = await axios.delete(`http://localhost:${port}/api/MediaFileChunker?operation=delete&requestId=${folderName}`);
855
+ t.is(deleteResponse.status, 200, 'Delete should succeed through legacy endpoint');
856
+ t.true(Array.isArray(deleteResponse.data.body), 'Response should be an array of deleted files');
857
+ t.true(deleteResponse.data.body.length > 0, 'Should have deleted at least one file');
858
+ t.true(deleteResponse.data.body[0].includes(folderName), 'Deleted file should contain folder name');
859
+ });
860
+
861
+ test.serial('should handle parameter validation through legacy MediaFileChunker endpoint', async t => {
862
+ // Test missing parameters
863
+ const response = await axios.get(`http://localhost:${port}/api/MediaFileChunker`, {
864
+ validateStatus: status => true,
865
+ timeout: 5000
866
+ });
867
+
868
+ t.is(response.status, 400, 'Should return 400 for missing parameters');
869
+ t.is(
870
+ response.data,
871
+ 'Please pass a uri and requestId on the query string or in the request body',
872
+ 'Should return proper error message'
873
+ );
874
+ });
875
+
876
+ test.serial('should handle empty POST request through legacy MediaFileChunker endpoint', async t => {
877
+ const form = new FormData();
878
+ try {
879
+ await axios.post(
880
+ `http://localhost:${port}/api/MediaFileChunker`,
881
+ form,
882
+ {
883
+ headers: form.getHeaders(),
884
+ timeout: 5000
885
+ }
886
+ );
887
+ t.fail('Should have thrown error');
888
+ } catch (error) {
889
+ t.is(error.response.status, 400, 'Should return 400 for empty POST request');
890
+ t.is(error.response.data, 'No file provided in request', 'Should return proper error message');
891
+ }
892
+ });
893
+
894
+ test.serial('should handle complete upload-request-delete-verify sequence through legacy MediaFileChunker endpoint', async t => {
895
+ const testContent = 'test content for legacy sequence';
896
+ const testHash = 'test-legacy-sequence-hash';
897
+ const form = new FormData();
898
+ form.append('file', Buffer.from(testContent), 'sequence-test.txt');
899
+ form.append('hash', testHash);
900
+
901
+ // Upload file with hash through legacy endpoint
902
+ const uploadResponse = await axios.post(
903
+ `http://localhost:${port}/api/MediaFileChunker`,
904
+ form,
905
+ {
906
+ headers: form.getHeaders(),
907
+ validateStatus: status => true,
908
+ timeout: 5000
909
+ }
910
+ );
911
+ t.is(uploadResponse.status, 200, 'Upload should succeed through legacy endpoint');
912
+ t.truthy(uploadResponse.data.url, 'Response should contain URL');
913
+
914
+ await cleanupUploadedFile(t, uploadResponse.data.url);
915
+
916
+ // Verify hash is gone by trying to get the file URL through legacy endpoint
917
+ const hashCheckResponse = await axios.get(`http://localhost:${port}/api/MediaFileChunker`, {
918
+ params: {
919
+ hash: testHash,
920
+ checkHash: true
921
+ },
922
+ validateStatus: status => true
923
+ });
924
+ t.is(hashCheckResponse.status, 404, 'Hash should not exist after deletion');
925
+ });
926
+
927
+ // Cleanup
928
+ test.after.always('cleanup', async t => {
929
+ // Add any necessary cleanup here
930
+ });