@aj-archipelago/cortex 1.3.58 → 1.3.60

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 (57) hide show
  1. package/config/default.example.json +15 -1
  2. package/config.js +42 -0
  3. package/helper-apps/cortex-file-handler/INTERFACE.md +20 -9
  4. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  5. package/helper-apps/cortex-file-handler/package.json +1 -1
  6. package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +17 -17
  7. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +35 -35
  8. package/helper-apps/cortex-file-handler/src/blobHandler.js +1010 -909
  9. package/helper-apps/cortex-file-handler/src/constants.js +98 -98
  10. package/helper-apps/cortex-file-handler/src/docHelper.js +27 -27
  11. package/helper-apps/cortex-file-handler/src/fileChunker.js +224 -214
  12. package/helper-apps/cortex-file-handler/src/helper.js +93 -93
  13. package/helper-apps/cortex-file-handler/src/index.js +584 -550
  14. package/helper-apps/cortex-file-handler/src/localFileHandler.js +86 -86
  15. package/helper-apps/cortex-file-handler/src/redis.js +186 -90
  16. package/helper-apps/cortex-file-handler/src/services/ConversionService.js +301 -273
  17. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +55 -55
  18. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +174 -154
  19. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +239 -223
  20. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +161 -159
  21. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +73 -71
  22. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +46 -45
  23. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +256 -213
  24. package/helper-apps/cortex-file-handler/src/start.js +4 -1
  25. package/helper-apps/cortex-file-handler/src/utils/filenameUtils.js +59 -25
  26. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +119 -116
  27. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +257 -257
  28. package/helper-apps/cortex-file-handler/tests/cleanup.test.js +676 -0
  29. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +124 -124
  30. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +249 -208
  31. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +439 -380
  32. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +299 -263
  33. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +265 -239
  34. package/helper-apps/cortex-file-handler/tests/start.test.js +1230 -1201
  35. package/helper-apps/cortex-file-handler/tests/storage/AzureStorageProvider.test.js +110 -105
  36. package/helper-apps/cortex-file-handler/tests/storage/GCSStorageProvider.test.js +201 -175
  37. package/helper-apps/cortex-file-handler/tests/storage/LocalStorageProvider.test.js +128 -125
  38. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +78 -73
  39. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +99 -99
  40. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -70
  41. package/lib/azureAuthTokenHelper.js +78 -0
  42. package/lib/entityConstants.js +5 -4
  43. package/package.json +1 -1
  44. package/pathways/bing_afagent.js +13 -0
  45. package/pathways/gemini_15_vision.js +4 -0
  46. package/pathways/system/entity/tools/sys_tool_bing_search.js +1 -1
  47. package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +141 -0
  48. package/pathways/system/entity/tools/sys_tool_browser_jina.js +1 -1
  49. package/pathways/system/entity/tools/sys_tool_readfile.js +4 -0
  50. package/pathways/system/workspaces/workspace_applet_edit.js +4 -0
  51. package/pathways/transcribe_gemini.js +4 -0
  52. package/pathways/translate_subtitle.js +15 -8
  53. package/server/modelExecutor.js +4 -0
  54. package/server/plugins/azureFoundryAgentsPlugin.js +372 -0
  55. package/server/plugins/gemini15ChatPlugin.js +3 -3
  56. package/tests/azureAuthTokenHelper.test.js +150 -0
  57. package/tests/azureFoundryAgents.test.js +212 -0
@@ -1,1407 +1,1436 @@
1
1
  /* eslint-disable no-unused-vars */
2
- import { execSync } from 'child_process';
3
- import fs from 'fs';
4
- import os from 'os';
5
- import path from 'path';
6
- import { PassThrough } from 'stream';
7
-
8
- import test from 'ava';
9
- import axios from 'axios';
2
+ import { execSync } from "child_process";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { PassThrough } from "stream";
7
+
8
+ import test from "ava";
9
+ import axios from "axios";
10
10
  // eslint-disable-next-line import/no-extraneous-dependencies
11
- import FormData from 'form-data';
12
- import { v4 as uuidv4 } from 'uuid';
11
+ import FormData from "form-data";
12
+ import { v4 as uuidv4 } from "uuid";
13
13
 
14
- import { port, publicFolder, ipAddress } from '../src/start.js';
15
- import { cleanupHashAndFile, getFolderNameFromUrl } from './testUtils.helper.js';
14
+ import { port, publicFolder, ipAddress } from "../src/start.js";
15
+ import {
16
+ cleanupHashAndFile,
17
+ getFolderNameFromUrl,
18
+ } from "./testUtils.helper.js";
16
19
 
17
20
  // Add these helper functions at the top after imports
18
21
  const baseUrl = `http://localhost:${port}/api/CortexFileHandler`;
19
22
 
20
23
  // Helper function to determine if Azure is configured
21
24
  function isAzureConfigured() {
22
- return (
23
- process.env.AZURE_STORAGE_CONNECTION_STRING &&
24
- process.env.AZURE_STORAGE_CONNECTION_STRING !== 'UseDevelopmentStorage=true'
25
- );
25
+ return (
26
+ process.env.AZURE_STORAGE_CONNECTION_STRING &&
27
+ process.env.AZURE_STORAGE_CONNECTION_STRING !== "UseDevelopmentStorage=true"
28
+ );
26
29
  }
27
30
 
28
31
  // Helper function to convert URLs for testing
29
32
  function convertToLocalUrl(url) {
30
- // If it's an Azurite URL (contains 127.0.0.1:10000), use it as is
31
- if (url.includes('127.0.0.1:10000')) {
32
- return url;
33
- }
34
- // For local storage URLs, convert any IP:port to localhost:port
35
- const urlObj = new URL(url);
36
- return url.replace(urlObj.host, `localhost:${port}`);
33
+ // If it's an Azurite URL (contains 127.0.0.1:10000), use it as is
34
+ if (url.includes("127.0.0.1:10000")) {
35
+ return url;
36
+ }
37
+ // For local storage URLs, convert any IP:port to localhost:port
38
+ const urlObj = new URL(url);
39
+ return url.replace(urlObj.host, `localhost:${port}`);
37
40
  }
38
41
 
39
42
  // Helper function to clean up uploaded files
40
43
  async function cleanupUploadedFile(t, url) {
41
- // Convert URL to use localhost
42
- url = convertToLocalUrl(url);
43
- const folderName = getFolderNameFromUrl(url);
44
-
45
- // Delete the file
46
- const deleteResponse = await axios.delete(
47
- `${baseUrl}?operation=delete&requestId=${folderName}`,
48
- );
49
- t.is(deleteResponse.status, 200, 'Delete should succeed');
50
- t.true(
51
- Array.isArray(deleteResponse.data.body),
52
- 'Delete response should be an array',
53
- );
54
- t.true(
55
- deleteResponse.data.body.length > 0,
56
- 'Should have deleted at least one file',
57
- );
58
-
59
- // Verify file is gone
60
- const verifyResponse = await axios.get(url, {
61
- validateStatus: (status) => true,
62
- timeout: 5000,
63
- });
64
- t.is(verifyResponse.status, 404, 'File should not exist after deletion');
44
+ // Convert URL to use localhost
45
+ url = convertToLocalUrl(url);
46
+ const folderName = getFolderNameFromUrl(url);
47
+
48
+ // Delete the file
49
+ const deleteResponse = await axios.delete(
50
+ `${baseUrl}?operation=delete&requestId=${folderName}`,
51
+ );
52
+ t.is(deleteResponse.status, 200, "Delete should succeed");
53
+ t.true(
54
+ Array.isArray(deleteResponse.data.body),
55
+ "Delete response should be an array",
56
+ );
57
+ t.true(
58
+ deleteResponse.data.body.length > 0,
59
+ "Should have deleted at least one file",
60
+ );
61
+
62
+ // Verify file is gone
63
+ const verifyResponse = await axios.get(url, {
64
+ validateStatus: (status) => true,
65
+ timeout: 5000,
66
+ });
67
+ t.is(verifyResponse.status, 404, "File should not exist after deletion");
65
68
  }
66
69
 
67
70
  // Helper function to upload files
68
71
  async function uploadFile(file, requestId, hash = null) {
69
- const form = new FormData();
70
-
71
- // If file is a Buffer, create a Readable stream
72
- if (Buffer.isBuffer(file)) {
73
- const { Readable } = await import('stream');
74
- const stream = Readable.from(file);
75
- form.append('file', stream, { filename: 'test.txt' });
76
- } else {
77
- form.append('file', file);
78
- }
72
+ const form = new FormData();
73
+
74
+ // If file is a Buffer, create a Readable stream
75
+ if (Buffer.isBuffer(file)) {
76
+ const { Readable } = await import("stream");
77
+ const stream = Readable.from(file);
78
+ form.append("file", stream, { filename: "test.txt" });
79
+ } else {
80
+ form.append("file", file);
81
+ }
82
+
83
+ if (requestId) form.append("requestId", requestId);
84
+ if (hash) form.append("hash", hash);
85
+
86
+ const response = await axios.post(baseUrl, form, {
87
+ headers: {
88
+ ...form.getHeaders(),
89
+ "Content-Type": "multipart/form-data",
90
+ },
91
+ validateStatus: (status) => true,
92
+ timeout: 5000,
93
+ maxContentLength: Infinity,
94
+ maxBodyLength: Infinity,
95
+ });
79
96
 
80
- if (requestId) form.append('requestId', requestId);
81
- if (hash) form.append('hash', hash);
97
+ if (response.data?.url) {
98
+ response.data.url = convertToLocalUrl(response.data.url);
99
+ }
82
100
 
83
- const response = await axios.post(baseUrl, form, {
84
- headers: {
85
- ...form.getHeaders(),
86
- 'Content-Type': 'multipart/form-data',
87
- },
88
- validateStatus: (status) => true,
89
- timeout: 5000,
90
- maxContentLength: Infinity,
91
- maxBodyLength: Infinity,
92
- });
93
-
94
- if (response.data?.url) {
95
- response.data.url = convertToLocalUrl(response.data.url);
96
- }
97
-
98
- return response;
101
+ return response;
99
102
  }
100
103
 
101
104
  // Ensure server is ready before tests
