/** * Azure Blob Storage Adapter Integration Tests * * This test verifies that the Azure adapter: * - Properly authenticates with various credential types * - Implements UUID-based sharding correctly * - Handles pagination across shards * - Persists data correctly * - Manages statistics and counts * - v4.0.0: Tier management (Hot/Cool/Archive) * - v4.0.0: Lifecycle policies for automatic cost optimization * - v4.0.0: Batch operations (batch delete, batch tier changes) * - v4.0.0: Archive rehydration */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { AzureBlobStorage } from '../../src/storage/adapters/azureBlobStorage.js' import { randomUUID } from 'node:crypto' describe('Azure Blob Storage Adapter', () => { // Mock Azure client for testing let mockAzureBlobs: Map = new Map() let mockBlobTiers: Map = new Map() let mockLifecyclePolicy: any = null let storage: AzureBlobStorage | null = null // Helper to create mock Azure client function createMockAzureClient() { const mockContainerClient = { exists: async () => true, create: async () => ({}), getProperties: async () => ({ lastModified: new Date(), etag: 'mock-etag' }), getBlockBlobClient: (name: string) => ({ upload: async (content: string, length: number, options: any) => { mockAzureBlobs.set(name, JSON.parse(content)) mockBlobTiers.set(name, 'Hot') // Default tier return {} }, download: async (offset: number) => { const data = mockAzureBlobs.get(name) if (!data) { const error: any = new Error('Blob not found') error.statusCode = 404 error.code = 'BlobNotFound' throw error } const buffer = Buffer.from(JSON.stringify(data)) return { readableStreamBody: { on: (event: string, callback: Function) => { if (event === 'data') { callback(buffer) } else if (event === 'end') { callback() } return { on: () => ({}) } } } } }, delete: async () => { mockAzureBlobs.delete(name) mockBlobTiers.delete(name) return {} }, setAccessTier: async (tier: string, options?: any) => { if (!mockAzureBlobs.has(name)) { const error: any = new Error('Blob not found') error.statusCode = 404 error.code = 'BlobNotFound' throw error } mockBlobTiers.set(name, tier) return {} }, getProperties: async () => { if (!mockAzureBlobs.has(name)) { const error: any = new Error('Blob not found') error.statusCode = 404 error.code = 'BlobNotFound' throw error } const tier = mockBlobTiers.get(name) || 'Hot' return { accessTier: tier, archiveStatus: tier === 'Archive' ? 'rehydrate-pending-to-hot' : undefined, rehydratePriority: undefined } }, url: `https://test.blob.core.windows.net/test-container/${name}` }), getBlobBatchClient: () => ({ deleteBlobs: async (urls: string[]) => { const subResponses = urls.map(url => { const name = url.split('/').slice(4).join('/') if (mockAzureBlobs.has(name)) { mockAzureBlobs.delete(name) mockBlobTiers.delete(name) return { status: 202, errorCode: null } } else { return { status: 404, errorCode: 'BlobNotFound' } } }) return { subResponses } } }), listBlobsFlat: async function* (options: any = {}) { const prefix = options.prefix || '' const allKeys = Array.from(mockAzureBlobs.keys()) const matchingKeys = allKeys.filter(key => key.startsWith(prefix)).sort() for (const key of matchingKeys) { yield { name: key } } } } const mockBlobServiceClient = { getContainerClient: (name: string) => mockContainerClient, getProperties: async () => ({ blobAnalyticsLogging: {}, hourMetrics: {}, minuteMetrics: {}, cors: [], deleteRetentionPolicy: {}, staticWebsite: {}, lifecyclePolicy: mockLifecyclePolicy }), setProperties: async (props: any) => { if (props.lifecyclePolicy !== undefined) { mockLifecyclePolicy = props.lifecyclePolicy } return {} } } return mockBlobServiceClient } beforeEach(() => { mockAzureBlobs.clear() mockBlobTiers.clear() mockLifecyclePolicy = null }) afterEach(async () => { if (storage) { try { await storage.clear() } catch (error) { // Ignore cleanup errors } storage = null } }) it('should initialize with connection string', async () => { // Create storage with connection string storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'DefaultEndpointsProtocol=https;AccountName=test;AccountKey=fake;' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Verify initialization expect((storage as any).containerName).toBe('test-container') expect((storage as any).connectionString).toBeTruthy() }) it('should initialize with account key', async () => { // Create storage with account key storage = new AzureBlobStorage({ containerName: 'test-container', accountName: 'test-account', accountKey: 'fake-key' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Verify initialization expect((storage as any).accountName).toBe('test-account') expect((storage as any).accountKey).toBe('fake-key') }) it('should write and read data with UUID-based sharding', async () => { console.log('\n๐Ÿ“ Test: Write and read with UUID sharding...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'DefaultEndpointsProtocol=https;AccountName=test;AccountKey=fake;' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Generate UUIDs for testing const testData = [ { id: randomUUID(), vector: [0.1, 0.2, 0.3], metadata: { type: 'user', name: 'Alice' } }, { id: randomUUID(), vector: [0.4, 0.5, 0.6], metadata: { type: 'user', name: 'Bob' } }, { id: randomUUID(), vector: [0.7, 0.8, 0.9], metadata: { type: 'user', name: 'Charlie' } } ] // Save nouns with metadata for (const item of testData) { const noun = { id: item.id, vector: item.vector, connections: new Map(), level: 0 } await storage.saveNoun(noun) await (storage as any).saveNounMetadata_internal(item.id, item.metadata) } console.log(`โœ… Wrote ${testData.length} entities`) console.log(`๐Ÿ“Š Objects in mock storage: ${mockAzureBlobs.size}`) // Verify data was written to UUID-sharded paths const shardedKeys = Array.from(mockAzureBlobs.keys()).filter(k => k.match(/entities\/nouns\/vectors\/[0-9a-f]{2}\//) ) console.log(`๐Ÿ”‘ UUID-sharded keys: ${shardedKeys.length}`) expect(shardedKeys.length).toBe(testData.length) // Log shard distribution const shards = new Set(shardedKeys.map(k => k.match(/entities\/nouns\/vectors\/([0-9a-f]{2})\//)?.[1])) console.log(`๐Ÿ“ Data distributed across ${shards.size} shards: ${Array.from(shards).join(', ')}`) // Verify each entity can be retrieved for (const item of testData) { const noun = await storage.getNoun(item.id) expect(noun).toBeTruthy() expect(noun!.id).toBe(item.id) expect(noun!.vector).toEqual(item.vector) const metadata = await storage.getNounMetadata(item.id) expect(metadata).toBeTruthy() expect(metadata.name).toBe(item.metadata.name) } console.log('โœ… All entities retrieved successfully') }) it('should handle pagination', async () => { console.log('\n๐Ÿ”„ Test: Pagination...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Write 10 entities console.log('๐Ÿ“ Writing 10 entities...') for (let i = 0; i < 10; i++) { const id = randomUUID() const noun = { id, vector: [0.1 * i, 0.2 * i, 0.3 * i], connections: new Map(), level: 0 } await storage.saveNoun(noun) await (storage as any).saveNounMetadata_internal(id, { type: 'test', index: i }) } // Read with pagination (limit: 3) const result = await storage.getNounsWithPagination({ limit: 3 }) console.log(`๐Ÿ“„ Got ${result.items.length} entities`) expect(result.items.length).toBeLessThanOrEqual(3) console.log('โœ… Pagination working') }) it('should handle batch delete operations', async () => { console.log('\n๐Ÿ—‘๏ธ Test: Batch delete...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Write some entities const ids: string[] = [] for (let i = 0; i < 5; i++) { const id = randomUUID() ids.push(id) await storage.saveNoun({ id, vector: [0.1, 0.2, 0.3], connections: new Map(), level: 0 }) } console.log(`๐Ÿ“ Created ${ids.length} entities`) const beforeCount = mockAzureBlobs.size console.log(`๐Ÿ“Š Blobs before delete: ${beforeCount}`) // Batch delete const keys = ids.map(id => { const shardId = id.substring(0, 2) return `entities/nouns/vectors/${shardId}/${id}.json` }) const result = await storage.batchDelete(keys) console.log(`โœ… Deleted ${result.successfulDeletes}/${result.totalRequested}`) expect(result.successfulDeletes).toBe(ids.length) expect(result.failedDeletes).toBe(0) const afterCount = mockAzureBlobs.size console.log(`๐Ÿ“Š Blobs after delete: ${afterCount}`) console.log('โœ… Batch delete successful') }) it('should handle blob tier management', async () => { console.log('\n๐Ÿ”ฅ Test: Blob tier management...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Create a blob const id = randomUUID() await storage.saveNoun({ id, vector: [0.1, 0.2, 0.3], connections: new Map(), level: 0 }) const shardId = id.substring(0, 2) const blobName = `entities/nouns/vectors/${shardId}/${id}.json` // Check initial tier (should be Hot) const initialTier = await storage.getBlobTier(blobName) console.log(`๐Ÿ“Š Initial tier: ${initialTier}`) expect(initialTier).toBe('Hot') // Change to Cool tier await storage.setBlobTier(blobName, 'Cool') const coolTier = await storage.getBlobTier(blobName) console.log(`๐Ÿ“Š After Cool: ${coolTier}`) expect(coolTier).toBe('Cool') // Change to Archive tier await storage.setBlobTier(blobName, 'Archive') const archiveTier = await storage.getBlobTier(blobName) console.log(`๐Ÿ“Š After Archive: ${archiveTier}`) expect(archiveTier).toBe('Archive') console.log('โœ… Tier management successful') }) it('should handle batch tier changes', async () => { console.log('\n๐Ÿ”ฅ Test: Batch tier changes...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', accountName: 'test-account', accountKey: 'test-key' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Create multiple blobs const blobNames: string[] = [] for (let i = 0; i < 5; i++) { const id = randomUUID() await storage.saveNoun({ id, vector: [0.1, 0.2, 0.3], connections: new Map(), level: 0 }) const shardId = id.substring(0, 2) blobNames.push(`entities/nouns/vectors/${shardId}/${id}.json`) } console.log(`๐Ÿ“ Created ${blobNames.length} blobs`) // Batch change to Archive tier const result = await storage.setBlobTierBatch( blobNames.map(blobName => ({ blobName, tier: 'Archive' as const })) ) console.log(`โœ… Changed ${result.successfulChanges}/${result.totalRequested} to Archive`) expect(result.successfulChanges).toBe(blobNames.length) expect(result.failedChanges).toBe(0) // Verify tiers for (const blobName of blobNames) { const tier = await storage.getBlobTier(blobName) expect(tier).toBe('Archive') } console.log('โœ… Batch tier changes successful') }) it('should handle archive rehydration', async () => { console.log('\nโ„๏ธ Test: Archive rehydration...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Create and archive a blob const id = randomUUID() await storage.saveNoun({ id, vector: [0.1, 0.2, 0.3], connections: new Map(), level: 0 }) const shardId = id.substring(0, 2) const blobName = `entities/nouns/vectors/${shardId}/${id}.json` // Move to Archive tier await storage.setBlobTier(blobName, 'Archive') console.log('๐Ÿ“Š Blob archived') // Check rehydration status const status = await storage.checkRehydrationStatus(blobName) console.log(`๐Ÿ“Š Rehydration status: ${JSON.stringify(status)}`) expect(status.isArchived).toBe(true) // Rehydrate to Hot tier await storage.rehydrateBlob(blobName, 'Hot', 'Standard') console.log('๐Ÿ“Š Rehydration initiated') // In real Azure, this would take hours // In our mock, we'll just change the tier await storage.setBlobTier(blobName, 'Hot') const newTier = await storage.getBlobTier(blobName) console.log(`๐Ÿ“Š New tier after rehydration: ${newTier}`) expect(newTier).toBe('Hot') console.log('โœ… Rehydration successful') }) it('should handle lifecycle policies', async () => { console.log('\n๐Ÿ”„ Test: Lifecycle policies...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', accountName: 'test-account', accountKey: 'test-key' }) // Mock the Azure client with better service client support const mockClient = createMockAzureClient() ;(storage as any).blobServiceClient = mockClient ;(storage as any).containerClient = mockClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Mock lifecycle policy operations const mockSetLifecyclePolicy = async (rules: any) => { mockLifecyclePolicy = { rules } } const mockGetLifecyclePolicy = async () => { return mockLifecyclePolicy } const mockRemoveLifecyclePolicy = async () => { mockLifecyclePolicy = null } // Override lifecycle methods to use mocks const originalSet = storage.setLifecyclePolicy.bind(storage) const originalGet = storage.getLifecyclePolicy.bind(storage) const originalRemove = storage.removeLifecyclePolicy.bind(storage) storage.setLifecyclePolicy = async (options: any) => { await mockSetLifecyclePolicy(options.rules) } storage.getLifecyclePolicy = async () => { return mockGetLifecyclePolicy() } storage.removeLifecyclePolicy = async () => { await mockRemoveLifecyclePolicy() } // Set lifecycle policy await storage.setLifecyclePolicy({ rules: [ { name: 'archiveOldData', enabled: true, type: 'Lifecycle', definition: { filters: { blobTypes: ['blockBlob'], prefixMatch: ['entities/nouns/vectors/'] }, actions: { baseBlob: { tierToCool: { daysAfterModificationGreaterThan: 30 }, tierToArchive: { daysAfterModificationGreaterThan: 90 }, delete: { daysAfterModificationGreaterThan: 365 } } } } } ] }) console.log('๐Ÿ“Š Lifecycle policy set') // Get lifecycle policy const policy = await storage.getLifecyclePolicy() expect(policy).toBeTruthy() expect(policy!.rules.length).toBe(1) expect(policy!.rules[0].name).toBe('archiveOldData') console.log(`๐Ÿ“Š Found ${policy!.rules.length} rules`) // Remove lifecycle policy await storage.removeLifecyclePolicy() const removedPolicy = await storage.getLifecyclePolicy() expect(removedPolicy).toBeNull() console.log('๐Ÿ“Š Policy removed') // Restore original methods storage.setLifecyclePolicy = originalSet storage.getLifecyclePolicy = originalGet storage.removeLifecyclePolicy = originalRemove console.log('โœ… Lifecycle policy management successful') }) it('should handle verb operations with UUID sharding', async () => { console.log('\n๐Ÿ”— Test: Verb operations with UUID sharding...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Create verb const verbId = randomUUID() const sourceId = randomUUID() const targetId = randomUUID() const verb = { id: verbId, vector: [0.1, 0.2, 0.3], connections: new Map(), verb: 'owns', sourceId, targetId } await storage.saveVerb(verb) // Save verb metadata (v4.0.0 requires metadata) await (storage as any).saveVerbMetadata_internal(verbId, { type: 'owns' }) // Verify verb was saved with UUID sharding const shardedKeys = Array.from(mockAzureBlobs.keys()).filter(k => k.match(/entities\/verbs\/vectors\/[0-9a-f]{2}\//) ) expect(shardedKeys.length).toBeGreaterThan(0) // Retrieve verb const retrieved = await storage.getVerb(verbId) expect(retrieved).toBeTruthy() expect(retrieved!.id).toBe(verbId) expect(retrieved!.verb).toBe('owns') expect(retrieved!.sourceId).toBe(sourceId) expect(retrieved!.targetId).toBe(targetId) console.log('โœ… Verb operations successful') }) it('should handle throttling errors correctly', async () => { console.log('\n๐Ÿšฆ Test: Throttling detection...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Test throttling error detection const throttlingError = { statusCode: 429, message: 'Too Many Requests' } expect((storage as any).isThrottlingError(throttlingError)).toBe(true) const serverBusyError = { statusCode: 'ServerBusy', message: 'Server is busy' } expect((storage as any).isThrottlingError(serverBusyError)).toBe(true) const normalError = { statusCode: 500, message: 'Internal Server Error' } expect((storage as any).isThrottlingError(normalError)).toBe(false) console.log('โœ… Throttling detection working') }) it('should manage statistics correctly', async () => { console.log('\n๐Ÿ“Š Test: Statistics management...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Create statistics const stats = { nounCount: { 'test-service': 5 }, verbCount: { 'test-service': 3 }, metadataCount: {}, hnswIndexSize: 100, totalNodes: 5, totalEdges: 3, totalMetadata: 0, lastUpdated: new Date().toISOString() } // Save statistics await (storage as any).saveStatisticsData(stats) // Verify statistics key was created const statsKeys = Array.from(mockAzureBlobs.keys()).filter(k => k.includes('_system/statistics.json') ) expect(statsKeys.length).toBe(1) // Retrieve statistics const retrieved = await (storage as any).getStatisticsData() expect(retrieved).toBeTruthy() expect(retrieved.nounCount['test-service']).toBe(5) console.log('โœ… Statistics management successful') }) it('should get storage status', async () => { console.log('\n๐Ÿ“Š Test: Storage status...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true const status = await storage.getStorageStatus() expect(status.type).toBe('azure') expect(status.details).toBeTruthy() expect(status.details!.container).toBe('test-container') console.log('โœ… Storage status retrieved:', status) }) it('should clear all data correctly', async () => { console.log('\n๐Ÿงน Test: Clear all data...') // Create storage storage = new AzureBlobStorage({ containerName: 'test-container', connectionString: 'test-connection-string' }) // Mock the Azure client ;(storage as any).blobServiceClient = createMockAzureClient() ;(storage as any).containerClient = (storage as any).blobServiceClient.getContainerClient('test-container') ;(storage as any).isInitialized = true // Write some data for (let i = 0; i < 3; i++) { await storage.saveNoun({ id: randomUUID(), vector: [0.1, 0.2, 0.3], connections: new Map(), level: 0 }) } console.log(`๐Ÿ“Š Objects before clear: ${mockAzureBlobs.size}`) // Clear all data await storage.clear() console.log(`๐Ÿ“Š Objects after clear: ${mockAzureBlobs.size}`) expect(mockAzureBlobs.size).toBe(0) console.log('โœ… Clear successful') }) }) console.log('\nโœ… Azure Blob Storage Tests Complete')