feat(Azure Storage Node): New node (#12536)

This commit is contained in:
feelgood-interface 2025-02-28 11:17:56 +01:00 committed by GitHub
parent d550382a4a
commit 727f6f3c0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 4696 additions and 6 deletions

View file

@ -0,0 +1,32 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class AzureStorageOAuth2Api implements ICredentialType {
name = 'azureStorageOAuth2Api';
displayName = 'Azure Storage OAuth2 API';
extends = ['microsoftOAuth2Api'];
documentationUrl = 'azurestorage';
properties: INodeProperties[] = [
{
displayName: 'Account',
name: 'account',
type: 'string',
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'hidden',
default: '=https://{{ $self["account"] }}.blob.core.windows.net',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'https://storage.azure.com/.default',
},
];
}

View file

@ -0,0 +1,96 @@
import type {
ICredentialDataDecryptedObject,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
} from 'n8n-workflow';
import { createHmac } from 'node:crypto';
import {
getCanonicalizedHeadersString,
getCanonicalizedResourceString,
HeaderConstants,
} from '../nodes/Microsoft/Storage/GenericFunctions';
export class AzureStorageSharedKeyApi implements ICredentialType {
name = 'azureStorageSharedKeyApi';
displayName = 'Azure Storage Shared Key API';
documentationUrl = 'azurestorage';
properties: INodeProperties[] = [
{
displayName: 'Account',
name: 'account',
description: 'Account name',
type: 'string',
default: '',
},
{
displayName: 'Key',
name: 'key',
description: 'Account key',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'hidden',
default: '=https://{{ $self["account"] }}.blob.core.windows.net',
},
];
async authenticate(
credentials: ICredentialDataDecryptedObject,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
if (requestOptions.qs) {
for (const [key, value] of Object.entries(requestOptions.qs)) {
if (value === undefined) {
delete requestOptions.qs[key];
}
}
}
if (requestOptions.headers) {
for (const [key, value] of Object.entries(requestOptions.headers)) {
if (value === undefined) {
delete requestOptions.headers[key];
}
}
}
requestOptions.method ??= 'GET';
requestOptions.headers ??= {};
const stringToSign: string = [
requestOptions.method.toUpperCase(),
requestOptions.headers[HeaderConstants.CONTENT_LANGUAGE] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_ENCODING] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_LENGTH] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_MD5] ?? '',
requestOptions.headers[HeaderConstants.CONTENT_TYPE] ?? '',
requestOptions.headers[HeaderConstants.DATE] ?? '',
requestOptions.headers[HeaderConstants.IF_MODIFIED_SINCE] ?? '',
requestOptions.headers[HeaderConstants.IF_MATCH] ?? '',
requestOptions.headers[HeaderConstants.IF_NONE_MATCH] ?? '',
requestOptions.headers[HeaderConstants.IF_UNMODIFIED_SINCE] ?? '',
requestOptions.headers[HeaderConstants.RANGE] ?? '',
getCanonicalizedHeadersString(requestOptions) +
getCanonicalizedResourceString(requestOptions, credentials),
].join('\n');
const signature: string = createHmac('sha256', Buffer.from(credentials.key as string, 'base64'))
.update(stringToSign, 'utf8')
.digest('base64');
requestOptions.headers[HeaderConstants.AUTHORIZATION] =
`SharedKey ${credentials.account as string}:${signature}`;
return requestOptions;
}
}

View file

