Files
n8n-enterprise-unlocked/packages/nodes-base/credentials/test/Aws.credentials.test.ts
2025-09-11 08:55:52 +00:00

284 lines
7.9 KiB
TypeScript

import { sign, type Request } from 'aws4';
import type { IHttpRequestOptions } from 'n8n-workflow';
import { Aws, type AwsCredentialsType } from '../Aws.credentials';
jest.mock('aws4', () => ({
sign: jest.fn(),
}));
describe('Aws Credential', () => {
const aws = new Aws();
let mockSign: jest.Mock;
beforeEach(() => {
mockSign = sign as unknown as jest.Mock;
});
afterEach(() => {
jest.clearAllMocks();
});
it('should have correct properties', () => {
expect(aws.name).toBe('aws');
expect(aws.displayName).toBe('AWS');
expect(aws.documentationUrl).toBe('aws');
expect(aws.icon).toEqual({ light: 'file:icons/AWS.svg', dark: 'file:icons/AWS.dark.svg' });
expect(aws.properties.length).toBeGreaterThan(0);
expect(aws.test.request.baseURL).toBe(
// eslint-disable-next-line n8n-local-rules/no-interpolation-in-regular-string
'={{$credentials.region.startsWith("cn-") ? `https://sts.${$credentials.region}.amazonaws.com.cn` : `https://sts.${$credentials.region}.amazonaws.com`}}',
);
expect(aws.test.request.url).toBe('?Action=GetCallerIdentity&Version=2011-06-15');
expect(aws.test.request.method).toBe('POST');
});
describe('authenticate', () => {
const credentials: AwsCredentialsType = {
region: 'eu-central-1',
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
customEndpoints: false,
temporaryCredentials: false,
};
const requestOptions: IHttpRequestOptions = {
qs: {},
body: {},
headers: {},
baseURL: 'https://sts.eu-central-1.amazonaws.com',
url: '?Action=GetCallerIdentity&Version=2011-06-15',
method: 'POST',
returnFullResponse: true,
};
const signOpts: Request & IHttpRequestOptions = {
qs: {},
body: undefined,
headers: {},
baseURL: 'https://sts.eu-central-1.amazonaws.com',
url: '?Action=GetCallerIdentity&Version=2011-06-15',
method: 'POST',
returnFullResponse: true,
host: 'sts.eu-central-1.amazonaws.com',
path: '/?Action=GetCallerIdentity&Version=2011-06-15',
region: 'eu-central-1',
};
const securityHeaders = {
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
sessionToken: undefined,
};
it('should call sign with correct parameters', async () => {
const result = await aws.authenticate(credentials, requestOptions);
expect(mockSign).toHaveBeenCalledWith(signOpts, securityHeaders);
expect(result.method).toBe('POST');
expect(result.url).toBe(
'https://sts.eu-central-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15',
);
});
it('should return correct options with custom endpoint', async () => {
const customEndpoint = 'https://custom.endpoint.com';
const result = await aws.authenticate(
{ ...credentials, customEndpoints: true, snsEndpoint: customEndpoint },
{ ...requestOptions, url: '', baseURL: '', qs: { service: 'sns' } },
);
expect(mockSign).toHaveBeenCalledWith(
{
...signOpts,
baseURL: '',
path: '/',
url: '',
qs: {
service: 'sns',
},
host: 'custom.endpoint.com',
},
securityHeaders,
);
expect(result.method).toBe('POST');
expect(result.url).toBe(`${customEndpoint}/`);
});
it('should return correct options with temporary credentials', async () => {
const result = await aws.authenticate(
{ ...credentials, temporaryCredentials: true, sessionToken: 'test-token' },
requestOptions,
);
expect(mockSign).toHaveBeenCalledWith(signOpts, {
...securityHeaders,
sessionToken: 'test-token',
});
expect(result.method).toBe('POST');
expect(result.url).toBe(
'https://sts.eu-central-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15',
);
});
it('should return correct options for a global AWS service', async () => {
const result = await aws.authenticate(credentials, {
...requestOptions,
url: 'https://iam.amazonaws.com',
baseURL: '',
});
expect(mockSign).toHaveBeenCalledWith(
{
...signOpts,
baseURL: '',
path: '/',
host: 'iam.amazonaws.com',
url: 'https://iam.amazonaws.com',
},
securityHeaders,
);
expect(result.method).toBe('POST');
expect(result.url).toBe('https://iam.amazonaws.com/');
});
it('should handle an IRequestOptions object with form instead of body', async () => {
const result = await aws.authenticate({ ...credentials }, {
...requestOptions,
body: undefined,
form: {
Action: 'ListUsers',
Version: '2010-05-08',
},
baseURL: '',
url: 'https://iam.amazonaws.com',
host: 'iam.amazonaws.com',
path: '/',
} as IHttpRequestOptions);
expect(mockSign).toHaveBeenCalledWith(
{
...signOpts,
form: {
Action: 'ListUsers',
Version: '2010-05-08',
},
body: 'Action=ListUsers&Version=2010-05-08',
host: 'iam.amazonaws.com',
url: 'https://iam.amazonaws.com',
baseURL: '',
path: '/',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
// PR #14037 introduces region normalization for global endpoints
// This test works with or without the normalization
region: expect.stringMatching(/[a-z]{2}-[a-z]+-[0-9]+/),
},
securityHeaders,
);
expect(result.method).toBe('POST');
expect(result.url).toBe('https://iam.amazonaws.com/');
});
describe('China regions', () => {
const chinaCredentials: AwsCredentialsType = {
region: 'cn-north-1',
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
customEndpoints: false,
temporaryCredentials: false,
};
it('should use amazonaws.com.cn domain for cn-north-1 region', async () => {
const result = await aws.authenticate(chinaCredentials, {
...requestOptions,
url: '',
baseURL: '',
qs: { service: 's3' },
});
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
host: 's3.cn-north-1.amazonaws.com.cn',
region: 'cn-north-1',
}),
{
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
sessionToken: undefined,
},
);
expect(result.url).toBe('https://s3.cn-north-1.amazonaws.com.cn/');
});
it('should handle custom endpoints for China regions', async () => {
const customEndpoint = 'https://custom.china.endpoint.com.cn';
const result = await aws.authenticate(
{ ...chinaCredentials, customEndpoints: true, s3Endpoint: customEndpoint },
{ ...requestOptions, url: '', baseURL: '', qs: { service: 's3' } },
);
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
host: 'custom.china.endpoint.com.cn',
}),
{
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
sessionToken: undefined,
},
);
expect(result.url).toBe(`${customEndpoint}/`);
});
it('should parse China region URLs correctly', async () => {
const result = await aws.authenticate(chinaCredentials, {
...requestOptions,
url: 'https://s3.cn-north-1.amazonaws.com.cn/bucket/key',
baseURL: '',
});
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
host: 's3.cn-north-1.amazonaws.com.cn',
region: 'cn-north-1',
path: '/bucket/key',
}),
{
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
sessionToken: undefined,
},
);
expect(result.url).toBe('https://s3.cn-north-1.amazonaws.com.cn/bucket/key');
});
});
describe('Regular regions (non-China)', () => {
it('should use amazonaws.com domain for regular regions', async () => {
const result = await aws.authenticate(credentials, {
...requestOptions,
url: '',
baseURL: '',
qs: { service: 's3' },
});
expect(mockSign).toHaveBeenCalledWith(
expect.objectContaining({
host: 's3.eu-central-1.amazonaws.com',
region: 'eu-central-1',
}),
{
accessKeyId: 'hakuna',
secretAccessKey: 'matata',
sessionToken: undefined,
},
);
expect(result.url).toBe('https://s3.eu-central-1.amazonaws.com/');
});
});
});
});