@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.
- package/config/default.example.json +15 -1
- package/config.js +42 -0
- package/helper-apps/cortex-file-handler/INTERFACE.md +20 -9
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +17 -17
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +35 -35
- package/helper-apps/cortex-file-handler/src/blobHandler.js +1010 -909
- package/helper-apps/cortex-file-handler/src/constants.js +98 -98
- package/helper-apps/cortex-file-handler/src/docHelper.js +27 -27
- package/helper-apps/cortex-file-handler/src/fileChunker.js +224 -214
- package/helper-apps/cortex-file-handler/src/helper.js +93 -93
- package/helper-apps/cortex-file-handler/src/index.js +584 -550
- package/helper-apps/cortex-file-handler/src/localFileHandler.js +86 -86
- package/helper-apps/cortex-file-handler/src/redis.js +186 -90
- package/helper-apps/cortex-file-handler/src/services/ConversionService.js +301 -273
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +55 -55
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +174 -154
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +239 -223
- package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +161 -159
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +73 -71
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +46 -45
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +256 -213
- package/helper-apps/cortex-file-handler/src/start.js +4 -1
- package/helper-apps/cortex-file-handler/src/utils/filenameUtils.js +59 -25
- package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +119 -116
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +257 -257
- package/helper-apps/cortex-file-handler/tests/cleanup.test.js +676 -0
- package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +124 -124
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +249 -208
- package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +439 -380
- package/helper-apps/cortex-file-handler/tests/getOperations.test.js +299 -263
- package/helper-apps/cortex-file-handler/tests/postOperations.test.js +265 -239
- package/helper-apps/cortex-file-handler/tests/start.test.js +1230 -1201
- package/helper-apps/cortex-file-handler/tests/storage/AzureStorageProvider.test.js +110 -105
- package/helper-apps/cortex-file-handler/tests/storage/GCSStorageProvider.test.js +201 -175
- package/helper-apps/cortex-file-handler/tests/storage/LocalStorageProvider.test.js +128 -125
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +78 -73
- package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +99 -99
- package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -70
- package/lib/azureAuthTokenHelper.js +78 -0
- package/lib/entityConstants.js +5 -4
- package/package.json +1 -1
- package/pathways/bing_afagent.js +13 -0
- package/pathways/gemini_15_vision.js +4 -0
- package/pathways/system/entity/tools/sys_tool_bing_search.js +1 -1
- package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +141 -0
- package/pathways/system/entity/tools/sys_tool_browser_jina.js +1 -1
- package/pathways/system/entity/tools/sys_tool_readfile.js +4 -0
- package/pathways/system/workspaces/workspace_applet_edit.js +4 -0
- package/pathways/transcribe_gemini.js +4 -0
- package/pathways/translate_subtitle.js +15 -8
- package/server/modelExecutor.js +4 -0
- package/server/plugins/azureFoundryAgentsPlugin.js +372 -0
- package/server/plugins/gemini15ChatPlugin.js +3 -3
- package/tests/azureAuthTokenHelper.test.js +150 -0
- package/tests/azureFoundryAgents.test.js +212 -0
|
@@ -1,1407 +1,1436 @@
|
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
|
2
|
-
import { execSync } from
|
|
3
|
-
import fs from
|
|
4
|
-
import os from
|
|
5
|
-
import path from
|
|
6
|
-
import { PassThrough } from
|
|
7
|
-
|
|
8
|
-
import test from
|
|
9
|
-
import axios from
|
|
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
|
|
12
|
-
import { v4 as uuidv4 } from
|
|
11
|
+
import FormData from "form-data";
|
|
12
|
+
import { v4 as uuidv4 } from "uuid";
|
|
13
13
|
|
|
14
|
-
import { port, publicFolder, ipAddress } from
|
|
15
|
-
import {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
process.env.AZURE_STORAGE_CONNECTION_STRING !==
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
97
|
+
if (response.data?.url) {
|
|
98
|
+
response.data.url = convertToLocalUrl(response.data.url);
|
|
99
|
+
}
|
|
82
100
|
|
|
83
|
-
|
|
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
|
-
|
|
104
|
-
|
|
106
|
+
// Wait for server to be ready
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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,
|
|
139
|
+
t.is(response.status, 400, "Should return 400 for missing parameters");
|
|
197
140
|
t.is(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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(
|
|
148
|
+
test.serial(
|
|
149
|
+
"should validate required parameters on MediaFileChunker legacy endpoint",
|
|
150
|
+
async (t) => {
|
|
205
151
|
const response = await axios.get(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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,
|
|
159
|
+
t.is(response.status, 400, "Should return 400 for missing parameters");
|
|
218
160
|
t.is(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
//
|
|
249
|
-
test.serial(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
292
|
-
|
|
228
|
+
test.serial(
|
|
229
|
+
"should handle hash operations without hash parameter",
|
|
230
|
+
async (t) => {
|
|
293
231
|
const response = await axios.get(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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,
|
|
242
|
+
t.is(response.status, 400, "Should return 400 for missing hash");
|
|
305
243
|
t.is(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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,
|
|
550
|
-
t.truthy(uploadResponse.data.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
|
-
//
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
526
|
+
hashCheckResponse.status,
|
|
527
|
+
200,
|
|
528
|
+
"Hash check should return 200 for uploaded hash",
|
|
604
529
|
);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
await
|
|
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(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
629
|
+
// Clean up the uploaded file
|
|
630
|
+
await cleanupUploadedFile(t, uploadResponse.data.url);
|
|
631
631
|
});
|
|
632
632
|
|
|
633
|
-
test.serial(
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
714
|
+
test.serial(
|
|
715
|
+
"should handle multiple file uploads with unique hashes",
|
|
716
|
+
async (t) => {
|
|
717
|
+
const uploadedFiles = [];
|
|
748
718
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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
|
-
|
|
762
|
-
const originalUrl = upload1.data.url;
|
|
729
|
+
});
|
|
730
|
+
t.is(uploadResponse.status, 200, `Upload should succeed for file ${i}`);
|
|
763
731
|
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
798
|
-
const
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
762
|
+
// Clean up all files
|
|
763
|
+
for (const file of uploadedFiles) {
|
|
764
|
+
await cleanupUploadedFile(t, file.url);
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
);
|
|
811
768
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
830
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
899
|
+
if (isGCSConfigured()) {
|
|
869
900
|
// Verify GCS URL exists and is in correct format
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
925
|
+
if (isGCSConfigured()) {
|
|
895
926
|
// Verify file is also deleted from GCS
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
948
|
-
|
|
936
|
+
t.pass("Skipping test - GCS not configured");
|
|
937
|
+
return;
|
|
949
938
|
}
|
|
950
939
|
|
|
951
940
|
const requestId = uuidv4();
|
|
952
|
-
const testContent =
|
|
941
|
+
const testContent = "test content for dual storage";
|
|
953
942
|
const form = new FormData();
|
|
954
|
-
form.append(
|
|
943
|
+
form.append("file", Buffer.from(testContent), "dual-test.txt");
|
|
944
|
+
form.append("requestId", requestId);
|
|
955
945
|
|
|
956
|
-
// Upload
|
|
957
|
-
const uploadResponse = await
|
|
958
|
-
|
|
959
|
-
|
|
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.
|
|
971
|
-
t.truthy(uploadResponse.data.gcs,
|
|
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
|
-
|
|
974
|
-
|
|
955
|
+
uploadResponse.data.gcs.startsWith("gs://"),
|
|
956
|
+
"GCS URL should use gs:// protocol",
|
|
975
957
|
);
|
|
976
958
|
|
|
977
|
-
// Verify
|
|
978
|
-
|
|
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
|
-
//
|
|
983
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
//
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
1060
|
+
t.pass("Skipping test - GCS not configured");
|
|
1061
|
+
return;
|
|
1114
1062
|
}
|
|
1115
1063
|
|
|
1116
|
-
// Create a test file
|
|
1117
|
-
const
|
|
1064
|
+
// Create a large test file first
|
|
1065
|
+
const testFileUrl = await createAndUploadTestFile();
|
|
1066
|
+
const requestId = uuidv4();
|
|
1118
1067
|
|
|
1119
|
-
//
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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(
|
|
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
|
-
|
|
1133
|
-
|
|
1083
|
+
chunkResponse.data.length > 0,
|
|
1084
|
+
"Should have created at least one chunk",
|
|
1134
1085
|
);
|
|
1135
1086
|
|
|
1136
|
-
//
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1328
|
+
"should handle parameter validation through legacy MediaFileChunker endpoint",
|
|
1329
|
+
async (t) => {
|
|
1291
1330
|
// Test missing parameters
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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(
|
|
1406
|
-
|
|
1434
|
+
test.after.always("cleanup", async (t) => {
|
|
1435
|
+
// Add any necessary cleanup here
|
|
1407
1436
|
});
|