@ -7,7 +7,7 @@ import type { WorkflowTestData } from '@test/nodes/types';
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
describe('Gong Node', () => {
describe('Microsoft Entra Node', () => {
const baseUrl = 'https://graph.microsoft.com/v1.0';
beforeEach(() => {

View file

@ -15,7 +15,7 @@ import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
import { FAKE_CREDENTIALS_DATA } from '../../../../test/nodes/FakeCredentialsMap';
import { MicrosoftEntra } from '../MicrosoftEntra.node';
describe('Gong Node', () => {
describe('Microsoft Entra Node', () => {
const baseUrl = 'https://graph.microsoft.com/v1.0';
beforeEach(() => {

View file

@ -7,7 +7,7 @@ import type { WorkflowTestData } from '@test/nodes/types';
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
describe('Gong Node', () => {
describe('Microsoft Entra Node', () => {
const baseUrl = 'https://graph.microsoft.com/v1.0';
beforeEach(() => {

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.azureStorage",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.azurestorage/"
}
]
}
}

View file

@ -0,0 +1,97 @@
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { blobFields, blobOperations, containerFields, containerOperations } from './descriptions';
import { getBlobs, getContainers } from './GenericFunctions';
export class AzureStorage implements INodeType {
description: INodeTypeDescription = {
displayName: 'Azure Storage',
name: 'azureStorage',
icon: {
light: 'file:azureStorage.svg',
dark: 'file:azureStorage.dark.svg',
},
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Azure Storage API',
defaults: {
name: 'Azure Storage',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'azureStorageOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
{
name: 'azureStorageSharedKeyApi',
required: true,
displayOptions: {
show: {
authentication: ['sharedKey'],
},
},
},
],
requestDefaults: {
baseURL: '={{ $credentials.baseUrl }}',
},
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'Shared Key',
value: 'sharedKey',
},
],
default: 'sharedKey',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Blob',
value: 'blob',
},
{
name: 'Container',
value: 'container',
},
],
default: 'container',
},
...blobOperations,
...blobFields,
...containerOperations,
...containerFields,
],
};
methods = {
loadOptions: {},
listSearch: {
getBlobs,
getContainers,
},
};
}

View file

@ -0,0 +1,582 @@
import { camelCase } from 'change-case';
import type {
JsonObject,
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
INodeExecutionData,
IN8nHttpFullResponse,
INodeListSearchResult,
INodeListSearchItems,
INodeParameterResourceLocator,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { Parser } from 'xml2js';
import { firstCharLowerCase, parseBooleans, parseNumbers } from 'xml2js/lib/processors';
import { compareHeader } from './compare-header';
export const XMsVersion = '2021-12-02';
export const HeaderConstants = {
AUTHORIZATION: 'authorization',
CONTENT_ENCODING: 'content-encoding',
CONTENT_LANGUAGE: 'content-language',
CONTENT_LENGTH: 'content-length',
CONTENT_MD5: 'content-md5',
CONTENT_TYPE: 'content-Type',
DATE: 'date',
ETAG: 'etag',
IF_MATCH: 'if-match',
IF_MODIFIED_SINCE: 'if-Modified-since',
IF_NONE_MATCH: 'if-none-match',
IF_UNMODIFIED_SINCE: 'if-unmodified-since',
ORIGIN: 'origin',
RANGE: 'range',
X_MS_COPY_SOURCE: 'x-ms-copy-source',
X_MS_DATE: 'x-ms-date',
X_MS_VERSION: 'x-ms-version',
X_MS_BLOB_TYPE: 'x-ms-blob-type',
X_MS_BLOB_CONTENT_DISPOSITION: 'x-ms-blob-content-disposition',
X_MS_BLOB_PUBLIC_ACCESS: 'x-ms-blob-public-access',
X_MS_HAS_IMMUTABILITY_POLICY: 'x-ms-has-immutability-policy',
X_MS_HAS_LEGAL_HOLD: 'x-ms-has-legal-hold',
X_MS_CONTENT_CRC64: 'x-ms-content-crc64',
X_MS_REQUEST_SERVER_ENCRYPTED: 'x-ms-request-server-encrypted',
X_MS_ENCRYPTION_SCOPE: 'x-ms-encryption-scope',
X_MS_VERSION_ID: 'x-ms-version-id',
X_MS_TAG_COUNT: 'x-ms-tag-count',
X_MS_COPY_PROGRESS: 'x-ms-copy-progress',
X_MS_INCREMENTAL_COPY: 'x-ms-incremental-copy',
X_MS_BLOB_SEQUENCE_NUMBER: 'x-ms-blob-sequence-number',
X_MS_BLOB_COMMITTED_BLOCK_COUNT: 'x-ms-blob-committed-block-count',
X_MS_SERVER_ENCRYPTED: 'x-ms-server-encrypted',
X_MS_ENCRYPTION_CONTEXT: 'x-ms-encryption-context',
X_MS_BLOB_CONTENT_MD5: 'x-ms-blob-content-md5',
X_MS_BLOB_SEALED: 'x-ms-blob-sealed',
X_MS_IMMUTABILITY_POLICY_UNTIL_DATE: 'x-ms-immutability-policy-until-date',
X_MS_IMMUTABILITY_POLICY_MODE: 'x-ms-immutability-policy-mode',
X_MS_LEGAL_HOLD: 'x-ms-legal-hold',
X_MS_DELETE_TYPE_PERMANENT: 'x-ms-delete-type-permanent',
X_MS_ACCESS_TIER: 'x-ms-access-tier',
X_MS_BLOB_CACHE_CONTROL: 'x-ms-blob-cache-control',
X_MS_LEASE_ID: 'x-ms-lease-id',
X_MS_BLOB_CONTENT_ENCODING: 'x-ms-blob-content-encoding',
X_MS_BLOB_CONTENT_LANGUAGE: 'x-ms-blob-content-language',
X_MS_BLOB_CONTENT_TYPE: 'x-ms-blob-content-type',
X_MS_EXPIRY_OPTION: 'x-ms-expiry-option',
X_MS_EXPIRY_TIME: 'x-ms-expiry-time',
X_MS_TAGS: 'x-ms-tags',
X_MS_UPN: 'x-ms-upn',
PREFIX_X_MS: 'x-ms-',
PREFIX_X_MS_META: 'x-ms-meta-',
};
export async function azureStorageApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs?: IDataObject,
headers?: IDataObject,
url?: string,
): Promise<string> {
const authentication = this.getNodeParameter('authentication', 0) as 'oAuth2' | 'sharedKey';
const credentialsType =
authentication === 'oAuth2' ? 'azureStorageOAuth2Api' : 'azureStorageSharedKeyApi';
const credentials = await this.getCredentials<{
baseUrl: string;
}>(credentialsType);
const options: IHttpRequestOptions = {
method,
url: url ?? `${credentials.baseUrl}${endpoint}`,
headers,
body,
qs,
};
options.headers ??= {};
options.headers[HeaderConstants.X_MS_DATE] = new Date().toUTCString();
options.headers[HeaderConstants.X_MS_VERSION] = XMsVersion;
// XML response
const response = (await this.helpers.requestWithAuthentication.call(
this,
credentialsType,
options,
)) as string;
return response;
}
export async function handleErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
const resource = this.getNodeParameter('resource') as string;
const operation = this.getNodeParameter('operation') as string;
const parser = new Parser({
explicitArray: false,
tagNameProcessors: [firstCharLowerCase],
});
const { error } =
((await parser.parseStringPromise(data[0].json as unknown as string)) as {
error: {
code: string;
message: string;
headerName?: string;
headerValue?: string;
};
}) ?? {};
if (
error?.code === 'InvalidAuthenticationInfo' &&
((error as IDataObject)?.authenticationErrorDetail as string) ===
'Lifetime validation failed. The token is expired.'
) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: 'Lifetime validation failed. The token is expired.',
description: 'Please check your credentials and try again',
});
}
if (resource === 'blob') {
if (error?.code === 'ContainerNotFound') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "The required container doesn't match any existing one",
description: "Double-check the value in the parameter 'Container Name' and try again",
});
}
if (operation === 'create') {
if (
this.getNodeParameter('from') === 'url' &&
((error?.code === 'InvalidHeaderValue' &&
error?.headerName === HeaderConstants.X_MS_COPY_SOURCE) ||
error?.code === 'CannotVerifyCopySource')
) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: 'The provided URL is invalid',
description: "Double-check the value in the parameter 'URL' and try again",
});
}
} else if (operation === 'delete') {
if (error?.code === 'BlobNotFound') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "The required blob doesn't match any existing one",
description: "Double-check the value in the parameter 'Blob to Delete' and try again",
});
}
} else if (operation === 'get') {
if (error?.code === 'BlobNotFound') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "The required blob doesn't match any existing one",
description: "Double-check the value in the parameter 'Blob to Get' and try again",
});
}
} else if (operation === 'getAll') {
if (
error?.code === 'InvalidQueryParameterValue' &&
(this.getNodeParameter('fields', []) as string[]).includes('permissions')
) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message:
'Permissions field is only supported for accounts with a hierarchical namespace enabled',
description: "Exclude 'Permissions' from 'Fields' and try again",
});
}
if (
error?.code === 'UnsupportedQueryParameter' &&
(this.getNodeParameter('fields', []) as string[]).includes('deleted') &&
(this.getNodeParameter('filter', []) as string[]).includes('deleted')
) {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "Including 'Deleted' field and filter is not supported",
description: "Exclude 'Deleted' from 'Fields' or 'Filter' and try again",
});
}
}
} else if (resource === 'container') {
if (operation === 'create') {
if (error?.code === 'ContainerAlreadyExists') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: 'The specified container already exists',
description: "Use a unique value for 'Container Name' and try again",
});
}
if (error?.code === 'PublicAccessNotPermitted') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: 'Public access is not permitted on this storage account',
description: "Check the 'Access Level' and try again",
});
}
} else if (operation === 'delete') {
if (error?.code === 'ContainerNotFound') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "The required container doesn't match any existing one",
description:
"Double-check the value in the parameter 'Container to Delete' and try again",
});
}
} else if (operation === 'get') {
if (error?.code === 'ContainerNotFound') {
throw new NodeApiError(this.getNode(), error as JsonObject, {
message: "The required container doesn't match any existing one",
description: "Double-check the value in the parameter 'Container to Get' and try again",
});
}
} else if (operation === 'getAll') {
}
}
if (error) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: error.code,
description: error.message,
});
} else {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
parseXml: true,
});
}
}
return data;
}
export function getCanonicalizedHeadersString(requestOptions: IHttpRequestOptions): string {
let headersArray: Array<{ name: string; value: string }> = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
for (const [name, value] of Object.entries(requestOptions.headers!) as unknown as [
string,
string,
]) {
if (name.toLowerCase().startsWith(HeaderConstants.PREFIX_X_MS)) {
headersArray.push({ name, value });
}
}
headersArray.sort((a, b): number => {
return compareHeader(a.name.toLowerCase(), b.name.toLowerCase());
});
// Remove duplicate headers
headersArray = headersArray.filter((value, index, array) => {
if (index > 0 && value.name.toLowerCase() === array[index - 1].name.toLowerCase()) {
return false;
}
return true;
});
let canonicalizedHeadersStringToSign: string = '';
headersArray.forEach((header) => {
canonicalizedHeadersStringToSign += `${header.name
.toLowerCase()
.trimEnd()}:${header.value.trimStart()}\n`;
});
return canonicalizedHeadersStringToSign;
}
export function getCanonicalizedResourceString(
requestOptions: IHttpRequestOptions,
credentials: ICredentialDataDecryptedObject,
): string {
const path: string = new URL(requestOptions.baseURL + requestOptions.url).pathname || '/';
let canonicalizedResourceString = `/${credentials.account as string}${path}`;
if (requestOptions.qs && Object.keys(requestOptions.qs).length > 0) {
canonicalizedResourceString +=
'\n' +
Object.keys(requestOptions.qs)
.sort()
.map(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(key) => `${key.toLowerCase()}:${decodeURIComponent(requestOptions.qs![key] as string)}`,
)
.join('\n');
}
return canonicalizedResourceString;
}
export function parseHeaders(headers: IDataObject) {
const parseBooleanHeaders = [
HeaderConstants.X_MS_DELETE_TYPE_PERMANENT,
HeaderConstants.X_MS_INCREMENTAL_COPY,
HeaderConstants.X_MS_SERVER_ENCRYPTED,
HeaderConstants.X_MS_BLOB_SEALED,
HeaderConstants.X_MS_REQUEST_SERVER_ENCRYPTED,
HeaderConstants.X_MS_HAS_IMMUTABILITY_POLICY,
HeaderConstants.X_MS_HAS_LEGAL_HOLD,
];
const parseNumberHeaders = [
HeaderConstants.X_MS_TAG_COUNT,
HeaderConstants.CONTENT_LENGTH,
HeaderConstants.X_MS_BLOB_SEQUENCE_NUMBER,
HeaderConstants.X_MS_COPY_PROGRESS,
HeaderConstants.X_MS_BLOB_COMMITTED_BLOCK_COUNT,
];
const result: IDataObject = {};
const metadataKeys = Object.keys(headers).filter((x) =>
x.startsWith(HeaderConstants.PREFIX_X_MS_META),
);
for (const key in headers) {
if (metadataKeys.includes(key)) {
continue;
}
let newKey = key.startsWith(HeaderConstants.PREFIX_X_MS)
? camelCase(key.replace(HeaderConstants.PREFIX_X_MS, ''))
: camelCase(key);
newKey = newKey.replace('-', '');
const newValue = parseBooleanHeaders.includes(key)
? parseBooleans(headers[key] as string)
: parseNumberHeaders.includes(key)
? parseNumbers(headers[key] as string)
: headers[key];
result[newKey] = newValue;
}
if (metadataKeys.length > 0) {
result.metadata = {};
for (const key of metadataKeys) {
(result.metadata as IDataObject)[key.replace(HeaderConstants.PREFIX_X_MS_META, '')] =
headers[key];
}
}
return result;
}
export async function parseBlobList(
xml: string,
): Promise<{ blobs: IDataObject[]; maxResults?: number; nextMarker?: string }> {
const parser = new Parser({
explicitArray: false,
tagNameProcessors: [firstCharLowerCase, (name) => name.replace('-', '')],
valueProcessors: [
function (value, name) {
if (
[
'deleted',
'isCurrentVersion',
'serverEncrypted',
'incrementalCopy',
'accessTierInferred',
'isSealed',
'legalHold',
].includes(name)
) {
return parseBooleans(value);
} else if (
[
'maxResults',
'contentLength',
'blobSequenceNumber',
'remainingRetentionDays',
'tagCount',
'content-Length',
].includes(name)
) {
return parseNumbers(value);
}
return value;
},
],
});
const data = (await parser.parseStringPromise(xml)) as {
enumerationResults: {
blobs: { blob: IDataObject | IDataObject[] };
maxResults: number;
nextMarker: string;
prefix: string;
};
};
if (typeof data.enumerationResults.blobs !== 'object') {
// No items
return { blobs: [] };
}
if (!Array.isArray(data.enumerationResults.blobs.blob)) {
// Single item
data.enumerationResults.blobs.blob = [data.enumerationResults.blobs.blob];
}
for (const blob of data.enumerationResults.blobs.blob) {
if (blob.tags) {
if (!Array.isArray(((blob.tags as IDataObject).tagSet as IDataObject).tag)) {
((blob.tags as IDataObject).tagSet as IDataObject).tag = [
((blob.tags as IDataObject).tagSet as IDataObject).tag,
];
}
blob.tags = ((blob.tags as IDataObject).tagSet as IDataObject).tag;
}
}
for (const container of data.enumerationResults.blobs.blob) {
if (container.metadata === '') {
delete container.metadata;
}
if (container.orMetadata === '') {
delete container.orMetadata;
}
}
return {
blobs: data.enumerationResults.blobs.blob,
maxResults: data.enumerationResults.maxResults,
nextMarker: data.enumerationResults.nextMarker,
};
}
export async function parseContainerList(
xml: string,
): Promise<{ containers: IDataObject[]; maxResults?: number; nextMarker?: string }> {
const parser = new Parser({
explicitArray: false,
tagNameProcessors: [firstCharLowerCase, (name) => name.replace('-', '')],
valueProcessors: [
function (value, name) {
if (
[
'deleted',
'hasImmutabilityPolicy',
'hasLegalHold',
'preventEncryptionScopeOverride',
'isImmutableStorageWithVersioningEnabled',
].includes(name)
) {
return parseBooleans(value);
} else if (['maxResults', 'remainingRetentionDays'].includes(name)) {
return parseNumbers(value);
}
return value;
},
],
});
const data = (await parser.parseStringPromise(xml)) as {
enumerationResults: {
containers: { container: IDataObject | IDataObject[] };
maxResults: number;
nextMarker: string;
prefix: string;
};
};
if (typeof data.enumerationResults.containers !== 'object') {
// No items
return { containers: [] };
}
if (!Array.isArray(data.enumerationResults.containers.container)) {
// Single item
data.enumerationResults.containers.container = [data.enumerationResults.containers.container];
}
for (const container of data.enumerationResults.containers.container) {
if (container.metadata === '') {
delete container.metadata;
}
}
return {
containers: data.enumerationResults.containers.container,
maxResults: data.enumerationResults.maxResults,
nextMarker: data.enumerationResults.nextMarker,
};
}
export async function getBlobs(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const container = this.getNodeParameter('container') as INodeParameterResourceLocator;
let response: string;
const qs: IDataObject = {
restype: 'container',
comp: 'list',
};
if (paginationToken) {
qs.marker = paginationToken;
response = await azureStorageApiRequest.call(this, 'GET', `/${container.value}`, {}, qs);
} else {
qs.maxresults = 5000;
if (filter) {
qs.prefix = filter;
}
response = await azureStorageApiRequest.call(this, 'GET', `/${container.value}`, {}, qs);
}
const data = await parseBlobList(response);
const results: INodeListSearchItems[] = data.blobs
.map((c) => ({
name: c.name as string,
value: c.name as string,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return {
results,
paginationToken: data.nextMarker,
};
}
export async function getContainers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: string;
const qs: IDataObject = {
comp: 'list',
};
if (paginationToken) {
qs.marker = paginationToken;
response = await azureStorageApiRequest.call(this, 'GET', '/', {}, qs);
} else {
qs.maxresults = 5000;
if (filter) {
qs.prefix = filter;
}
response = await azureStorageApiRequest.call(this, 'GET', '/', {}, qs);
}
const data = await parseContainerList(response);
const results: INodeListSearchItems[] = data.containers
.map((c) => ({
name: c.name as string,
value: c.name as string,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return {
results,
paginationToken: data.nextMarker,
};
}

View file

@ -0,0 +1,14 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" width="200" height="200" >
<defs>
<linearGradient id="a" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b3b3b3"/>
<stop offset=".26" stop-color="#c1c1c1"/>
<stop offset="1" stop-color="#e6e6e6"/>
</linearGradient>
</defs>
<path d="M.5 5.79h17v9.48a.57.57 0 01-.57.57H1.07a.57.57 0 01-.57-.57V5.79z" fill="url(#a)"/>
<path d="M1.07 2.17h15.86a.57.57 0 01.57.57v3.05H.5V2.73a.57.57 0 01.57-.56z" fill="#37c2b1"/>
<path d="M2.81 6.89h12.37a.27.27 0 01.26.27v1.4a.27.27 0 01-.26.27H2.81a.27.27 0 01-.26-.27v-1.4a.27.27 0 01.26-.27z" fill="#fff"/>
<path d="M2.82 9.68h12.37a.27.27 0 01.26.27v1.41a.27.27 0 01-.26.27H2.82a.27.27 0 01-.26-.27V10a.27.27 0 01.26-.32z" fill="#37c2b1"/>
<path d="M2.82 12.5h12.37a.27.27 0 01.26.27v1.41a.27.27 0 01-.26.27H2.82a.27.27 0 01-.26-.27v-1.41a.27.27 0 01.26-.27z" fill="#258277"/>
</svg>

After

Width:  |  Height:  |  Size: 1,015 B

View file

@ -0,0 +1,14 @@
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" width="200" height="200" >
<defs>
<linearGradient id="a" x1="9" y1="15.83" x2="9" y2="5.79" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b3b3b3"/>
<stop offset=".26" stop-color="#c1c1c1"/>
<stop offset="1" stop-color="#e6e6e6"/>
</linearGradient>
</defs>
<path d="M.5 5.79h17v9.48a.57.57 0 01-.57.57H1.07a.57.57 0 01-.57-.57V5.79z" fill="url(#a)"/>
<path d="M1.07 2.17h15.86a.57.57 0 01.57.57v3.05H.5V2.73a.57.57 0 01.57-.56z" fill="#37c2b1"/>
<path d="M2.81 6.89h12.37a.27.27 0 01.26.27v1.4a.27.27 0 01-.26.27H2.81a.27.27 0 01-.26-.27v-1.4a.27.27 0 01.26-.27z" fill="#fff"/>
<path d="M2.82 9.68h12.37a.27.27 0 01.26.27v1.41a.27.27 0 01-.26.27H2.82a.27.27 0 01-.26-.27V10a.27.27 0 01.26-.32z" fill="#37c2b1"/>
<path d="M2.82 12.5h12.37a.27.27 0 01.26.27v1.41a.27.27 0 01-.26.27H2.82a.27.27 0 01-.26-.27v-1.41a.27.27 0 01.26-.27z" fill="#258277"/>
</svg>

After

Width:  |  Height:  |  Size: 1,015 B

View file

@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/*
* We need to imitate .Net culture-aware sorting, which is used in storage service.
* Below tables contain sort-keys for en-US culture.
*/
const table_lv0 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x71c, 0x0, 0x71f, 0x721,
0x723, 0x725, 0x0, 0x0, 0x0, 0x72d, 0x803, 0x0, 0x0, 0x733, 0x0, 0xd03, 0xd1a, 0xd1c, 0xd1e,
0xd20, 0xd22, 0xd24, 0xd26, 0xd28, 0xd2a, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe02, 0xe09, 0xe0a,
0xe1a, 0xe21, 0xe23, 0xe25, 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, 0xe70, 0xe7c, 0xe7e, 0xe89,
0xe8a, 0xe91, 0xe99, 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, 0x0, 0x0, 0x0, 0x743, 0x744, 0x748,
0xe02, 0xe09, 0xe0a, 0xe1a, 0xe21, 0xe23, 0xe25, 0xe2c, 0xe32, 0xe35, 0xe36, 0xe48, 0xe51, 0xe70,
0xe7c, 0xe7e, 0xe89, 0xe8a, 0xe91, 0xe99, 0xe9f, 0xea2, 0xea4, 0xea6, 0xea7, 0xea9, 0x0, 0x74c,
0x0, 0x750, 0x0,
]);
const table_lv2 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
]);
const table_lv4 = new Uint32Array([
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x8012, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8212, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
]);
function isLessThan(lhs: string, rhs: string): boolean {
const tables = [table_lv0, table_lv2, table_lv4];
let curr_level = 0;
let i = 0;
let j = 0;
while (curr_level < tables.length) {
if (curr_level === tables.length - 1 && i !== j) {
return i > j;
}
const weight1 = i < lhs.length ? tables[curr_level][lhs[i].charCodeAt(0)] : 0x1;
const weight2 = j < rhs.length ? tables[curr_level][rhs[j].charCodeAt(0)] : 0x1;
if (weight1 === 0x1 && weight2 === 0x1) {
i = 0;
j = 0;
++curr_level;
} else if (weight1 === weight2) {
++i;
++j;
} else if (weight1 === 0) {
++i;
} else if (weight2 === 0) {
++j;
} else {
return weight1 < weight2;
}
}
return false;
}
export function compareHeader(lhs: string, rhs: string): number {
if (isLessThan(lhs, rhs)) return -1;
return 1;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,600 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
INodeExecutionData,
INodeProperties,
ResourceMapperValue,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import {
handleErrorPostReceive,
HeaderConstants,
parseContainerList,
parseHeaders,
XMsVersion,
} from '../GenericFunctions';
export const containerOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['container'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'PUT',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["containerCreate"] }}',
headers: {
[HeaderConstants.X_MS_DATE]: '={{ new Date().toUTCString() }}',
[HeaderConstants.X_MS_VERSION]: XMsVersion,
},
},
output: {
postReceive: [
handleErrorPostReceive,
async function (
this: IExecuteSingleFunctions,
_data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
return [
{
json: parseHeaders(response.headers),
},
];
},
],
},
},
action: 'Create container',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'DELETE',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["container"] }}',
headers: {
[HeaderConstants.X_MS_DATE]: '={{ new Date().toUTCString() }}',
[HeaderConstants.X_MS_VERSION]: XMsVersion,
},
},
output: {
postReceive: [
handleErrorPostReceive,
async function (
this: IExecuteSingleFunctions,
_data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
return [
{
json: parseHeaders(response.headers),
},
];
},
],
},
},
action: 'Delete container',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve data for a specific container',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
qs: {
restype: 'container',
},
url: '=/{{ $parameter["container"] }}',
headers: {
[HeaderConstants.X_MS_DATE]: '={{ new Date().toUTCString() }}',
[HeaderConstants.X_MS_VERSION]: XMsVersion,
},
},
output: {
postReceive: [
handleErrorPostReceive,
async function (
this: IExecuteSingleFunctions,
_data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const { metadata, ...properties } = parseHeaders(response.headers);
const simplify = this.getNodeParameter('options.simplify', true) as boolean;
if (simplify) {
delete properties.contentLength;
delete properties.server;
delete properties.requestId;
delete properties.version;
delete properties.date;
delete properties.connection;
}
return [
{
json: {
name: (this.getNodeParameter('container') as ResourceMapperValue).value,
properties,
...(metadata ? { metadata: metadata as IDataObject } : {}),
},
},
];
},
],
},
},
action: 'Get container',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve a list of containers',
routing: {
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
qs: {
comp: 'list',
},
url: '/',
headers: {
[HeaderConstants.X_MS_DATE]: '={{ new Date().toUTCString() }}',
[HeaderConstants.X_MS_VERSION]: XMsVersion,
},
},
output: {
postReceive: [
handleErrorPostReceive,
async function (
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
return [
{
json: await parseContainerList(data[0].json as unknown as string),
},
];
},
],
},
},
action: 'Get many container',
},
],
default: 'getAll',
},
];
const createFields: INodeProperties[] = [
{
displayName: 'Container Name',
name: 'containerCreate',
default: '',
description: 'The name of the new container',
displayOptions: {
show: {
resource: ['container'],
operation: ['create'],
},
},
placeholder: 'e.g. mycontainer',
required: true,
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const container = this.getNodeParameter('containerCreate') as string;
if (container.length < 3 || container.length > 63) {
throw new NodeOperationError(
this.getNode(),
"'Container Name' must be from 3 through 63 characters long",
);
}
if (/[A-Z]/.test(container)) {
throw new NodeOperationError(
this.getNode(),
"All letters in 'Container Name' must be lowercase",
);
}
if (!/^[a-z0-9-]+$/.test(container)) {
throw new NodeOperationError(
this.getNode(),
"'Container Name' can contain only letters, numbers, and the hyphen/minus (-) character",
);
}
if (!/^[a-z0-9].*[a-z0-9]$/.test(container)) {
throw new NodeOperationError(
this.getNode(),
"'Container Name' must start or end with a letter or number",
);
}
if (/--/.test(container)) {
throw new NodeOperationError(
this.getNode(),
"Consecutive hyphens are not permitted in 'Container Name'",
);
}
return requestOptions;
},
],
},
},
type: 'string',
validateType: 'string',
},
{
displayName: 'Options',
name: 'options',
default: {},
displayOptions: {
show: {
resource: ['container'],
operation: ['create'],
},
},
options: [
{
displayName: 'Access Level',
name: 'accessLevel',
default: '',
options: [
{
name: 'Blob',
value: 'blob',
description:
"Specifies public read access for blobs. Blob data within this container can be read via anonymous request, but container data isn't available. Clients can't enumerate blobs within the container via anonymous request.",
},
{
name: 'Container',
value: 'container',
description:
"Specifies full public read access for container and blob data. Clients can enumerate blobs within the container via anonymous request, but they can't enumerate containers within the storage account.",
},
{
name: 'Private',
value: '',
description: 'Container data is private to the account owner',
},
],
routing: {
request: {
headers: {
[HeaderConstants.X_MS_BLOB_PUBLIC_ACCESS]: '={{ $value || undefined }}',
},
},
},
type: 'options',
validateType: 'options',
},
{
displayName: 'Metadata',
name: 'metadata',
default: [],
description: 'A name-value pair to associate with the container as metadata',
options: [
{
name: 'metadataValues',
displayName: 'Metadata',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
default: '',
description:
'Names must adhere to the naming rules for <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/">C# identifiers</a>',
type: 'string',
},
{
displayName: 'Field Value',
name: 'fieldValue',
default: '',
type: 'string',
},
],
},
],
placeholder: 'Add metadata',
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
requestOptions.headers ??= {};
const metadata = this.getNodeParameter('options.metadata') as IDataObject;
for (const data of metadata.metadataValues as IDataObject[]) {
requestOptions.headers[
`${HeaderConstants.PREFIX_X_MS_META}${data.fieldName as string}`
] = data.fieldValue as string;
}
return requestOptions;
},
],
},
},
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
},
],
placeholder: 'Add option',
type: 'collection',
},
];
const deleteFields: INodeProperties[] = [
{
displayName: 'Container',
name: 'container',
default: {
mode: 'list',
value: '',
},
description: 'Select the container to delete',
displayOptions: {
show: {
resource: ['container'],
operation: ['delete'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
];
const getFields: INodeProperties[] = [
{
displayName: 'Container',
name: 'container',
default: {
mode: 'list',
value: '',
},
description: 'Select the container to get',
displayOptions: {
show: {
resource: ['container'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'getContainers',
searchable: true,
},
},
{
displayName: 'By Name',
name: 'id',
placeholder: 'e.g. mycontainer',
type: 'string',
},
],
required: true,
type: 'resourceLocator',
},
{
displayName: 'Options',
name: 'options',
default: {},
displayOptions: {
show: {
resource: ['container'],
operation: ['get'],
},
},
options: [
{
displayName: 'Simplify',
name: 'simplify',
type: 'boolean',
default: true,
description:
'Whether to return a simplified version of the response instead of the raw data',
},
],
placeholder: 'Add option',
type: 'collection',
},
];
const getAllFields: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
},
},
routing: {
send: {
paginate: '={{ $value }}',
},
operations: {
async pagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
let executions: INodeExecutionData[] = [];
let marker: string | undefined = undefined;
requestOptions.options.qs ??= {};
do {
requestOptions.options.qs.marker = marker;
const responseData = await this.makeRoutingRequest(requestOptions);
marker = responseData[0].json.nextMarker as string | undefined;
executions = executions.concat(
(responseData[0].json.containers as IDataObject[]).map((item) => ({ json: item })),
);
} while (marker);
return executions;
},
},
},
type: 'boolean',
},
{
displayName: 'Limit',
name: 'limit',
default: 50,
description: 'Max number of results to return',
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
send: {
property: 'maxresults',
type: 'query',
value: '={{ $value }}',
},
output: {
postReceive: [
async function (
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
return (data[0].json.containers as IDataObject[]).map((item) => ({ json: item }));
},
],
},
},
type: 'number',
typeOptions: {
minValue: 1,
},
validateType: 'number',
},
{
displayName: 'Options',
name: 'options',
default: {},
displayOptions: {
show: {
resource: ['container'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
default: [],
description: 'The fields to add to the output',
options: [
{
name: 'Metadata',
value: 'metadata',
},
{
name: 'Deleted',
value: 'deleted',
},
{
name: 'System',
value: 'system',
},
],
routing: {
send: {
property: 'include',
type: 'query',
value: '={{ $value.join(",") || undefined }}',
},
},
type: 'multiOptions',
},
{
displayName: 'Filter',
name: 'filter',
default: '',
description:
'Filters the results to return only containers with a name that begins with the specified prefix',
placeholder: 'e.g. mycontainer',
routing: {
send: {
property: 'prefix',
type: 'query',
value: '={{ $value ? $value : undefined }}',
},
},
type: 'string',
validateType: 'string',
},
],
placeholder: 'Add option',
type: 'collection',
},
];
export const containerFields: INodeProperties[] = [
...createFields,
...deleteFields,
...getFields,
...getAllFields,
];

