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)
This commit is contained in:
saravanakumardb1 2026-03-12 16:44:52 -07:00
parent 4ca9b73d75
commit 9438085cc0

View File

@ -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');
});
});
});