UNPKG

@aj-archipelago/cortex

Version:

Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.

325 lines (286 loc) 10.1 kB
import test from "ava"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { v4 as uuidv4 } from "uuid"; import axios from "axios"; import FormData from "form-data"; import { port } from "../src/start.js"; import { cleanupHashAndFile, getFolderNameFromUrl, startTestServer, stopTestServer, setupTestDirectory, } from "./testUtils.helper.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const baseUrl = `http://localhost:${port}/api/CortexFileHandler`; // Helper function to determine if GCS is configured function isGCSConfigured() { return ( process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64 || process.env.GCP_SERVICE_ACCOUNT_KEY ); } // Helper function to create test files async function createTestFile(content, extension) { const testDir = path.join(__dirname, "test-files"); if (!fs.existsSync(testDir)) { fs.mkdirSync(testDir, { recursive: true }); } const filename = path.join(testDir, `${uuidv4()}.${extension}`); fs.writeFileSync(filename, content); return filename; } // Helper function to upload file async function uploadFile(filePath, requestId = null, hash = null) { const form = new FormData(); form.append("file", fs.createReadStream(filePath)); if (requestId) form.append("requestId", requestId); if (hash) form.append("hash", hash); const response = await axios.post(baseUrl, form, { headers: { ...form.getHeaders(), "Content-Type": "multipart/form-data", }, validateStatus: (status) => true, timeout: 30000, maxContentLength: Infinity, maxBodyLength: Infinity, }); return response; } // Setup: Create test directory and start server test.before(async (t) => { await startTestServer(); await setupTestDirectory(t); }); // Clean up server after tests test.after.always(async () => { await stopTestServer(); }); // Test: Upload with hash and verify Redis storage test.serial("should store file metadata in Redis with hash", async (t) => { const fileContent = "test content"; const filePath = await createTestFile(fileContent, "txt"); const requestId = uuidv4(); const hash = "test-hash-" + uuidv4(); let response; try { response = await uploadFile(filePath, requestId, hash); t.is(response.status, 200, "Upload should succeed"); t.truthy(response.data.url, "Should have file URL"); t.is(response.data.hash, hash, "Should return correct hash"); // Wait for Redis operations to complete await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify hash exists in Redis const checkResponse = await axios.get(baseUrl, { params: { hash, checkHash: true, }, validateStatus: (status) => true, }); t.is(checkResponse.status, 200, "Hash should exist in Redis"); t.truthy(checkResponse.data.url, "Hash check should return URL"); t.is( checkResponse.data.url, response.data.url, "Hash check should return correct URL", ); } finally { fs.unlinkSync(filePath); if (response?.data?.url) { await cleanupHashAndFile(hash, response.data.url, baseUrl); } } }); // Test: Upload with GCS backup verification test.serial("should create GCS backup when configured", async (t) => { if (!isGCSConfigured()) { t.pass("Skipping test - GCS not configured"); return; } const fileContent = "test content"; const filePath = await createTestFile(fileContent, "txt"); const requestId = uuidv4(); let response; try { response = await uploadFile(filePath, requestId); t.is(response.status, 200, "Upload should succeed"); t.truthy(response.data.url, "Should have primary storage URL"); t.truthy(response.data.gcs, "Should have GCS backup URL"); t.true( response.data.gcs.startsWith("gs://"), "GCS URL should use gs:// protocol", ); // Verify file exists in both storages const primaryResponse = await axios.get(response.data.url); t.is(primaryResponse.status, 200, "Primary file should be accessible"); t.is( primaryResponse.data, fileContent, "Primary file content should match", ); // GCS file should be accessible through the primary URL // since we can't directly access gs:// URLs const gcsResponse = await axios.get(response.data.url); t.is(gcsResponse.status, 200, "GCS file should be accessible"); t.is(gcsResponse.data, fileContent, "GCS file content should match"); } finally { fs.unlinkSync(filePath); if (response?.data?.url) { await cleanupHashAndFile(null, response.data.url, baseUrl); } } }); // Test: Upload with large file test.serial("should handle large file upload", async (t) => { const largeContent = "x".repeat(10 * 1024 * 1024); // 10MB const filePath = await createTestFile(largeContent, "txt"); const requestId = uuidv4(); let response; try { response = await uploadFile(filePath, requestId); t.is(response.status, 200, "Large file upload should succeed"); t.truthy(response.data.url, "Should have file URL"); // Verify file content const fileResponse = await axios.get(response.data.url); t.is(fileResponse.status, 200, "File should be accessible"); t.is( fileResponse.data.length, largeContent.length, "File size should match", ); } finally { fs.unlinkSync(filePath); if (response?.data?.url) { await cleanupHashAndFile(null, response.data.url, baseUrl); } } }); // Test: Upload with special characters in filename test("should handle special characters in filename", async (t) => { const fileContent = "test content"; const specialFilename = `test file with spaces and special chars !@#$%^&*()_+-=[]{}|;:,.<>?${uuidv4()}.txt`; const filePath = await createTestFile(fileContent, specialFilename); const requestId = uuidv4(); let response; try { response = await uploadFile(filePath, requestId); t.is(response.status, 200, "Upload should succeed"); t.truthy(response.data.url, "Should have file URL"); t.truthy(response.data.filename, "Should have filename in response"); // Verify file is accessible const fileResponse = await axios.get(response.data.url); t.is(fileResponse.status, 200, "File should be accessible"); t.is(fileResponse.data, fileContent, "File content should match"); } finally { fs.unlinkSync(filePath); if (response?.data?.url) { await cleanupHashAndFile(null, response.data.url, baseUrl); } } }); // Test: Upload with concurrent requests test.serial("should handle concurrent uploads", async (t) => { const requestId = uuidv4(); const uploads = []; const numUploads = 5; let responses = []; // Move declaration outside try block // Create and upload multiple files concurrently for (let i = 0; i < numUploads; i++) { const fileContent = `test content ${i}`; const filePath = await createTestFile(fileContent, "txt"); uploads.push({ filePath, promise: uploadFile(filePath, requestId), }); } try { // Wait for all uploads to complete responses = await Promise.all(uploads.map((u) => u.promise)); // Verify all uploads succeeded responses.forEach((response, i) => { t.is(response.status, 200, `Upload ${i} should succeed`); t.truthy(response.data.url, `Upload ${i} should have URL`); }); // Verify all files are accessible for (const response of responses) { const fileResponse = await axios.get(response.data.url); t.is(fileResponse.status, 200, "File should be accessible"); } } finally { // Cleanup all files for (const upload of uploads) { fs.unlinkSync(upload.filePath); } // Cleanup uploaded files for (const response of responses) { if (response?.data?.url) { await cleanupHashAndFile(null, response.data.url, baseUrl); } } } }); // Test: Upload with missing file test.serial("should handle missing file in request", async (t) => { const form = new FormData(); form.append("requestId", uuidv4()); const response = await axios.post(baseUrl, form, { headers: { ...form.getHeaders(), "Content-Type": "multipart/form-data", }, validateStatus: (status) => true, }); t.is(response.status, 400, "Should reject request without file"); t.is( response.data, "No file provided in request", "Should return correct error message", ); }); // Test: Upload with empty file test.serial("should handle empty file upload", async (t) => { const filePath = await createTestFile("", "txt"); const requestId = uuidv4(); let response; try { response = await uploadFile(filePath, requestId); t.is(response.status, 400, "Should reject empty file"); t.is( response.data, "Invalid file: file is empty", "Should return correct error message", ); } finally { fs.unlinkSync(filePath); } }); // Test: Upload without requestId should generate one test.serial("should generate requestId when not provided", async (t) => { const fileContent = "test content"; const filePath = await createTestFile(fileContent, "txt"); let response; try { response = await uploadFile(filePath); t.is(response.status, 200, "Upload should succeed without requestId"); t.truthy(response.data.url, "Should have file URL"); // Extract requestId from the URL const urlParts = response.data.url.split("/"); const requestId = urlParts[urlParts.length - 2]; // requestId is the second-to-last part of the URL t.truthy(requestId, "URL should contain a requestId"); t.true(requestId.length > 0, "requestId should not be empty"); // Verify file is accessible const fileResponse = await axios.get(response.data.url); t.is(fileResponse.status, 200, "File should be accessible"); t.is(fileResponse.data, fileContent, "File content should match"); } finally { fs.unlinkSync(filePath); if (response?.data?.url) { await cleanupHashAndFile(null, response.data.url, baseUrl); } } });