View file

@ -0,0 +1,2 @@
export * from './BlobDescription';
export * from './ContainerDescription';

View file

@ -0,0 +1,72 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/blob_create.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should create blob', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'put',
path: '/mycontainer/myblob',
statusCode: 201,
requestHeaders: {
'x-ms-access-tier': 'Hot',
'x-ms-blob-type': 'BlockBlob',
'x-ms-blob-cache-control': 'no-cache',
'x-ms-content-crc64': '3EDB64E77CB16A4C',
'x-ms-blob-content-encoding': 'utf8',
'x-ms-blob-content-language': 'en-US',
'x-ms-blob-content-md5': 'b97f46db5f3be7709d942eefe30e5b45',
'x-ms-blob-content-type': 'application/json',
'x-ms-encryption-context': 'context',
'x-ms-encryption-scope': 'encryptionScope',
'x-ms-expiry-option': 'Absolute',
'x-ms-blob-content-disposition': 'attachment; filename="file.json"',
'x-ms-immutability-policy-until-date': 'Wed, 01 Jan 2025 00:00:00 -0500',
'x-ms-immutability-policy-mode': 'unlocked',
'x-ms-lease-id': 'leaseId123',
'x-ms-legal-hold': 'true',
'x-ms-meta-key1': 'value1',
},
responseBody: '',
responseHeaders: {
server: 'Azurite-Blob/3.33.0',
etag: '"0x22769D26D3F3740"',
'last-modified': 'Thu, 23 Jan 2025 17:53:23 GMT',
'content-md5': 'aWQGHD8kGQd5ZtEN/S1/aw==',
'x-ms-request-id': '75b87ee3-a7f7-468d-b7d1-e7e7b3173dab',
'x-ms-version': '2025-01-05',
date: 'Thu, 23 Jan 2025 17:53:23 GMT',
'x-ms-request-server-encrypted': 'true',
'keep-alive': 'timeout=5',
'content-length': '0',
'x-ms-version-id': 'Thu, 23 Jan 2025 17:53:23 GMT',
'access-control-allow-credentials': 'access-control-allow-credentials',
'access-control-allow-origin': 'access-control-allow-origin',
'access-control-expose-headers': 'access-control-expose-headers',
'x-ms-content-crc64': 'x-ms-content-crc64',
'x-ms-encryption-key-sha256': 'x-ms-encryption-key-sha256',
'x-ms-encryption-scope': 'x-ms-encryption-scope',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,41 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/blob_delete.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should delete blob', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'delete',
path: '/mycontainer/myblob',
statusCode: 202,
responseBody: '',
responseHeaders: {
'x-ms-request-id': '75b87ee3-a7f7-468d-b7d1-e7e7b3173dab',
'x-ms-version': '2025-01-05',
date: 'Thu, 23 Jan 2025 17:53:23 GMT',
'x-ms-delete-type-permanent': 'true',
'x-ms-client-request-id': 'x-ms-client-request-id',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,93 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/blob_get.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get blob', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer/myblob',
statusCode: 200,
responseBody: {
type: 'Buffer',
data: [
123, 10, 34, 100, 97, 116, 97, 34, 58, 123, 10, 34, 109, 121, 95, 102, 105, 101,
108, 100, 95, 49, 34, 58, 34, 118, 97, 108, 117, 101, 34, 44, 10, 34, 109, 121, 95,
102, 105, 101, 108, 100, 95, 50, 34, 58, 49, 10, 125, 10, 125,
],
},
responseHeaders: {
'x-ms-request-id': '75b87ee3-a7f7-468d-b7d1-e7e7b3173dab',
'x-ms-version': '2025-01-05',
date: 'Thu, 23 Jan 2025 17:53:23 GMT',
'x-ms-client-request-id': 'x-ms-client-request-id',
'last-modified': 'last-modified',
'x-ms-creation-time': 'x-ms-creation-time',
'x-ms-tag-count': 'x-ms-tag-count',
'content-type': 'application/json',
'content-range': 'content-range',
etag: '"0x22769D26D3F3740"',
'content-md5': 'content-md5',
'x-ms-content-crc64': 'x-ms-content-crc64',
'content-encoding': 'content-encoding',
'content-language': 'content-language',
'cache-control': 'cache-control',
'content-disposition': 'attachment; filename="file.json"',
'x-ms-blob-sequence-number': 'x-ms-blob-sequence-number',
'x-ms-blob-type': 'x-ms-blob-type',
'x-ms-copy-completion-time': 'x-ms-copy-completion-time',
'x-ms-copy-status-description': 'x-ms-copy-status-description',
'x-ms-copy-id': 'x-ms-copy-id',
'x-ms-copy-progress': 'x-ms-copy-progress',
'x-ms-copy-source': 'x-ms-copy-source',
'x-ms-copy-status': 'x-ms-copy-status',
'x-ms-incremental-copy': 'x-ms-incremental-copy',
'x-ms-lease-duration': 'x-ms-lease-duration',
'x-ms-lease-state': 'x-ms-lease-state',
'x-ms-lease-status': 'x-ms-lease-status',
'accept-ranges': 'accept-ranges',
'access-control-allow-origin': 'access-control-allow-origin',
'access-control-expose-headers': 'access-control-expose-headers',
vary: 'vary',
'access-control-allow-credentials': 'access-control-allow-credentials',
'x-ms-blob-committed-block-count': 'x-ms-blob-committed-block-count',
'x-ms-server-encrypted': 'x-ms-server-encrypted',
'x-ms-encryption-key-sha256': 'x-ms-encryption-key-sha256',
'x-ms-encryption-context': 'x-ms-encryption-context',
'x-ms-encryption-scope': 'x-ms-encryption-scope',
'x-ms-blob-content-md5': 'x-ms-blob-content-md5',
'x-ms-last-access-time': 'x-ms-last-access-time',
'x-ms-blob-sealed': 'x-ms-blob-sealed',
'x-ms-immutability-policy-until-date': 'x-ms-immutability-policy-until-date',
'x-ms-immutability-policy-mode': 'x-ms-immutability-policy-mode',
'x-ms-legal-hold': 'x-ms-legal-hold',
'x-ms-owner': 'x-ms-owner',
'x-ms-group': 'x-ms-group',
'x-ms-permissions': 'x-ms-permissions',
'x-ms-acl': 'x-ms-acl',
'x-ms-resource-type': 'x-ms-resource-type',
'x-ms-meta-key1': 'value1',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,42 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/blob_getAll.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get all blobs', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer?restype=container&comp=list',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net" 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>',
},
{
method: 'get',
path: '/mycontainer?restype=container&comp=list&marker=myblob2',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net" 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></NextMarker></EnumerationResults>',
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,37 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = [
'nodes/Microsoft/Storage/test/workflows/blob_getAllLimitOptions.workflow.json',
];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get all blobs with limit and options', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer?restype=container&comp=list&maxresults=1&include=copy%2Cdeleted%2Cdeletedwithversions%2Cimmutabilitypolicy%2Cmetadata%2Clegalhold%2Cversions%2Cuncommittedblobs%2Ctags%2Csnapshots%2Cpermissions&showonly=deleted%2Cfiles%2Cdirectories',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net" 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>',
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,44 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/container_create.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should create container', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'put',
path: '/mycontainer?restype=container',
statusCode: 201,
requestHeaders: { 'x-ms-blob-public-access': 'blob', 'x-ms-meta-key1': 'value1' },
responseBody: '',
responseHeaders: {
etag: '"0x22769D26D3F3740"',
'last-modified': 'Thu, 23 Jan 2025 17:53:23 GMT',
'x-ms-request-id': '75b87ee3-a7f7-468d-b7d1-e7e7b3173dab',
'x-ms-version': '2025-01-05',
date: 'Thu, 23 Jan 2025 17:53:23 GMT',
'x-ms-request-server-encrypted': 'true',
'x-ms-client-request-id': 'client-request-id-123',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,41 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/container_delete.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should delete container', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'delete',
path: '/mycontainer?restype=container',
statusCode: 202,
responseBody: '',
responseHeaders: {
'content-length': '0',
server: 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0',
'x-ms-request-id': 'ca3a8907-601e-0050-1929-723410000000',
'x-ms-version': '2020-10-02',
date: 'Wed, 29 Jan 2025 08:38:21 GMT',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,50 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/container_get.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get container', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer?restype=container',
statusCode: 200,
responseBody: '',
responseHeaders: {
'content-length': '0',
'last-modified': 'Tue, 28 Jan 2025 16:40:21 GMT',
etag: '"0x8DD3FBA74CF3620"',
server: 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0',
'x-ms-request-id': '49edb268-b01e-0053-6e29-72d574000000',
'x-ms-version': '2020-10-02',
'x-ms-lease-status': 'unlocked',
'x-ms-lease-state': 'available',
'x-ms-has-immutability-policy': 'false',
'x-ms-has-legal-hold': 'false',
date: 'Wed, 29 Jan 2025 08:43:08 GMT',
'x-ms-meta-key1': 'field1',
'x-ms-blob-public-access': 'blob',
'x-ms-lease-duration': 'infinite',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,42 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/container_getAll.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get all containers', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/?comp=list',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net/"><Prefix>mycontainer</Prefix><MaxResults>1</MaxResults><Containers><Container><Name>mycontainer1</Name><Deleted>true</Deleted><Version>01DB7228F6BEE6E7</Version><Properties><Last-Modified>Wed, 29 Jan 2025 08:37:00 GMT</Last-Modified><Etag>"0x8DD40401935032C"</Etag><LeaseStatus>unlocked</LeaseStatus><LeaseState>expired</LeaseState><HasImmutabilityPolicy>false</HasImmutabilityPolicy><HasLegalHold>false</HasLegalHold><ImmutableStorageWithVersioningEnabled>false</ImmutableStorageWithVersioningEnabled><DeletedTime>Wed, 29 Jan 2025 08:38:21 GMT</DeletedTime><RemainingRetentionDays>7</RemainingRetentionDays></Properties><Metadata><key1>value1</key1></Metadata></Container></Containers><NextMarker>mycontainer2</NextMarker></EnumerationResults>',
},
{
method: 'get',
path: '/?comp=list&marker=mycontainer2',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net/"><Prefix>mycontainer</Prefix><MaxResults>1</MaxResults><Containers><Container><Name>mycontainer1</Name><Deleted>true</Deleted><Version>01DB7228F6BEE6E7</Version><Properties><Last-Modified>Wed, 29 Jan 2025 08:37:00 GMT</Last-Modified><Etag>"0x8DD40401935032C"</Etag><LeaseStatus>unlocked</LeaseStatus><LeaseState>expired</LeaseState><HasImmutabilityPolicy>false</HasImmutabilityPolicy><HasLegalHold>false</HasLegalHold><ImmutableStorageWithVersioningEnabled>false</ImmutableStorageWithVersioningEnabled><DeletedTime>Wed, 29 Jan 2025 08:38:21 GMT</DeletedTime><RemainingRetentionDays>7</RemainingRetentionDays></Properties><Metadata><key1>value1</key1></Metadata></Container></Containers><NextMarker></NextMarker></EnumerationResults>',
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,37 @@
import nock from 'nock';
import { equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = [
'nodes/Microsoft/Storage/test/workflows/container_getAllLimitOptions.workflow.json',
];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should get all containers with limit and options', () => {
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/?comp=list&maxresults=1&include=metadata%2Cdeleted%2Csystem&prefix=mycontainer',
statusCode: 200,
responseBody:
'<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net/"><Prefix>mycontainer</Prefix><MaxResults>1</MaxResults><Containers><Container><Name>mycontainer1</Name><Deleted>true</Deleted><Version>01DB7228F6BEE6E7</Version><Properties><Last-Modified>Wed, 29 Jan 2025 08:37:00 GMT</Last-Modified><Etag>"0x8DD40401935032C"</Etag><LeaseStatus>unlocked</LeaseStatus><LeaseState>expired</LeaseState><HasImmutabilityPolicy>false</HasImmutabilityPolicy><HasLegalHold>false</HasLegalHold><ImmutableStorageWithVersioningEnabled>false</ImmutableStorageWithVersioningEnabled><DeletedTime>Wed, 29 Jan 2025 08:38:21 GMT</DeletedTime><RemainingRetentionDays>7</RemainingRetentionDays></Properties><Metadata><key1>value1</key1></Metadata></Container></Containers><NextMarker>mycontainer2</NextMarker></EnumerationResults>',
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,86 @@
import type {
ICredentialDataDecryptedObject,
IDataObject,
IHttpRequestOptions,
} from 'n8n-workflow';
import nock from 'nock';
import { CredentialsHelper, equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/credentials_oauth2.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should use correct oauth2 credentials', () => {
beforeAll(() => {
nock.disableNetConnect();
jest
.spyOn(CredentialsHelper.prototype, 'authenticate')
.mockImplementation(
async (
credentials: ICredentialDataDecryptedObject,
typeName: string,
requestParams: IHttpRequestOptions,
): Promise<IHttpRequestOptions> => {
if (typeName === 'azureStorageOAuth2Api') {
return {
...requestParams,
headers: {
authorization: `bearer ${(credentials.oauthTokenData as IDataObject).access_token as string}`,
},
};
} else {
return requestParams;
}
},
);
});
afterAll(() => {
nock.restore();
jest.restoreAllMocks();
});
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer?restype=container',
statusCode: 200,
requestHeaders: { authorization: 'bearer ACCESSTOKEN' },
responseBody: '',
responseHeaders: {
'content-length': '0',
'last-modified': 'Tue, 28 Jan 2025 16:40:21 GMT',
etag: '"0x8DD3FBA74CF3620"',
server: 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0',
'x-ms-request-id': '49edb268-b01e-0053-6e29-72d574000000',
'x-ms-version': '2020-10-02',
'x-ms-lease-status': 'unlocked',
'x-ms-lease-state': 'available',
'x-ms-has-immutability-policy': 'false',
'x-ms-has-legal-hold': 'false',
date: 'Wed, 29 Jan 2025 08:43:08 GMT',
'x-ms-meta-key1': 'field1',
'x-ms-blob-public-access': 'blob',
'x-ms-lease-duration': 'infinite',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
});

View file

@ -0,0 +1,158 @@
import type { ICredentialDataDecryptedObject, IHttpRequestOptions } from 'n8n-workflow';
import nock from 'nock';
import { CredentialsHelper, equalityTest, setup, workflowToTests } from '@test/nodes/Helpers';
import { AzureStorageSharedKeyApi } from '../../../../../credentials/AzureStorageSharedKeyApi.credentials';
import { FAKE_CREDENTIALS_DATA } from '../../../../../test/nodes/FakeCredentialsMap';
describe('Azure Storage Node', () => {
const workflows = ['nodes/Microsoft/Storage/test/workflows/credentials_sharedKey.workflow.json'];
const workflowTests = workflowToTests(workflows);
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('should use correct shared key credentials', () => {
beforeAll(() => {
nock.disableNetConnect();
jest
.spyOn(CredentialsHelper.prototype, 'authenticate')
.mockImplementation(
async (
_credentials: ICredentialDataDecryptedObject,
typeName: string,
requestParams: IHttpRequestOptions,
): Promise<IHttpRequestOptions> => {
if (typeName === 'azureStorageSharedKeyApi') {
return {
...requestParams,
headers: {
authorization:
'SharedKey Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
},
};
} else {
return requestParams;
}
},
);
});
afterAll(() => {
nock.restore();
jest.restoreAllMocks();
});
const nodeTypes = setup(workflowTests);
for (const workflow of workflowTests) {
workflow.nock = {
baseUrl: 'https://myaccount.blob.core.windows.net',
mocks: [
{
method: 'get',
path: '/mycontainer?restype=container',
statusCode: 200,
requestHeaders: {
authorization:
'SharedKey Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
},
responseBody: '',
responseHeaders: {
'content-length': '0',
'last-modified': 'Tue, 28 Jan 2025 16:40:21 GMT',
etag: '"0x8DD3FBA74CF3620"',
server: 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0',
'x-ms-request-id': '49edb268-b01e-0053-6e29-72d574000000',
'x-ms-version': '2020-10-02',
'x-ms-lease-status': 'unlocked',
'x-ms-lease-state': 'available',
'x-ms-has-immutability-policy': 'false',
'x-ms-has-legal-hold': 'false',
date: 'Wed, 29 Jan 2025 08:43:08 GMT',
'x-ms-meta-key1': 'field1',
'x-ms-blob-public-access': 'blob',
'x-ms-lease-duration': 'infinite',
},
},
],
};
test(workflow.description, async () => await equalityTest(workflow, nodeTypes));
}
});
describe('authenticate', () => {
const azureStorageSharedKeyApi = new AzureStorageSharedKeyApi();
it('should remove undefined query parameters and headers', async () => {
const credentials: ICredentialDataDecryptedObject = {
account: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.account,
key: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.key,
};
const requestOptions: IHttpRequestOptions = {
url: `${FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.baseUrl}/mycontainer`,
qs: { restype: 'container', query1: undefined },
headers: {
'Content-Length': undefined,
},
method: 'GET',
};
const result = await azureStorageSharedKeyApi.authenticate(credentials, requestOptions);
expect(result.qs).toEqual({ restype: 'container' });
expect(result.headers).not.toHaveProperty('Content-Length');
});
it('should default method to GET if not provided', async () => {
const credentials: ICredentialDataDecryptedObject = {
account: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.account,
key: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.key,
};
const requestOptions: IHttpRequestOptions = {
url: `${FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.baseUrl}/mycontainer`,
qs: { restype: 'container' },
headers: {
'Content-Length': undefined,
},
};
const result = await azureStorageSharedKeyApi.authenticate(credentials, requestOptions);
expect(result.method).toBe('GET');
});
it('should generate a valid authorization header', async () => {
const credentials: ICredentialDataDecryptedObject = {
account: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.account,
key: FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.key,
};
const requestOptions: IHttpRequestOptions = {
url: `${FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi.baseUrl}/mycontainer/myblob`,
qs: { param1: 'value1' },
headers: {
'x-ms-date': 'Thu, 27 Feb 2025 11:05:49 GMT',
'x-ms-version': '2021-12-02',
'x-ms-blob-content-language': 'en-EN',
'x-ms-blob-content-type': 'image/jpeg',
'x-ms-expiry-option': 'Absolute',
'x-ms-blob-type': 'BlockBlob',
'x-ms-blob-content-disposition': 'attachment; filename="image.jpg"',
'x-ms-meta-key1': 'value1',
'x-ms-tags': 'tag1=value1',
},
method: 'PUT',
};
const result = await azureStorageSharedKeyApi.authenticate(credentials, requestOptions);
expect(result.headers?.authorization).toBe(
'SharedKey devstoreaccount1:6sSQ3N4yNFQynBs/iLptIRPS5DQeaFBocW+dyYbAdOI=',
);
});
});
});

View file

@ -0,0 +1,123 @@
import type { ILoadOptionsFunctions, INodeParameterResourceLocator } from 'n8n-workflow';
import nock from 'nock';
import { FAKE_CREDENTIALS_DATA } from '../../../../../test/nodes/FakeCredentialsMap';
import { AzureStorage } from '../../AzureStorage.node';
import { XMsVersion } from '../../GenericFunctions';
describe('Azure Storage Node', () => {
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('List search', () => {
it('should list search blobs', async () => {
const mockResponse =
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net" 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 = jest.fn().mockReturnValue(mockResponse);
const mockGetNodeParameter = jest.fn((parameterName, _fallbackValue, _options) => {
if (parameterName === 'authentication') {
return 'sharedKey';
}
if (parameterName === 'container') {
return {
value: 'mycontainer',
} as INodeParameterResourceLocator;
}
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error('Unknown parameter');
});
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
if (type === 'azureStorageSharedKeyApi') {
return FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi;
}
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error('Unknown credentials');
});
const mockContext = {
getCredentials: mockGetCredentials,
getNodeParameter: mockGetNodeParameter,
helpers: {
requestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
const node = new AzureStorage();
const listSearchResult = await node.methods.listSearch.getBlobs.call(mockContext);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('azureStorageSharedKeyApi', {
method: 'GET',
url: 'https://myaccount.blob.core.windows.net/mycontainer',
headers: {
'x-ms-date': 'Wed, 01 Jan 2025 00:00:00 GMT',
'x-ms-version': XMsVersion,
},
qs: {
comp: 'list',
maxresults: 5000,
restype: 'container',
},
body: {},
});
expect(listSearchResult).toEqual({
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
results: [{ name: 'myblob1', value: 'myblob1' }],
paginationToken: 'myblob2',
});
});
it('should list search containers', async () => {
const mockResponse =
'<?xml version="1.0" encoding="utf-8"?><EnumerationResults ServiceEndpoint="https://myaccount.blob.core.windows.net/"><Prefix>mycontainer</Prefix><MaxResults>1</MaxResults><Containers><Container><Name>mycontainer1</Name><Deleted>true</Deleted><Version>01DB7228F6BEE6E7</Version><Properties><Last-Modified>Wed, 29 Jan 2025 08:37:00 GMT</Last-Modified><Etag>"0x8DD40401935032C"</Etag><LeaseStatus>unlocked</LeaseStatus><LeaseState>expired</LeaseState><HasImmutabilityPolicy>false</HasImmutabilityPolicy><HasLegalHold>false</HasLegalHold><ImmutableStorageWithVersioningEnabled>false</ImmutableStorageWithVersioningEnabled><DeletedTime>Wed, 29 Jan 2025 08:38:21 GMT</DeletedTime><RemainingRetentionDays>7</RemainingRetentionDays></Properties><Metadata><key1>value1</key1></Metadata></Container></Containers><NextMarker>mycontainer2</NextMarker></EnumerationResults>';
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
const mockGetNodeParameter = jest.fn((parameterName, _fallbackValue, _options) => {
if (parameterName === 'authentication') {
return 'sharedKey';
}
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error('Unknown parameter');
});
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
if (type === 'azureStorageSharedKeyApi') {
return FAKE_CREDENTIALS_DATA.azureStorageSharedKeyApi;
}
// eslint-disable-next-line n8n-local-rules/no-plain-errors
throw new Error('Unknown credentials');
});
const mockContext = {
getCredentials: mockGetCredentials,
getNodeParameter: mockGetNodeParameter,
helpers: {
requestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
const node = new AzureStorage();
const listSearchResult = await node.methods.listSearch.getContainers.call(mockContext);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('azureStorageSharedKeyApi', {
method: 'GET',
url: 'https://myaccount.blob.core.windows.net/',
headers: {
'x-ms-date': 'Wed, 01 Jan 2025 00:00:00 GMT',
'x-ms-version': XMsVersion,
},
qs: {
comp: 'list',
maxresults: 5000,
},
body: {},
});
expect(listSearchResult).toEqual({
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
results: [{ name: 'mycontainer1', value: 'mycontainer1' }],
paginationToken: 'mycontainer2',
});
});
});
});

View file

@ -0,0 +1,109 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "blob",
"operation": "create",
"container": { "__rl": true, "value": "mycontainer", "mode": "list" },
"blobCreate": "myblob",
"options": {
"accessTier": "Hot",
"blobType": "BlockBlob",
"cacheControl": "no-cache",
"contentCrc64": "3EDB64E77CB16A4C",
"contentEncoding": "utf8",
"contentLanguage": "en-US",
"contentMd5": "b97f46db5f3be7709d942eefe30e5b45",
"contentType": "application/json",
"encryptionContext": "context",
"encryptionScope": "encryptionScope",
"expiryOption": "Absolute",
"filename": "file.json",
"immutabilityPolicyUntilDate": "2025-01-01T00:00:00",
"immutabilityPolicyMode": "unlocked",
"leaseId": "leaseId123",
"legalHold": true,
"metadata": { "metadataValues": [{ "fieldName": "key1", "fieldValue": "value1" }] },
"origin": "http://contoso.com",
"tags": { "tagValues": [{ "tagName": "tag1", "tagValue": "value1" }] }
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [660, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
},
{
"parameters": {
"mode": "jsonToBinary",
"convertAllData": false,
"options": { "useRawData": true }
},
"name": "Move Binary Data",
"type": "n8n-nodes-base.moveBinaryData",
"typeVersion": 1,
"position": [440, 0],
"id": "221e00ed-8c9b-4313-a4d6-b87c64b09d80"
},
{
"parameters": {
"mode": "raw",
"jsonOutput": "{\n \"data\": {\n \"my_field_1\": \"value\",\n \"my_field_2\": 1\n }\n}\n",
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [220, 0],
"id": "6c03903b-0177-4e94-b1ad-4f6ad9e84f62",
"name": "Edit Fields"
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Edit Fields", "type": "main", "index": 0 }]]
},
"Move Binary Data": { "main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]] },
"Edit Fields": { "main": [[{ "node": "Move Binary Data", "type": "main", "index": 0 }]] }
},
"pinData": {
"Azure Storage": [
{
"json": {
"etag": "\"0x22769D26D3F3740\"",
"lastModified": "Thu, 23 Jan 2025 17:53:23 GMT",
"contentMd5": "aWQGHD8kGQd5ZtEN/S1/aw==",
"requestId": "75b87ee3-a7f7-468d-b7d1-e7e7b3173dab",
"version": "2025-01-05",
"date": "Thu, 23 Jan 2025 17:53:23 GMT",
"requestServerEncrypted": true,
"accessControlAllowCredentials": "access-control-allow-credentials",
"accessControlAllowOrigin": "access-control-allow-origin",
"accessControlExposeHeaders": "access-control-expose-headers",
"contentCrc64": "x-ms-content-crc64",
"contentLength": 0,
"encryptionKeySha256": "x-ms-encryption-key-sha256",
"encryptionScope": "x-ms-encryption-scope",
"versionId": "Thu, 23 Jan 2025 17:53:23 GMT",
"keepAlive": "timeout=5",
"server": "Azurite-Blob/3.33.0"
}
}
]
}
}

View file

@ -0,0 +1,51 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "blob",
"operation": "delete",
"container": { "__rl": true, "mode": "id", "value": "mycontainer" },
"blob": { "__rl": true, "mode": "id", "value": "myblob" },
"options": { "leaseId": "leaseId123" },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [660, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"requestId": "75b87ee3-a7f7-468d-b7d1-e7e7b3173dab",
"version": "2025-01-05",
"date": "Thu, 23 Jan 2025 17:53:23 GMT",
"deleteTypePermanent": true,
"clientRequestId": "x-ms-client-request-id"
}
}
]
}
}

View file

@ -0,0 +1,114 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "blob",
"operation": "get",
"container": { "__rl": true, "value": "mycontainer", "mode": "list" },
"blob": { "__rl": true, "value": "myblob", "mode": "list" },
"options": {
"leaseId": "leaseId123",
"origin": "origin123",
"simplify": false,
"upn": true
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [660, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"binary": {
"data": {
"fileExtension": "json",
"fileName": "file.json",
"fileSize": "51 B",
"fileType": "json",
"mimeType": "application/json"
}
},
"json": {
"name": "myblob",
"properties": {
"clientRequestId": "x-ms-client-request-id",
"lastModified": "last-modified",
"creationTime": "x-ms-creation-time",
"tagCount": "x-ms-tag-count",
"contentType": "application/json",
"contentRange": "content-range",
"etag": "\"0x22769D26D3F3740\"",
"contentMd5": "content-md5",
"contentCrc64": "x-ms-content-crc64",
"contentEncoding": "content-encoding",
"contentLanguage": "content-language",
"cacheControl": "cache-control",
"contentDisposition": "attachment; filename=\"file.json\"",
"blobSequenceNumber": "x-ms-blob-sequence-number",
"blobType": "x-ms-blob-type",
"copyCompletionTime": "x-ms-copy-completion-time",
"copyStatusDescription": "x-ms-copy-status-description",
"copyId": "x-ms-copy-id",
"copyProgress": "x-ms-copy-progress",
"copySource": "x-ms-copy-source",
"copyStatus": "x-ms-copy-status",
"incrementalCopy": "x-ms-incremental-copy",
"leaseDuration": "x-ms-lease-duration",
"leaseState": "x-ms-lease-state",
"leaseStatus": "x-ms-lease-status",
"accessControlAllowOrigin": "access-control-allow-origin",
"accessControlExposeHeaders": "access-control-expose-headers",
"vary": "vary",
"accessControlAllowCredentials": "access-control-allow-credentials",
"blobCommittedBlockCount": "x-ms-blob-committed-block-count",
"serverEncrypted": "x-ms-server-encrypted",
"encryptionKeySha256": "x-ms-encryption-key-sha256",
"encryptionContext": "x-ms-encryption-context",
"encryptionScope": "x-ms-encryption-scope",
"blobContentMd5": "x-ms-blob-content-md5",
"lastAccessTime": "x-ms-last-access-time",
"blobSealed": "x-ms-blob-sealed",
"immutabilityPolicyUntilDate": "x-ms-immutability-policy-until-date",
"immutabilityPolicyMode": "x-ms-immutability-policy-mode",
"legalHold": "x-ms-legal-hold",
"owner": "x-ms-owner",
"group": "x-ms-group",
"permissions": "x-ms-permissions",
"acl": "x-ms-acl",
"resourceType": "x-ms-resource-type",
"acceptRanges": "accept-ranges",
"date": "Thu, 23 Jan 2025 17:53:23 GMT",
"requestId": "75b87ee3-a7f7-468d-b7d1-e7e7b3173dab",
"version": "2025-01-05"
},
"metadata": { "key1": "value1" }
}
}
]
}
}

View file

@ -0,0 +1,82 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "blob",
"operation": "getAll",
"container": { "__rl": true, "value": "mycontainer", "mode": "list" },
"returnAll": true,
"options": { "simplify": false },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [660, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "myblob1",
"properties": {
"creationTime": "Wed, 22 Jan 2025 18:53:15 GMT",
"lastModified": "Wed, 22 Jan 2025 18:53:15 GMT",
"etag": "0x1F8268B228AA730",
"contentLength": 37,
"contentType": "application/json",
"contentMD5": "aWQGHD8kGQd5ZtEN/S1/aw==",
"blobType": "BlockBlob",
"leaseStatus": "unlocked",
"leaseState": "available",
"serverEncrypted": true,
"accessTier": "Hot",
"accessTierInferred": true,
"accessTierChangeTime": "Wed, 22 Jan 2025 18:53:15 GMT"
}
}
},
{
"json": {
"name": "myblob1",
"properties": {
"creationTime": "Wed, 22 Jan 2025 18:53:15 GMT",
"lastModified": "Wed, 22 Jan 2025 18:53:15 GMT",
"etag": "0x1F8268B228AA730",
"contentLength": 37,
"contentType": "application/json",
"contentMD5": "aWQGHD8kGQd5ZtEN/S1/aw==",
"blobType": "BlockBlob",
"leaseStatus": "unlocked",
"leaseState": "available",
"serverEncrypted": true,
"accessTier": "Hot",
"accessTierInferred": true,
"accessTierChangeTime": "Wed, 22 Jan 2025 18:53:15 GMT"
}
}
}
]
}
}

