From 9438085cc0f7660ee69cb3dfc463061ef99f01e2 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 12 Mar 2026 16:44:52 -0700 Subject: [PATCH] fix(blob): rewrite tests for storage-delegated API - Old tests imported removed functions (getBlobServiceClient, getContainerClient) - Old tests mocked @azure/storage-blob directly but blob now delegates to @bytelyst/storage - New tests use MemoryStorageProvider for functional validation - Tests cover: isBlobStorageConfigured (4 cases), storage provider/bucket ops (5 cases), BLOB_CONTAINERS (1 case) - 10/10 tests passing (was 2/8) --- packages/blob/src/__tests__/blob.test.ts | 200 +++++++++-------------- 1 file changed, 81 insertions(+), 119 deletions(-) diff --git a/packages/blob/src/__tests__/blob.test.ts b/packages/blob/src/__tests__/blob.test.ts index 67187bf3..c735c967 100644 --- a/packages/blob/src/__tests__/blob.test.ts +++ b/packages/blob/src/__tests__/blob.test.ts @@ -1,146 +1,108 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -const { - MockBlobServiceClient, - mockFromConnectionString, - mockGetContainerClient, - MockStorageSharedKeyCredential, - mockGenerateBlobSASQueryParameters, -} = vi.hoisted(() => { - const mockGetContainerClient = vi.fn(() => ({ - createIfNotExists: vi.fn(), - })); - - const mockFromConnectionString = vi.fn(() => ({ - getContainerClient: mockGetContainerClient, - })); - - const MockBlobServiceClient = vi.fn(() => ({ - getContainerClient: mockGetContainerClient, - })) as unknown as { - new (...args: any[]): any; - fromConnectionString: typeof mockFromConnectionString; - }; - - (MockBlobServiceClient as any).fromConnectionString = mockFromConnectionString; - - const MockStorageSharedKeyCredential = vi.fn(() => ({ kind: 'shared-key-cred' })); - - const mockGenerateBlobSASQueryParameters = vi.fn(() => ({ - toString: () => 'sig=fake', - })); - - return { - MockBlobServiceClient, - mockFromConnectionString, - mockGetContainerClient, - MockStorageSharedKeyCredential, - mockGenerateBlobSASQueryParameters, - }; -}); - -vi.mock('@azure/storage-blob', () => ({ - BlobServiceClient: MockBlobServiceClient, - ContainerClient: class {}, - StorageSharedKeyCredential: MockStorageSharedKeyCredential, - BlobSASPermissions: class { - read = false; - write = false; - create = false; - delete = false; - }, - generateBlobSASQueryParameters: mockGenerateBlobSASQueryParameters, - SASProtocol: { Https: 'Https' }, -})); +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { setStorage, MemoryStorageProvider } from '@bytelyst/storage'; import { _resetBlobClient, generateSasUrl, - getBlobServiceClient, - getContainerClient, + getBucket, + getStorageProvider, isBlobStorageConfigured, + BLOB_CONTAINERS, } from '../index.js'; describe('blob', () => { beforeEach(() => { _resetBlobClient(); - MockBlobServiceClient.mockClear(); - mockFromConnectionString.mockClear(); - mockGetContainerClient.mockClear(); - MockStorageSharedKeyCredential.mockClear(); - mockGenerateBlobSASQueryParameters.mockClear(); - delete process.env.AZURE_BLOB_CONNECTION_STRING; delete process.env.AZURE_BLOB_ACCOUNT_NAME; delete process.env.AZURE_BLOB_ACCOUNT_KEY; + delete process.env.STORAGE_PROVIDER; }); afterEach(() => { + _resetBlobClient(); delete process.env.AZURE_BLOB_CONNECTION_STRING; delete process.env.AZURE_BLOB_ACCOUNT_NAME; delete process.env.AZURE_BLOB_ACCOUNT_KEY; + delete process.env.STORAGE_PROVIDER; }); - it('isBlobStorageConfigured is false when unset', () => { - expect(isBlobStorageConfigured()).toBe(false); + describe('isBlobStorageConfigured', () => { + it('is false when unset', () => { + expect(isBlobStorageConfigured()).toBe(false); + }); + + it('is true when connection string is set', () => { + process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=x;AccountKey=y;'; + expect(isBlobStorageConfigured()).toBe(true); + }); + + it('is true when account name + key are set', () => { + process.env.AZURE_BLOB_ACCOUNT_NAME = 'acc'; + process.env.AZURE_BLOB_ACCOUNT_KEY = 'key=='; + expect(isBlobStorageConfigured()).toBe(true); + }); + + it('is true when provider is memory', () => { + process.env.STORAGE_PROVIDER = 'memory'; + expect(isBlobStorageConfigured()).toBe(true); + }); }); - it('isBlobStorageConfigured is true when connection string is set', () => { - process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=x;AccountKey=y;'; - expect(isBlobStorageConfigured()).toBe(true); + describe('with memory provider', () => { + let memoryProvider: MemoryStorageProvider; + + beforeEach(() => { + memoryProvider = new MemoryStorageProvider(); + setStorage(memoryProvider); + }); + + it('getStorageProvider returns the provider', async () => { + const provider = await getStorageProvider(); + expect(provider).toBe(memoryProvider); + }); + + it('getBucket returns a bucket by name', async () => { + const bucket = await getBucket('audio'); + expect(bucket).toBeDefined(); + // Upload and download to verify it works + await bucket.upload('test.wav', Buffer.from('hello')); + const data = await bucket.download('test.wav'); + expect(data.toString()).toBe('hello'); + }); + + it('generateSasUrl returns a signed URL', async () => { + const url = await generateSasUrl('audio', 'path/file.wav', 'r', 10); + expect(url).toContain('audio'); + expect(url).toContain('path/file.wav'); + expect(url).toContain('signed=true'); + }); + + it('generateSasUrl defaults to read permissions', async () => { + const url = await generateSasUrl('audio', 'file.wav'); + expect(url).toContain('signed=true'); + }); + + it('_resetBlobClient resets the storage singleton', async () => { + const p1 = await getStorageProvider(); + _resetBlobClient(); + // After reset, inject a new provider + const newProvider = new MemoryStorageProvider(); + setStorage(newProvider); + const p2 = await getStorageProvider(); + expect(p1).not.toBe(p2); + }); }); - it('getBlobServiceClient uses connection string when present', () => { - process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; - const c1 = getBlobServiceClient(); - const c2 = getBlobServiceClient(); - expect(c1).toBe(c2); - expect(mockFromConnectionString).toHaveBeenCalledTimes(1); - expect(MockBlobServiceClient).not.toHaveBeenCalled(); // should not call constructor in this path - }); - - it('getBlobServiceClient uses account name + key when provided', () => { - process.env.AZURE_BLOB_ACCOUNT_NAME = 'acc'; - process.env.AZURE_BLOB_ACCOUNT_KEY = 'key=='; - - getBlobServiceClient(); - - expect(MockStorageSharedKeyCredential).toHaveBeenCalledWith('acc', 'key=='); - expect(MockBlobServiceClient).toHaveBeenCalledWith( - 'https://acc.blob.core.windows.net', - expect.anything() - ); - }); - - it('getBlobServiceClient throws when credentials missing', () => { - expect(() => getBlobServiceClient()).toThrow('Azure Blob Storage not configured'); - }); - - it('getContainerClient creates container and caches client', async () => { - process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; - - const c1 = await getContainerClient('audio'); - const c2 = await getContainerClient('audio'); - expect(c1).toBe(c2); - - expect(mockGetContainerClient).toHaveBeenCalledTimes(1); - expect(c1.createIfNotExists).toHaveBeenCalledTimes(1); - }); - - it('generateSasUrl uses parsed connection string when account vars not set', () => { - process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=acc;AccountKey=key==;'; - - const url = generateSasUrl('audio', 'path/file.wav', 'rw', 10); - expect(url).toContain('https://acc.blob.core.windows.net/audio/path/file.wav?'); - expect(mockGenerateBlobSASQueryParameters).toHaveBeenCalledTimes(1); - }); - - it('generateSasUrl prefers explicit account vars when provided', () => { - process.env.AZURE_BLOB_CONNECTION_STRING = 'AccountName=fromconn;AccountKey=key==;'; - process.env.AZURE_BLOB_ACCOUNT_NAME = 'acc'; - process.env.AZURE_BLOB_ACCOUNT_KEY = 'key=='; - - const url = generateSasUrl('audio', 'x', 'r', 1); - expect(url).toContain('https://acc.blob.core.windows.net/audio/x?'); + describe('BLOB_CONTAINERS', () => { + it('has expected container names', () => { + expect(BLOB_CONTAINERS.audio).toBe('audio'); + expect(BLOB_CONTAINERS.transcripts).toBe('transcripts'); + expect(BLOB_CONTAINERS.attachments).toBe('attachments'); + expect(BLOB_CONTAINERS.avatars).toBe('avatars'); + expect(BLOB_CONTAINERS.releases).toBe('releases'); + expect(BLOB_CONTAINERS.backups).toBe('backups'); + expect(BLOB_CONTAINERS.feedbackScreenshots).toBe('feedback-screenshots'); + }); }); });