mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(Azure Storage Node): Add Azure cloud selection and custom endpoints to the Shared Key credential (#38181)
This commit is contained in:
parent
2f55165c11
commit
59c7d15a60
|
|
@ -14,6 +14,18 @@ describe('SecurityConfig', () => {
|
|||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('azureStorageCustomEndpoints', () => {
|
||||
test('defaults to false', () => {
|
||||
process.env = {};
|
||||
expect(Container.get(SecurityConfig).azureStorageCustomEndpoints).toBe(false);
|
||||
});
|
||||
|
||||
test('is enabled by N8N_AZURE_STORAGE_CUSTOM_ENDPOINTS_ENABLED=true', () => {
|
||||
process.env = { N8N_AZURE_STORAGE_CUSTOM_ENDPOINTS_ENABLED: 'true' };
|
||||
expect(Container.get(SecurityConfig).azureStorageCustomEndpoints).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awsSystemCredentialsSdkSources', () => {
|
||||
test('defaults to "all"', () => {
|
||||
process.env = {};
|
||||
|
|
|
|||
|
|
@ -161,6 +161,14 @@ export class SecurityConfig {
|
|||
@Env('N8N_AWS_SYSTEM_CREDENTIALS_SDK_SOURCES', awsSystemCredentialsSdkSourcesSchema)
|
||||
awsSystemCredentialsSdkSources: string = 'all';
|
||||
|
||||
/**
|
||||
* Whether Azure Storage Shared Key credentials can target a custom endpoint, such as a private
|
||||
* endpoint or a custom domain. Off by default. The Azure sovereign clouds are always available
|
||||
* and do not need this setting.
|
||||
*/
|
||||
@Env('N8N_AZURE_STORAGE_CUSTOM_ENDPOINTS_ENABLED')
|
||||
azureStorageCustomEndpoints: boolean = false;
|
||||
|
||||
/**
|
||||
* Whether to enable hooks (like pre-commit hooks) for the Git node.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ describe('GlobalConfig', () => {
|
|||
disableBareRepos: true,
|
||||
awsSystemCredentialsAccess: false,
|
||||
awsSystemCredentialsSdkSources: 'all',
|
||||
azureStorageCustomEndpoints: false,
|
||||
enableGitNodeHooks: false,
|
||||
enableGitNodeAllConfigKeys: false,
|
||||
postMessageAllowedOrigins: '',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { SecurityConfig } from '@n8n/config';
|
||||
import { Container } from '@n8n/di';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestRequest,
|
||||
|
|
@ -5,6 +7,7 @@ import type {
|
|||
IHttpRequestOptions,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
import {
|
||||
|
|
@ -14,6 +17,44 @@ import {
|
|||
XMsVersion,
|
||||
} from '../nodes/Microsoft/Storage/GenericFunctions';
|
||||
|
||||
const AZURE_CLOUD_SUFFIXES = [
|
||||
'blob.core.windows.net',
|
||||
'blob.core.usgovcloudapi.net',
|
||||
'blob.core.chinacloudapi.cn',
|
||||
];
|
||||
|
||||
function assertEndpointAllowed(credentials: ICredentialDataDecryptedObject): void {
|
||||
if (!/^[a-z0-9]+$/i.test(String(credentials.account ?? ''))) {
|
||||
throw new UserError('The account name can contain only letters and numbers.');
|
||||
}
|
||||
|
||||
const environment = credentials.environment ?? AZURE_CLOUD_SUFFIXES[0];
|
||||
if (environment !== 'custom') {
|
||||
if (!AZURE_CLOUD_SUFFIXES.includes(String(environment))) {
|
||||
throw new UserError('Select an Azure cloud from the list.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Container.get(SecurityConfig).azureStorageCustomEndpoints) {
|
||||
throw new UserError(
|
||||
'Custom Azure Storage endpoints are disabled on this instance, contact your administrator.',
|
||||
);
|
||||
}
|
||||
if (!credentials.customEndpoint) {
|
||||
throw new UserError('Endpoint is required when Azure Cloud is set to Custom.');
|
||||
}
|
||||
let endpoint: URL | undefined;
|
||||
try {
|
||||
endpoint = new URL(String(credentials.customEndpoint));
|
||||
} catch {}
|
||||
if (!endpoint || endpoint.protocol !== 'https:' || endpoint.href !== `${endpoint.origin}/`) {
|
||||
throw new UserError(
|
||||
'Endpoint must be an https:// URL with only a hostname and an optional port, for example https://myaccount.privatelink.blob.core.windows.net',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureStorageSharedKeyApi implements ICredentialType {
|
||||
name = 'azureStorageSharedKeyApi';
|
||||
|
||||
|
|
@ -39,11 +80,56 @@ export class AzureStorageSharedKeyApi implements ICredentialType {
|
|||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Azure Cloud',
|
||||
name: 'environment',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Azure Public Cloud',
|
||||
value: 'blob.core.windows.net',
|
||||
description: 'Uses <code>blob.core.windows.net</code>',
|
||||
},
|
||||
{
|
||||
name: 'Azure US Government',
|
||||
value: 'blob.core.usgovcloudapi.net',
|
||||
description: 'Uses <code>blob.core.usgovcloudapi.net</code>',
|
||||
},
|
||||
{
|
||||
name: 'Azure China',
|
||||
value: 'blob.core.chinacloudapi.cn',
|
||||
description: 'Uses <code>blob.core.chinacloudapi.cn</code>',
|
||||
},
|
||||
{
|
||||
name: 'Custom',
|
||||
value: 'custom',
|
||||
description:
|
||||
'A private endpoint or a custom domain. An administrator must enable custom endpoints on this n8n instance.',
|
||||
},
|
||||
],
|
||||
default: 'blob.core.windows.net',
|
||||
},
|
||||
{
|
||||
displayName: 'Endpoint',
|
||||
name: 'customEndpoint',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'https://myaccount.privatelink.blob.core.windows.net',
|
||||
displayOptions: {
|
||||
show: {
|
||||
environment: ['custom'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'The https:// URL of the storage endpoint. The account name must be in the hostname. An administrator must set <code>N8N_AZURE_STORAGE_CUSTOM_ENDPOINTS_ENABLED=true</code> on this n8n instance. Endpoints with the account name in the path, such as Azurite, do not work.',
|
||||
},
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'baseUrl',
|
||||
type: 'hidden',
|
||||
default: '=https://{{ $self["account"] }}.blob.core.windows.net',
|
||||
default:
|
||||
'={{ $self["environment"] === "custom" ? $self["customEndpoint"] : "https://" + $self["account"] + "." + $self["environment"] }}',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -51,6 +137,8 @@ export class AzureStorageSharedKeyApi implements ICredentialType {
|
|||
credentials: ICredentialDataDecryptedObject,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
assertEndpointAllowed(credentials);
|
||||
|
||||
if (requestOptions.qs) {
|
||||
for (const [key, value] of Object.entries(requestOptions.qs)) {
|
||||
if (value === undefined) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IHttpRequestOptions,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypes,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers, UserError, Workflow } from 'n8n-workflow';
|
||||
|
||||
import { HeaderConstants } from '../../nodes/Microsoft/Storage/GenericFunctions';
|
||||
import { AzureStorageSharedKeyApi } from '../AzureStorageSharedKeyApi.credentials';
|
||||
|
||||
const { mockContainer, MockSecurityConfig } = vi.hoisted(() => {
|
||||
class MockSecurityConfig {
|
||||
azureStorageCustomEndpoints = false;
|
||||
}
|
||||
return { mockContainer: { get: vi.fn() }, MockSecurityConfig };
|
||||
});
|
||||
|
||||
vi.mock('@n8n/di', () => ({ Container: mockContainer }));
|
||||
vi.mock('@n8n/config', () => ({ SecurityConfig: MockSecurityConfig }));
|
||||
|
||||
describe('AzureStorageSharedKeyApi Credential', () => {
|
||||
const credential = new AzureStorageSharedKeyApi();
|
||||
const property = (name: string) => credential.properties.find((p) => p.name === name);
|
||||
const securityConfig = new MockSecurityConfig();
|
||||
const credentials: ICredentialDataDecryptedObject = {
|
||||
account: 'myaccount',
|
||||
key: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
|
||||
};
|
||||
const privateEndpoint = 'https://myaccount.privatelink.blob.core.windows.net';
|
||||
const publicCloudSignature = 'SharedKey myaccount:LTfRKI8awvSOEoMJ6moAHQKF9Rt38hRdut6PsXHhnUI=';
|
||||
const listBlobsRequest = (baseURL: string): IHttpRequestOptions => ({
|
||||
method: 'GET',
|
||||
baseURL,
|
||||
url: '/mycontainer',
|
||||
qs: { restype: 'container', comp: 'list' },
|
||||
headers: { 'x-ms-date': 'Wed, 01 Jan 2025 00:00:00 GMT', 'x-ms-version': '2021-12-02' },
|
||||
});
|
||||
|
||||
const nodeTypes: INodeTypes = {
|
||||
getByName: () => ({ description: { properties: [] } }) as unknown as INodeType,
|
||||
getByNameAndVersion: () => ({ description: { properties: [] } }) as unknown as INodeType,
|
||||
getKnownTypes: () => ({}),
|
||||
};
|
||||
const resolveDefault = (expression: unknown, selfData: ICredentialDataDecryptedObject) => {
|
||||
const workflow = new Workflow({
|
||||
id: '1',
|
||||
nodes: [
|
||||
{
|
||||
name: 'Credential',
|
||||
type: 'n8n-nodes-base.noOp',
|
||||
typeVersion: 1,
|
||||
id: 'credential-1',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
return workflow.expression.getComplexParameterValue(
|
||||
workflow.getNode('Credential')!,
|
||||
expression as string,
|
||||
'internal',
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
selfData,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
securityConfig.azureStorageCustomEndpoints = false;
|
||||
mockContainer.get.mockReset();
|
||||
mockContainer.get.mockReturnValue(securityConfig);
|
||||
});
|
||||
|
||||
describe('properties', () => {
|
||||
it('should offer the Azure clouds and a custom endpoint, defaulting to the public cloud', () => {
|
||||
const environment = property('environment');
|
||||
|
||||
expect(environment?.type).toBe('options');
|
||||
expect(environment?.default).toBe('blob.core.windows.net');
|
||||
expect(environment?.options?.map((option) => 'value' in option && option.value)).toEqual([
|
||||
'blob.core.windows.net',
|
||||
'blob.core.usgovcloudapi.net',
|
||||
'blob.core.chinacloudapi.cn',
|
||||
'custom',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ask for the endpoint only for a custom cloud', () => {
|
||||
const endpoint = property('customEndpoint');
|
||||
|
||||
expect(endpoint?.type).toBe('string');
|
||||
expect(endpoint?.required).toBe(true);
|
||||
expect(endpoint?.displayOptions).toEqual({ show: { environment: ['custom'] } });
|
||||
});
|
||||
|
||||
it.each([
|
||||
['blob.core.windows.net', '', 'https://myaccount.blob.core.windows.net'],
|
||||
['blob.core.usgovcloudapi.net', '', 'https://myaccount.blob.core.usgovcloudapi.net'],
|
||||
['blob.core.chinacloudapi.cn', '', 'https://myaccount.blob.core.chinacloudapi.cn'],
|
||||
['custom', privateEndpoint, privateEndpoint],
|
||||
])('should build the hidden base URL for %s', (environment, customEndpoint, expected) => {
|
||||
const baseUrl = property('baseUrl');
|
||||
|
||||
expect(baseUrl?.type).toBe('hidden');
|
||||
expect(
|
||||
resolveDefault(baseUrl?.default, { account: 'myaccount', environment, customEndpoint }),
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
it('should resolve a credential saved before the cloud selection existed to the public cloud', () => {
|
||||
const stored = { account: 'myaccount', key: 'secret' };
|
||||
const withDefaults = NodeHelpers.getNodeParameters(
|
||||
credential.properties,
|
||||
stored as INodeParameters,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
) as ICredentialDataDecryptedObject;
|
||||
|
||||
expect(withDefaults.environment).toBe('blob.core.windows.net');
|
||||
expect(resolveDefault(withDefaults.baseUrl, withDefaults)).toBe(
|
||||
'https://myaccount.blob.core.windows.net',
|
||||
);
|
||||
});
|
||||
|
||||
it('should test the credential against the configured base URL', () => {
|
||||
expect(credential.test.request.baseURL).toBe('={{$credentials.baseUrl}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
const custom = (customEndpoint: string): ICredentialDataDecryptedObject => ({
|
||||
...credentials,
|
||||
environment: 'custom',
|
||||
customEndpoint,
|
||||
});
|
||||
|
||||
it('should sign with the account name and path, independent of the host', async () => {
|
||||
const publicCloud = await credential.authenticate(
|
||||
credentials,
|
||||
listBlobsRequest('https://myaccount.blob.core.windows.net'),
|
||||
);
|
||||
const sovereignCloud = await credential.authenticate(
|
||||
{ ...credentials, environment: 'blob.core.chinacloudapi.cn' },
|
||||
listBlobsRequest('https://myaccount.blob.core.chinacloudapi.cn'),
|
||||
);
|
||||
|
||||
expect(publicCloud.headers?.[HeaderConstants.AUTHORIZATION]).toBe(publicCloudSignature);
|
||||
expect(sovereignCloud.headers?.[HeaderConstants.AUTHORIZATION]).toBe(publicCloudSignature);
|
||||
});
|
||||
|
||||
it('should not consult the instance setting for an Azure cloud', async () => {
|
||||
mockContainer.get.mockImplementation(() => {
|
||||
throw new Error('instance setting read');
|
||||
});
|
||||
|
||||
const result = await credential.authenticate(
|
||||
{ ...credentials, environment: 'blob.core.usgovcloudapi.net' },
|
||||
listBlobsRequest('https://myaccount.blob.core.usgovcloudapi.net'),
|
||||
);
|
||||
|
||||
expect(result.headers?.[HeaderConstants.AUTHORIZATION]).toBe(publicCloudSignature);
|
||||
});
|
||||
|
||||
it.each(['my-account', 'myaccount.example.com', 'myaccount/x', ''])(
|
||||
'should reject the account name %j',
|
||||
async (account) => {
|
||||
const request = credential.authenticate(
|
||||
{ ...credentials, account },
|
||||
listBlobsRequest('https://myaccount.blob.core.windows.net'),
|
||||
);
|
||||
|
||||
await expect(request).rejects.toThrow(UserError);
|
||||
await expect(request).rejects.toThrow(
|
||||
'The account name can contain only letters and numbers.',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should reject a cloud that is not in the list', async () => {
|
||||
const request = credential.authenticate(
|
||||
{ ...credentials, environment: 'blob.example.com' },
|
||||
listBlobsRequest('https://myaccount.blob.example.com'),
|
||||
);
|
||||
|
||||
await expect(request).rejects.toThrow(UserError);
|
||||
await expect(request).rejects.toThrow('Select an Azure cloud from the list.');
|
||||
});
|
||||
|
||||
it('should refuse a custom endpoint when the instance does not allow it', async () => {
|
||||
const request = credential.authenticate(
|
||||
custom(privateEndpoint),
|
||||
listBlobsRequest(privateEndpoint),
|
||||
);
|
||||
|
||||
await expect(request).rejects.toThrow(UserError);
|
||||
await expect(request).rejects.toThrow(
|
||||
'Custom Azure Storage endpoints are disabled on this instance, contact your administrator.',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([privateEndpoint, `${privateEndpoint}/`, 'https://myaccount.example.com:10000'])(
|
||||
'should sign a request to the custom endpoint %j when the instance allows it',
|
||||
async (customEndpoint) => {
|
||||
securityConfig.azureStorageCustomEndpoints = true;
|
||||
|
||||
const result = await credential.authenticate(
|
||||
custom(customEndpoint),
|
||||
listBlobsRequest(customEndpoint),
|
||||
);
|
||||
|
||||
expect(result.headers?.[HeaderConstants.AUTHORIZATION]).toBe(publicCloudSignature);
|
||||
},
|
||||
);
|
||||
|
||||
it('should require an endpoint for a custom cloud', async () => {
|
||||
securityConfig.azureStorageCustomEndpoints = true;
|
||||
|
||||
const request = credential.authenticate(
|
||||
custom(''),
|
||||
listBlobsRequest('https://myaccount.blob.core.windows.net'),
|
||||
);
|
||||
|
||||
await expect(request).rejects.toThrow(UserError);
|
||||
await expect(request).rejects.toThrow(
|
||||
'Endpoint is required when Azure Cloud is set to Custom.',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'myaccount.privatelink.blob.core.windows.net',
|
||||
'ftp://myaccount.example.com',
|
||||
'http://myaccount.example.com',
|
||||
'https://user:pass@myaccount.example.com',
|
||||
'https://myaccount.example.com/devstoreaccount1',
|
||||
'https://myaccount.example.com/?x=1',
|
||||
'https://myaccount.example.com/#fragment',
|
||||
])('should reject the endpoint %j', async (customEndpoint) => {
|
||||
securityConfig.azureStorageCustomEndpoints = true;
|
||||
|
||||
const request = credential.authenticate(
|
||||
custom(customEndpoint),
|
||||
listBlobsRequest('https://myaccount.blob.core.windows.net'),
|
||||
);
|
||||
|
||||
await expect(request).rejects.toThrow(UserError);
|
||||
await expect(request).rejects.toThrow(
|
||||
'Endpoint must be an https:// URL with only a hostname and an optional port',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -97,7 +97,7 @@ export async function azureStorageApiRequest(
|
|||
|
||||
const options: IHttpRequestOptions = {
|
||||
method,
|
||||
url: url ?? `${credentials.baseUrl}${endpoint}`,
|
||||
url: url ?? `${credentials.baseUrl.replace(/\/+$/, '')}${endpoint}`,
|
||||
headers,
|
||||
body,
|
||||
qs,
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { credentials } from '../credentials';
|
|||
|
||||
describe('Azure Storage Node', () => {
|
||||
const { baseUrl } = credentials.azureStorageOAuth2Api;
|
||||
const blobListResponse = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="${baseUrl}" ContainerName="item1"><Prefix/><Marker/><MaxResults>1</MaxResults><Blobs><Blob><Name>myblob1</Name><Properties><Creation-Time>Wed, 22 Jan 2025 18:53:15 GMT</Creation-Time><Last-Modified>Wed, 22 Jan 2025 18:53:15 GMT</Last-Modified><Etag>0x1F8268B228AA730</Etag><Content-Length>37</Content-Length><Content-Type>application/json</Content-Type><Content-MD5>aWQGHD8kGQd5ZtEN/S1/aw==</Content-MD5><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState><ServerEncrypted>true</ServerEncrypted><AccessTier>Hot</AccessTier><AccessTierInferred>true</AccessTierInferred><AccessTierChangeTime>Wed, 22 Jan 2025 18:53:15 GMT</AccessTierChangeTime></Properties></Blob></Blobs><NextMarker>myblob2</NextMarker></EnumerationResults>`;
|
||||
|
||||
describe('List search', () => {
|
||||
it('should list search blobs', async () => {
|
||||
const mockResponse = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="${baseUrl}" ContainerName="item1"><Prefix/><Marker/><MaxResults>1</MaxResults><Blobs><Blob><Name>myblob1</Name><Properties><Creation-Time>Wed, 22 Jan 2025 18:53:15 GMT</Creation-Time><Last-Modified>Wed, 22 Jan 2025 18:53:15 GMT</Last-Modified><Etag>0x1F8268B228AA730</Etag><Content-Length>37</Content-Length><Content-Type>application/json</Content-Type><Content-MD5>aWQGHD8kGQd5ZtEN/S1/aw==</Content-MD5><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState><ServerEncrypted>true</ServerEncrypted><AccessTier>Hot</AccessTier><AccessTierInferred>true</AccessTierInferred><AccessTierChangeTime>Wed, 22 Jan 2025 18:53:15 GMT</AccessTierChangeTime></Properties></Blob></Blobs><NextMarker>myblob2</NextMarker></EnumerationResults>`;
|
||||
const mockRequestWithAuthentication = vi.fn().mockReturnValue(mockResponse);
|
||||
const mockRequestWithAuthentication = vi.fn().mockReturnValue(blobListResponse);
|
||||
const mockGetNodeParameter = vi.fn((parameterName, _fallbackValue, _options) => {
|
||||
if (parameterName === 'authentication') {
|
||||
return 'sharedKey';
|
||||
|
|
@ -111,5 +111,30 @@ describe('Azure Storage Node', () => {
|
|||
paginationToken: 'mycontainer2',
|
||||
});
|
||||
});
|
||||
it('should build the request URL without a double slash when the base URL ends with one', async () => {
|
||||
const mockRequestWithAuthentication = vi.fn().mockReturnValue(blobListResponse);
|
||||
const mockContext = {
|
||||
getCredentials: vi.fn(async () => ({
|
||||
...credentials.azureStorageSharedKeyApi,
|
||||
baseUrl: 'https://myaccount.blob.core.chinacloudapi.cn/',
|
||||
})),
|
||||
getNodeParameter: vi.fn((parameterName: string) =>
|
||||
parameterName === 'authentication' ? 'sharedKey' : { value: 'mycontainer' },
|
||||
),
|
||||
helpers: {
|
||||
requestWithAuthentication: mockRequestWithAuthentication,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
const node = new AzureStorage();
|
||||
|
||||
await node.methods.listSearch.getBlobs.call(mockContext);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'azureStorageSharedKeyApi',
|
||||
expect.objectContaining({
|
||||
url: 'https://myaccount.blob.core.chinacloudapi.cn/mycontainer',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user