View file

@ -0,0 +1,76 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "blob",
"operation": "getAll",
"container": { "__rl": true, "value": "mycontainer", "mode": "list" },
"limit": 1,
"options": {
"fields": [
"copy",
"deleted",
"deletedwithversions",
"immutabilitypolicy",
"metadata",
"legalhold",
"versions",
"uncommittedblobs",
"tags",
"snapshots",
"permissions"
],
"filter": ["deleted", "files", "directories"],
"simplify": true,
"upn": true
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [660, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "myblob1",
"properties": {
"creationTime": "Wed, 22 Jan 2025 18:53:15 GMT",
"lastModified": "Wed, 22 Jan 2025 18:53:15 GMT",
"etag": "0x1F8268B228AA730",
"contentLength": 37,
"contentType": "application/json",
"contentMD5": "aWQGHD8kGQd5ZtEN/S1/aw==",
"blobType": "BlockBlob",
"leaseStatus": "unlocked",
"leaseState": "available",
"serverEncrypted": true
}
}
}
]
}
}

View file

@ -0,0 +1,54 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "create",
"containerCreate": "mycontainer",
"options": {
"accessLevel": "blob",
"metadata": { "metadataValues": [{ "fieldName": "key1", "fieldValue": "value1" }] }
},
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"etag": "\"0x22769D26D3F3740\"",
"lastModified": "Thu, 23 Jan 2025 17:53:23 GMT",
"requestId": "75b87ee3-a7f7-468d-b7d1-e7e7b3173dab",
"version": "2025-01-05",
"date": "Thu, 23 Jan 2025 17:53:23 GMT",
"clientRequestId": "client-request-id-123",
"requestServerEncrypted": true
}
}
]
}
}

