Files
n8n-enterprise-unlocked/packages/nodes-base/nodes/Aws/IAM/test/helpers/utils.test.ts
Stanimira Rikova ab1047ebde feat(AWS IAM Node): Add new AWS IAM Node (#11963)
Co-authored-by: Adina Totorean <adinatotorean99@gmail.com>
2025-05-15 11:59:19 +01:00

422 lines
13 KiB
TypeScript

import type { IHttpRequestOptions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import {
preprocessTags,
deleteGroupMembers,
validatePath,
validateUserPath,
removeUserFromGroups,
findUsersForGroup,
simplifyGetGroupsResponse,
simplifyGetAllGroupsResponse,
simplifyGetAllUsersResponse,
validateName,
validatePermissionsBoundary,
encodeBodyAsFormUrlEncoded,
} from '../../helpers/utils';
import { awsApiRequest } from '../../transport';
jest.mock('../../transport', () => ({
awsApiRequest: jest.fn(),
}));
describe('AWS IAM - Helper Functions', () => {
let mockNode: any;
beforeEach(() => {
mockNode = {
getNodeParameter: jest.fn(),
getNode: jest.fn(),
helpers: {
returnJsonArray: jest.fn((input: unknown[]) => input.map((i) => ({ json: i }))),
},
};
});
describe('encodeBodyAsFormUrlEncoded', () => {
it('should encode the body as application/x-www-form-urlencoded', async () => {
const requestOptions: IHttpRequestOptions = {
body: {
client_id: 'myClient',
client_secret: 'mySecret',
grant_type: 'client_credentials',
},
url: '',
headers: {},
};
const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);
expect(result.body).toBe(
'client_id=myClient&client_secret=mySecret&grant_type=client_credentials',
);
});
it('should return unchanged options if no body is present', async () => {
const requestOptions: IHttpRequestOptions = { url: '', headers: {} };
const result = await encodeBodyAsFormUrlEncoded.call(mockNode, requestOptions);
expect(result).toEqual({ url: '', headers: {} });
});
});
describe('findUsersForGroup', () => {
it('should return users for a valid groupName', async () => {
mockNode.getNodeParameter.mockReturnValue('groupName');
const mockResponse = {
GetGroupResponse: { GetGroupResult: { Users: [{ UserName: 'user1' }] } },
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
const result = await findUsersForGroup.call(mockNode, 'groupName');
expect(result).toEqual([{ UserName: 'user1' }]);
});
});
describe('preprocessTags', () => {
it('should preprocess tags correctly into request body', async () => {
mockNode.getNodeParameter.mockReturnValue({
tags: [
{ key: 'Department', value: 'Engineering' },
{ key: 'Role', value: 'Developer' },
],
});
const requestOptions = { body: '', headers: {}, url: '' };
const result = await preprocessTags.call(mockNode, requestOptions);
expect(result.body).toBe(
'Tags.member.1.Key=Department&Tags.member.1.Value=Engineering&Tags.member.2.Key=Role&Tags.member.2.Value=Developer',
);
});
it('should throw error if a tag is missing a key', async () => {
mockNode.getNodeParameter.mockReturnValue({
tags: [{ key: '', value: 'Engineering' }],
});
const requestOptions = { body: '', headers: {}, url: '' };
await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
NodeOperationError,
);
await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
"Tag at position 1 is missing 'Key'. Both 'Key' and 'Value' are required.",
);
});
it('should throw error if a tag is missing a value', async () => {
mockNode.getNodeParameter.mockReturnValue({
tags: [{ key: 'Department', value: '' }],
});
const requestOptions = { body: '', headers: {}, url: '' };
await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
NodeOperationError,
);
await expect(preprocessTags.call(mockNode, requestOptions)).rejects.toThrow(
"Tag at position 1 is missing 'Value'. Both 'Key' and 'Value' are required.",
);
});
});
describe('deleteGroupMembers', () => {
it('should attempt to remove users from a group', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'group') {
return 'groupName';
}
return null;
});
const mockUsers = [{ UserName: 'user1' }];
(awsApiRequest as jest.Mock).mockResolvedValue(mockUsers);
const requestOptions = { headers: {}, url: '' };
const result = await deleteGroupMembers.call(mockNode, requestOptions);
expect(result).toEqual(requestOptions);
expect(awsApiRequest).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('GroupName=groupName'),
}),
);
});
});
describe('validatePath', () => {
it('should throw an error for invalid path length', async () => {
mockNode.getNodeParameter.mockReturnValue('');
await expect(validatePath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
NodeOperationError,
);
});
it('should throw an error for invalid path format', async () => {
mockNode.getNodeParameter.mockReturnValue('/invalidPath');
await expect(validatePath.call(mockNode, { url: '' })).rejects.toThrowError(
NodeOperationError,
);
});
it('should pass for a valid path', async () => {
mockNode.getNodeParameter.mockReturnValue('/valid/path/');
const result = await validatePath.call(mockNode, { headers: {}, url: '' });
expect(result).toEqual({ headers: {}, url: '' });
});
});
describe('validateUserPath', () => {
it('should throw an error for invalid path prefix', async () => {
mockNode.getNodeParameter.mockReturnValue('/invalidPrefix');
const mockResponse = {
ListUsersResponse: {
ListUsersResult: {
Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
},
},
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
await expect(validateUserPath.call(mockNode, { headers: {}, url: '' })).rejects.toThrowError(
NodeOperationError,
);
});
it('should modify the request body with a valid path', async () => {
mockNode.getNodeParameter.mockReturnValue('/validPrefix/');
const requestOptions = { body: {}, headers: {}, url: '' };
const mockResponse = {
ListUsersResponse: {
ListUsersResult: {
Users: [{ UserName: 'user1', Path: '/validPrefix/user1' }],
},
},
};
(awsApiRequest as jest.Mock).mockResolvedValue(mockResponse);
const result = await validateUserPath.call(mockNode, requestOptions);
expect(result.body).toHaveProperty('PathPrefix', '/validPrefix/');
});
});
describe('validateName', () => {
const requestOptions: IHttpRequestOptions = { body: {}, url: '' };
it('should throw an error if userName contains spaces', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'user';
if (param === 'userName') return 'John Doe';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
new NodeOperationError(mockNode.getNode(), 'User name should not contain spaces.'),
);
});
it('should throw an error if userName contains invalid characters', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'user';
if (param === 'userName') return 'John@Doe';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
new NodeOperationError(
mockNode.getNode(),
'User name can have up to 64 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
),
);
});
it('should pass validation for valid userName', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'user';
if (param === 'userName') return 'John_Doe123';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
});
it('should throw an error if groupName contains spaces', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'group';
if (param === 'groupName') return 'Group Name';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
new NodeOperationError(mockNode.getNode(), 'Group name should not contain spaces.'),
);
});
it('should throw an error if groupName contains invalid characters', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'group';
if (param === 'groupName') return 'Group@Name';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).rejects.toThrowError(
new NodeOperationError(
mockNode.getNode(),
'Group name can have up to 128 characters. Valid characters: letters, numbers, hyphens (-), and underscores (_).',
),
);
});
it('should pass validation for valid groupName', async () => {
mockNode.getNodeParameter.mockImplementation((param: string) => {
if (param === 'resource') return 'group';
if (param === 'groupName') return 'Group_Name-123';
return '';
});
await expect(validateName.call(mockNode, requestOptions)).resolves.toEqual(requestOptions);
});
});
describe('validatePermissionsBoundary', () => {
const requestOptions: IHttpRequestOptions = { body: {}, url: '' };
it('should return the request options unchanged if no permissions boundary is set', async () => {
mockNode.getNodeParameter.mockReturnValue(undefined);
const result = await validatePermissionsBoundary.call(mockNode, requestOptions);
expect(result).toEqual(requestOptions);
});
it('should add a valid permissions boundary to the request body', async () => {
const validArn = 'arn:aws:iam::123456789012:policy/ExamplePolicy';
mockNode.getNodeParameter.mockReturnValue(validArn);
const result = await validatePermissionsBoundary.call(mockNode, requestOptions);
expect(result.body).toEqual({ PermissionsBoundary: validArn });
});
it('should throw an error for invalid permissions boundary format', async () => {
const invalidArn = 'invalid:arn:format';
mockNode.getNodeParameter.mockReturnValue(invalidArn);
await expect(
validatePermissionsBoundary.call(mockNode, { body: {}, url: '', headers: {} }),
).rejects.toThrow(NodeOperationError);
});
});
describe('simplifyGetGroupsResponse', () => {
it('should return group data', async () => {
mockNode.getNodeParameter.mockReturnValue(false);
const mockResponse = {
body: { GetGroupResponse: { GetGroupResult: { Group: { GroupName: 'TestGroup' } } } },
headers: {},
url: '',
statusCode: 200,
};
const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);
expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
});
it('should include users if "includeUsers" is true', async () => {
mockNode.getNodeParameter.mockReturnValue(true);
const mockResponse = {
body: {
GetGroupResponse: {
GetGroupResult: { Group: { GroupName: 'TestGroup' }, Users: [{ UserName: 'user1' }] },
},
},
headers: {},
url: '',
statusCode: 200,
};
const result = await simplifyGetGroupsResponse.call(mockNode, [], mockResponse);
expect(result).toEqual([
{ json: { GroupName: 'TestGroup', Users: [{ UserName: 'user1' }] } },
]);
});
});
describe('simplifyGetAllGroupsResponse', () => {
it('should return groups without users if "includeUsers" is false', async () => {
mockNode.getNodeParameter.mockReturnValue(false);
const mockResponse = {
body: {
ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
},
headers: {},
url: '',
statusCode: 200,
};
const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);
expect(result).toEqual([{ json: { GroupName: 'TestGroup' } }]);
});
it('should return groups with users if "includeUsers" is true', async () => {
mockNode.getNodeParameter.mockReturnValue(true);
const mockResponse = {
body: {
ListGroupsResponse: { ListGroupsResult: { Groups: [{ GroupName: 'TestGroup' }] } },
},
headers: {},
url: '',
statusCode: 200,
};
const mockUsers = [{ UserName: 'user1' }];
(awsApiRequest as jest.Mock).mockResolvedValueOnce({
GetGroupResponse: { GetGroupResult: { Users: mockUsers } },
});
const result = await simplifyGetAllGroupsResponse.call(mockNode, [], mockResponse);
expect(result).toEqual([{ json: { GroupName: 'TestGroup', Users: mockUsers } }]);
});
});
describe('simplifyGetAllUsersResponse', () => {
it('should return all users', async () => {
const mockResponse = {
body: { ListUsersResponse: { ListUsersResult: { Users: [{ UserName: 'user1' }] } } },
headers: {},
url: '',
statusCode: 200,
};
const result = await simplifyGetAllUsersResponse.call(mockNode, [], mockResponse);
expect(result).toEqual([{ json: { UserName: 'user1' } }]);
});
});
describe('removeUserFromGroups', () => {
it('should remove a user from all groups', async () => {
mockNode.getNodeParameter.mockReturnValue('user1');
const mockUserGroups = { results: [{ value: 'group1' }, { value: 'group2' }] };
(awsApiRequest as jest.Mock).mockResolvedValue(mockUserGroups);
const requestOptions = { headers: {}, url: '' };
const result = await removeUserFromGroups.call(mockNode, requestOptions);
expect(result).toEqual(requestOptions);
});
});
});