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.

318 lines (273 loc) 11.7 kB
import test from 'ava'; import axios from 'axios'; import FormData from 'form-data'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import fs from 'fs'; import XLSX from 'xlsx'; import os from 'os'; import { fileURLToPath } from 'url'; import { startTestServer, stopTestServer, cleanupHashAndFile } from './testUtils.helper.js'; import { port } from '../src/start.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const baseUrl = `http://localhost:${port}/api/CortexFileHandler`; // Helper function to extract file extension from URL function getFileExtensionFromUrl(url) { try { const urlObj = new URL(url); const pathname = urlObj.pathname; const lastDotIndex = pathname.lastIndexOf('.'); if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) { return pathname.substring(lastDotIndex); } } catch (error) { console.log("Error parsing extension from URL:", error); } return null; } // Helper function to upload files 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(), validateStatus: (status) => true, timeout: 60000, }); return response; } // Test setup test.before(async (t) => { await startTestServer(); // Create a test directory for temporary files t.context.testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-shortlived-conversion-')); }); test.after(async (t) => { // Clean up test directory if (t.context.testDir && fs.existsSync(t.context.testDir)) { fs.rmSync(t.context.testDir, { recursive: true, force: true }); } await stopTestServer(); }); /** * Test that short-lived URLs are generated from converted files, not original files */ test.serial( "checkHash should generate shortLivedUrl from converted file URL for Excel files", async (t) => { // Create a minimal XLSX workbook in-memory const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet([ ["Product", "Price", "Quantity"], ["Apple", 1.50, 100], ["Banana", 0.75, 200], ["Cherry", 2.00, 50], ]); XLSX.utils.book_append_sheet(workbook, worksheet, "Products"); // Write it to a temp file const filePath = path.join(t.context.testDir, `${uuidv4()}.xlsx`); XLSX.writeFile(workbook, filePath); const hash = `test-shortlived-conversion-${uuidv4()}`; let uploadedUrl; let convertedUrl; try { // 1. Upload the XLSX file with hash (conversion should run automatically) const uploadResponse = await uploadFile(filePath, null, hash); t.is(uploadResponse.status, 200, "Upload should succeed"); t.truthy(uploadResponse.data.converted, "Upload response must contain converted info"); t.truthy(uploadResponse.data.converted.url, "Converted URL should be present"); uploadedUrl = uploadResponse.data.url; convertedUrl = uploadResponse.data.converted.url; // Verify the original file is .xlsx and converted file is .csv t.is(getFileExtensionFromUrl(uploadedUrl), '.xlsx', "Original URL should point to .xlsx file"); t.is(getFileExtensionFromUrl(convertedUrl), '.csv', "Converted URL should point to .csv file"); // 2. Give Redis a moment to persist await new Promise((resolve) => setTimeout(resolve, 2000)); // 3. Check hash to get short-lived URL const checkResponse = await axios.get(baseUrl, { params: { hash, checkHash: true }, validateStatus: (status) => true, timeout: 30000, }); t.is(checkResponse.status, 200, "checkHash should succeed"); t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl"); t.truthy(checkResponse.data.converted, "Response should include converted info"); // 4. CRITICAL TEST: Verify short-lived URL is based on converted file, not original const shortLivedUrlBase = checkResponse.data.shortLivedUrl.split('?')[0]; // Remove query params const convertedUrlBase = checkResponse.data.converted.url.split('?')[0]; // Remove query params const originalUrlBase = checkResponse.data.url.split('?')[0]; // Remove query params // The short-lived URL should be based on the converted file URL t.is( shortLivedUrlBase, convertedUrlBase, "Short-lived URL should be based on converted file URL (.csv), not original file URL (.xlsx)" ); // Double-check: short-lived URL should NOT be based on original file t.not( shortLivedUrlBase, originalUrlBase, "Short-lived URL should NOT be based on original file URL (.xlsx)" ); // Verify the short-lived URL points to a CSV file, not Excel t.true( checkResponse.data.shortLivedUrl.includes('.csv'), "Short-lived URL should point to .csv file (converted), not .xlsx file (original)" ); t.false( checkResponse.data.shortLivedUrl.includes('.xlsx'), "Short-lived URL should NOT contain .xlsx extension" ); } finally { // Clean up fs.unlinkSync(filePath); await cleanupHashAndFile(hash, uploadedUrl, baseUrl); if (convertedUrl) { await cleanupHashAndFile(null, convertedUrl, baseUrl); } } }, ); /** * Test that initial upload response includes shortLivedUrl for converted files */ test.serial( "upload should generate shortLivedUrl for converted file in initial response", async (t) => { // Create a minimal XLSX workbook in-memory const workbook = XLSX.utils.book_new(); const worksheet = XLSX.utils.aoa_to_sheet([ ["Product", "Price"], ["Apple", 1.50], ["Banana", 0.75], ]); XLSX.utils.book_append_sheet(workbook, worksheet, "Products"); // Write it to a temp file const filePath = path.join(t.context.testDir, `${uuidv4()}.xlsx`); XLSX.writeFile(workbook, filePath); const hash = `test-upload-converted-${uuidv4()}`; let uploadedUrl; let convertedUrl; try { // Upload the XLSX file (conversion should run automatically) const uploadResponse = await uploadFile(filePath, null, hash); t.is(uploadResponse.status, 200, "Upload should succeed"); t.truthy(uploadResponse.data.converted, "Upload response must contain converted info"); t.truthy(uploadResponse.data.converted.url, "Converted URL should be present"); uploadedUrl = uploadResponse.data.url; convertedUrl = uploadResponse.data.converted.url; // Verify the original file is .xlsx and converted file is .csv t.is(getFileExtensionFromUrl(uploadedUrl), '.xlsx', "Original URL should point to .xlsx file"); t.is(getFileExtensionFromUrl(convertedUrl), '.csv', "Converted URL should point to .csv file"); // CRITICAL TEST: Verify converted.shortLivedUrl is present in upload response t.truthy( uploadResponse.data.converted.shortLivedUrl, "Upload response should include converted.shortLivedUrl" ); // Verify converted.shortLivedUrl points to the converted file (.csv) const convertedShortLivedBase = uploadResponse.data.converted.shortLivedUrl.split('?')[0]; const convertedUrlBase = convertedUrl.split('?')[0]; t.is( convertedShortLivedBase, convertedUrlBase, "converted.shortLivedUrl should be based on converted file URL (.csv)" ); // Verify converted.shortLivedUrl points to CSV, not XLSX t.true( uploadResponse.data.converted.shortLivedUrl.includes('.csv'), "converted.shortLivedUrl should point to .csv file" ); t.false( uploadResponse.data.converted.shortLivedUrl.includes('.xlsx'), "converted.shortLivedUrl should NOT contain .xlsx extension" ); // Verify main shortLivedUrl points to original file (for reference) // The converted file's shortLivedUrl is in result.converted.shortLivedUrl if (uploadResponse.data.shortLivedUrl) { const mainShortLivedBase = uploadResponse.data.shortLivedUrl.split('?')[0]; const originalUrlBase = uploadedUrl.split('?')[0]; t.is( mainShortLivedBase, originalUrlBase, "Main shortLivedUrl should point to original file (.xlsx)" ); t.true( uploadResponse.data.shortLivedUrl.includes('.xlsx'), "Main shortLivedUrl should point to .xlsx file (original)" ); t.false( uploadResponse.data.shortLivedUrl.includes('.csv'), "Main shortLivedUrl should NOT point to .csv file (that's in converted.shortLivedUrl)" ); } } finally { // Clean up fs.unlinkSync(filePath); await cleanupHashAndFile(hash, uploadedUrl, baseUrl); if (convertedUrl) { await cleanupHashAndFile(null, convertedUrl, baseUrl); } } }, ); /** * Test that short-lived URLs fallback to original files when no conversion exists */ test.serial( "checkHash should generate shortLivedUrl from original file URL when no conversion exists", async (t) => { // Create a simple text file (no conversion needed) const testContent = "This is a simple text file that doesn't need conversion."; const filePath = path.join(t.context.testDir, `${uuidv4()}.txt`); fs.writeFileSync(filePath, testContent); const hash = `test-shortlived-original-${uuidv4()}`; let uploadedUrl; try { // 1. Upload the text file with hash (no conversion should occur) const uploadResponse = await uploadFile(filePath, null, hash); t.is(uploadResponse.status, 200, "Upload should succeed"); t.falsy(uploadResponse.data.converted, "Upload response should NOT contain converted info for .txt files"); uploadedUrl = uploadResponse.data.url; t.is(getFileExtensionFromUrl(uploadedUrl), '.txt', "Original URL should point to .txt file"); // 2. Give Redis a moment to persist await new Promise((resolve) => setTimeout(resolve, 1000)); // 3. Check hash to get short-lived URL const checkResponse = await axios.get(baseUrl, { params: { hash, checkHash: true }, validateStatus: (status) => true, timeout: 30000, }); t.is(checkResponse.status, 200, "checkHash should succeed"); t.truthy(checkResponse.data.shortLivedUrl, "Response should include shortLivedUrl"); t.falsy(checkResponse.data.converted, "Response should NOT include converted info for .txt files"); // 4. CRITICAL TEST: Verify short-lived URL is based on original file when no conversion exists const shortLivedUrlBase = checkResponse.data.shortLivedUrl.split('?')[0]; // Remove query params const originalUrlBase = checkResponse.data.url.split('?')[0]; // Remove query params // The short-lived URL should be based on the original file URL t.is( shortLivedUrlBase, originalUrlBase, "Short-lived URL should be based on original file URL when no conversion exists" ); // Verify the short-lived URL points to the text file t.true( checkResponse.data.shortLivedUrl.includes('.txt'), "Short-lived URL should point to .txt file (original)" ); } finally { // Clean up fs.unlinkSync(filePath); await cleanupHashAndFile(hash, uploadedUrl, baseUrl); } }, );