View file

@ -0,0 +1,48 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "delete",
"container": { "__rl": true, "mode": "list", "value": "mycontainer" },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"contentLength": 0,
"requestId": "ca3a8907-601e-0050-1929-723410000000",
"server": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0",
"version": "2020-10-02",
"date": "Wed, 29 Jan 2025 08:38:21 GMT"
}
}
]
}
}

View file

@ -0,0 +1,61 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "get",
"container": { "__rl": true, "mode": "list", "value": "mycontainer" },
"options": { "simplify": false },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "mycontainer",
"properties": {
"lastModified": "Tue, 28 Jan 2025 16:40:21 GMT",
"etag": "\"0x8DD3FBA74CF3620\"",
"leaseStatus": "unlocked",
"leaseState": "available",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"leaseDuration": "infinite",
"blobPublicAccess": "blob",
"contentLength": 0,
"date": "Wed, 29 Jan 2025 08:43:08 GMT",
"requestId": "49edb268-b01e-0053-6e29-72d574000000",
"server": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0",
"version": "2020-10-02"
},
"metadata": { "key1": "field1" }
}
}
]
}
}

View file

@ -0,0 +1,73 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": { "operation": "getAll", "returnAll": true, "requestOptions": {} },
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "mycontainer1",
"deleted": true,
"version": "01DB7228F6BEE6E7",
"properties": {
"lastModified": "Wed, 29 Jan 2025 08:37:00 GMT",
"etag": "\"0x8DD40401935032C\"",
"leaseStatus": "unlocked",
"leaseState": "expired",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"immutableStorageWithVersioningEnabled": "false",
"deletedTime": "Wed, 29 Jan 2025 08:38:21 GMT",
"remainingRetentionDays": 7
},
"metadata": { "key1": "value1" }
}
},
{
"json": {
"name": "mycontainer1",
"deleted": true,
"version": "01DB7228F6BEE6E7",
"properties": {
"lastModified": "Wed, 29 Jan 2025 08:37:00 GMT",
"etag": "\"0x8DD40401935032C\"",
"leaseStatus": "unlocked",
"leaseState": "expired",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"immutableStorageWithVersioningEnabled": "false",
"deletedTime": "Wed, 29 Jan 2025 08:38:21 GMT",
"remainingRetentionDays": 7
},
"metadata": { "key1": "value1" }
}
}
]
}
}

