@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.
455 lines (386 loc) • 13.8 kB
JavaScript
import test from "ava";
import { StorageService } from "../../src/services/storage/StorageService.js";
import { StorageFactory } from "../../src/services/storage/StorageFactory.js";
import { getFileStoreMap, setFileStoreMap, removeFromFileStoreMap } from "../../src/redis.js";
import path from "path";
import os from "os";
import fs from "fs";
test("should create storage service with factory", (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
t.truthy(service);
});
test("should get primary provider", (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const provider = service.getPrimaryProvider();
t.truthy(provider);
});
test("should get backup provider", (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const provider = service.getBackupProvider();
if (!provider) {
t.log("GCS not configured, skipping test");
t.pass();
} else {
t.truthy(provider);
}
});
test("should upload file to primary storage", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testContent = "test content";
const buffer = Buffer.from(testContent);
const result = await service.uploadFile(buffer, "test.txt");
t.truthy(result.url);
// Cleanup
await service.deleteFile(result.url);
});
test("should upload file to backup storage", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const provider = await service.getBackupProvider();
if (!provider) {
t.log("Backup provider not configured, skipping test");
t.pass();
return;
}
try {
const testContent = "test content";
const buffer = Buffer.from(testContent);
const result = await service.uploadFileToBackup(buffer, "test.txt");
t.truthy(result.url);
// Cleanup
await service.deleteFileFromBackup(result.url);
} catch (error) {
if (error.message === "Backup provider not configured") {
t.log("Backup provider not configured, skipping test");
t.pass();
} else {
throw error;
}
}
});
test("should download file from primary storage", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testContent = "test content";
const buffer = Buffer.from(testContent);
// Upload first
const uploadResult = await service.uploadFile(buffer, "test.txt");
// Download
const downloadResult = await service.downloadFile(uploadResult.url);
t.deepEqual(downloadResult, buffer);
// Cleanup
await service.deleteFile(uploadResult.url);
});
test("should download file from backup storage", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const provider = await service.getBackupProvider();
if (!provider) {
t.log("Backup provider not configured, skipping test");
t.pass();
return;
}
try {
const testContent = "test content";
const buffer = Buffer.from(testContent);
// Upload first
const uploadResult = await service.uploadFileToBackup(buffer, "test.txt");
// Create temp file for download
const tempFile = path.join(os.tmpdir(), "test-download.txt");
try {
// Download
await service.downloadFileFromBackup(uploadResult.url, tempFile);
const downloadedContent = await fs.promises.readFile(tempFile);
t.deepEqual(downloadedContent, buffer);
// Cleanup
await service.deleteFileFromBackup(uploadResult.url);
} finally {
// Cleanup temp file
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
} catch (error) {
if (error.message === "Backup provider not configured") {
t.log("Backup provider not configured, skipping test");
t.pass();
} else {
throw error;
}
}
});
test("should delete file by hash", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testContent = "test content for hash deletion";
const buffer = Buffer.from(testContent);
const testHash = "test-hash-123";
try {
// Upload file first
const uploadResult = await service.uploadFile(buffer, "test-hash-delete.txt");
t.truthy(uploadResult.url);
// Store file info in Redis map
const fileInfo = {
url: uploadResult.url,
filename: "test-hash-delete.txt",
hash: testHash,
timestamp: new Date().toISOString()
};
await setFileStoreMap(testHash, fileInfo);
// Verify file exists in map
const storedInfo = await getFileStoreMap(testHash);
t.truthy(storedInfo);
t.is(storedInfo.url, uploadResult.url);
// Delete file by hash
const deleteResult = await service.deleteFileByHash(testHash);
t.truthy(deleteResult);
t.is(deleteResult.hash, testHash);
t.is(deleteResult.filename, "test-hash-delete.txt");
t.truthy(deleteResult.deleted);
t.true(Array.isArray(deleteResult.deleted));
// Verify file is removed from Redis map
const removedInfo = await getFileStoreMap(testHash);
t.falsy(removedInfo);
} catch (error) {
// Cleanup in case of error
try {
await removeFromFileStoreMap(testHash);
} catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
});
test("should handle delete file by hash when file not found", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const nonExistentHash = "non-existent-hash-456";
try {
await service.deleteFileByHash(nonExistentHash);
t.fail("Should have thrown an error for non-existent hash");
} catch (error) {
t.true(error.message.includes("not found"));
}
});
test("should handle delete file by hash with missing hash parameter", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
try {
await service.deleteFileByHash("");
t.fail("Should have thrown an error for empty hash");
} catch (error) {
t.true(error.message.includes("Missing hash parameter"));
}
try {
await service.deleteFileByHash(null);
t.fail("Should have thrown an error for null hash");
} catch (error) {
t.true(error.message.includes("Missing hash parameter"));
}
try {
await service.deleteFileByHash(undefined);
t.fail("Should have thrown an error for undefined hash");
} catch (error) {
t.true(error.message.includes("Missing hash parameter"));
}
});
test("should delete file by hash with backup storage", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testContent = "test content for backup deletion";
const buffer = Buffer.from(testContent);
const testHash = "test-hash-backup-456";
try {
// Upload file first
const uploadResult = await service.uploadFile(buffer, "test-backup-delete.txt");
t.truthy(uploadResult.url);
// Store file info in Redis map with backup URL
const fileInfo = {
url: uploadResult.url,
gcs: "gs://test-bucket/test-backup-file.txt", // Mock backup URL
filename: "test-backup-delete.txt",
hash: testHash,
timestamp: new Date().toISOString()
};
await setFileStoreMap(testHash, fileInfo);
// Delete file by hash
const deleteResult = await service.deleteFileByHash(testHash);
t.truthy(deleteResult);
t.is(deleteResult.hash, testHash);
t.is(deleteResult.filename, "test-backup-delete.txt");
t.truthy(deleteResult.deleted);
t.true(Array.isArray(deleteResult.deleted));
// Should have attempted both primary and backup deletion
const deletionResults = deleteResult.deleted;
t.true(deletionResults.length >= 1, "Should have at least primary deletion result");
// Verify file is removed from Redis map
const removedInfo = await getFileStoreMap(testHash);
t.falsy(removedInfo);
} catch (error) {
// Cleanup in case of error
try {
await removeFromFileStoreMap(testHash);
} catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
});
test("should handle delete file by hash when Redis map is corrupted", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testHash = "test-hash-corrupted-789";
try {
// Store corrupted data in Redis map
const corruptedInfo = {
// Missing required fields like url
corrupted: true,
timestamp: new Date().toISOString()
};
await setFileStoreMap(testHash, corruptedInfo);
// Delete file by hash should handle corrupted data gracefully
const deleteResult = await service.deleteFileByHash(testHash);
t.truthy(deleteResult);
t.is(deleteResult.hash, testHash);
t.truthy(deleteResult.deleted);
t.true(Array.isArray(deleteResult.deleted));
// Should have removed the corrupted entry from Redis
const removedInfo = await getFileStoreMap(testHash);
t.falsy(removedInfo);
} catch (error) {
// Cleanup in case of error
try {
await removeFromFileStoreMap(testHash);
} catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
});
test("should handle delete file by hash with empty URL in Redis", async (t) => {
const factory = new StorageFactory();
const service = new StorageService(factory);
const testHash = "test-hash-empty-url-654";
try {
// Store file info in Redis map with empty/null URL
const fileInfo = {
url: null,
filename: "test-empty-url-delete.txt",
hash: testHash,
timestamp: new Date().toISOString()
};
await setFileStoreMap(testHash, fileInfo);
// Verify the hash exists in Redis before deletion (skip lazy cleanup)
const storedInfo = await getFileStoreMap(testHash, true);
t.truthy(storedInfo, "Hash should exist in Redis before deletion");
// Delete file by hash - should handle missing URL gracefully
const deleteResult = await service.deleteFileByHash(testHash);
t.truthy(deleteResult);
t.is(deleteResult.hash, testHash);
t.is(deleteResult.filename, "test-empty-url-delete.txt");
t.truthy(deleteResult.deleted);
t.true(Array.isArray(deleteResult.deleted));
// Should still remove from Redis map even if no actual file to delete
const removedInfo = await getFileStoreMap(testHash);
t.falsy(removedInfo);
} catch (error) {
// Cleanup in case of error
try {
await removeFromFileStoreMap(testHash);
} catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
});
// Container-specific tests - now using single container only
test("should upload file using default container", async (t) => {
if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
t.pass("Skipping test - Azure not configured");
return;
}
const factory = new StorageFactory();
const service = new StorageService(factory);
// Create a temporary file
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-"));
const testFile = path.join(tempDir, "test.txt");
fs.writeFileSync(testFile, "test content");
try {
// Test upload - container parameter is ignored, always uses default
const result = await service.uploadFileWithProviders(
{ log: () => {} }, // mock context
testFile,
"test-request",
null,
null
);
t.truthy(result.url);
t.truthy(result.shortLivedUrl);
// Cleanup
await service.deleteFiles("test-request");
} finally {
// Cleanup temp file
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test("should use default container when no container specified", async (t) => {
if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
t.pass("Skipping test - Azure not configured");
return;
}
const factory = new StorageFactory();
const service = new StorageService(factory);
// Create a temporary file
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-"));
const testFile = path.join(tempDir, "test.txt");
fs.writeFileSync(testFile, "test content");
try {
// Test upload without container (should use default)
const result = await service.uploadFileWithProviders(
{ log: () => {} }, // mock context
testFile,
"test-request",
null,
null // no container specified
);
t.truthy(result.url);
// Cleanup
await service.deleteFiles("test-request");
} finally {
// Cleanup temp file
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test("should upload file using uploadFile method (container parameter ignored)", async (t) => {
if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
t.pass("Skipping test - Azure not configured");
return;
}
const factory = new StorageFactory();
const service = new StorageService(factory);
// Create a temporary file
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-"));
const testFile = path.join(tempDir, "test.txt");
fs.writeFileSync(testFile, "test content");
try {
// Test upload using the uploadFile method - container parameter is ignored
const result = await service.uploadFile(
{ log: () => {} }, // context
testFile, // filePath
"test-request", // requestId
null, // hash
null // filename (containerName parameter removed)
);
t.truthy(result.url);
t.truthy(result.shortLivedUrl);
// Cleanup
await service.deleteFiles("test-request");
} finally {
// Cleanup temp file
fs.rmSync(tempDir, { recursive: true, force: true });
}
});