Skip to content

Commit

Permalink
feat(north): Test Azure Blob connection
Browse files Browse the repository at this point in the history
  • Loading branch information
burgerni10 committed Aug 2, 2023
1 parent 96164da commit 5cfb61d
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 7 deletions.
2 changes: 1 addition & 1 deletion backend/src/north/north-azure-blob/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const manifest: NorthConnectorManifest = {
{
key: 'authentication',
type: 'OibSelect',
options: ['sasToken', 'accessKey', 'aad'],
options: ['external', 'sasToken', 'accessKey', 'aad', 'powershell'],
label: 'Authentication',
defaultValue: 'accessKey',
newRow: true,
Expand Down
100 changes: 98 additions & 2 deletions backend/src/north/north-azure-blob/north-azure-blob.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import RepositoryService from '../../service/repository.service';
import RepositoryServiceMock from '../../tests/__mocks__/repository-service.mock';
import { NorthConnectorDTO } from '../../../../shared/model/north-connector.model';
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import { DefaultAzureCredential } from '@azure/identity';
import { AzurePowerShellCredential, DefaultAzureCredential } from '@azure/identity';
import ValueCacheServiceMock from '../../tests/__mocks__/value-cache-service.mock';
import FileCacheServiceMock from '../../tests/__mocks__/file-cache-service.mock';
import { NorthAzureBlobSettings } from '../../../../shared/model/north-settings.model';
Expand All @@ -28,8 +28,9 @@ jest.mock('@azure/storage-blob', () => ({
})),
StorageSharedKeyCredential: jest.fn()
}));
jest.mock('@azure/identity', () => ({ DefaultAzureCredential: jest.fn() }));
jest.mock('@azure/identity', () => ({ DefaultAzureCredential: jest.fn(), AzurePowerShellCredential: jest.fn() }));
jest.mock('node:fs/promises');
jest.mock('../../service/utils');
jest.mock(
'../../service/cache/archive.service',
() =>
Expand Down Expand Up @@ -184,4 +185,99 @@ describe('NorthAzureBlob', () => {
expect(getBlockBlobClientMock).toHaveBeenCalledWith('example.file');
expect(uploadMock).toHaveBeenCalledWith('content', 666);
});

it('should properly handle files with external auth', async () => {
const filePath = '/path/to/file/example-123.file';
(fs.stat as jest.Mock).mockImplementationOnce(() => Promise.resolve({ size: 666 }));
(fs.readFile as jest.Mock).mockImplementationOnce(() => Promise.resolve('content'));
const defaultAzureCredential = jest.fn();
(DefaultAzureCredential as jest.Mock).mockImplementationOnce(() => defaultAzureCredential);

configuration.settings.authentication = 'external';
const north = new NorthAzureBlob(configuration, encryptionService, repositoryService, logger, 'baseFolder');

await north.start();
await north.handleFile(filePath);

expect(fs.stat).toHaveBeenCalledWith(filePath);
expect(fs.readFile).toHaveBeenCalledWith(filePath);
expect(DefaultAzureCredential).toHaveBeenCalled();
expect(BlobServiceClient).toHaveBeenCalledWith(
`https://${configuration.settings.account}.blob.core.windows.net`,
defaultAzureCredential
);
expect(getContainerClientMock).toHaveBeenCalledWith(configuration.settings.container);
expect(getBlockBlobClientMock).toHaveBeenCalledWith('example.file');
expect(uploadMock).toHaveBeenCalledWith('content', 666);
});

it('should properly handle files with powershell credentials', async () => {
const filePath = '/path/to/file/example-123.file';
(fs.stat as jest.Mock).mockImplementationOnce(() => Promise.resolve({ size: 666 }));
(fs.readFile as jest.Mock).mockImplementationOnce(() => Promise.resolve('content'));
const defaultAzureCredential = jest.fn();
(AzurePowerShellCredential as jest.Mock).mockImplementationOnce(() => defaultAzureCredential);

configuration.settings.path = 'my path';
configuration.settings.authentication = 'powershell';
const north = new NorthAzureBlob(configuration, encryptionService, repositoryService, logger, 'baseFolder');

await north.start();
await north.handleFile(filePath);

expect(fs.stat).toHaveBeenCalledWith(filePath);
expect(fs.readFile).toHaveBeenCalledWith(filePath);
expect(AzurePowerShellCredential).toHaveBeenCalled();
expect(BlobServiceClient).toHaveBeenCalledWith(
`https://${configuration.settings.account}.blob.core.windows.net`,
defaultAzureCredential
);
expect(getContainerClientMock).toHaveBeenCalledWith(configuration.settings.container);
expect(getBlockBlobClientMock).toHaveBeenCalledWith('my path/example.file');
expect(uploadMock).toHaveBeenCalledWith('content', 666);
});

it('should successfully test', async () => {
const defaultAzureCredential = jest.fn();
(AzurePowerShellCredential as jest.Mock).mockImplementationOnce(() => defaultAzureCredential);
const exists = jest.fn().mockReturnValueOnce(true).mockReturnValueOnce(false);
(getContainerClientMock as jest.Mock).mockImplementation(() => {
return {
exists: exists
};
});
const north = new NorthAzureBlob(configuration, encryptionService, repositoryService, logger, 'baseFolder');
await north.testConnection();
expect(AzurePowerShellCredential).toHaveBeenCalled();
expect(BlobServiceClient).toHaveBeenCalledWith(
`https://${configuration.settings.account}.blob.core.windows.net`,
defaultAzureCredential
);
expect(getContainerClientMock).toHaveBeenCalledWith(configuration.settings.container);
expect(logger.info).toHaveBeenCalledWith(`Access to container ${configuration.settings.container} ok`);
await expect(north.testConnection()).rejects.toThrow(new Error(`Container ${configuration.settings.container} does not exist`));
});

it('should manage test error', async () => {
const defaultAzureCredential = jest.fn();
(AzurePowerShellCredential as jest.Mock).mockImplementationOnce(() => defaultAzureCredential);
(getContainerClientMock as jest.Mock).mockImplementation(() => {
throw new Error('connection error');
});
const north = new NorthAzureBlob(configuration, encryptionService, repositoryService, logger, 'baseFolder');

await expect(north.testConnection()).rejects.toThrow(new Error('connection error'));
expect(getContainerClientMock).toHaveBeenCalledWith(configuration.settings.container);
expect(logger.error).toHaveBeenCalledWith(`Error testing Azure Blob connection. ${new Error('connection error')}`);
});

it('should manage bad authentication type', async () => {
// @ts-ignore
configuration.settings.authentication = 'bad';
const north = new NorthAzureBlob(configuration, encryptionService, repositoryService, logger, 'baseFolder');

await expect(north.start()).rejects.toThrow(
new Error(`Authentication "${configuration.settings.authentication}" not supported for North "${configuration.name}"`)
);
});
});
40 changes: 38 additions & 2 deletions backend/src/north/north-azure-blob/north-azure-blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
import path from 'node:path';
import pino from 'pino';
import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob';
import { DefaultAzureCredential } from '@azure/identity';
import { AzurePowerShellCredential, DefaultAzureCredential } from '@azure/identity';
import NorthConnector from '../north-connector';
import manifest from '../north-azure-blob/manifest';
import { NorthConnectorDTO } from '../../../../shared/model/north-connector.model';
Expand All @@ -27,6 +27,11 @@ export default class NorthAzureBlob extends NorthConnector<NorthAzureBlobSetting
}