View file

@ -0,0 +1,59 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "99f866fa-f63c-477d-a0d0-48fbdb8a344a",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "getAll",
"limit": 1,
"options": { "fields": ["metadata", "deleted", "system"], "filter": "mycontainer" },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "ab1b6258-5c75-4893-90bf-ef591264420c",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "mycontainer1",
"deleted": true,
"version": "01DB7228F6BEE6E7",
"properties": {
"lastModified": "Wed, 29 Jan 2025 08:37:00 GMT",
"etag": "\"0x8DD40401935032C\"",
"leaseStatus": "unlocked",
"leaseState": "expired",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"immutableStorageWithVersioningEnabled": "false",
"deletedTime": "Wed, 29 Jan 2025 08:38:21 GMT",
"remainingRetentionDays": 7
},
"metadata": { "key1": "value1" }
}
}
]
}
}

View file

@ -0,0 +1,65 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "1307e408-a8a5-464e-b858-494953e2f43b",
"name": "When clicking 'Test workflow'"
},
{
"parameters": {
"authentication": "oAuth2",
"resource": "container",
"operation": "get",
"container": { "__rl": true, "value": "mycontainer", "mode": "id" },
"options": { "simplify": false },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "3429f7f2-dfca-4b72-8913-43a582e96e66",
"name": "Azure Storage",
"credentials": {
"azureStorageOAuth2Api": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage OAuth2 account"
}
}
}
],
"connections": {
"When clicking 'Test workflow'": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "mycontainer",
"properties": {
"lastModified": "Tue, 28 Jan 2025 16:40:21 GMT",
"etag": "\"0x8DD3FBA74CF3620\"",
"leaseStatus": "unlocked",
"leaseState": "available",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"leaseDuration": "infinite",
"blobPublicAccess": "blob",
"contentLength": 0,
"date": "Wed, 29 Jan 2025 08:43:08 GMT",
"requestId": "49edb268-b01e-0053-6e29-72d574000000",
"server": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0",
"version": "2020-10-02"
},
"metadata": {
"key1": "field1"
}
}
}
]
}
}

