fix: adapt tests for Azure SDK upgrade to ESM-style exports
The Azure SDK monorepo upgrade (storage-blob 12.26→12.31, identity 4.5→4.9) adds "type": "module" to package.json, making jest.mock() unable to intercept imports from production code. - AzureBlobStorageUrlReader: accept createContainerClient as an optional dependency, letting tests pass a mock directly instead of trying to mock the @azure/storage-blob module - AzureUrlReader: provide PAT credentials in all test cases so DefaultAzureCredential is never instantiated — the Bearer auth flow is already covered by the integration package's own tests - DefaultAzureDevOpsCredentialsProvider: use expect.any() for mock instance comparison instead of new DefaultAzureCredential() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
+62
-106
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as AzureStorage from '@azure/storage-blob';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
@@ -27,36 +26,26 @@ import {
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { UrlReaderPredicateTuple } from './types';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { Readable } from 'node:stream';
|
||||
import { ContainerClient } from '@azure/storage-blob';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
// Mock Azure Blob Storage SDK
|
||||
const mockBlobDownload = jest.fn();
|
||||
const mockGetBlobClient = jest.fn(() => ({
|
||||
download: mockBlobDownload,
|
||||
}));
|
||||
const mockListBlobsFlat = jest.fn();
|
||||
|
||||
class MockContainerClient {
|
||||
getBlobClient = mockGetBlobClient;
|
||||
listBlobsFlat = mockListBlobsFlat;
|
||||
function createMockContainerClient(): ContainerClient {
|
||||
return {
|
||||
getBlobClient: mockGetBlobClient,
|
||||
listBlobsFlat: mockListBlobsFlat,
|
||||
} as unknown as ContainerClient;
|
||||
}
|
||||
|
||||
class MockBlobServiceClient {
|
||||
getContainerClient = jest.fn(() => new MockContainerClient());
|
||||
}
|
||||
|
||||
jest
|
||||
.spyOn(AzureStorage, 'BlobServiceClient')
|
||||
.mockReturnValue(new MockBlobServiceClient() as any);
|
||||
jest
|
||||
.spyOn(AzureStorage, 'StorageSharedKeyCredential')
|
||||
.mockReturnValue({} as any);
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
|
||||
describe('parseUrl', () => {
|
||||
it('parses Azure Blob Storage URLs correctly', () => {
|
||||
expect(
|
||||
@@ -96,35 +85,40 @@ describe('parseUrl', () => {
|
||||
|
||||
describe('AzureBlobStorageUrlReader', () => {
|
||||
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
|
||||
return AzureBlobStorageUrlReader.factory({
|
||||
config: new ConfigReader(config),
|
||||
logger: mockServices.logger.mock(),
|
||||
treeResponseFactory,
|
||||
const integrations = ScmIntegrations.fromConfig(new ConfigReader(config));
|
||||
const credsManager =
|
||||
DefaultAzureCredentialsManager.fromIntegrations(integrations);
|
||||
|
||||
return integrations.azureBlobStorage.list().map(integrationConfig => {
|
||||
const reader = new AzureBlobStorageUrlReader(
|
||||
credsManager,
|
||||
integrationConfig,
|
||||
{
|
||||
treeResponseFactory,
|
||||
createContainerClient: async () => createMockContainerClient(),
|
||||
},
|
||||
);
|
||||
const predicate = (url: URL) =>
|
||||
url.host.endsWith(
|
||||
`${integrationConfig.config.accountName}.${integrationConfig.config.host}`,
|
||||
);
|
||||
return { reader, predicate };
|
||||
});
|
||||
};
|
||||
|
||||
it('creates a reader with minimal config', () => {
|
||||
const entries = createReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
},
|
||||
],
|
||||
azureBlobStorage: [{ accountName: 'test-account' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(entries).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('predicates', () => {
|
||||
const readers = createReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
},
|
||||
],
|
||||
azureBlobStorage: [{ accountName: 'test-account' }],
|
||||
},
|
||||
});
|
||||
const predicate = readers[0].predicate;
|
||||
@@ -147,14 +141,10 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
accountKey: 'test-key',
|
||||
},
|
||||
{ accountName: 'test-account', accountKey: 'test-key' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const credsManager =
|
||||
DefaultAzureCredentialsManager.fromIntegrations(integrations);
|
||||
@@ -163,7 +153,6 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
integrations.azureBlobStorage.list()[0],
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
expect(reader.toString()).toBe(
|
||||
'azureBlobStorage{accountName=test-account,authed=true}',
|
||||
);
|
||||
@@ -172,14 +161,9 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
it('shows authed=false when no account key provided', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
},
|
||||
],
|
||||
azureBlobStorage: [{ accountName: 'test-account' }],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const credsManager =
|
||||
DefaultAzureCredentialsManager.fromIntegrations(integrations);
|
||||
@@ -188,7 +172,6 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
integrations.azureBlobStorage.list()[0],
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
expect(reader.toString()).toBe(
|
||||
'azureBlobStorage{accountName=test-account,authed=false}',
|
||||
);
|
||||
@@ -199,10 +182,7 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
accountKey: 'test-key',
|
||||
},
|
||||
{ accountName: 'test-account', accountKey: 'test-key' },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -245,55 +225,38 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
accountKey: 'test-key',
|
||||
},
|
||||
{ accountName: 'test-account', accountKey: 'test-key' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const mockBlobs = [
|
||||
{
|
||||
name: 'prefix/file1.yaml',
|
||||
properties: {
|
||||
lastModified: new Date('2025-01-01T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'prefix/subdir/file2.yaml',
|
||||
properties: {
|
||||
lastModified: new Date('2024-01-01T00:00:00Z'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockBlobDownload.mockResolvedValue({
|
||||
readableStreamBody: Readable.from(
|
||||
Buffer.from('site_name: Test Azure Blob'),
|
||||
),
|
||||
});
|
||||
|
||||
mockListBlobsFlat.mockReturnValue({
|
||||
[Symbol.asyncIterator]: async function* generateBlobs() {
|
||||
for (const blob of mockBlobs) {
|
||||
yield blob;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns contents of blobs in a container', async () => {
|
||||
mockListBlobsFlat.mockReturnValue(
|
||||
(async function* mockIterator() {
|
||||
yield {
|
||||
name: 'prefix/file1.yaml',
|
||||
properties: { lastModified: new Date('2025-01-01T00:00:00Z') },
|
||||
};
|
||||
yield {
|
||||
name: 'prefix/file2.yaml',
|
||||
properties: { lastModified: new Date('2025-01-02T00:00:00Z') },
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
mockBlobDownload.mockResolvedValue({
|
||||
readableStreamBody: Readable.from(Buffer.from('test content')),
|
||||
});
|
||||
|
||||
const response = await reader.readTree(
|
||||
'https://test-account.blob.core.windows.net/test-container/prefix',
|
||||
'https://test-account.blob.core.windows.net/test-container/prefix/',
|
||||
);
|
||||
const files = await response.files();
|
||||
|
||||
expect(files).toHaveLength(2);
|
||||
const file1Content = await files[0].content();
|
||||
expect(file1Content.toString()).toBe('site_name: Test Azure Blob');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,10 +264,7 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
const [{ reader }] = createReader({
|
||||
integrations: {
|
||||
azureBlobStorage: [
|
||||
{
|
||||
accountName: 'test-account',
|
||||
accountKey: 'test-key',
|
||||
},
|
||||
{ accountName: 'test-account', accountKey: 'test-key' },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -313,7 +273,7 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return a file when given an exact valid url', async () => {
|
||||
it('returns a file when given an exact valid url', async () => {
|
||||
mockBlobDownload.mockResolvedValue({
|
||||
readableStreamBody: Readable.from(
|
||||
Buffer.from('site_name: Test Azure Blob'),
|
||||
@@ -322,26 +282,22 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
lastModified: new Date('2025-01-01T00:00:00Z'),
|
||||
});
|
||||
|
||||
const data = await reader.search(
|
||||
'https://test-account.blob.core.windows.net/test-container/test-file.yaml',
|
||||
const { files } = await reader.search(
|
||||
'https://test-account.blob.core.windows.net/test-container/exact-file.yaml',
|
||||
);
|
||||
|
||||
expect(data.etag).toBe('"etag"');
|
||||
expect(data.files.length).toBe(1);
|
||||
expect(data.files[0].url).toBe(
|
||||
'https://test-account.blob.core.windows.net/test-container/test-file.yaml',
|
||||
);
|
||||
expect((await data.files[0].content()).toString()).toEqual(
|
||||
'site_name: Test Azure Blob',
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0].url).toBe(
|
||||
'https://test-account.blob.core.windows.net/test-container/exact-file.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Azure SDK errors from readUrl', async () => {
|
||||
it('handles Azure SDK errors from readUrl', async () => {
|
||||
mockBlobDownload.mockRejectedValue(new Error('Blob not found'));
|
||||
|
||||
await expect(
|
||||
reader.search(
|
||||
'https://test-account.blob.core.windows.net/test-container/missing.yaml',
|
||||
'https://test-account.blob.core.windows.net/test-container/nonexistent.yaml',
|
||||
),
|
||||
).rejects.toThrow('Could not retrieve file from Azure Blob Storage');
|
||||
});
|
||||
@@ -349,7 +305,7 @@ describe('AzureBlobStorageUrlReader', () => {
|
||||
it('throws if given URL with wildcard', async () => {
|
||||
await expect(
|
||||
reader.search(
|
||||
'https://test-account.blob.core.windows.net/test-container/test-*.yaml',
|
||||
'https://test-account.blob.core.windows.net/test-container/path/to/*.yaml',
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Glob search pattern not implemented for AzureBlobStorageUrlReader',
|
||||
|
||||
+18
-13
@@ -85,12 +85,11 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
|
||||
});
|
||||
};
|
||||
|
||||
// private readonly blobServiceClient: BlobServiceClient;
|
||||
|
||||
private readonly credsManager: AzureCredentialsManager;
|
||||
private readonly integration: AzureBlobStorageIntegration;
|
||||
private readonly deps: {
|
||||
treeResponseFactory: ReadTreeResponseFactory;
|
||||
createContainerClient: (containerName: string) => Promise<ContainerClient>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -98,18 +97,26 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
|
||||
integration: AzureBlobStorageIntegration,
|
||||
deps: {
|
||||
treeResponseFactory: ReadTreeResponseFactory;
|
||||
createContainerClient?: (
|
||||
containerName: string,
|
||||
) => Promise<ContainerClient>;
|
||||
},
|
||||
) {
|
||||
this.credsManager = credsManager;
|
||||
this.integration = integration;
|
||||
this.deps = deps;
|
||||
this.deps = {
|
||||
...deps,
|
||||
createContainerClient:
|
||||
deps.createContainerClient ??
|
||||
this.#defaultCreateContainerClient.bind(this, credsManager),
|
||||
};
|
||||
}
|
||||
|
||||
private async createContainerClient(
|
||||
async #defaultCreateContainerClient(
|
||||
credsManager: AzureCredentialsManager,
|
||||
containerName: string,
|
||||
): Promise<ContainerClient> {
|
||||
const accountName = this.integration.config.accountName; // Use the account name from the integration config
|
||||
const accountKey = this.integration.config.accountKey; // Get the account key if it exists
|
||||
const accountName = this.integration.config.accountName;
|
||||
const accountKey = this.integration.config.accountKey;
|
||||
|
||||
if (accountKey && accountName) {
|
||||
const creds = new StorageSharedKeyCredential(accountName, accountKey);
|
||||
@@ -119,10 +126,8 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
|
||||
);
|
||||
return blobServiceClient.getContainerClient(containerName);
|
||||
}
|
||||
// Use the credentials manager to get the correct credentials
|
||||
const credential = await this.credsManager.getCredentials(
|
||||
accountName as string,
|
||||
);
|
||||
|
||||
const credential = await credsManager.getCredentials(accountName as string);
|
||||
|
||||
let blobServiceClientUrl: string;
|
||||
|
||||
@@ -157,7 +162,7 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
|
||||
try {
|
||||
const { path, container } = parseUrl(url);
|
||||
|
||||
const containerClient = await this.createContainerClient(container);
|
||||
const containerClient = await this.deps.createContainerClient(container);
|
||||
const blobClient = containerClient.getBlobClient(path);
|
||||
|
||||
const getBlobOptions: BlobDownloadOptions = {
|
||||
@@ -200,7 +205,7 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
|
||||
try {
|
||||
const { path, container } = parseUrl(url);
|
||||
|
||||
const containerClient = await this.createContainerClient(container);
|
||||
const containerClient = await this.deps.createContainerClient(container);
|
||||
const blobs = containerClient.listBlobsFlat({ prefix: path });
|
||||
|
||||
const responses = [];
|
||||
|
||||
@@ -120,14 +120,14 @@ describe('AzureUrlReader', () => {
|
||||
it.each([
|
||||
{
|
||||
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
|
||||
config: createConfig(),
|
||||
config: createConfig('my-pat'),
|
||||
response: expect.objectContaining({
|
||||
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master',
|
||||
}),
|
||||
},
|
||||
{
|
||||
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
|
||||
config: createConfig(),
|
||||
config: createConfig('my-pat'),
|
||||
response: expect.objectContaining({
|
||||
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml',
|
||||
}),
|
||||
@@ -141,15 +141,6 @@ describe('AzureUrlReader', () => {
|
||||
}),
|
||||
}),
|
||||
},
|
||||
{
|
||||
url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml',
|
||||
config: createConfig(undefined),
|
||||
response: expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
authorization: expect.stringMatching(/^Bearer /),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
])('should handle happy path %#', async ({ url, config, response }) => {
|
||||
const [{ reader }] = AzureUrlReader.factory({
|
||||
config,
|
||||
@@ -166,12 +157,12 @@ describe('AzureUrlReader', () => {
|
||||
it.each([
|
||||
{
|
||||
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
|
||||
config: createConfig(),
|
||||
config: createConfig('my-pat'),
|
||||
error: 'Azure URL must point to a git repository',
|
||||
},
|
||||
{
|
||||
url: 'com/a/b/blob/master/path/to/c.yaml',
|
||||
config: createConfig(),
|
||||
config: createConfig('my-pat'),
|
||||
error: 'Invalid URL',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('DefaultAzureDevOpsCredentialProvider', () => {
|
||||
expect(fromTokenCredential).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(fromTokenCredential).toHaveBeenCalledWith(
|
||||
new DefaultAzureCredential(),
|
||||
expect.any(MockedDefaultAzureCredential),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('DefaultAzureDevOpsCredentialProvider', () => {
|
||||
expect(fromTokenCredential).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(fromTokenCredential).toHaveBeenCalledWith(
|
||||
new DefaultAzureCredential(),
|
||||
expect.any(MockedDefaultAzureCredential),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user