async start(): Promise<void> {
await super.start();
await this.prepareConnection();
}

async prepareConnection(): Promise<void> {
this.logger.info(
`Connecting to Azure Blob Storage for account ${this.connector.settings.account} and container ${this.connector.settings.container}`
);
Expand All @@ -51,8 +56,21 @@ export default class NorthAzureBlob extends NorthConnector<NorthAzureBlobSetting
const defaultAzureCredential = new DefaultAzureCredential();
this.blobClient = new BlobServiceClient(`https://${this.connector.settings.account}.blob.core.windows.net`, defaultAzureCredential);
break;
case 'external':
const externalAzureCredential = new DefaultAzureCredential();
this.blobClient = new BlobServiceClient(
`https://${this.connector.settings.account}.blob.core.windows.net`,
externalAzureCredential
);
break;
case 'powershell':
this.blobClient = new BlobServiceClient(
`https://${this.connector.settings.account}.blob.core.windows.net`,
new AzurePowerShellCredential()
);
break;
default:
throw new Error(`Authentication "${this.connector.settings.authentication}" not supported for South "${this.connector.name}"`);
throw new Error(`Authentication "${this.connector.settings.authentication}" not supported for North "${this.connector.name}"`);
}
}

Expand All @@ -76,4 +94,22 @@ export default class NorthAzureBlob extends NorthConnector<NorthAzureBlobSetting
const uploadBlobResponse = await blockBlobClient.upload(content, stats.size);
this.logger.info(`Upload block blob "${blobName}" successfully with requestId: ${uploadBlobResponse.requestId}`);
}

override async testConnection(): Promise<void> {
this.logger.info('Testing Azure Blob connection');
await this.prepareConnection();

let result = false;
try {
result = await this.blobClient!.getContainerClient(this.connector.settings.container).exists();
} catch (error) {
this.logger.error(`Error testing Azure Blob connection. ${error}`);
throw error;
}
if (result) {
this.logger.info(`Access to container ${this.connector.settings.container} ok`);
} else {
throw new Error(`Container ${this.connector.settings.container} does not exist`);
}
}
}
2 changes: 1 addition & 1 deletion shared/model/north-settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This file is auto-generated by the script located backend/src/settings-interface.generator.ts
// To update the typescript model run npm run generate-settings-interface on the backend project

const NORTH_AZURE_BLOB_SETTINGS_AUTHENTICATIONS = ['sasToken', 'accessKey', 'aad'] as const
const NORTH_AZURE_BLOB_SETTINGS_AUTHENTICATIONS = ['external', 'sasToken', 'accessKey', 'aad', 'powershell'] as const
export type NorthAzureBlobSettingsAuthentication = (typeof NORTH_AZURE_BLOB_SETTINGS_AUTHENTICATIONS)[number];

const NORTH_CSV_TO_HTTP_SETTINGS_REQUEST_METHODS = ['GET', 'POST', 'PUT', 'PATCH'] as const
Expand Down
2 changes: 1 addition & 1 deletion shared/model/south-settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,4 +616,4 @@ export type SouthItemSettings =
| SouthOracleItemSettings
| SouthPostgreSQLItemSettings
| SouthSlimsItemSettings
| SouthSQLiteItemSettings
| SouthSQLiteItemSettings

0 comments on commit 5cfb61d

Please sign in to comment.