View file

@ -0,0 +1,62 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0],
"id": "1307e408-a8a5-464e-b858-494953e2f43b",
"name": "When clicking 'Test workflow'"
},
{
"parameters": {
"resource": "container",
"operation": "get",
"container": { "__rl": true, "value": "mycontainer", "mode": "id" },
"options": { "simplify": false },
"requestOptions": {}
},
"type": "n8n-nodes-base.azureStorage",
"typeVersion": 1,
"position": [220, 0],
"id": "3429f7f2-dfca-4b72-8913-43a582e96e66",
"name": "Azure Storage",
"credentials": {
"azureStorageSharedKeyApi": {
"id": "VPmcFM58eDDexWQL",
"name": "Azure Storage Shared Key account"
}
}
}
],
"connections": {
"When clicking 'Test workflow'": {
"main": [[{ "node": "Azure Storage", "type": "main", "index": 0 }]]
}
},
"pinData": {
"Azure Storage": [
{
"json": {
"name": "mycontainer",
"properties": {
"lastModified": "Tue, 28 Jan 2025 16:40:21 GMT",
"etag": "\"0x8DD3FBA74CF3620\"",
"leaseStatus": "unlocked",
"leaseState": "available",
"hasImmutabilityPolicy": false,
"hasLegalHold": false,
"leaseDuration": "infinite",
"blobPublicAccess": "blob",
"contentLength": 0,
"date": "Wed, 29 Jan 2025 08:43:08 GMT",
"requestId": "49edb268-b01e-0053-6e29-72d574000000",
"server": "Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0",
"version": "2020-10-02"
},
"metadata": { "key1": "field1" }
}
}
]
}
}

