@aj-archipelago/cortex 1.3.62 → 1.3.63
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/.github/workflows/cortex-file-handler-test.yml +61 -0
- package/README.md +31 -7
- package/config/default.example.json +15 -0
- package/config.js +133 -12
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +22 -0
- package/helper-apps/cortex-autogen2/Dockerfile +31 -0
- package/helper-apps/cortex-autogen2/Dockerfile.worker +41 -0
- package/helper-apps/cortex-autogen2/README.md +183 -0
- package/helper-apps/cortex-autogen2/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/agents.py +131 -0
- package/helper-apps/cortex-autogen2/docker-compose.yml +20 -0
- package/helper-apps/cortex-autogen2/function_app.py +55 -0
- package/helper-apps/cortex-autogen2/host.json +15 -0
- package/helper-apps/cortex-autogen2/main.py +126 -0
- package/helper-apps/cortex-autogen2/poetry.lock +3652 -0
- package/helper-apps/cortex-autogen2/pyproject.toml +36 -0
- package/helper-apps/cortex-autogen2/requirements.txt +20 -0
- package/helper-apps/cortex-autogen2/send_task.py +105 -0
- package/helper-apps/cortex-autogen2/services/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/services/azure_queue.py +85 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +153 -0
- package/helper-apps/cortex-autogen2/task_processor.py +488 -0
- package/helper-apps/cortex-autogen2/tools/__init__.py +24 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +175 -0
- package/helper-apps/cortex-autogen2/tools/azure_foundry_agents.py +601 -0
- package/helper-apps/cortex-autogen2/tools/coding_tools.py +72 -0
- package/helper-apps/cortex-autogen2/tools/download_tools.py +48 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +545 -0
- package/helper-apps/cortex-autogen2/tools/search_tools.py +646 -0
- package/helper-apps/cortex-azure-cleaner/README.md +36 -0
- package/helper-apps/cortex-file-converter/README.md +93 -0
- package/helper-apps/cortex-file-converter/key_to_pdf.py +104 -0
- package/helper-apps/cortex-file-converter/list_blob_extensions.py +89 -0
- package/helper-apps/cortex-file-converter/process_azure_keynotes.py +181 -0
- package/helper-apps/cortex-file-converter/requirements.txt +1 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.ci +7 -0
- package/helper-apps/cortex-file-handler/.env.test.azure.sample +1 -1
- package/helper-apps/cortex-file-handler/.env.test.gcs.ci +10 -0
- package/helper-apps/cortex-file-handler/.env.test.gcs.sample +2 -2
- package/helper-apps/cortex-file-handler/INTERFACE.md +41 -0
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-azure-container.js +41 -17
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +30 -15
- package/helper-apps/cortex-file-handler/scripts/test-azure.sh +32 -6
- package/helper-apps/cortex-file-handler/scripts/test-gcs.sh +24 -2
- package/helper-apps/cortex-file-handler/scripts/validate-env.js +128 -0
- package/helper-apps/cortex-file-handler/src/blobHandler.js +161 -51
- package/helper-apps/cortex-file-handler/src/constants.js +3 -0
- package/helper-apps/cortex-file-handler/src/fileChunker.js +10 -8
- package/helper-apps/cortex-file-handler/src/index.js +116 -9
- package/helper-apps/cortex-file-handler/src/redis.js +61 -1
- package/helper-apps/cortex-file-handler/src/services/ConversionService.js +11 -8
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +2 -2
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +88 -6
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +58 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +25 -5
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +9 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +120 -16
- package/helper-apps/cortex-file-handler/src/start.js +27 -17
- package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +52 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +40 -0
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +553 -0
- package/helper-apps/cortex-file-handler/tests/cleanup.test.js +46 -52
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +451 -0
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +229 -0
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +392 -0
- package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +7 -2
- package/helper-apps/cortex-file-handler/tests/deleteOperations.test.js +348 -0
- package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +23 -2
- package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +11 -5
- package/helper-apps/cortex-file-handler/tests/getOperations.test.js +58 -24
- package/helper-apps/cortex-file-handler/tests/postOperations.test.js +11 -4
- package/helper-apps/cortex-file-handler/tests/shortLivedUrlConversion.test.js +225 -0
- package/helper-apps/cortex-file-handler/tests/start.test.js +8 -12
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +80 -0
- package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +388 -22
- package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +74 -0
- package/lib/cortexResponse.js +153 -0
- package/lib/entityConstants.js +21 -3
- package/lib/logger.js +21 -4
- package/lib/pathwayTools.js +28 -9
- package/lib/util.js +49 -0
- package/package.json +1 -1
- package/pathways/basePathway.js +1 -0
- package/pathways/bing_afagent.js +54 -1
- package/pathways/call_tools.js +2 -3
- package/pathways/chat_jarvis.js +1 -1
- package/pathways/google_cse.js +27 -0
- package/pathways/grok_live_search.js +18 -0
- package/pathways/system/entity/memory/sys_memory_lookup_required.js +1 -0
- package/pathways/system/entity/memory/sys_memory_required.js +1 -0
- package/pathways/system/entity/memory/sys_search_memory.js +1 -0
- package/pathways/system/entity/sys_entity_agent.js +56 -4
- package/pathways/system/entity/sys_generator_quick.js +1 -0
- package/pathways/system/entity/tools/sys_tool_bing_search_afagent.js +26 -0
- package/pathways/system/entity/tools/sys_tool_google_search.js +141 -0
- package/pathways/system/entity/tools/sys_tool_grok_x_search.js +237 -0
- package/pathways/system/entity/tools/sys_tool_image.js +1 -1
- package/pathways/system/rest_streaming/sys_claude_37_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_41_opus.js +21 -0
- package/pathways/system/rest_streaming/sys_claude_4_sonnet.js +21 -0
- package/pathways/system/rest_streaming/sys_google_gemini_25_flash.js +25 -0
- package/pathways/system/rest_streaming/{sys_google_gemini_chat.js → sys_google_gemini_25_pro.js} +6 -4
- package/pathways/system/rest_streaming/sys_grok_4.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_non_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_grok_4_fast_reasoning.js +23 -0
- package/pathways/system/rest_streaming/sys_openai_chat.js +3 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41.js +22 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt41_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_35_sonnet.js → sys_openai_chat_gpt4_omni.js} +6 -4
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_omni_mini.js +21 -0
- package/pathways/system/rest_streaming/{sys_claude_3_haiku.js → sys_openai_chat_gpt5.js} +7 -5
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_chat.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_mini.js +21 -0
- package/pathways/system/rest_streaming/sys_openai_chat_gpt5_nano.js +21 -0
- package/pathways/system/rest_streaming/{sys_openai_chat_o1.js → sys_openai_chat_o3.js} +6 -3
- package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +3 -0
- package/pathways/system/workspaces/run_workspace_prompt.js +99 -0
- package/pathways/vision.js +1 -1
- package/server/graphql.js +1 -1
- package/server/modelExecutor.js +8 -0
- package/server/pathwayResolver.js +166 -16
- package/server/pathwayResponseParser.js +16 -8
- package/server/plugins/azureFoundryAgentsPlugin.js +1 -1
- package/server/plugins/claude3VertexPlugin.js +193 -45
- package/server/plugins/gemini15ChatPlugin.js +21 -0
- package/server/plugins/gemini15VisionPlugin.js +360 -0
- package/server/plugins/googleCsePlugin.js +94 -0
- package/server/plugins/grokVisionPlugin.js +365 -0
- package/server/plugins/modelPlugin.js +3 -1
- package/server/plugins/openAiChatPlugin.js +106 -13
- package/server/plugins/openAiVisionPlugin.js +42 -30
- package/server/resolver.js +28 -4
- package/server/rest.js +270 -53
- package/server/typeDef.js +1 -0
- package/tests/{mocks.js → helpers/mocks.js} +5 -2
- package/tests/{server.js → helpers/server.js} +2 -2
- package/tests/helpers/sseAssert.js +23 -0
- package/tests/helpers/sseClient.js +73 -0
- package/tests/helpers/subscriptionAssert.js +11 -0
- package/tests/helpers/subscriptions.js +113 -0
- package/tests/{sublong.srt → integration/features/translate/sublong.srt} +4543 -4543
- package/tests/integration/features/translate/translate_chunking_stream.test.js +100 -0
- package/tests/{translate_srt.test.js → integration/features/translate/translate_srt.test.js} +2 -2
- package/tests/integration/graphql/async/stream/agentic.test.js +477 -0
- package/tests/integration/graphql/async/stream/subscription_streaming.test.js +62 -0
- package/tests/integration/graphql/async/stream/sys_entity_start_streaming.test.js +71 -0
- package/tests/integration/graphql/async/stream/vendors/claude_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/gemini_streaming.test.js +66 -0
- package/tests/integration/graphql/async/stream/vendors/grok_streaming.test.js +56 -0
- package/tests/integration/graphql/async/stream/vendors/openai_streaming.test.js +72 -0
- package/tests/integration/graphql/features/google/sysToolGoogleSearch.test.js +96 -0
- package/tests/integration/graphql/features/grok/grok.test.js +688 -0
- package/tests/integration/graphql/features/grok/grok_x_search_tool.test.js +354 -0
- package/tests/{main.test.js → integration/graphql/features/main.test.js} +1 -1
- package/tests/{call_tools.test.js → integration/graphql/features/tools/call_tools.test.js} +2 -2
- package/tests/{vision.test.js → integration/graphql/features/vision/vision.test.js} +1 -1
- package/tests/integration/graphql/subscriptions/connection.test.js +26 -0
- package/tests/{openai_api.test.js → integration/rest/oai/openai_api.test.js} +63 -238
- package/tests/integration/rest/oai/tool_calling_api.test.js +343 -0
- package/tests/integration/rest/oai/tool_calling_streaming.test.js +85 -0
- package/tests/integration/rest/vendors/claude_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/claude_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/gemini_streaming.test.js +47 -0
- package/tests/integration/rest/vendors/gemini_tool_calling_streaming.test.js +75 -0
- package/tests/integration/rest/vendors/grok_streaming.test.js +55 -0
- package/tests/integration/rest/vendors/grok_tool_calling_streaming.test.js +75 -0
- package/tests/{azureAuthTokenHelper.test.js → unit/core/azureAuthTokenHelper.test.js} +1 -1
- package/tests/{chunkfunction.test.js → unit/core/chunkfunction.test.js} +2 -2
- package/tests/{config.test.js → unit/core/config.test.js} +3 -3
- package/tests/{encodeCache.test.js → unit/core/encodeCache.test.js} +1 -1
- package/tests/{fastLruCache.test.js → unit/core/fastLruCache.test.js} +1 -1
- package/tests/{handleBars.test.js → unit/core/handleBars.test.js} +1 -1
- package/tests/{memoryfunction.test.js → unit/core/memoryfunction.test.js} +2 -2
- package/tests/unit/core/mergeResolver.test.js +952 -0
- package/tests/{parser.test.js → unit/core/parser.test.js} +3 -3
- package/tests/unit/core/pathwayResolver.test.js +187 -0
- package/tests/{requestMonitor.test.js → unit/core/requestMonitor.test.js} +1 -1
- package/tests/{requestMonitorDurationEstimator.test.js → unit/core/requestMonitorDurationEstimator.test.js} +1 -1
- package/tests/{truncateMessages.test.js → unit/core/truncateMessages.test.js} +3 -3
- package/tests/{util.test.js → unit/core/util.test.js} +1 -1
- package/tests/{apptekTranslatePlugin.test.js → unit/plugins/apptekTranslatePlugin.test.js} +3 -3
- package/tests/{azureFoundryAgents.test.js → unit/plugins/azureFoundryAgents.test.js} +136 -1
- package/tests/{claude3VertexPlugin.test.js → unit/plugins/claude3VertexPlugin.test.js} +32 -10
- package/tests/{claude3VertexToolConversion.test.js → unit/plugins/claude3VertexToolConversion.test.js} +3 -3
- package/tests/unit/plugins/googleCsePlugin.test.js +111 -0
- package/tests/unit/plugins/grokVisionPlugin.test.js +1392 -0
- package/tests/{modelPlugin.test.js → unit/plugins/modelPlugin.test.js} +3 -3
- package/tests/{multimodal_conversion.test.js → unit/plugins/multimodal_conversion.test.js} +4 -4
- package/tests/{openAiChatPlugin.test.js → unit/plugins/openAiChatPlugin.test.js} +13 -4
- package/tests/{openAiToolPlugin.test.js → unit/plugins/openAiToolPlugin.test.js} +35 -27
- package/tests/{tokenHandlingTests.test.js → unit/plugins/tokenHandlingTests.test.js} +5 -5
- package/tests/{translate_apptek.test.js → unit/plugins/translate_apptek.test.js} +3 -3
- package/tests/{streaming.test.js → unit/plugins.streaming/plugin_stream_events.test.js} +19 -58
- package/helper-apps/mogrt-handler/tests/test-files/test.gif +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mogrt +0 -1
- package/helper-apps/mogrt-handler/tests/test-files/test.mp4 +0 -1
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_32.js +0 -19
- package/pathways/system/rest_streaming/sys_openai_chat_gpt4_turbo.js +0 -19
- package/pathways/system/workspaces/run_claude35_sonnet.js +0 -21
- package/pathways/system/workspaces/run_claude3_haiku.js +0 -20
- package/pathways/system/workspaces/run_gpt35turbo.js +0 -20
- package/pathways/system/workspaces/run_gpt4.js +0 -20
- package/pathways/system/workspaces/run_gpt4_32.js +0 -20
- package/tests/agentic.test.js +0 -256
- package/tests/pathwayResolver.test.js +0 -78
- package/tests/subscription.test.js +0 -387
- /package/tests/{subchunk.srt → integration/features/translate/subchunk.srt} +0 -0
- /package/tests/{subhorizontal.srt → integration/features/translate/subhorizontal.srt} +0 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import test from "ava";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import FormData from "form-data";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { v4 as uuidv4 } from "uuid";
|
|
8
|
+
|
|
9
|
+
import { port } from "../src/start.js";
|
|
10
|
+
import { AZURITE_ACCOUNT_NAME } from "../src/constants.js";
|
|
11
|
+
import {
|
|
12
|
+
cleanupHashAndFile,
|
|
13
|
+
createTestMediaFile,
|
|
14
|
+
startTestServer,
|
|
15
|
+
stopTestServer,
|
|
16
|
+
setupTestDirectory
|
|
17
|
+
} from "./testUtils.helper.js";
|
|
18
|
+
import {
|
|
19
|
+
setFileStoreMap,
|
|
20
|
+
getFileStoreMap,
|
|
21
|
+
removeFromFileStoreMap
|
|
22
|
+
} from "../src/redis.js";
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(__filename);
|
|
26
|
+
const baseUrl = `http://localhost:${port}/api/CortexFileHandler`;
|
|
27
|
+
|
|
28
|
+
// Helper function to create test files
|
|
29
|
+
async function createTestFile(content, extension = "txt") {
|
|
30
|
+
const testDir = path.join(__dirname, "test-files");
|
|
31
|
+
if (!fs.existsSync(testDir)) {
|
|
32
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const filename = path.join(
|
|
35
|
+
testDir,
|
|
36
|
+
`test-checkhash-${uuidv4().slice(0, 8)}.${extension}`,
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(filename, content);
|
|
39
|
+
return filename;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Helper function to upload file
|
|
43
|
+
async function uploadFile(filePath, requestId = null, hash = null) {
|
|
44
|
+
const form = new FormData();
|
|
45
|
+
form.append("file", fs.createReadStream(filePath));
|
|
46
|
+
if (requestId) form.append("requestId", requestId);
|
|
47
|
+
if (hash) form.append("hash", hash);
|
|
48
|
+
|
|
49
|
+
return await axios.post(baseUrl, form, {
|
|
50
|
+
headers: form.getHeaders(),
|
|
51
|
+
validateStatus: (status) => true,
|
|
52
|
+
timeout: 15000,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Helper function to check hash
|
|
57
|
+
async function checkHash(hash, shortLivedMinutes = null) {
|
|
58
|
+
const params = { hash, checkHash: true };
|
|
59
|
+
if (shortLivedMinutes !== null) {
|
|
60
|
+
params.shortLivedMinutes = shortLivedMinutes;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return await axios.get(baseUrl, {
|
|
64
|
+
params,
|
|
65
|
+
validateStatus: (status) => true,
|
|
66
|
+
timeout: 10000,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Helper to check if Azure is configured (real Azure, not local emulator)
|
|
71
|
+
function isAzureConfigured() {
|
|
72
|
+
return process.env.AZURE_STORAGE_CONNECTION_STRING &&
|
|
73
|
+
!process.env.AZURE_STORAGE_CONNECTION_STRING.includes("UseDevelopmentStorage=true");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Helper to check if using Azure storage provider (including Azurite emulator)
|
|
77
|
+
function isUsingAzureStorage() {
|
|
78
|
+
return process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Helper to check if GCS is configured
|
|
82
|
+
function isGCSConfigured() {
|
|
83
|
+
return process.env.GOOGLE_CLOUD_PROJECT_ID &&
|
|
84
|
+
process.env.GOOGLE_CLOUD_BUCKET_NAME;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Test setup
|
|
88
|
+
test.before(async (t) => {
|
|
89
|
+
await setupTestDirectory(__dirname);
|
|
90
|
+
await startTestServer();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test.after(async (t) => {
|
|
94
|
+
await stopTestServer();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Core functionality tests
|
|
98
|
+
test.serial("checkHash should always return shortLivedUrl", async (t) => {
|
|
99
|
+
const filePath = await createTestFile("Content for shortLivedUrl test");
|
|
100
|
+
const hash = `test-shortlived-${uuidv4()}`;
|
|
101
|
+
let uploadResponse;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Upload file with hash
|
|
105
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
106
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
107
|
+
t.truthy(uploadResponse.data.url, "Upload should return URL");
|
|
108
|
+
|
|
109
|
+
// Check hash - should always return shortLivedUrl now
|
|
110
|
+
const checkResponse = await checkHash(hash);
|
|
111
|
+
|
|
112
|
+
t.is(checkResponse.status, 200, "checkHash should succeed");
|
|
113
|
+
t.truthy(checkResponse.data, "checkHash should return data");
|
|
114
|
+
t.truthy(checkResponse.data.url, "Response should include original URL");
|
|
115
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
116
|
+
t.truthy(checkResponse.data.expiresInMinutes, "Response should include expiration time");
|
|
117
|
+
t.is(checkResponse.data.expiresInMinutes, 5, "Default expiration should be 5 minutes");
|
|
118
|
+
|
|
119
|
+
// Verify shortLivedUrl behavior based on storage provider
|
|
120
|
+
if (isUsingAzureStorage()) {
|
|
121
|
+
// With Azure (including Azurite), shortLivedUrl should be different from original URL
|
|
122
|
+
t.not(
|
|
123
|
+
checkResponse.data.shortLivedUrl,
|
|
124
|
+
checkResponse.data.url,
|
|
125
|
+
"With Azure storage, shortLivedUrl should be different from original URL"
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
// With LocalStorage, shortLivedUrl equals original URL (fallback behavior)
|
|
129
|
+
t.is(
|
|
130
|
+
checkResponse.data.shortLivedUrl,
|
|
131
|
+
checkResponse.data.url,
|
|
132
|
+
"With LocalStorage provider, shortLivedUrl should equal original URL (fallback behavior)"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Verify base URLs are the same (only SAS token should differ)
|
|
137
|
+
const originalUrlBase = checkResponse.data.url.split('?')[0];
|
|
138
|
+
const shortLivedUrlBase = checkResponse.data.shortLivedUrl.split('?')[0];
|
|
139
|
+
t.is(originalUrlBase, shortLivedUrlBase, "Base URLs should be the same");
|
|
140
|
+
|
|
141
|
+
} finally {
|
|
142
|
+
fs.unlinkSync(filePath);
|
|
143
|
+
if (uploadResponse?.data?.url) {
|
|
144
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test.serial("checkHash should respect custom shortLivedMinutes parameter", async (t) => {
|
|
150
|
+
const filePath = await createTestFile("Content for custom duration test");
|
|
151
|
+
const hash = `test-custom-duration-${uuidv4()}`;
|
|
152
|
+
const customMinutes = 15;
|
|
153
|
+
let uploadResponse;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Upload file with hash
|
|
157
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
158
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
159
|
+
|
|
160
|
+
// Check hash with custom duration
|
|
161
|
+
const checkResponse = await checkHash(hash, customMinutes);
|
|
162
|
+
|
|
163
|
+
t.is(checkResponse.status, 200, "checkHash should succeed");
|
|
164
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
165
|
+
t.is(checkResponse.data.expiresInMinutes, customMinutes, `Expiration should be ${customMinutes} minutes`);
|
|
166
|
+
|
|
167
|
+
} finally {
|
|
168
|
+
fs.unlinkSync(filePath);
|
|
169
|
+
if (uploadResponse?.data?.url) {
|
|
170
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test.serial("checkHash should handle invalid shortLivedMinutes parameter gracefully", async (t) => {
|
|
176
|
+
const filePath = await createTestFile("Content for invalid parameter test");
|
|
177
|
+
const hash = `test-invalid-param-${uuidv4()}`;
|
|
178
|
+
let uploadResponse;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Upload file with hash
|
|
182
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
183
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
184
|
+
|
|
185
|
+
// Check hash with invalid shortLivedMinutes
|
|
186
|
+
const checkResponse = await axios.get(baseUrl, {
|
|
187
|
+
params: {
|
|
188
|
+
hash,
|
|
189
|
+
checkHash: true,
|
|
190
|
+
shortLivedMinutes: "invalid",
|
|
191
|
+
},
|
|
192
|
+
validateStatus: (status) => true,
|
|
193
|
+
timeout: 10000,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
t.is(checkResponse.status, 200, "checkHash should succeed even with invalid shortLivedMinutes");
|
|
197
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
198
|
+
t.is(checkResponse.data.expiresInMinutes, 5, "Should default to 5 minutes for invalid input");
|
|
199
|
+
|
|
200
|
+
} finally {
|
|
201
|
+
fs.unlinkSync(filePath);
|
|
202
|
+
if (uploadResponse?.data?.url) {
|
|
203
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test.serial("checkHash shortLivedUrl should be accessible", async (t) => {
|
|
209
|
+
const testContent = "Content for accessibility test";
|
|
210
|
+
const filePath = await createTestFile(testContent);
|
|
211
|
+
const hash = `test-accessible-${uuidv4()}`;
|
|
212
|
+
let uploadResponse;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Upload file with hash
|
|
216
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
217
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
218
|
+
|
|
219
|
+
// Check hash to get shortLivedUrl
|
|
220
|
+
const checkResponse = await checkHash(hash);
|
|
221
|
+
|
|
222
|
+
t.is(checkResponse.status, 200, "checkHash should succeed");
|
|
223
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
224
|
+
|
|
225
|
+
// Verify the shortLivedUrl is accessible
|
|
226
|
+
// Skip this test for Azure emulator as it may have network issues
|
|
227
|
+
if (isAzureConfigured() && !checkResponse.data.shortLivedUrl.includes(AZURITE_ACCOUNT_NAME)) {
|
|
228
|
+
// Test with real Azure storage
|
|
229
|
+
const fileResponse = await axios.get(checkResponse.data.shortLivedUrl, {
|
|
230
|
+
validateStatus: (status) => true,
|
|
231
|
+
timeout: 10000,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
t.is(fileResponse.status, 200, "shortLivedUrl should be accessible with real Azure storage");
|
|
235
|
+
t.is(fileResponse.data, testContent, "File content should match through shortLivedUrl");
|
|
236
|
+
} else {
|
|
237
|
+
// For LocalStorage provider, shortLivedUrl should be accessible
|
|
238
|
+
const fileResponse = await axios.get(checkResponse.data.shortLivedUrl, {
|
|
239
|
+
validateStatus: (status) => true,
|
|
240
|
+
timeout: 10000,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
t.is(fileResponse.status, 200, "shortLivedUrl should be accessible");
|
|
244
|
+
t.is(fileResponse.data, testContent, "File content should match through shortLivedUrl");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
} finally {
|
|
248
|
+
fs.unlinkSync(filePath);
|
|
249
|
+
if (uploadResponse?.data?.url) {
|
|
250
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test.serial("checkHash should return consistent response structure", async (t) => {
|
|
256
|
+
const filePath = await createTestFile("Content for structure test");
|
|
257
|
+
const hash = `test-structure-${uuidv4()}`;
|
|
258
|
+
let uploadResponse;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Upload file with hash
|
|
262
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
263
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
264
|
+
|
|
265
|
+
// Test checkHash with default parameters
|
|
266
|
+
const checkResponse1 = await axios.get(baseUrl, {
|
|
267
|
+
params: { hash, checkHash: true },
|
|
268
|
+
validateStatus: (status) => true,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Test checkHash with custom shortLivedMinutes
|
|
272
|
+
const checkResponse2 = await axios.get(baseUrl, {
|
|
273
|
+
params: { hash, checkHash: true, shortLivedMinutes: 10 },
|
|
274
|
+
validateStatus: (status) => true,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Both responses should have the same structure
|
|
278
|
+
const expectedKeys = [
|
|
279
|
+
'message', 'filename', 'url', 'hash', 'timestamp',
|
|
280
|
+
'shortLivedUrl', 'expiresInMinutes'
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const response of [checkResponse1, checkResponse2]) {
|
|
284
|
+
t.is(response.status, 200, "checkHash should succeed");
|
|
285
|
+
|
|
286
|
+
for (const key of expectedKeys) {
|
|
287
|
+
t.truthy(response.data[key] !== undefined, `Response should include ${key}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// shortLivedUrl behavior depends on storage provider
|
|
292
|
+
if (isUsingAzureStorage()) {
|
|
293
|
+
// With Azure storage (including Azurite), different expiration times should result in different SAS tokens
|
|
294
|
+
t.not(
|
|
295
|
+
checkResponse1.data.shortLivedUrl,
|
|
296
|
+
checkResponse2.data.shortLivedUrl,
|
|
297
|
+
"With Azure storage, shortLivedUrls with different expiration should be different"
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
// With LocalStorage provider, shortLivedUrl should be the same
|
|
301
|
+
t.is(
|
|
302
|
+
checkResponse1.data.shortLivedUrl,
|
|
303
|
+
checkResponse2.data.shortLivedUrl,
|
|
304
|
+
"With LocalStorage provider, shortLivedUrls are the same regardless of expiration time"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
} finally {
|
|
309
|
+
fs.unlinkSync(filePath);
|
|
310
|
+
if (uploadResponse?.data?.url) {
|
|
311
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test.serial("checkHash should handle fallback when SAS token generation is not supported", async (t) => {
|
|
317
|
+
const filePath = await createTestFile("Content for fallback test");
|
|
318
|
+
const hash = `test-fallback-${uuidv4()}`;
|
|
319
|
+
let uploadResponse;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
// Upload file with hash
|
|
323
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
324
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
325
|
+
|
|
326
|
+
// Check hash - should handle fallback gracefully
|
|
327
|
+
const checkResponse = await axios.get(baseUrl, {
|
|
328
|
+
params: {
|
|
329
|
+
hash,
|
|
330
|
+
checkHash: true,
|
|
331
|
+
},
|
|
332
|
+
validateStatus: (status) => true,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
t.is(checkResponse.status, 200, "checkHash should succeed");
|
|
336
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
337
|
+
t.truthy(checkResponse.data.expiresInMinutes, "Response should include expiresInMinutes");
|
|
338
|
+
|
|
339
|
+
if (isUsingAzureStorage()) {
|
|
340
|
+
// When Azure is configured (including Azurite), shortLivedUrl should be different from original URL
|
|
341
|
+
t.not(
|
|
342
|
+
checkResponse.data.shortLivedUrl,
|
|
343
|
+
checkResponse.data.url,
|
|
344
|
+
"With Azure storage, shortLivedUrl should be different from original URL"
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Azure URLs should contain query parameters (SAS token)
|
|
348
|
+
t.true(
|
|
349
|
+
checkResponse.data.shortLivedUrl.includes('?'),
|
|
350
|
+
"Azure shortLivedUrl should contain query parameters"
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
// When using LocalStorageProvider (fallback), shortLivedUrl should equal original URL
|
|
354
|
+
t.is(
|
|
355
|
+
checkResponse.data.shortLivedUrl,
|
|
356
|
+
checkResponse.data.url,
|
|
357
|
+
"With LocalStorage provider, shortLivedUrl should equal original URL (fallback behavior)"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Verify the shortLivedUrl is accessible regardless of storage provider
|
|
362
|
+
if (isAzureConfigured() && !checkResponse.data.shortLivedUrl.includes(AZURITE_ACCOUNT_NAME)) {
|
|
363
|
+
// Test with real Azure storage
|
|
364
|
+
const fileResponse = await axios.get(checkResponse.data.shortLivedUrl, {
|
|
365
|
+
validateStatus: (status) => true,
|
|
366
|
+
timeout: 10000,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
t.is(fileResponse.status, 200, "shortLivedUrl should be accessible with real Azure storage");
|
|
370
|
+
} else {
|
|
371
|
+
// Test with LocalStorage
|
|
372
|
+
const fileResponse = await axios.get(checkResponse.data.shortLivedUrl, {
|
|
373
|
+
validateStatus: (status) => true,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
t.is(fileResponse.status, 200, "shortLivedUrl should be accessible with LocalStorage");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
} finally {
|
|
380
|
+
fs.unlinkSync(filePath);
|
|
381
|
+
if (uploadResponse?.data?.url) {
|
|
382
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test.serial("checkHash should maintain consistent behavior across multiple calls", async (t) => {
|
|
388
|
+
const filePath = await createTestFile("Content for consistency test");
|
|
389
|
+
const hash = `test-consistency-${uuidv4()}`;
|
|
390
|
+
let uploadResponse;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
// Upload file with hash
|
|
394
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
395
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
396
|
+
|
|
397
|
+
// Make multiple checkHash calls
|
|
398
|
+
const responses = [];
|
|
399
|
+
for (let i = 0; i < 3; i++) {
|
|
400
|
+
const checkResponse = await axios.get(baseUrl, {
|
|
401
|
+
params: { hash, checkHash: true },
|
|
402
|
+
validateStatus: (status) => true,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
t.is(checkResponse.status, 200, `checkHash call ${i + 1} should succeed`);
|
|
406
|
+
responses.push(checkResponse.data);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// All responses should have the required fields
|
|
410
|
+
for (const [index, response] of responses.entries()) {
|
|
411
|
+
t.truthy(response.shortLivedUrl, `Response ${index + 1} should have shortLivedUrl`);
|
|
412
|
+
t.truthy(response.expiresInMinutes, `Response ${index + 1} should have expiresInMinutes`);
|
|
413
|
+
t.is(response.hash, hash, `Response ${index + 1} should have correct hash`);
|
|
414
|
+
t.truthy(response.filename, `Response ${index + 1} should have filename`);
|
|
415
|
+
t.truthy(response.url, `Response ${index + 1} should have original URL`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (isAzureConfigured()) {
|
|
419
|
+
// All shortLivedUrls should be different (new SAS tokens each time)
|
|
420
|
+
const shortLivedUrls = responses.map(r => r.shortLivedUrl);
|
|
421
|
+
const uniqueUrls = new Set(shortLivedUrls);
|
|
422
|
+
t.is(uniqueUrls.size, shortLivedUrls.length, "Each call should generate unique short-lived URL with Azure");
|
|
423
|
+
} else {
|
|
424
|
+
// With LocalStorage, all should be the same
|
|
425
|
+
const firstUrl = responses[0].shortLivedUrl;
|
|
426
|
+
for (const response of responses) {
|
|
427
|
+
t.is(response.shortLivedUrl, firstUrl, "LocalStorage shortLivedUrls should be consistent");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
} finally {
|
|
432
|
+
fs.unlinkSync(filePath);
|
|
433
|
+
if (uploadResponse?.data?.url) {
|
|
434
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test.serial("checkHash should handle zero and negative shortLivedMinutes", async (t) => {
|
|
440
|
+
const filePath = await createTestFile("Content for edge case test");
|
|
441
|
+
const hash = `test-edge-case-${uuidv4()}`;
|
|
442
|
+
let uploadResponse;
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
// Upload file with hash
|
|
446
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
447
|
+
t.is(uploadResponse.status, 200, "Upload should succeed");
|
|
448
|
+
|
|
449
|
+
// Test with zero minutes
|
|
450
|
+
const checkResponse1 = await checkHash(hash, 0);
|
|
451
|
+
t.is(checkResponse1.status, 200, "checkHash should succeed with 0 minutes");
|
|
452
|
+
t.truthy(checkResponse1.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
453
|
+
t.is(checkResponse1.data.expiresInMinutes, 5, "Should default to 5 minutes for 0 input");
|
|
454
|
+
|
|
455
|
+
// Test with negative minutes - the implementation passes through negative values
|
|
456
|
+
const checkResponse2 = await checkHash(hash, -5);
|
|
457
|
+
t.is(checkResponse2.status, 200, "checkHash should succeed with negative minutes");
|
|
458
|
+
t.truthy(checkResponse2.data.shortLivedUrl, "Response should include shortLivedUrl");
|
|
459
|
+
t.is(checkResponse2.data.expiresInMinutes, -5, "Implementation passes through negative values as-is");
|
|
460
|
+
|
|
461
|
+
} finally {
|
|
462
|
+
fs.unlinkSync(filePath);
|
|
463
|
+
if (uploadResponse?.data?.url) {
|
|
464
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test.serial("checkHash should return 404 for non-existent hash but still include shortLivedUrl in error context", async (t) => {
|
|
470
|
+
const nonExistentHash = `non-existent-${uuidv4()}`;
|
|
471
|
+
|
|
472
|
+
const checkResponse = await checkHash(nonExistentHash);
|
|
473
|
+
|
|
474
|
+
t.is(checkResponse.status, 404, "checkHash should return 404 for non-existent hash");
|
|
475
|
+
t.truthy(checkResponse.data, "Should have error response");
|
|
476
|
+
t.true(
|
|
477
|
+
typeof checkResponse.data === 'string' && checkResponse.data.includes('not found'),
|
|
478
|
+
"Error message should indicate hash not found"
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test.serial("checkHash with large file should return shortLivedUrl", async (t) => {
|
|
483
|
+
const largeContent = "Large file content ".repeat(1000); // ~20KB content (reduced size)
|
|
484
|
+
const filePath = await createTestFile(largeContent);
|
|
485
|
+
const hash = `test-large-${uuidv4()}`;
|
|
486
|
+
let uploadResponse;
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
// Upload large file with hash
|
|
490
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
491
|
+
t.is(uploadResponse.status, 200, "Large file upload should succeed");
|
|
492
|
+
t.truthy(uploadResponse.data.url, "Should have upload URL");
|
|
493
|
+
|
|
494
|
+
// If hash is in response, use it; otherwise, the upload may not have stored the hash
|
|
495
|
+
const uploadedHash = uploadResponse.data.hash || hash;
|
|
496
|
+
|
|
497
|
+
// Check hash for large file
|
|
498
|
+
const checkResponse = await checkHash(uploadedHash);
|
|
499
|
+
|
|
500
|
+
if (checkResponse.status === 200) {
|
|
501
|
+
t.truthy(checkResponse.data.shortLivedUrl, "Large file should have shortLivedUrl");
|
|
502
|
+
t.truthy(checkResponse.data.expiresInMinutes, "Large file should have expiration time");
|
|
503
|
+
|
|
504
|
+
// Verify base URLs match
|
|
505
|
+
const originalUrlBase = checkResponse.data.url.split('?')[0];
|
|
506
|
+
const shortLivedUrlBase = checkResponse.data.shortLivedUrl.split('?')[0];
|
|
507
|
+
t.is(originalUrlBase, shortLivedUrlBase, "Base URLs should match for large file");
|
|
508
|
+
} else {
|
|
509
|
+
// If hash wasn't stored properly, skip this test
|
|
510
|
+
t.pass("Large file test skipped - hash not stored properly in upload");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
} finally {
|
|
514
|
+
fs.unlinkSync(filePath);
|
|
515
|
+
if (uploadResponse?.data?.url) {
|
|
516
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test.serial("checkHash with different file types should return shortLivedUrl", async (t) => {
|
|
522
|
+
const fileTypes = [
|
|
523
|
+
{ ext: "txt", content: "Text file content" },
|
|
524
|
+
{ ext: "json", content: '{"key": "value"}' },
|
|
525
|
+
{ ext: "xml", content: "<root><data>test</data></root>" },
|
|
526
|
+
{ ext: "csv", content: "name,value\ntest,123" }
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
for (const fileType of fileTypes) {
|
|
530
|
+
const filePath = await createTestFile(fileType.content, fileType.ext);
|
|
531
|
+
const hash = `test-${fileType.ext}-${uuidv4()}`;
|
|
532
|
+
let uploadResponse;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Upload file with hash
|
|
536
|
+
uploadResponse = await uploadFile(filePath, null, hash);
|
|
537
|
+
t.is(uploadResponse.status, 200, `${fileType.ext} file upload should succeed`);
|
|
538
|
+
|
|
539
|
+
// Check hash
|
|
540
|
+
const checkResponse = await checkHash(hash);
|
|
541
|
+
|
|
542
|
+
t.is(checkResponse.status, 200, `checkHash should succeed for ${fileType.ext} file`);
|
|
543
|
+
t.truthy(checkResponse.data.shortLivedUrl, `${fileType.ext} file should have shortLivedUrl`);
|
|
544
|
+
t.truthy(checkResponse.data.expiresInMinutes, `${fileType.ext} file should have expiration time`);
|
|
545
|
+
|
|
546
|
+
} finally {
|
|
547
|
+
fs.unlinkSync(filePath);
|
|
548
|
+
if (uploadResponse?.data?.url) {
|
|
549
|
+
await cleanupHashAndFile(hash, uploadResponse.data.url, baseUrl);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|