102
105
  test.before(async (t) => {
103
- // Wait for server to be ready
104
- await new Promise((resolve) => setTimeout(resolve, 1000));
106
+ // Wait for server to be ready
107
+ await new Promise((resolve) => setTimeout(resolve, 1000));
105
108
 
106
- // Verify server is responding
107
- try {
108
- await axios.get(`http://localhost:${port}/files`);
109
- } catch (error) {
109
+ // Verify server is responding
110
+ try {
111
+ await axios.get(`http://localhost:${port}/files`);
112
+ } catch (error) {
110
113
  // 404 is fine, it means server is running but directory is empty
111
- if (error.response?.status !== 404) {
112
- throw new Error('Server not ready');
113
- }
114
+ if (error.response?.status !== 404) {
115
+ throw new Error("Server not ready");
114
116
  }
117
+ }
115
118
  });
116
119
 
117
120
  // Configuration Tests
118
- test('should have valid server configuration', (t) => {
119
- t.truthy(port, 'Port should be defined');
120
- t.truthy(publicFolder, 'Public folder should be defined');
121
- t.truthy(ipAddress, 'IP address should be defined');
121
+ test("should have valid server configuration", (t) => {
122
+ t.truthy(port, "Port should be defined");
123
+ t.truthy(publicFolder, "Public folder should be defined");
124
+ t.truthy(ipAddress, "IP address should be defined");
122
125
  });
123
126
 
124
127
  // Parameter Validation Tests
125
128
  test.serial(
126
- 'should validate required parameters on CortexFileHandler endpoint',
127
- async (t) => {
128
- const response = await axios.get(
129
- `http://localhost:${port}/api/CortexFileHandler`,
130
- {
131
- validateStatus: (status) => true,
132
- timeout: 5000,
133
- },
134
- );
135
-
136
- t.is(response.status, 400, 'Should return 400 for missing parameters');
137
- t.is(
138
- response.data,
139
- 'Please pass a uri and requestId on the query string or in the request body',
140
- 'Should return proper error message',
141
- );
142
- },
143
- );
144
-
145
- test.serial(
146
- 'should validate required parameters on MediaFileChunker legacy endpoint',
147
- async (t) => {
148
- const response = await axios.get(
149
- `http://localhost:${port}/api/MediaFileChunker`,
150
- {
151
- validateStatus: (status) => true,
152
- timeout: 5000,
153
- },
154
- );
155
-
156
- t.is(response.status, 400, 'Should return 400 for missing parameters');
157
- t.is(
158
- response.data,
159
- 'Please pass a uri and requestId on the query string or in the request body',
160
- 'Should return proper error message',
161
- );
162
- },
163
- );
164
-
165
- // Static Files Tests
166
- test.serial('should serve static files from public directory', async (t) => {
167
- try {
168
- const response = await axios.get(`http://localhost:${port}/files`, {
169
- timeout: 5000,
170
- validateStatus: (status) => status === 200 || status === 404,
171
- });
172
-
173
- t.true(
174
- response.status === 200 || response.status === 404,
175
- 'Should respond with 200 or 404 for static files',
176
- );
177
- } catch (error) {
178
- t.fail(`Failed to connect to files endpoint: ${error.message}`);
179
- }
180
- });
181
-
182
- // Hash Operation Tests
183
- test.serial('should handle non-existent hash check', async (t) => {
129
+ "should validate required parameters on CortexFileHandler endpoint",
130
+ async (t) => {
184
131
  const response = await axios.get(
185
- `http://localhost:${port}/api/CortexFileHandler`,
186
- {
187
- params: {
188
- hash: 'nonexistent-hash',
189
- checkHash: true,
190
- },
191
- validateStatus: (status) => true,
192
- timeout: 5000,
193
- },
132
+ `http://localhost:${port}/api/CortexFileHandler`,
133
+ {
134
+ validateStatus: (status) => true,
135
+ timeout: 5000,
136
+ },
194
137
  );
195
138
 
196
- t.is(response.status, 404, 'Should return 404 for non-existent hash');
139
+ t.is(response.status, 400, "Should return 400 for missing parameters");
197
140
  t.is(
198
- response.data,
199
- 'Hash nonexistent-hash not found',
200
- 'Should return proper error message',
141
+ response.data,
142
+ "Please pass a uri and requestId on the query string or in the request body",
143
+ "Should return proper error message",
201
144
  );
202
- });
145
+ },
146
+ );
203
147
 
204
- test.serial('should handle hash clearing for non-existent hash', async (t) => {
148
+ test.serial(
149
+ "should validate required parameters on MediaFileChunker legacy endpoint",
150
+ async (t) => {
205
151
  const response = await axios.get(
206
- `http://localhost:${port}/api/CortexFileHandler`,
207
- {
208
- params: {
209
- hash: 'nonexistent-hash',
210
- clearHash: true,
211
- },
212
- validateStatus: (status) => true,
213
- timeout: 5000,
214
- },
152
+ `http://localhost:${port}/api/MediaFileChunker`,
153
+ {
154
+ validateStatus: (status) => true,
155
+ timeout: 5000,
156
+ },
215
157
  );
216
158
 
217
- t.is(response.status, 404, 'Should return 404 for non-existent hash');
159
+ t.is(response.status, 400, "Should return 400 for missing parameters");
218
160
  t.is(
219
- response.data,
220
- 'Hash nonexistent-hash not found',
221
- 'Should return proper message',
161
+ response.data,
162
+ "Please pass a uri and requestId on the query string or in the request body",
163
+ "Should return proper error message",
222
164
  );
223
- });
224
-
225
- test.serial(
226
- 'should handle hash operations without hash parameter',
227
- async (t) => {
228
- const response = await axios.get(
229
- `http://localhost:${port}/api/CortexFileHandler`,
230
- {
231
- params: {
232
- checkHash: true,
233
- },
234
- validateStatus: (status) => true,
235
- timeout: 5000,
236
- },
237
- );
238
-
239
- t.is(response.status, 400, 'Should return 400 for missing hash');
240
- t.is(
241
- response.data,
242
- 'Please pass a uri and requestId on the query string or in the request body',
243
- 'Should return proper error message',
244
- );
245
- },
165
+ },
246
166
  );
247
167
 
248
- // URL Validation Tests
249
- test.serial('should reject invalid URLs', async (t) => {
250
- const response = await axios.get(
251
- `http://localhost:${port}/api/CortexFileHandler`,
252
- {
253
- params: {
254
- uri: 'not-a-valid-url',
255
- requestId: 'test-request',
256
- },
257
- validateStatus: (status) => true,
258
- timeout: 5000,
259
- },
260
- );
168
+ // Static Files Tests
169
+ test.serial("should serve static files from public directory", async (t) => {
170
+ try {
171
+ const response = await axios.get(`http://localhost:${port}/files`, {
172
+ timeout: 5000,
173
+ validateStatus: (status) => status === 200 || status === 404,
174
+ });
261
175
 
262
- t.is(response.status, 400, 'Should return 400 for invalid URL');
263
- t.is(
264
- response.data,
265
- 'Invalid URL format',
266
- 'Should indicate invalid URL format in error message',
176
+ t.true(
177
+ response.status === 200 || response.status === 404,
178
+ "Should respond with 200 or 404 for static files",
267
179
  );
180
+ } catch (error) {
181
+ t.fail(`Failed to connect to files endpoint: ${error.message}`);
182
+ }
268
183
  });
269
184
 
270
- test.serial('should reject unsupported protocols', async (t) => {
271
- const response = await axios.get(
272
- `http://localhost:${port}/api/CortexFileHandler`,
273
- {
274
- params: {
275
- uri: 'ftp://example.com/test.mp3',
276
- requestId: 'test-request',
277
- },
278
- validateStatus: (status) => true,
279
- timeout: 5000,
280
- },
281
- );
185
+ // Hash Operation Tests
186
+ test.serial("should handle non-existent hash check", async (t) => {
187
+ const response = await axios.get(
188
+ `http://localhost:${port}/api/CortexFileHandler`,
189
+ {
190
+ params: {
191
+ hash: "nonexistent-hash",
192
+ checkHash: true,
193
+ },
194
+ validateStatus: (status) => true,
195
+ timeout: 5000,
196
+ },
197
+ );
198
+
199
+ t.is(response.status, 404, "Should return 404 for non-existent hash");
200
+ t.is(
201
+ response.data,
202
+ "Hash nonexistent-hash not found",
203
+ "Should return proper error message",
204
+ );
205
+ });
282
206
 
283
- t.is(response.status, 400, 'Should return 400 for unsupported protocol');
284
- t.is(
285
- response.data,
286
- 'Invalid URL protocol - only HTTP, HTTPS, and GCS URLs are supported',
287
- 'Should indicate invalid protocol in error message',
288
- );
207
+ test.serial("should handle hash clearing for non-existent hash", async (t) => {
208
+ const response = await axios.get(
209
+ `http://localhost:${port}/api/CortexFileHandler`,
210
+ {
211
+ params: {
212
+ hash: "nonexistent-hash",
213
+ clearHash: true,
214
+ },
215
+ validateStatus: (status) => true,
216
+ timeout: 5000,
217
+ },
218
+ );
219
+
220
+ t.is(response.status, 404, "Should return 404 for non-existent hash");
221
+ t.is(
222
+ response.data,
223
+ "Hash nonexistent-hash not found",
224
+ "Should return proper message",
225
+ );
289
226
  });
290
227
 
291
- // Remote File Operation Tests
292
- test.serial('should validate remote file URL format', async (t) => {
228
+ test.serial(
229
+ "should handle hash operations without hash parameter",
230
+ async (t) => {
293
231
  const response = await axios.get(
294
- `http://localhost:${port}/api/CortexFileHandler`,
295
- {
296
- params: {
297
- fetch: 'not-a-valid-url',
298
- },
299
- validateStatus: (status) => true,
300
- timeout: 5000,
232
+ `http://localhost:${port}/api/CortexFileHandler`,
233
+ {
234
+ params: {
235
+ checkHash: true,
301
236
  },
237
+ validateStatus: (status) => true,
238
+ timeout: 5000,
239
+ },
302
240
  );
303
241
 
304
- t.is(response.status, 400, 'Should return 400 for invalid remote URL');
242
+ t.is(response.status, 400, "Should return 400 for missing hash");
305
243
  t.is(
306
- response.data,
307
- 'Invalid or inaccessible URL',
308
- 'Should return proper error message',
244
+ response.data,
245
+ "Please pass a uri and requestId on the query string or in the request body",
246
+ "Should return proper error message",
309
247
  );
248
+ },
249
+ );
250
+
251
+ // URL Validation Tests
252
+ test.serial("should reject invalid URLs", async (t) => {
253
+ const response = await axios.get(
254
+ `http://localhost:${port}/api/CortexFileHandler`,
255
+ {
256
+ params: {
257
+ uri: "not-a-valid-url",
258
+ requestId: "test-request",
259
+ },
260
+ validateStatus: (status) => true,
261
+ timeout: 5000,
262
+ },
263
+ );
264
+
265
+ t.is(response.status, 400, "Should return 400 for invalid URL");
266
+ t.is(
267
+ response.data,
268
+ "Invalid URL format",
269
+ "Should indicate invalid URL format in error message",
270
+ );
310
271
  });
311
272
 
312
- test.serial('should handle restore operation with invalid URL', async (t) => {
313
- const response = await axios.get(
314
- `http://localhost:${port}/api/CortexFileHandler`,
315
- {
316
- params: {
317
- restore: 'not-a-valid-url',
318
- },
319
- validateStatus: (status) => true,
320
- timeout: 5000,
321
- },
322
- );
273
+ test.serial("should reject unsupported protocols", async (t) => {
274
+ const response = await axios.get(
275
+ `http://localhost:${port}/api/CortexFileHandler`,
276
+ {
277
+ params: {
278
+ uri: "ftp://example.com/test.mp3",
279
+ requestId: "test-request",
280
+ },
281
+ validateStatus: (status) => true,
282
+ timeout: 5000,
283
+ },
284
+ );
285
+
286
+ t.is(response.status, 400, "Should return 400 for unsupported protocol");
287
+ t.is(
288
+ response.data,
289
+ "Invalid URL protocol - only HTTP, HTTPS, and GCS URLs are supported",
290
+ "Should indicate invalid protocol in error message",
291
+ );
292
+ });
323
293
 
324
- t.is(response.status, 400, 'Should return 400 for invalid restore URL');
325
- t.is(
326
- response.data,
327
- 'Invalid or inaccessible URL',
328
- 'Should return proper error message',
329
- );
294
+ // Remote File Operation Tests
295
+ test.serial("should validate remote file URL format", async (t) => {
296
+ const response = await axios.get(
297
+ `http://localhost:${port}/api/CortexFileHandler`,
298
+ {
299
+ params: {
300
+ fetch: "not-a-valid-url",
301
+ },
302
+ validateStatus: (status) => true,
303
+ timeout: 5000,
304
+ },
305
+ );
306
+
307
+ t.is(response.status, 400, "Should return 400 for invalid remote URL");
308
+ t.is(
309
+ response.data,
310
+ "Invalid or inaccessible URL",
311
+ "Should return proper error message",
312
+ );
330
313
  });
331
314
 
332
- test.serial('should handle load operation with invalid URL', async (t) => {
333
- const response = await axios.get(
334
- `http://localhost:${port}/api/CortexFileHandler`,
335
- {
336
- params: {
337
- load: 'not-a-valid-url',
338
- },
339
- validateStatus: (status) => true,
340
- timeout: 5000,
341
- },
342
- );
315
+ test.serial("should handle restore operation with invalid URL", async (t) => {
316
+ const response = await axios.get(
317
+ `http://localhost:${port}/api/CortexFileHandler`,
318
+ {
319
+ params: {
320
+ restore: "not-a-valid-url",
321
+ },
322
+ validateStatus: (status) => true,
323
+ timeout: 5000,
324
+ },
325
+ );
326
+
327
+ t.is(response.status, 400, "Should return 400 for invalid restore URL");
328
+ t.is(
329
+ response.data,
330
+ "Invalid or inaccessible URL",
331
+ "Should return proper error message",
332
+ );
333
+ });
343
334
 
344
- t.is(response.status, 400, 'Should return 400 for invalid load URL');
345
- t.is(
346
- response.data,
347
- 'Invalid or inaccessible URL',
348
- 'Should return proper error message',
349
- );
335
+ test.serial("should handle load operation with invalid URL", async (t) => {
336
+ const response = await axios.get(
337
+ `http://localhost:${port}/api/CortexFileHandler`,
338
+ {
339
+ params: {
340
+ load: "not-a-valid-url",
341
+ },
342
+ validateStatus: (status) => true,
343
+ timeout: 5000,
344
+ },
345
+ );
346
+
347
+ t.is(response.status, 400, "Should return 400 for invalid load URL");
348
+ t.is(
349
+ response.data,
350
+ "Invalid or inaccessible URL",
351
+ "Should return proper error message",
352
+ );
350
353
  });
351
354
 
352
355
  // Delete Operation Tests
353
- test.serial('should validate requestId for delete operation', async (t) => {
354
- const response = await axios.delete(
355
- `http://localhost:${port}/api/CortexFileHandler`,
356
- {
357
- validateStatus: (status) => true,
358
- timeout: 5000,
359
- },
360
- );
361
-
362
- t.is(response.status, 400, 'Should return 400 for missing requestId');
363
- t.is(
364
- response.data,
365
- 'Please pass a requestId on the query string',
366
- 'Should return proper error message',
367
- );
356
+ test.serial("should validate requestId for delete operation", async (t) => {
357
+ const response = await axios.delete(
358
+ `http://localhost:${port}/api/CortexFileHandler`,
359
+ {
360
+ validateStatus: (status) => true,
361
+ timeout: 5000,
362
+ },
363
+ );
364
+
365
+ t.is(response.status, 400, "Should return 400 for missing requestId");
366
+ t.is(
367
+ response.data,
368
+ "Please pass a requestId on the query string",
369
+ "Should return proper error message",
370
+ );
368
371
  });
369
372
 
370
- test.serial('should handle delete with valid requestId', async (t) => {
371
- const testRequestId = 'test-delete-request';
372
- const testContent = 'test content';
373
- const form = new FormData();
374
- form.append('file', Buffer.from(testContent), 'test.txt');
375
-
376
- // Upload a file first
377
- const uploadResponse = await axios.post(baseUrl, form, {
378
- headers: form.getHeaders(),
379
- validateStatus: (status) => true,
380
- timeout: 5000,
381
- });
382
- t.is(uploadResponse.status, 200, 'Upload should succeed');
383
-
384
- // Extract the folder name from the URL
385
- const url = uploadResponse.data.url;
386
- const folderName = getFolderNameFromUrl(url);
387
-
388
- // Delete the file
389
- const deleteResponse = await axios.delete(
390
- `${baseUrl}?operation=delete&requestId=${folderName}`,
391
- );
392
- t.is(deleteResponse.status, 200, 'Delete should succeed');
393
- t.true(
394
- Array.isArray(deleteResponse.data.body),
395
- 'Response should be an array of deleted files',
396
- );
397
- t.true(
398
- deleteResponse.data.body.length > 0,
399
- 'Should have deleted at least one file',
400
- );
401
- t.true(
402
- deleteResponse.data.body[0].includes(folderName),
403
- 'Deleted file should contain folder name',
404
- );
373
+ test.serial("should handle delete with valid requestId", async (t) => {
374
+ const testRequestId = "test-delete-request";
375
+ const testContent = "test content";
376
+ const form = new FormData();
377
+ form.append("file", Buffer.from(testContent), "test.txt");
378
+
379
+ // Upload a file first
380
+ const uploadResponse = await axios.post(baseUrl, form, {
381
+ headers: form.getHeaders(),
382
+ validateStatus: (status) => true,
383
+ timeout: 5000,
384
+ });
385
+ t.is(uploadResponse.status, 200, "Upload should succeed");
386
+
387
+ // Extract the folder name from the URL
388
+ const url = uploadResponse.data.url;
389
+ const folderName = getFolderNameFromUrl(url);
390
+
391
+ // Delete the file
392
+ const deleteResponse = await axios.delete(
393
+ `${baseUrl}?operation=delete&requestId=${folderName}`,
394
+ );
395
+ t.is(deleteResponse.status, 200, "Delete should succeed");
396
+ t.true(
397
+ Array.isArray(deleteResponse.data.body),
398
+ "Response should be an array of deleted files",
399
+ );
400
+ t.true(
401
+ deleteResponse.data.body.length > 0,
402
+ "Should have deleted at least one file",
403
+ );
404
+ t.true(
405
+ deleteResponse.data.body[0].includes(folderName),
406
+ "Deleted file should contain folder name",
407
+ );
405
408
  });
406
409
 
407
- test.serial('should handle delete with non-existent requestId', async (t) => {
408
- const response = await axios.delete(
409
- `http://localhost:${port}/api/CortexFileHandler`,
410
- {
411
- params: {
412
- requestId: 'nonexistent-request',
413
- },
414
- validateStatus: (status) => true,
415
- timeout: 30000,
416
- },
417
- );
410
+ test.serial("should handle delete with non-existent requestId", async (t) => {
411
+ const response = await axios.delete(
412
+ `http://localhost:${port}/api/CortexFileHandler`,
413
+ {
414
+ params: {
415
+ requestId: "nonexistent-request",
416
+ },
417
+ validateStatus: (status) => true,
418
+ timeout: 30000,
419
+ },
420
+ );
421
+
422
+ t.is(
423
+ response.status,
424
+ 200,
425
+ "Should return 200 even for non-existent requestId",
426
+ );
427
+ t.deepEqual(
428
+ response.data.body,
429
+ [],
430
+ "Should return empty array for non-existent requestId",
431
+ );
432
+ });
418
433
 
419
- t.is(
420
- response.status,
421
- 200,
422
- 'Should return 200 even for non-existent requestId',
423
- );
424
- t.deepEqual(
425
- response.data.body,
426
- [],
427
- 'Should return empty array for non-existent requestId',
428
- );
434
+ test("should handle delete with invalid requestId", async (t) => {
435
+ const response = await axios.get(
436
+ `http://localhost:${port}/api/CortexFileHandler`,
437
+ {
438
+ params: {
439
+ requestId: "nonexistent-request",
440
+ operation: "delete",
441
+ },
442
+ timeout: 5000,
443
+ },
444
+ );
445
+ t.is(
446
+ response.status,
447
+ 200,
448
+ "Should return 200 for delete with invalid requestId",
449
+ );
450
+ t.true(Array.isArray(response.data.body), "Response should be an array");
451
+ t.is(
452
+ response.data.body.length,
453
+ 0,
454
+ "Response should be empty array for non-existent requestId",
455
+ );
429
456
  });
430
457
 
431
- test('should handle delete with invalid requestId', async (t) => {
432
- const response = await axios.get(
433
- `http://localhost:${port}/api/CortexFileHandler`,
434
- {
435
- params: {
436
- requestId: 'nonexistent-request',
437
- operation: 'delete',
438
- },
439
- timeout: 5000,
440
- },
441
- );
458
+ // POST Operation Tests
459
+ test("should handle empty POST request", async (t) => {
460
+ const form = new FormData();
461
+ try {
462
+ await axios.post(`http://localhost:${port}/api/CortexFileHandler`, form, {
463
+ headers: form.getHeaders(),
464
+ timeout: 5000,
465
+ });
466
+ t.fail("Should have thrown error");
467
+ } catch (error) {
442
468
  t.is(
443
- response.status,
444
- 200,
445
- 'Should return 200 for delete with invalid requestId',
469
+ error.response.status,
470
+ 400,
471
+ "Should return 400 for empty POST request",
446
472
  );
447
- t.true(Array.isArray(response.data.body), 'Response should be an array');
448
473
  t.is(
449
- response.data.body.length,
450
- 0,
451
- 'Response should be empty array for non-existent requestId',
474
+ error.response.data,
475
+ "No file provided in request",
476
+ "Should return proper error message",
452
477
  );
453
- });
454
-
455
- // POST Operation Tests
456
- test('should handle empty POST request', async (t) => {
457
- const form = new FormData();
458
- try {
459
- await axios.post(`http://localhost:${port}/api/CortexFileHandler`, form, {
460
- headers: form.getHeaders(),
461
- timeout: 5000,
462
- });
463
- t.fail('Should have thrown error');
464
- } catch (error) {
465
- t.is(
466
- error.response.status,
467
- 400,
468
- 'Should return 400 for empty POST request',
469
- );
470
- t.is(
471
- error.response.data,
472
- 'No file provided in request',
473
- 'Should return proper error message',
474
- );
475
- }
478
+ }
476
479
  });
477
480
 
478
481
  // Upload Tests
479
- test.serial('should handle successful file upload with hash', async (t) => {
480
- const form = new FormData();
481
- const testHash = 'test-hash-123';
482
- const testContent = 'test content';
483
- form.append('file', Buffer.from(testContent), 'test.txt');
484
- form.append('hash', testHash);
485
-
486
- let uploadedUrl;
487
- try {
488
- // Upload file with hash
489
- const uploadResponse = await axios.post(
490
- `http://localhost:${port}/api/CortexFileHandler`,
491
- form,
492
- {
493
- headers: {
494
- ...form.getHeaders(),
495
- 'Content-Type': 'multipart/form-data',
496
- },
497
- validateStatus: (status) => true,
498
- timeout: 5000,
499
- },
500
- );
501
-
502
- t.is(uploadResponse.status, 200, 'Upload should succeed');
503
- t.truthy(uploadResponse.data.url, 'Response should contain file URL');
504
- uploadedUrl = uploadResponse.data.url;
505
-
506
- // Wait a bit for Redis to be updated
507
- await new Promise((resolve) => setTimeout(resolve, 1000));
508
-
509
- // Verify hash exists and returns the file info
510
- const hashCheckResponse = await axios.get(
511
- `http://localhost:${port}/api/CortexFileHandler`,
512
- {
513
- params: {
514
- hash: testHash,
515
- checkHash: true,
516
- },
517
- validateStatus: (status) => true,
518
- timeout: 5000,
519
- },
520
- );
521
-
522
- t.is(hashCheckResponse.status, 200, 'Hash check should return 200 for uploaded hash');
523
- t.truthy(hashCheckResponse.data.url, 'Hash check should return file URL');
524
- } finally {
525
- await cleanupHashAndFile(testHash, uploadedUrl, baseUrl);
526
- }
527
- });
528
-
529
- test.serial('should handle hash clearing', async (t) => {
530
- const testHash = 'test-hash-to-clear';
531
- const form = new FormData();
532
- form.append('file', Buffer.from('test content'), 'test.txt');
533
- form.append('hash', testHash);
534
-
535
- // First upload a file with the hash
482
+ test.serial("should handle successful file upload with hash", async (t) => {
483
+ const form = new FormData();
484
+ const testHash = "test-hash-123";
485
+ const testContent = "test content";
486
+ form.append("file", Buffer.from(testContent), "test.txt");
487
+ form.append("hash", testHash);
488
+
489
+ let uploadedUrl;
490
+ try {
491
+ // Upload file with hash
536
492
  const uploadResponse = await axios.post(
537
- `http://localhost:${port}/api/CortexFileHandler`,
538
- form,
539
- {
540
- headers: {
541
- ...form.getHeaders(),
542
- 'Content-Type': 'multipart/form-data',
543
- },
544
- validateStatus: (status) => true,
545
- timeout: 5000,
493
+ `http://localhost:${port}/api/CortexFileHandler`,
494
+ form,
495
+ {
496
+ headers: {
497
+ ...form.getHeaders(),
498
+ "Content-Type": "multipart/form-data",
546
499
  },
500
+ validateStatus: (status) => true,
501
+ timeout: 5000,
502
+ },
547
503
  );
548
504
 
549
- t.is(uploadResponse.status, 200, 'Upload should succeed');
550
- t.truthy(uploadResponse.data.url, 'Response should contain file URL');
505
+ t.is(uploadResponse.status, 200, "Upload should succeed");
506
+ t.truthy(uploadResponse.data.url, "Response should contain file URL");
507
+ uploadedUrl = uploadResponse.data.url;
551
508
 
552
509
  // Wait a bit for Redis to be updated
553
510
  await new Promise((resolve) => setTimeout(resolve, 1000));
554
511
 
555
- // Clear the hash (should succeed)
556
- const clearResponse = await axios.get(
557
- `http://localhost:${port}/api/CortexFileHandler`,
558
- {
559
- params: {
560
- hash: testHash,
561
- clearHash: true,
562
- },
563
- validateStatus: (status) => true,
564
- timeout: 5000,
565
- },
566
- );
567
-
568
- t.is(clearResponse.status, 200, 'Hash clearing should return 200 for existing hash');
569
- t.is(clearResponse.data, `Hash ${testHash} removed`, 'Should indicate hash was removed');
570
-
571
- // Second clear (should return 404)
572
- const clearAgainResponse = await axios.get(
573
- `http://localhost:${port}/api/CortexFileHandler`,
574
- {
575
- params: {
576
- hash: testHash,
577
- clearHash: true,
578
- },
579
- validateStatus: (status) => true,
580
- timeout: 5000,
581
- },
582
- );
583
- t.is(clearAgainResponse.status, 404, 'Hash clearing should return 404 for already removed hash');
584
- t.is(clearAgainResponse.data, `Hash ${testHash} not found`, 'Should indicate hash not found');
585
-
586
- // Verify hash no longer exists
587
- const verifyResponse = await axios.get(
588
- `http://localhost:${port}/api/CortexFileHandler`,
589
- {
590
- params: {
591
- hash: testHash,
592
- checkHash: true,
593
- },
594
- validateStatus: (status) => true,
595
- timeout: 5000,
512
+ // Verify hash exists and returns the file info
513
+ const hashCheckResponse = await axios.get(
514
+ `http://localhost:${port}/api/CortexFileHandler`,
515
+ {
516
+ params: {
517
+ hash: testHash,
518
+ checkHash: true,
596
519
  },
520
+ validateStatus: (status) => true,
521
+ timeout: 5000,
522
+ },
597
523
  );
598
524
 
599
- t.is(verifyResponse.status, 404, 'Hash should not exist');
600
525
  t.is(
601
- verifyResponse.data,
602
- `Hash ${testHash} not found`,
603
- 'Should indicate hash not found',
526
+ hashCheckResponse.status,
527
+ 200,
528
+ "Hash check should return 200 for uploaded hash",
604
529
  );
605
-
606
- // Clean up the uploaded file
607
- await cleanupUploadedFile(t, uploadResponse.data.url);
530
+ t.truthy(hashCheckResponse.data.url, "Hash check should return file URL");
531
+ } finally {
532
+ await cleanupHashAndFile(testHash, uploadedUrl, baseUrl);
533
+ }
608
534
  });
609
535
 
610
- test.serial('should handle file upload without hash', async (t) => {
611
- const form = new FormData();
612
- form.append('file', Buffer.from('test content'), 'test.txt');
613
-
614
- const response = await axios.post(
615
- `http://localhost:${port}/api/CortexFileHandler`,
616
- form,
617
- {
618
- headers: {
619
- ...form.getHeaders(),
620
- 'Content-Type': 'multipart/form-data',
621
- },
622
- validateStatus: (status) => true,
623
- timeout: 5000,
624
- },
625
- );
536
+ test.serial("should handle hash clearing", async (t) => {
537
+ const testHash = "test-hash-to-clear";
538
+ const form = new FormData();
539
+ form.append("file", Buffer.from("test content"), "test.txt");
540
+ form.append("hash", testHash);
541
+
542
+ // First upload a file with the hash
543
+ const uploadResponse = await axios.post(
544
+ `http://localhost:${port}/api/CortexFileHandler`,
545
+ form,
546
+ {
547
+ headers: {
548
+ ...form.getHeaders(),
549
+ "Content-Type": "multipart/form-data",
550
+ },
551
+ validateStatus: (status) => true,
552
+ timeout: 5000,
553
+ },
554
+ );
555
+
556
+ t.is(uploadResponse.status, 200, "Upload should succeed");
557
+ t.truthy(uploadResponse.data.url, "Response should contain file URL");
558
+
559
+ // Wait a bit for Redis to be updated
560
+ await new Promise((resolve) => setTimeout(resolve, 1000));
561
+
562
+ // Clear the hash (should succeed)
563
+ const clearResponse = await axios.get(
564
+ `http://localhost:${port}/api/CortexFileHandler`,
565
+ {
566
+ params: {
567
+ hash: testHash,
568
+ clearHash: true,
569
+ },
570
+ validateStatus: (status) => true,
571
+ timeout: 5000,
572
+ },
573
+ );
574
+
575
+ t.is(
576
+ clearResponse.status,
577
+ 200,
578
+ "Hash clearing should return 200 for existing hash",
579
+ );
580
+ t.is(
581
+ clearResponse.data,
582
+ `Hash ${testHash} removed`,
583
+ "Should indicate hash was removed",
584
+ );
585
+
586
+ // Second clear (should return 404)
587
+ const clearAgainResponse = await axios.get(
588
+ `http://localhost:${port}/api/CortexFileHandler`,
589
+ {
590
+ params: {
591
+ hash: testHash,
592
+ clearHash: true,
593
+ },
594
+ validateStatus: (status) => true,
595
+ timeout: 5000,
596
+ },
597
+ );
598
+ t.is(
599
+ clearAgainResponse.status,
600
+ 404,
601
+ "Hash clearing should return 404 for already removed hash",
602
+ );
603
+ t.is(
604
+ clearAgainResponse.data,
605
+ `Hash ${testHash} not found`,
606
+ "Should indicate hash not found",
607
+ );
608
+
609
+ // Verify hash no longer exists
610
+ const verifyResponse = await axios.get(
611
+ `http://localhost:${port}/api/CortexFileHandler`,
612
+ {
613
+ params: {
614
+ hash: testHash,
615
+ checkHash: true,
616
+ },
617
+ validateStatus: (status) => true,
618
+ timeout: 5000,
619
+ },
620
+ );
626
621
 
627
- t.is(response.status, 200, 'Upload should succeed');
628
- t.truthy(response.data.url, 'Response should contain file URL');
622
+ t.is(verifyResponse.status, 404, "Hash should not exist");
623
+ t.is(
624
+ verifyResponse.data,
625
+ `Hash ${testHash} not found`,
626
+ "Should indicate hash not found",
627
+ );
629
628
 
630
- await cleanupUploadedFile(t, response.data.url);
629
+ // Clean up the uploaded file
630
+ await cleanupUploadedFile(t, uploadResponse.data.url);
631
631
  });
632
632
 
633
- test.serial('should handle upload with empty file', async (t) => {
634
- const form = new FormData();
635
- // Empty file
636
- form.append('file', Buffer.from(''), 'empty.txt');
633
+ test.serial("should handle file upload without hash", async (t) => {
634
+ const form = new FormData();
635
+ form.append("file", Buffer.from("test content"), "test.txt");
636
+
637
+ const response = await axios.post(
638
+ `http://localhost:${port}/api/CortexFileHandler`,
639
+ form,
640
+ {
641
+ headers: {
642
+ ...form.getHeaders(),
643
+ "Content-Type": "multipart/form-data",
644
+ },
645
+ validateStatus: (status) => true,
646
+ timeout: 5000,
647
+ },
648
+ );
637
649
 
638
- const response = await axios.post(
639
- `http://localhost:${port}/api/CortexFileHandler`,
640
- form,
641
- {
642
- headers: {
643
- ...form.getHeaders(),
644
- 'Content-Type': 'multipart/form-data',
645
- },
646
- validateStatus: (status) => true,
647
- timeout: 5000,
648
- },
649
- );
650
+ t.is(response.status, 200, "Upload should succeed");
651
+ t.truthy(response.data.url, "Response should contain file URL");
650
652
 
651
- t.is(response.status, 400, 'Should reject empty file');
652
- t.is(response.data, 'Invalid file: file is empty', 'Should return proper error message');
653
+ await cleanupUploadedFile(t, response.data.url);
653
654
  });
654
655
 
655
- test.serial(
656
- 'should handle complete upload-request-delete-verify sequence',
657
- async (t) => {
658
- const testContent = 'test content for sequence';
659
- const testHash = 'test-sequence-hash';
660
- const form = new FormData();
661
- form.append('file', Buffer.from(testContent), 'sequence-test.txt');
662
- form.append('hash', testHash);
663
-
664
- // Upload file with hash
665
- const uploadResponse = await axios.post(baseUrl, form, {
666
- headers: form.getHeaders(),
667
- validateStatus: (status) => true,
668
- timeout: 5000,
669
- });
670
- t.is(uploadResponse.status, 200, 'Upload should succeed');
671
- t.truthy(uploadResponse.data.url, 'Response should contain URL');
672
-
673
- await cleanupHashAndFile(testHash, uploadResponse.data.url, baseUrl);
674
-
675
- // Verify hash is gone by trying to get the file URL
676
- const hashCheckResponse = await axios.get(`${baseUrl}`, {
677
- params: {
678
- hash: testHash,
679
- checkHash: true,
680
- },
681
- validateStatus: (status) => true,
682
- });
683
- t.is(hashCheckResponse.status, 404, 'Hash should not exist after deletion');
656
+ test.serial("should handle upload with empty file", async (t) => {
657
+ const form = new FormData();
658
+ // Empty file
659
+ form.append("file", Buffer.from(""), "empty.txt");
660
+
661
+ const response = await axios.post(
662
+ `http://localhost:${port}/api/CortexFileHandler`,
663
+ form,
664
+ {
665
+ headers: {
666
+ ...form.getHeaders(),
667
+ "Content-Type": "multipart/form-data",
668
+ },
669
+ validateStatus: (status) => true,
670
+ timeout: 5000,
684
671
  },
685
- );
672
+ );
673
+
674
+ t.is(response.status, 400, "Should reject empty file");
675
+ t.is(
676
+ response.data,
677
+ "Invalid file: file is empty",
678
+ "Should return proper error message",
679
+ );
680
+ });
686
681
 
687
682
  test.serial(
688
- 'should handle multiple file uploads with unique hashes',
689
- async (t) => {
690
- const uploadedFiles = [];
691
-
692
- // Upload 10 files
693
- for (let i = 0; i < 10; i++) {
694
- const content = `test content for file ${i}`;
695
- const form = new FormData();
696
- form.append('file', Buffer.from(content), `file-${i}.txt`);
697
-
698
- const uploadResponse = await axios.post(baseUrl, form, {
699
- headers: form.getHeaders(),
700
- validateStatus: (status) => true,
701
- timeout: 5000,
702
- });
703
- t.is(uploadResponse.status, 200, `Upload should succeed for file ${i}`);
704
-
705
- const url = uploadResponse.data.url;
706
- t.truthy(url, `Response should contain URL for file ${i}`);
707
-
708
- uploadedFiles.push({
709
- url: convertToLocalUrl(url),
710
- content,
711
- });
712
-
713
- // Small delay between uploads
714
- await new Promise((resolve) => setTimeout(resolve, 100));
715
- }
716
-
717
- // Verify files are stored and can be fetched
718
- for (const file of uploadedFiles) {
719
- const fileResponse = await axios.get(file.url, {
720
- validateStatus: (status) => true,
721
- timeout: 5000,
722
- });
723
- t.is(
724
- fileResponse.status,
725
- 200,
726
- `File should be accessible at ${file.url}`,
727
- );
728
- t.is(
729
- fileResponse.data,
730
- file.content,
731
- 'File content should match original content',
732
- );
733
- }
734
-
735
- // Clean up all files
736
- for (const file of uploadedFiles) {
737
- await cleanupUploadedFile(t, file.url);
738
- }
739
- },
683
+ "should handle complete upload-request-delete-verify sequence",
684
+ async (t) => {
685
+ const testContent = "test content for sequence";
686
+ const testHash = "test-sequence-hash";
687
+ const form = new FormData();
688
+ form.append("file", Buffer.from(testContent), "sequence-test.txt");
689
+ form.append("hash", testHash);
690
+
691
+ // Upload file with hash
692
+ const uploadResponse = await axios.post(baseUrl, form, {
693
+ headers: form.getHeaders(),
694
+ validateStatus: (status) => true,
695
+ timeout: 5000,
696
+ });
697
+ t.is(uploadResponse.status, 200, "Upload should succeed");
698
+ t.truthy(uploadResponse.data.url, "Response should contain URL");
699
+
700
+ await cleanupHashAndFile(testHash, uploadResponse.data.url, baseUrl);
701
+
702
+ // Verify hash is gone by trying to get the file URL
703
+ const hashCheckResponse = await axios.get(`${baseUrl}`, {
704
+ params: {
705
+ hash: testHash,
706
+ checkHash: true,
707
+ },
708
+ validateStatus: (status) => true,
709
+ });
710
+ t.is(hashCheckResponse.status, 404, "Hash should not exist after deletion");
711
+ },
740
712
  );
741
713
 
742
- // Example of a hash-specific test that only runs with Azure
743
- test.serial('should handle hash reuse with Azure storage', async (t) => {
744
- if (!isAzureConfigured()) {
745
- t.pass('Skipping hash test - Azure not configured');
746
- return;
747
- }
714
+ test.serial(
715
+ "should handle multiple file uploads with unique hashes",
716
+ async (t) => {
717
+ const uploadedFiles = [];
748
718
 
749
- const testHash = 'test-hash-reuse';
750
- const testContent = 'test content for hash reuse';
751
- const form = new FormData();
752
- form.append('file', Buffer.from(testContent), 'test.txt');
753
- form.append('hash', testHash);
719
+ // Upload 10 files
720
+ for (let i = 0; i < 10; i++) {
721
+ const content = `test content for file ${i}`;
722
+ const form = new FormData();
723
+ form.append("file", Buffer.from(content), `file-${i}.txt`);
754
724
 
755
- // First upload
756
- const upload1 = await axios.post(baseUrl, form, {
725
+ const uploadResponse = await axios.post(baseUrl, form, {
757
726
  headers: form.getHeaders(),
758
727
  validateStatus: (status) => true,
759
728
  timeout: 5000,
760
- });
761
- t.is(upload1.status, 200, 'First upload should succeed');
762
- const originalUrl = upload1.data.url;
729
+ });
730
+ t.is(uploadResponse.status, 200, `Upload should succeed for file ${i}`);
763
731
 
764
- // Check hash exists and returns the correct URL
765
- const hashCheck1 = await axios.get(
766
- baseUrl,
767
- { hash: testHash, checkHash: true },
768
- {
769
- validateStatus: (status) => true,
770
- },
771
- );
772
- t.is(hashCheck1.status, 200, 'Hash should exist after first upload');
773
- t.truthy(hashCheck1.data.url, 'Hash check should return URL');
774
- t.is(
775
- hashCheck1.data.url,
776
- originalUrl,
777
- 'Hash check should return original upload URL',
778
- );
732
+ const url = uploadResponse.data.url;
733
+ t.truthy(url, `Response should contain URL for file ${i}`);
779
734
 
780
- // Verify file is accessible via URL from hash check
781
- const fileResponse = await axios.get(convertToLocalUrl(hashCheck1.data.url), {
782
- validateStatus: (status) => true,
783
- timeout: 5000,
784
- });
785
- t.is(fileResponse.status, 200, 'File should be accessible');
786
- t.is(fileResponse.data, testContent, 'File content should match original');
735
+ uploadedFiles.push({
736
+ url: convertToLocalUrl(url),
737
+ content,
738
+ });
787
739
 
788
- // Second upload with same hash
789
- const upload2 = await axios.post(baseUrl, form, {
790
- headers: form.getHeaders(),
791
- validateStatus: (status) => true,
792
- timeout: 5000,
793
- });
794
- t.is(upload2.status, 200, 'Second upload should succeed');
795
- t.is(upload2.data.url, originalUrl, 'URLs should match for same hash');
740
+ // Small delay between uploads
741
+ await new Promise((resolve) => setTimeout(resolve, 100));
742
+ }
796
743
 
797
- // Verify file is still accessible after second upload
798
- const fileResponse2 = await axios.get(convertToLocalUrl(upload2.data.url), {
744
+ // Verify files are stored and can be fetched
745
+ for (const file of uploadedFiles) {
746
+ const fileResponse = await axios.get(file.url, {
799
747
  validateStatus: (status) => true,
800
748
  timeout: 5000,
801
- });
802
- t.is(fileResponse2.status, 200, 'File should still be accessible');
803
- t.is(
804
- fileResponse2.data,
805
- testContent,
806
- 'File content should still match original',
807
- );
749
+ });
750
+ t.is(
751
+ fileResponse.status,
752
+ 200,
753
+ `File should be accessible at ${file.url}`,
754
+ );
755
+ t.is(
756
+ fileResponse.data,
757
+ file.content,
758
+ "File content should match original content",
759
+ );
760
+ }
808
761
 
809
- // Clean up
810
- await cleanupUploadedFile(t, originalUrl);
762
+ // Clean up all files
763
+ for (const file of uploadedFiles) {
764
+ await cleanupUploadedFile(t, file.url);
765
+ }
766
+ },
767
+ );
811
768
 
812
- // Verify hash is now gone
813
- const hashCheckAfterDelete = await axios.get(
814
- baseUrl,
815
- { hash: testHash, checkHash: true },
816
- {
817
- validateStatus: (status) => true,
818
- },
819
- );
820
- t.is(
821
- hashCheckAfterDelete.status,
822
- 404,
823
- 'Hash should be gone after file deletion',
824
- );
769
+ // Example of a hash-specific test that only runs with Azure
770
+ test.serial("should handle hash reuse with Azure storage", async (t) => {
771
+ if (!isAzureConfigured()) {
772
+ t.pass("Skipping hash test - Azure not configured");
773
+ return;
774
+ }
775
+
776
+ const testHash = "test-hash-reuse";
777
+ const testContent = "test content for hash reuse";
778
+ const form = new FormData();
779
+ form.append("file", Buffer.from(testContent), "test.txt");
780
+ form.append("hash", testHash);
781
+
782
+ // First upload
783
+ const upload1 = await axios.post(baseUrl, form, {
784
+ headers: form.getHeaders(),
785
+ validateStatus: (status) => true,
786
+ timeout: 5000,
787
+ });
788
+ t.is(upload1.status, 200, "First upload should succeed");
789
+ const originalUrl = upload1.data.url;
790
+
791
+ // Check hash exists and returns the correct URL
792
+ const hashCheck1 = await axios.get(
793
+ baseUrl,
794
+ { hash: testHash, checkHash: true },
795
+ {
796
+ validateStatus: (status) => true,
797
+ },
798
+ );
799
+ t.is(hashCheck1.status, 200, "Hash should exist after first upload");
800
+ t.truthy(hashCheck1.data.url, "Hash check should return URL");
801
+ t.is(
802
+ hashCheck1.data.url,
803
+ originalUrl,
804
+ "Hash check should return original upload URL",
805
+ );
806
+
807
+ // Verify file is accessible via URL from hash check
808
+ const fileResponse = await axios.get(convertToLocalUrl(hashCheck1.data.url), {
809
+ validateStatus: (status) => true,
810
+ timeout: 5000,
811
+ });
812
+ t.is(fileResponse.status, 200, "File should be accessible");
813
+ t.is(fileResponse.data, testContent, "File content should match original");
814
+
815
+ // Second upload with same hash
816
+ const upload2 = await axios.post(baseUrl, form, {
817
+ headers: form.getHeaders(),
818
+ validateStatus: (status) => true,
819
+ timeout: 5000,
820
+ });
821
+ t.is(upload2.status, 200, "Second upload should succeed");
822
+ t.is(upload2.data.url, originalUrl, "URLs should match for same hash");
823
+
824
+ // Verify file is still accessible after second upload
825
+ const fileResponse2 = await axios.get(convertToLocalUrl(upload2.data.url), {
826
+ validateStatus: (status) => true,
827
+ timeout: 5000,
828
+ });
829
+ t.is(fileResponse2.status, 200, "File should still be accessible");
830
+ t.is(
831
+ fileResponse2.data,
832
+ testContent,
833
+ "File content should still match original",
834
+ );
835
+
836
+ // Clean up
837
+ await cleanupUploadedFile(t, originalUrl);
838
+
839
+ // Verify hash is now gone
840
+ const hashCheckAfterDelete = await axios.get(
841
+ baseUrl,
842
+ { hash: testHash, checkHash: true },
843
+ {
844
+ validateStatus: (status) => true,
845
+ },
846
+ );
847
+ t.is(
848
+ hashCheckAfterDelete.status,
849
+ 404,
850
+ "Hash should be gone after file deletion",
851
+ );
825
852
  });
826
853
 
827
854
  // Helper to check if GCS is configured
828
855
  function isGCSConfigured() {
829
- return (
830
- process.env.GCP_SERVICE_ACCOUNT_KEY && process.env.STORAGE_EMULATOR_HOST
831
- );
856
+ return (
857
+ process.env.GCP_SERVICE_ACCOUNT_KEY && process.env.STORAGE_EMULATOR_HOST
858
+ );
832
859
  }
833
860
 
834
861
  // Helper function to check if file exists in fake GCS
835
862
  async function checkGCSFile(gcsUrl) {
836
- // Convert gs:// URL to bucket and object path
837
- const [, , bucket, ...objectParts] = gcsUrl.split('/');
838
- const object = objectParts.join('/');
839
-
840
- console.log(`[checkGCSFile] Checking file in GCS: ${gcsUrl}`);
841
- console.log(`[checkGCSFile] Bucket: ${bucket}, Object: ${object}`);
842
- console.log(`[checkGCSFile] Using emulator at ${process.env.STORAGE_EMULATOR_HOST}`);
843
-
844
- // Query fake-gcs-server
845
- const response = await axios.get(
846
- `${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucket}/o/${encodeURIComponent(object)}`,
847
- {
848
- validateStatus: (status) => true,
849
- },
850
- );
851
- console.log(`[checkGCSFile] Response status: ${response.status}`);
852
- console.log(`[checkGCSFile] File ${response.status === 200 ? 'exists' : 'does not exist'}`);
853
- return response.status === 200;
863
+ // Convert gs:// URL to bucket and object path
864
+ const [, , bucket, ...objectParts] = gcsUrl.split("/");
865
+ const object = objectParts.join("/");
866
+
867
+ console.log(`[checkGCSFile] Checking file in GCS: ${gcsUrl}`);
868
+ console.log(`[checkGCSFile] Bucket: ${bucket}, Object: ${object}`);
869
+ console.log(
870
+ `[checkGCSFile] Using emulator at ${process.env.STORAGE_EMULATOR_HOST}`,
871
+ );
872
+
873
+ // Query fake-gcs-server
874
+ const response = await axios.get(
875
+ `${process.env.STORAGE_EMULATOR_HOST}/storage/v1/b/${bucket}/o/${encodeURIComponent(object)}`,
876
+ {
877
+ validateStatus: (status) => true,
878
+ },
879
+ );
880
+ console.log(`[checkGCSFile] Response status: ${response.status}`);
881
+ console.log(
882
+ `[checkGCSFile] File ${response.status === 200 ? "exists" : "does not exist"}`,
883
+ );
884
+ return response.status === 200;
854
885
  }
855
886
 
856
887
  // Helper function to verify file exists in both storages
857
888
  async function verifyFileInBothStorages(t, uploadResponse) {
858
- // Verify Azure URL is accessible
859
- const azureResponse = await axios.get(
860
- convertToLocalUrl(uploadResponse.data.url),
861
- {
862
- validateStatus: (status) => true,
863
- timeout: 5000,
864
- },
865
- );
866
- t.is(azureResponse.status, 200, 'File should be accessible in Azure');
889
+ // Verify Azure URL is accessible
890
+ const azureResponse = await axios.get(
891
+ convertToLocalUrl(uploadResponse.data.url),
892
+ {
893
+ validateStatus: (status) => true,
894
+ timeout: 5000,
895
+ },
896
+ );
897
+ t.is(azureResponse.status, 200, "File should be accessible in Azure");
867
898
 
868
- if (isGCSConfigured()) {
899
+ if (isGCSConfigured()) {
869
900
  // Verify GCS URL exists and is in correct format
870
- t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
871
- t.true(
872
- uploadResponse.data.gcs.startsWith('gs://'),
873
- 'GCS URL should use gs:// protocol',
874
- );
875
-
876
- // Check if file exists in fake GCS
877
- const exists = await checkGCSFile(uploadResponse.data.gcs);
878
- t.true(exists, 'File should exist in GCS');
879
- }
901
+ t.truthy(uploadResponse.data.gcs, "Response should contain GCS URL");
902
+ t.true(
903
+ uploadResponse.data.gcs.startsWith("gs://"),
904
+ "GCS URL should use gs:// protocol",
905
+ );
906
+
907
+ // Check if file exists in fake GCS
908
+ const exists = await checkGCSFile(uploadResponse.data.gcs);
909
+ t.true(exists, "File should exist in GCS");
910
+ }
880
911
  }
881
912
 
882
913
  // Helper function to verify file is deleted from both storages
883
914
  async function verifyFileDeletedFromBothStorages(t, uploadResponse) {
884
- // Verify Azure URL is no longer accessible
885
- const azureResponse = await axios.get(
886
- convertToLocalUrl(uploadResponse.data.url),
887
- {
888
- validateStatus: (status) => true,
889
- timeout: 5000,
890
- },
891
- );
892
- t.is(azureResponse.status, 404, 'File should not be accessible in Azure');
915
+ // Verify Azure URL is no longer accessible
916
+ const azureResponse = await axios.get(
917
+ convertToLocalUrl(uploadResponse.data.url),
918
+ {
919
+ validateStatus: (status) => true,
920
+ timeout: 5000,
921
+ },
922
+ );
923
+ t.is(azureResponse.status, 404, "File should not be accessible in Azure");
893
924
 
894
- if (isGCSConfigured()) {
925
+ if (isGCSConfigured()) {
895
926
  // Verify file is also deleted from GCS
896
- const exists = await checkGCSFile(uploadResponse.data.gcs);
897
- t.false(exists, 'File should not exist in GCS');
898
- }
927
+ const exists = await checkGCSFile(uploadResponse.data.gcs);
928
+ t.false(exists, "File should not exist in GCS");
929
+ }
899
930
  }
900
931
 
901
932
  test.serial(
902
- 'should handle dual storage upload and cleanup when GCS configured',
903
- async (t) => {
904
- if (!isGCSConfigured()) {
905
- t.pass('Skipping test - GCS not configured');
906
- return;
907
- }
908
-
909
- const requestId = uuidv4();
910
- const testContent = 'test content for dual storage';
911
- const form = new FormData();
912
- form.append('file', Buffer.from(testContent), 'dual-test.txt');
913
- form.append('requestId', requestId);
914
-
915
- // Upload file
916
- const uploadResponse = await uploadFile(
917
- Buffer.from(testContent),
918
- requestId,
919
- );
920
- t.is(uploadResponse.status, 200, 'Upload should succeed');
921
- t.truthy(uploadResponse.data.url, 'Response should contain Azure URL');
922
- t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
923
- t.true(
924
- uploadResponse.data.gcs.startsWith('gs://'),
925
- 'GCS URL should use gs:// protocol',
926
- );
927
-
928
- // Verify file exists in both storages
929
- await verifyFileInBothStorages(t, uploadResponse);
930
-
931
- // Get the folder name (requestId) from the URL
932
- const fileRequestId = getFolderNameFromUrl(uploadResponse.data.url);
933
-
934
- // Delete file using the correct requestId
935
- const deleteResponse = await axios.delete(
936
- `${baseUrl}?operation=delete&requestId=${fileRequestId}`,
937
- );
938
- t.is(deleteResponse.status, 200, 'Delete should succeed');
939
-
940
- // Verify file is deleted from both storages
941
- await verifyFileDeletedFromBothStorages(t, uploadResponse);
942
- },
943
- );
944
-
945
- test.serial('should handle GCS URL format and accessibility', async (t) => {
933
+ "should handle dual storage upload and cleanup when GCS configured",
934
+ async (t) => {
946
935
  if (!isGCSConfigured()) {
947
- t.pass('Skipping test - GCS not configured');
948
- return;
936
+ t.pass("Skipping test - GCS not configured");
937
+ return;
949
938
  }
950
939
 
951
940
  const requestId = uuidv4();
952
- const testContent = 'test content for GCS URL verification';
941
+ const testContent = "test content for dual storage";
953
942
  const form = new FormData();
954
- form.append('file', Buffer.from(testContent), 'gcs-url-test.txt');
943
+ form.append("file", Buffer.from(testContent), "dual-test.txt");
944
+ form.append("requestId", requestId);
955
945
 
956
- // Upload with explicit GCS preference
957
- const uploadResponse = await axios.post(
958
- `http://localhost:${port}/api/CortexFileHandler`,
959
- form,
960
- {
961
- params: {
962
- operation: 'upload',
963
- requestId,
964
- useGCS: true,
965
- },
966
- headers: form.getHeaders(),
967
- },
946
+ // Upload file
947
+ const uploadResponse = await uploadFile(
948
+ Buffer.from(testContent),
949
+ requestId,
968
950
  );
969
-
970
- t.is(uploadResponse.status, 200, 'Upload should succeed');
971
- t.truthy(uploadResponse.data.gcs, 'Response should contain GCS URL');
951
+ t.is(uploadResponse.status, 200, "Upload should succeed");
952
+ t.truthy(uploadResponse.data.url, "Response should contain Azure URL");
953
+ t.truthy(uploadResponse.data.gcs, "Response should contain GCS URL");
972
954
  t.true(
973
- uploadResponse.data.gcs.startsWith('gs://'),
974
- 'GCS URL should use gs:// protocol',
955
+ uploadResponse.data.gcs.startsWith("gs://"),
956
+ "GCS URL should use gs:// protocol",
975
957
  );
976
958
 
977
- // Verify content is accessible via normal URL since we can't directly access gs:// URLs
978
- const fileResponse = await axios.get(uploadResponse.data.url);
979
- t.is(fileResponse.status, 200, 'File should be accessible');
980
- t.is(fileResponse.data, testContent, 'Content should match original');
959
+ // Verify file exists in both storages
960
+ await verifyFileInBothStorages(t, uploadResponse);
981
961
 
982
- // Clean up
983
- await cleanupUploadedFile(t, uploadResponse.data.url);
962
+ // Get the folder name (requestId) from the URL
963
+ const fileRequestId = getFolderNameFromUrl(uploadResponse.data.url);
964
+
965
+ // Delete file using the correct requestId
966
+ const deleteResponse = await axios.delete(
967
+ `${baseUrl}?operation=delete&requestId=${fileRequestId}`,
968
+ );
969
+ t.is(deleteResponse.status, 200, "Delete should succeed");
970
+
971
+ // Verify file is deleted from both storages
972
+ await verifyFileDeletedFromBothStorages(t, uploadResponse);
973
+ },
974
+ );
975
+
976
+ test.serial("should handle GCS URL format and accessibility", async (t) => {
977
+ if (!isGCSConfigured()) {
978
+ t.pass("Skipping test - GCS not configured");
979
+ return;
980
+ }
981
+
982
+ const requestId = uuidv4();
983
+ const testContent = "test content for GCS URL verification";
984
+ const form = new FormData();
985
+ form.append("file", Buffer.from(testContent), "gcs-url-test.txt");
986
+
987
+ // Upload with explicit GCS preference
988
+ const uploadResponse = await axios.post(
989
+ `http://localhost:${port}/api/CortexFileHandler`,
990
+ form,
991
+ {
992
+ params: {
993
+ operation: "upload",
994
+ requestId,
995
+ useGCS: true,
996
+ },
997
+ headers: form.getHeaders(),
998
+ },
999
+ );
1000
+
1001
+ t.is(uploadResponse.status, 200, "Upload should succeed");
1002
+ t.truthy(uploadResponse.data.gcs, "Response should contain GCS URL");
1003
+ t.true(
1004
+ uploadResponse.data.gcs.startsWith("gs://"),
1005
+ "GCS URL should use gs:// protocol",
1006
+ );
1007
+
1008
+ // Verify content is accessible via normal URL since we can't directly access gs:// URLs
1009
+ const fileResponse = await axios.get(uploadResponse.data.url);
1010
+ t.is(fileResponse.status, 200, "File should be accessible");
1011
+ t.is(fileResponse.data, testContent, "Content should match original");
1012
+
1013
+ // Clean up
1014
+ await cleanupUploadedFile(t, uploadResponse.data.url);
984
1015
  });
985
1016
 
986
1017
  // Add this helper function after other helper functions
987
1018
  async function createAndUploadTestFile() {
988
- // Create a temporary file path
989
- const tempDir = path.join(os.tmpdir(), uuidv4());
990
- fs.mkdirSync(tempDir, { recursive: true });
991
- const tempFile = path.join(tempDir, 'test.mp3');
1019
+ // Create a temporary file path
1020
+ const tempDir = path.join(os.tmpdir(), uuidv4());
1021
+ fs.mkdirSync(tempDir, { recursive: true });
1022
+ const tempFile = path.join(tempDir, "test.mp3");
1023
+
1024
+ // Generate a real MP3 file using ffmpeg
1025
+ try {
1026
+ execSync(
1027
+ `ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 10 -q:a 9 -acodec libmp3lame "${tempFile}"`,
1028
+ {
1029
+ stdio: ["ignore", "pipe", "pipe"],
1030
+ },
1031
+ );
992
1032
 
993
- // Generate a real MP3 file using ffmpeg
994
- try {
995
- execSync(
996
- `ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t 10 -q:a 9 -acodec libmp3lame "${tempFile}"`,
997
- {
998
- stdio: ['ignore', 'pipe', 'pipe'],
999
- },
1000
- );
1001
-
1002
- // Upload the real media file
1003
- const form = new FormData();
1004
- form.append('file', fs.createReadStream(tempFile));
1005
-
1006
- const uploadResponse = await axios.post(baseUrl, form, {
1007
- headers: form.getHeaders(),
1008
- validateStatus: (status) => true,
1009
- timeout: 5000,
1010
- });
1011
-
1012
- // Wait a short time to ensure file is available
1013
- await new Promise((resolve) => setTimeout(resolve, 1000));
1014
-
1015
- // Clean up temp file
1016
- fs.rmSync(tempDir, { recursive: true, force: true });
1017
-
1018
- return uploadResponse.data.url;
1019
- } catch (error) {
1020
- console.error('Error creating test file:', error);
1021
- throw error;
1022
- }
1033
+ // Upload the real media file
1034
+ const form = new FormData();
1035
+ form.append("file", fs.createReadStream(tempFile));
1036
+
1037
+ const uploadResponse = await axios.post(baseUrl, form, {
1038
+ headers: form.getHeaders(),
1039
+ validateStatus: (status) => true,
1040
+ timeout: 5000,
1041
+ });
1042
+
1043
+ // Wait a short time to ensure file is available
1044
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1045
+
1046
+ // Clean up temp file
1047
+ fs.rmSync(tempDir, { recursive: true, force: true });
1048
+
1049
+ return uploadResponse.data.url;
1050
+ } catch (error) {
1051
+ console.error("Error creating test file:", error);
1052
+ throw error;
1053
+ }
1023
1054
  }
1024
1055
 
1025
1056
  test.serial(
1026
- 'should handle chunking with GCS integration when configured',
1027
- async (t) => {
1028
- if (!isGCSConfigured()) {
1029
- t.pass('Skipping test - GCS not configured');
1030
- return;
1031
- }
1032
-
1033
- // Create a large test file first
1034
- const testFileUrl = await createAndUploadTestFile();
1035
- const requestId = uuidv4();
1036
-
1037
- // Request chunking via GET
1038
- const chunkResponse = await axios.get(baseUrl, {
1039
- params: {
1040
- uri: testFileUrl,
1041
- requestId,
1042
- useGCS: true,
1043
- },
1044
- validateStatus: (status) => true,
1045
- timeout: 5000,
1046
- });
1047
-
1048
- t.is(chunkResponse.status, 200, 'Chunked request should succeed');
1049
- t.truthy(chunkResponse.data, 'Response should contain data');
1050
- t.true(Array.isArray(chunkResponse.data), 'Response should be an array');
1051
- t.true(
1052
- chunkResponse.data.length > 0,
1053
- 'Should have created at least one chunk',
1054
- );
1055
-
1056
- // Verify each chunk exists in both Azure/Local and GCS
1057
- for (const chunk of chunkResponse.data) {
1058
- // Verify Azure/Local URL is accessible
1059
- const azureResponse = await axios.get(convertToLocalUrl(chunk.uri), {
1060
- validateStatus: (status) => true,
1061
- timeout: 5000,
1062
- });
1063
- t.is(
1064
- azureResponse.status,
1065
- 200,
1066
- `Chunk should be accessible in Azure/Local: ${chunk.uri}`,
1067
- );
1068
-
1069
- // Verify GCS URL exists and is in correct format
1070
- t.truthy(chunk.gcs, 'Chunk should contain GCS URL');
1071
- t.true(
1072
- chunk.gcs.startsWith('gs://'),
1073
- 'GCS URL should use gs:// protocol',
1074
- );
1075
-
1076
- // Check if chunk exists in fake GCS
1077
- const exists = await checkGCSFile(chunk.gcs);
1078
- t.true(exists, `Chunk should exist in GCS: ${chunk.gcs}`);
1079
- }
1080
-
1081
- // Clean up chunks
1082
- const deleteResponse = await axios.delete(
1083
- `${baseUrl}?operation=delete&requestId=${requestId}`,
1084
- );
1085
- t.is(deleteResponse.status, 200, 'Delete should succeed');
1086
-
1087
- // Verify all chunks are deleted from both storages
1088
- for (const chunk of chunkResponse.data) {
1089
- // Verify Azure/Local chunk is gone
1090
- const azureResponse = await axios.get(convertToLocalUrl(chunk.uri), {
1091
- validateStatus: (status) => true,
1092
- timeout: 5000,
1093
- });
1094
- t.is(
1095
- azureResponse.status,
1096
- 404,
1097
- `Chunk should not be accessible in Azure/Local after deletion: ${chunk.uri}`,
1098
- );
1099
-
1100
- // Verify GCS chunk is gone
1101
- const exists = await checkGCSFile(chunk.gcs);
1102
- t.false(
1103
- exists,
1104
- `Chunk should not exist in GCS after deletion: ${chunk.gcs}`,
1105
- );
1106
- }
1107
- },
1108
- );
1109
-
1110
- test.serial('should handle chunking errors gracefully with GCS', async (t) => {
1057
+ "should handle chunking with GCS integration when configured",
1058
+ async (t) => {
1111
1059
  if (!isGCSConfigured()) {
1112
- t.pass('Skipping test - GCS not configured');
1113
- return;
1060
+ t.pass("Skipping test - GCS not configured");
1061
+ return;
1114
1062
  }
1115
1063
 
1116
- // Create a test file to get a valid URL format
1117
- const validFileUrl = await createAndUploadTestFile();
1064
+ // Create a large test file first
1065
+ const testFileUrl = await createAndUploadTestFile();
1066
+ const requestId = uuidv4();
1118
1067
 
1119
- // Test with invalid URL that matches the format of our real URLs
1120
- const invalidUrl = validFileUrl.replace(/[^/]+$/, 'nonexistent-file.mp3');
1121
- const invalidResponse = await axios.get(baseUrl, {
1122
- params: {
1123
- uri: invalidUrl,
1124
- requestId: uuidv4(),
1125
- },
1126
- validateStatus: (status) => true,
1127
- timeout: 5000,
1068
+ // Request chunking via GET
1069
+ const chunkResponse = await axios.get(baseUrl, {
1070
+ params: {
1071
+ uri: testFileUrl,
1072
+ requestId,
1073
+ useGCS: true,
1074
+ },
1075
+ validateStatus: (status) => true,
1076
+ timeout: 5000,
1128
1077
  });
1129
1078
 
1130
- t.is(invalidResponse.status, 500, 'Should reject nonexistent file URL');
1079
+ t.is(chunkResponse.status, 200, "Chunked request should succeed");
1080
+ t.truthy(chunkResponse.data, "Response should contain data");
1081
+ t.true(Array.isArray(chunkResponse.data), "Response should be an array");
1131
1082
  t.true(
1132
- invalidResponse.data.includes('Error processing media file'),
1133
- 'Should indicate error processing media file',
1083
+ chunkResponse.data.length > 0,
1084
+ "Should have created at least one chunk",
1134
1085
  );
1135
1086
 
1136
- // Test with missing URI
1137
- const noUriResponse = await axios.get(baseUrl, {
1138
- params: {
1139
- requestId: uuidv4(),
1140
- },
1087
+ // Verify each chunk exists in both Azure/Local and GCS
1088
+ for (const chunk of chunkResponse.data) {
1089
+ // Verify Azure/Local URL is accessible
1090
+ const azureResponse = await axios.get(convertToLocalUrl(chunk.uri), {
1141
1091
  validateStatus: (status) => true,
1142
1092
  timeout: 5000,
1143
- });
1093
+ });
1094
+ t.is(
1095
+ azureResponse.status,
1096
+ 200,
1097
+ `Chunk should be accessible in Azure/Local: ${chunk.uri}`,
1098
+ );
1099
+
1100
+ // Verify GCS URL exists and is in correct format
1101
+ t.truthy(chunk.gcs, "Chunk should contain GCS URL");
1102
+ t.true(
1103
+ chunk.gcs.startsWith("gs://"),
1104
+ "GCS URL should use gs:// protocol",
1105
+ );
1106
+
1107
+ // Check if chunk exists in fake GCS
1108
+ const exists = await checkGCSFile(chunk.gcs);
1109
+ t.true(exists, `Chunk should exist in GCS: ${chunk.gcs}`);
1110
+ }
1144
1111
 
1145
- t.is(noUriResponse.status, 400, 'Should reject request with no URI');
1146
- t.is(
1147
- noUriResponse.data,
1148
- 'Please pass a uri and requestId on the query string or in the request body',
1149
- 'Should return proper error message',
1112
+ // Clean up chunks
1113
+ const deleteResponse = await axios.delete(
1114
+ `${baseUrl}?operation=delete&requestId=${requestId}`,
1150
1115
  );
1116
+ t.is(deleteResponse.status, 200, "Delete should succeed");
1117
+
1118
+ // Verify all chunks are deleted from both storages
1119
+ for (const chunk of chunkResponse.data) {
1120
+ // Verify Azure/Local chunk is gone
1121
+ const azureResponse = await axios.get(convertToLocalUrl(chunk.uri), {
1122
+ validateStatus: (status) => true,
1123
+ timeout: 5000,
1124
+ });
1125
+ t.is(
1126
+ azureResponse.status,
1127
+ 404,
1128
+ `Chunk should not be accessible in Azure/Local after deletion: ${chunk.uri}`,
1129
+ );
1130
+
1131
+ // Verify GCS chunk is gone
1132
+ const exists = await checkGCSFile(chunk.gcs);
1133
+ t.false(
1134
+ exists,
1135
+ `Chunk should not exist in GCS after deletion: ${chunk.gcs}`,
1136
+ );
1137
+ }
1138
+ },
1139
+ );
1140
+
1141
+ test.serial("should handle chunking errors gracefully with GCS", async (t) => {
1142
+ if (!isGCSConfigured()) {
1143
+ t.pass("Skipping test - GCS not configured");
1144
+ return;
1145
+ }
1146
+
1147
+ // Create a test file to get a valid URL format
1148
+ const validFileUrl = await createAndUploadTestFile();
1149
+
1150
+ // Test with invalid URL that matches the format of our real URLs
1151
+ const invalidUrl = validFileUrl.replace(/[^/]+$/, "nonexistent-file.mp3");
1152
+ const invalidResponse = await axios.get(baseUrl, {
1153
+ params: {
1154
+ uri: invalidUrl,
1155
+ requestId: uuidv4(),
1156
+ },
1157
+ validateStatus: (status) => true,
1158
+ timeout: 5000,
1159
+ });
1160
+
1161
+ t.is(invalidResponse.status, 500, "Should reject nonexistent file URL");
1162
+ t.true(
1163
+ invalidResponse.data.includes("Error processing media file"),
1164
+ "Should indicate error processing media file",
1165
+ );
1166
+
1167
+ // Test with missing URI
1168
+ const noUriResponse = await axios.get(baseUrl, {
1169
+ params: {
1170
+ requestId: uuidv4(),
1171
+ },
1172
+ validateStatus: (status) => true,
1173
+ timeout: 5000,
1174
+ });
1175
+
1176
+ t.is(noUriResponse.status, 400, "Should reject request with no URI");
1177
+ t.is(
1178
+ noUriResponse.data,
1179
+ "Please pass a uri and requestId on the query string or in the request body",
1180
+ "Should return proper error message",
1181
+ );
1151
1182
  });
1152
1183
 
1153
1184
  // Legacy MediaFileChunker Tests
1154
1185
  test.serial(
1155
- 'should handle file upload through legacy MediaFileChunker endpoint',
1156
- async (t) => {
1157
- const form = new FormData();
1158
- form.append('file', Buffer.from('test content'), 'test.txt');
1159
-
1160
- const response = await axios.post(
1161
- `http://localhost:${port}/api/MediaFileChunker`,
1162
- form,
1163
- {
1164
- headers: {
1165
- ...form.getHeaders(),
1166
- 'Content-Type': 'multipart/form-data',
1167
- },
1168
- validateStatus: (status) => true,
1169
- timeout: 5000,
1170
- },
1171
- );
1172
-
1173
- t.is(response.status, 200, 'Upload through legacy endpoint should succeed');
1174
- t.truthy(response.data.url, 'Response should contain file URL');
1175
-
1176
- await cleanupUploadedFile(t, response.data.url);
1177
- },
1186
+ "should handle file upload through legacy MediaFileChunker endpoint",
1187
+ async (t) => {
1188
+ const form = new FormData();
1189
+ form.append("file", Buffer.from("test content"), "test.txt");
1190
+
1191
+ const response = await axios.post(
1192
+ `http://localhost:${port}/api/MediaFileChunker`,
1193
+ form,
1194
+ {
1195
+ headers: {
1196
+ ...form.getHeaders(),
1197
+ "Content-Type": "multipart/form-data",
1198
+ },
1199
+ validateStatus: (status) => true,
1200
+ timeout: 5000,
1201
+ },
1202
+ );
1203
+
1204
+ t.is(response.status, 200, "Upload through legacy endpoint should succeed");
1205
+ t.truthy(response.data.url, "Response should contain file URL");
1206
+
1207
+ await cleanupUploadedFile(t, response.data.url);
1208
+ },
1178
1209
  );
1179
1210
 
1180
1211
  test.serial(
1181
- 'should handle hash operations through legacy MediaFileChunker endpoint',
1182
- async (t) => {
1183
- const testHash = 'test-hash-legacy';
1184
- const form = new FormData();
1185
- form.append('file', Buffer.from('test content'), 'test.txt');
1186
- form.append('hash', testHash);
1187
-
1188
- let uploadedUrl;
1189
- try {
1190
- // Upload file with hash through legacy endpoint
1191
- const uploadResponse = await axios.post(
1192
- `http://localhost:${port}/api/MediaFileChunker`,
1193
- form,
1194
- {
1195
- headers: {
1196
- ...form.getHeaders(),
1197
- 'Content-Type': 'multipart/form-data',
1198
- },
1199
- validateStatus: (status) => true,
1200
- timeout: 5000,
1201
- },
1202
- );
1203
-
1204
- t.is(
1205
- uploadResponse.status,
1206
- 200,
1207
- 'Upload should succeed through legacy endpoint',
1208
- );
1209
- t.truthy(uploadResponse.data.url, 'Response should contain file URL');
1210
- uploadedUrl = uploadResponse.data.url;
1211
-
1212
- // Wait a bit for Redis to be updated
1213
- await new Promise((resolve) => setTimeout(resolve, 1000));
1214
-
1215
- // Check hash through legacy endpoint
1216
- const hashCheckResponse = await axios.get(
1217
- `http://localhost:${port}/api/MediaFileChunker`,
1218
- {
1219
- params: {
1220
- hash: testHash,
1221
- checkHash: true,
1222
- },
1223
- validateStatus: (status) => true,
1224
- timeout: 5000,
1225
- },
1226
- );
1227
-
1228
- t.is(hashCheckResponse.status, 200, 'Hash check should return 200 for uploaded hash');
1229
- t.truthy(hashCheckResponse.data.url, 'Hash check should return file URL');
1230
- } finally {
1231
- await cleanupHashAndFile(testHash, uploadedUrl, `http://localhost:${port}/api/MediaFileChunker`);
1232
- }
1233
- },
1212
+ "should handle hash operations through legacy MediaFileChunker endpoint",
1213
+ async (t) => {
1214
+ const testHash = "test-hash-legacy";
1215
+ const form = new FormData();
1216
+ form.append("file", Buffer.from("test content"), "test.txt");
1217
+ form.append("hash", testHash);
1218
+
1219
+ let uploadedUrl;
1220
+ try {
1221
+ // Upload file with hash through legacy endpoint
1222
+ const uploadResponse = await axios.post(
1223
+ `http://localhost:${port}/api/MediaFileChunker`,
1224
+ form,
1225
+ {
1226
+ headers: {
1227
+ ...form.getHeaders(),
1228
+ "Content-Type": "multipart/form-data",
1229
+ },
1230
+ validateStatus: (status) => true,
1231
+ timeout: 5000,
1232
+ },
1233
+ );
1234
+
1235
+ t.is(
1236
+ uploadResponse.status,
1237
+ 200,
1238
+ "Upload should succeed through legacy endpoint",
1239
+ );
1240
+ t.truthy(uploadResponse.data.url, "Response should contain file URL");
1241
+ uploadedUrl = uploadResponse.data.url;
1242
+
1243
+ // Wait a bit for Redis to be updated
1244
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1245
+
1246
+ // Check hash through legacy endpoint
1247
+ const hashCheckResponse = await axios.get(
1248
+ `http://localhost:${port}/api/MediaFileChunker`,
1249
+ {
1250
+ params: {
1251
+ hash: testHash,
1252
+ checkHash: true,
1253
+ },
1254
+ validateStatus: (status) => true,
1255
+ timeout: 5000,
1256
+ },
1257
+ );
1258
+
1259
+ t.is(
1260
+ hashCheckResponse.status,
1261
+ 200,
1262
+ "Hash check should return 200 for uploaded hash",
1263
+ );
1264
+ t.truthy(hashCheckResponse.data.url, "Hash check should return file URL");
1265
+ } finally {
1266
+ await cleanupHashAndFile(
1267
+ testHash,
1268
+ uploadedUrl,
1269
+ `http://localhost:${port}/api/MediaFileChunker`,
1270
+ );
1271
+ }
1272
+ },
1234
1273
  );
1235
1274
 
1236
1275
  test.serial(
1237
- 'should handle delete operation through legacy MediaFileChunker endpoint',
1238
- async (t) => {
1239
- const testRequestId = 'test-delete-request-legacy';
1240
- const testContent = 'test content';
1241
- const form = new FormData();
1242
- form.append('file', Buffer.from(testContent), 'test.txt');
1243
-
1244
- // Upload a file first through legacy endpoint
1245
- const uploadResponse = await axios.post(
1246
- `http://localhost:${port}/api/MediaFileChunker`,
1247
- form,
1248
- {
1249
- headers: form.getHeaders(),
1250
- validateStatus: (status) => true,
1251
- timeout: 5000,
1252
- },
1253
- );
1254
- t.is(
1255
- uploadResponse.status,
1256
- 200,
1257
- 'Upload should succeed through legacy endpoint',
1258
- );
1259
-
1260
- // Extract the folder name from the URL
1261
- const url = uploadResponse.data.url;
1262
- const folderName = getFolderNameFromUrl(url);
1263
-
1264
- // Delete the file through legacy endpoint
1265
- const deleteResponse = await axios.delete(
1266
- `http://localhost:${port}/api/MediaFileChunker?operation=delete&requestId=${folderName}`,
1267
- );
1268
- t.is(
1269
- deleteResponse.status,
1270
- 200,
1271
- 'Delete should succeed through legacy endpoint',
1272
- );
1273
- t.true(
1274
- Array.isArray(deleteResponse.data.body),
1275
- 'Response should be an array of deleted files',
1276
- );
1277
- t.true(
1278
- deleteResponse.data.body.length > 0,
1279
- 'Should have deleted at least one file',
1280
- );
1281
- t.true(
1282
- deleteResponse.data.body[0].includes(folderName),
1283
- 'Deleted file should contain folder name',
1284
- );
1285
- },
1276
+ "should handle delete operation through legacy MediaFileChunker endpoint",
1277
+ async (t) => {
1278
+ const testRequestId = "test-delete-request-legacy";
1279
+ const testContent = "test content";
1280
+ const form = new FormData();
1281
+ form.append("file", Buffer.from(testContent), "test.txt");
1282
+
1283
+ // Upload a file first through legacy endpoint
1284
+ const uploadResponse = await axios.post(
1285
+ `http://localhost:${port}/api/MediaFileChunker`,
1286
+ form,
1287
+ {
1288
+ headers: form.getHeaders(),
1289
+ validateStatus: (status) => true,
1290
+ timeout: 5000,
1291
+ },
1292
+ );
1293
+ t.is(
1294
+ uploadResponse.status,
1295
+ 200,
1296
+ "Upload should succeed through legacy endpoint",
1297
+ );
1298
+
1299
+ // Extract the folder name from the URL
1300
+ const url = uploadResponse.data.url;
1301
+ const folderName = getFolderNameFromUrl(url);
1302
+
1303
+ // Delete the file through legacy endpoint
1304
+ const deleteResponse = await axios.delete(
1305
+ `http://localhost:${port}/api/MediaFileChunker?operation=delete&requestId=${folderName}`,
1306
+ );
1307
+ t.is(
1308
+ deleteResponse.status,
1309
+ 200,
1310
+ "Delete should succeed through legacy endpoint",
1311
+ );
1312
+ t.true(
1313
+ Array.isArray(deleteResponse.data.body),
1314
+ "Response should be an array of deleted files",
1315
+ );
1316
+ t.true(
1317
+ deleteResponse.data.body.length > 0,
1318
+ "Should have deleted at least one file",
1319
+ );
1320
+ t.true(
1321
+ deleteResponse.data.body[0].includes(folderName),
1322
+ "Deleted file should contain folder name",
1323
+ );
1324
+ },
1286
1325
  );
1287
1326
 
1288
1327
  test.serial(
1289
- 'should handle parameter validation through legacy MediaFileChunker endpoint',
1290
- async (t) => {
1328
+ "should handle parameter validation through legacy MediaFileChunker endpoint",
1329
+ async (t) => {
1291
1330
  // Test missing parameters
1292
- const response = await axios.get(
1293
- `http://localhost:${port}/api/MediaFileChunker`,
1294
- {
1295
- validateStatus: (status) => true,
1296
- timeout: 5000,
1297
- },
1298
- );
1299
-
1300
- t.is(response.status, 400, 'Should return 400 for missing parameters');
1301
- t.is(
1302
- response.data,
1303
- 'Please pass a uri and requestId on the query string or in the request body',
1304
- 'Should return proper error message',
1305
- );
1306
- },
1331
+ const response = await axios.get(
1332
+ `http://localhost:${port}/api/MediaFileChunker`,
1333
+ {
1334
+ validateStatus: (status) => true,
1335
+ timeout: 5000,
1336
+ },
1337
+ );
1338
+
1339
+ t.is(response.status, 400, "Should return 400 for missing parameters");
1340
+ t.is(
1341
+ response.data,
1342
+ "Please pass a uri and requestId on the query string or in the request body",
1343
+ "Should return proper error message",
1344
+ );
1345
+ },
1307
1346
  );
1308
1347
 
1309
1348
  test.serial(
1310
- 'should handle empty POST request through legacy MediaFileChunker endpoint',
1311
- async (t) => {
1312
- const form = new FormData();
1313
- try {
1314
- await axios.post(`http://localhost:${port}/api/MediaFileChunker`, form, {
1315
- headers: form.getHeaders(),
1316
- timeout: 5000,
1317
- });
1318
- t.fail('Should have thrown error');
1319
- } catch (error) {
1320
- t.is(
1321
- error.response.status,
1322
- 400,
1323
- 'Should return 400 for empty POST request',
1324
- );
1325
- t.is(
1326
- error.response.data,
1327
- 'No file provided in request',
1328
- 'Should return proper error message',
1329
- );
1330
- }
1331
- },
1349
+ "should handle empty POST request through legacy MediaFileChunker endpoint",
1350
+ async (t) => {
1351
+ const form = new FormData();
1352
+ try {
1353
+ await axios.post(`http://localhost:${port}/api/MediaFileChunker`, form, {
1354
+ headers: form.getHeaders(),
1355
+ timeout: 5000,
1356
+ });
1357
+ t.fail("Should have thrown error");
1358
+ } catch (error) {
1359
+ t.is(
1360
+ error.response.status,
1361
+ 400,
1362
+ "Should return 400 for empty POST request",
1363
+ );
1364
+ t.is(
1365
+ error.response.data,
1366
+ "No file provided in request",
1367
+ "Should return proper error message",
1368
+ );
1369
+ }
1370
+ },
1332
1371
  );
1333
1372
 
1334
1373
  test.serial(
1335
- 'should handle complete upload-request-delete-verify sequence through legacy MediaFileChunker endpoint',
1336
- async (t) => {
1337
- const testContent = 'test content for legacy sequence';
1338
- const testHash = 'test-legacy-sequence-hash';
1339
- const legacyBaseUrl = `http://localhost:${port}/api/MediaFileChunker`;
1340
- const form = new FormData();
1341
- form.append('file', Buffer.from(testContent), 'sequence-test.txt');
1342
- form.append('hash', testHash);
1343
-
1344
- // Upload file with hash through legacy endpoint
1345
- const uploadResponse = await axios.post(
1346
- legacyBaseUrl,
1347
- form,
1348
- {
1349
- headers: form.getHeaders(),
1350
- validateStatus: (status) => true,
1351
- timeout: 5000,
1352
- },
1353
- );
1354
- t.is(
1355
- uploadResponse.status,
1356
- 200,
1357
- 'Upload should succeed through legacy endpoint',
1358
- );
1359
- t.truthy(uploadResponse.data.url, 'Response should contain URL');
1360
-
1361
- // Wait for Redis to be updated after upload
1362
- await new Promise((resolve) => setTimeout(resolve, 1000));
1363
-
1364
- // Verify hash exists after upload using legacy endpoint
1365
- const initialHashCheck = await axios.get(
1366
- legacyBaseUrl,
1367
- {
1368
- params: {
1369
- hash: testHash,
1370
- checkHash: true,
1371
- },
1372
- validateStatus: (status) => true,
1373
- timeout: 5000,
1374
- },
1375
- );
1376
- t.is(initialHashCheck.status, 200, 'Hash should exist after upload');
1377
- t.truthy(initialHashCheck.data.url, 'Hash check should return file URL');
1378
-
1379
- // Wait for Redis to be updated after initial check
1380
- await new Promise((resolve) => setTimeout(resolve, 1000));
1381
-
1382
- // Clean up the file and hash using the legacy endpoint
1383
- await cleanupHashAndFile(testHash, uploadResponse.data.url, legacyBaseUrl);
1384
-
1385
- // Wait for Redis to be updated after cleanup
1386
- await new Promise((resolve) => setTimeout(resolve, 1000));
1387
-
1388
- // Verify hash is gone by trying to get the file URL through legacy endpoint
1389
- const hashCheckResponse = await axios.get(
1390
- legacyBaseUrl,
1391
- {
1392
- params: {
1393
- hash: testHash,
1394
- checkHash: true,
1395
- },
1396
- validateStatus: (status) => true,
1397
- timeout: 5000,
1398
- },
1399
- );
1400
- t.is(hashCheckResponse.status, 404, 'Hash should not exist after deletion');
1401
- },
1374
+ "should handle complete upload-request-delete-verify sequence through legacy MediaFileChunker endpoint",
1375
+ async (t) => {
1376
+ const testContent = "test content for legacy sequence";
1377
+ const testHash = "test-legacy-sequence-hash";
1378
+ const legacyBaseUrl = `http://localhost:${port}/api/MediaFileChunker`;
1379
+ const form = new FormData();
1380
+ form.append("file", Buffer.from(testContent), "sequence-test.txt");
1381
+ form.append("hash", testHash);
1382
+
1383
+ // Upload file with hash through legacy endpoint
1384
+ const uploadResponse = await axios.post(legacyBaseUrl, form, {
1385
+ headers: form.getHeaders(),
1386
+ validateStatus: (status) => true,
1387
+ timeout: 5000,
1388
+ });
1389
+ t.is(
1390
+ uploadResponse.status,
1391
+ 200,
1392
+ "Upload should succeed through legacy endpoint",
1393
+ );
1394
+ t.truthy(uploadResponse.data.url, "Response should contain URL");
1395
+
1396
+ // Wait for Redis to be updated after upload
1397
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1398
+
1399
+ // Verify hash exists after upload using legacy endpoint
1400
+ const initialHashCheck = await axios.get(legacyBaseUrl, {
1401
+ params: {
1402
+ hash: testHash,
1403
+ checkHash: true,
1404
+ },
1405
+ validateStatus: (status) => true,
1406
+ timeout: 5000,
1407
+ });
1408
+ t.is(initialHashCheck.status, 200, "Hash should exist after upload");
1409
+ t.truthy(initialHashCheck.data.url, "Hash check should return file URL");
1410
+
1411
+ // Wait for Redis to be updated after initial check
1412
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1413
+
1414
+ // Clean up the file and hash using the legacy endpoint
1415
+ await cleanupHashAndFile(testHash, uploadResponse.data.url, legacyBaseUrl);
1416
+
1417
+ // Wait for Redis to be updated after cleanup
1418
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1419
+
1420
+ // Verify hash is gone by trying to get the file URL through legacy endpoint
1421
+ const hashCheckResponse = await axios.get(legacyBaseUrl, {
1422
+ params: {
1423
+ hash: testHash,
1424
+ checkHash: true,
1425
+ },
1426
+ validateStatus: (status) => true,
1427
+ timeout: 5000,
1428
+ });
1429
+ t.is(hashCheckResponse.status, 404, "Hash should not exist after deletion");
1430
+ },
1402
1431
  );
1403
1432
 
1404
1433
  // Cleanup
1405
- test.after.always('cleanup', async (t) => {
1406
- // Add any necessary cleanup here
1434
+ test.after.always("cleanup", async (t) => {
1435
+ // Add any necessary cleanup here
1407
1436
  });