View file

@ -39,6 +39,8 @@
"dist/credentials/AutomizyApi.credentials.js",
"dist/credentials/AutopilotApi.credentials.js",
"dist/credentials/Aws.credentials.js",
"dist/credentials/AzureStorageOAuth2Api.credentials.js",
"dist/credentials/AzureStorageSharedKeyApi.credentials.js",
"dist/credentials/BambooHrApi.credentials.js",
"dist/credentials/BannerbearApi.credentials.js",
"dist/credentials/BaserowApi.credentials.js",
@ -640,6 +642,7 @@
"dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js",
"dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js",
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
"dist/nodes/Microsoft/Storage/AzureStorage.node.js",
"dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
"dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",
"dist/nodes/Mindee/Mindee.node.js",

View file

@ -10,8 +10,29 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo
if (testData.nock) {
const { baseUrl, mocks } = testData.nock;
const agent = nock(baseUrl);
mocks.forEach(({ method, path, statusCode, requestBody, responseBody }) =>
agent[method](path, requestBody).reply(statusCode, responseBody),
mocks.forEach(
({
method,
path,
statusCode,
requestBody,
requestHeaders,
responseBody,
responseHeaders,
}) => {
let mock = agent[method](path, requestBody);
// nock interceptor reqheaders option is ignored, so we chain matchHeader()
// agent[method](path, requestBody, { reqheaders: requestHeaders }).reply(statusCode, responseBody, responseHeaders)
// https://github.com/nock/nock/issues/2545
if (requestHeaders && Object.keys(requestHeaders).length > 0) {
Object.entries(requestHeaders).forEach(([key, value]) => {
mock = mock.matchHeader(key, value);
});
}
mock.reply(statusCode, responseBody, responseHeaders);
},
);
}
const executionMode = testData.trigger?.mode ?? 'manual';

View file

@ -54,6 +54,33 @@ BQIDAQAB
airtableApi: {
apiKey: 'key123',
},
azureStorageOAuth2Api: {
grantType: 'authorizationCode',
authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
clientId: 'CLIENTID',
clientSecret: 'CLIENTSECRET',
scope: 'https://storage.azure.com/user_impersonation',
authQueryParameters: 'response_mode=query',
authentication: 'body',
oauthTokenData: {
token_type: 'Bearer',
scope: 'https://storage.azure.com/user_impersonation',
expires_in: 4730,
ext_expires_in: 4730,
access_token: 'ACCESSTOKEN',
callbackQueryString: {
session_state: 'SESSIONSTATE',
},
},
account: 'myaccount',
baseUrl: 'https://myaccount.blob.core.windows.net',
},
azureStorageSharedKeyApi: {
account: 'devstoreaccount1',
key: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
baseUrl: 'https://myaccount.blob.core.windows.net',
},
gongApi: {
baseUrl: 'https://api.gong.io',
accessKey: 'accessKey123',

View file

@ -6,7 +6,7 @@ import type * as express from 'express';
import type FormData from 'form-data';
import type { PathLike } from 'fs';
import type { IncomingHttpHeaders } from 'http';
import type { RequestBodyMatcher } from 'nock';
import type { ReplyHeaders, RequestBodyMatcher, RequestHeaderMatcher } from 'nock';
import type { Client as SSHClient } from 'ssh2';
import type { Readable } from 'stream';
import type { SecureContextOptions } from 'tls';
@ -2412,8 +2412,10 @@ export interface WorkflowTestData {
method: 'delete' | 'get' | 'patch' | 'post' | 'put';
path: string;
requestBody?: RequestBodyMatcher;
requestHeaders?: Record<string, RequestHeaderMatcher>;
statusCode: number;
responseBody: string | object;
responseHeaders?: ReplyHeaders;
}>;
};
trigger?: {