Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

2013 lines
48 KiB
TypeScript

import { mockDeep } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INode,
INodeExecutionData,
INodeParameterResourceLocator,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { SlackV2 } from '../../V2/SlackV2.node';
import * as GenericFunctions from '../../V2/GenericFunctions';
describe('SlackV2', () => {
let node: SlackV2;
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
let slackApiRequestSpy: jest.SpyInstance;
let slackApiRequestAllItemsSpy: jest.SpyInstance;
const mockNode: INode = {
id: 'test-node-id',
name: 'Slack Test',
type: 'n8n-nodes-base.slack',
typeVersion: 2,
position: [0, 0],
parameters: {},
};
beforeEach(() => {
node = new SlackV2({
name: 'Slack',
displayName: 'Slack',
description: 'Slack node test',
group: ['input'],
});
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
slackApiRequestSpy = jest.spyOn(GenericFunctions, 'slackApiRequest');
slackApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'slackApiRequestAllItems');
jest.clearAllMocks();
// Default mocks
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const mockParams: Record<string, any> = {
authentication: 'accessToken',
resource: 'channel',
operation: 'kick',
};
return mockParams[paramName];
});
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
// Mock helper functions
(mockExecuteFunctions.helpers.constructExecutionMetaData as jest.Mock).mockImplementation(
(data: any, options: any) => {
return data.map((item: any, index: number) => ({
...item,
pairedItem: { item: options?.itemData?.item ?? index },
}));
},
);
(mockExecuteFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: any) => {
// returnJsonArray should always return array with single item containing the data
return [{ json: data }];
});
});
afterEach(() => {
jest.resetAllMocks();
});
describe('Channel Operations - Kick and Join', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
authentication: 'accessToken',
resource: 'channel',
operation: 'kick',
channelId: 'C123456789',
userId: 'U987654321',
};
return params[paramName];
});
});
it('should kick user from channel successfully', async () => {
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.kick',
{
channel: 'C123456789',
user: 'U987654321',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
it('should join channel successfully', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
authentication: 'accessToken',
resource: 'channel',
operation: 'join',
channelId: 'C123456789',
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'C123456789',
name: 'general',
is_channel: true,
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.join',
{
channel: 'C123456789',
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should handle API error when kicking user', async () => {
slackApiRequestSpy.mockRejectedValue(new Error('Channel not found'));
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([[{ json: { error: 'Channel not found' } }]]);
});
});
describe('Channel Operations - History, Invite, Leave, Members, etc.', () => {
it('should get channel history with pagination', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'history',
channelId: 'C123456789',
returnAll: false,
limit: 50,
filters: {},
};
return params[paramName];
});
const mockResponse = {
messages: [
{ type: 'message', text: 'Hello', ts: '1234567890.123456' },
{ type: 'message', text: 'World', ts: '1234567891.123456' },
],
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/conversations.history',
{},
{
channel: 'C123456789',
limit: 50,
},
);
expect(result).toEqual([
[
{
json: mockResponse.messages,
pairedItem: { item: 0 },
},
],
]);
});
it('should return channel history sorted by timestamp descending for node version >= 2.4', async () => {
mockExecuteFunctions.getNode.mockReturnValue({
...mockNode,
typeVersion: 2.4,
});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'history',
channelId: 'C123456789',
returnAll: true,
filters: {},
};
return params[paramName];
});
// Mock unsorted messages
const mockResponse = [
{ type: 'message', text: 'Message 2', ts: '1234567892.123456' },
{ type: 'message', text: 'Message 4', ts: '1234567894.123456' },
{ type: 'message', text: 'Message 1', ts: '1234567891.123456' },
{ type: 'message', text: 'Message 3', ts: '1234567893.123456' },
];
slackApiRequestAllItemsSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
// Verify messages are sorted by timestamp descending (newest first)
expect(result[0][0].json).toEqual([
{ type: 'message', text: 'Message 4', ts: '1234567894.123456' },
{ type: 'message', text: 'Message 3', ts: '1234567893.123456' },
{ type: 'message', text: 'Message 2', ts: '1234567892.123456' },
{ type: 'message', text: 'Message 1', ts: '1234567891.123456' },
]);
});
it('should get all channel history', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'history',
channelId: 'C123456789',
returnAll: true,
filters: {
latest: '2023-12-01',
oldest: '2023-11-01',
inclusive: true,
},
};
return params[paramName];
});
const mockMessages = [
{ type: 'message', text: 'Message 1', ts: '1234567890.123456' },
{ type: 'message', text: 'Message 2', ts: '1234567891.123456' },
];
slackApiRequestAllItemsSpy.mockResolvedValue(mockMessages);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
'messages',
'GET',
'/conversations.history',
{},
{
channel: 'C123456789',
inclusive: true,
latest: new Date('2023-12-01').getTime() / 1000,
oldest: new Date('2023-11-01').getTime() / 1000,
},
);
expect(result).toEqual([
[
{
json: mockMessages,
pairedItem: { item: 0 },
},
],
]);
});
it('should invite users to channel', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'invite',
channelId: 'C123456789',
userIds: ['U111111111', 'U222222222'],
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'C123456789',
name: 'general',
members: ['U111111111', 'U222222222'],
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.invite',
{
channel: 'C123456789',
users: 'U111111111,U222222222',
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should leave channel', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'leave',
channelId: 'C123456789',
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.leave',
{
channel: 'C123456789',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
it('should get channel members with resolve data', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'member',
channelId: 'C123456789',
returnAll: true,
resolveData: true,
};
return params[paramName];
});
const mockMembers = ['U111111111', 'U222222222'];
slackApiRequestAllItemsSpy.mockResolvedValue(mockMembers);
// Mock user info responses
slackApiRequestSpy
.mockResolvedValueOnce({
user: {
id: 'U111111111',
name: 'john.doe',
real_name: 'John Doe',
},
})
.mockResolvedValueOnce({
user: {
id: 'U222222222',
name: 'jane.smith',
real_name: 'Jane Smith',
},
});
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
'members',
'GET',
'/conversations.members',
{},
{
channel: 'C123456789',
},
);
expect(slackApiRequestSpy).toHaveBeenCalledTimes(2);
expect(result[0][0].json).toEqual([
{
id: 'U111111111',
name: 'john.doe',
real_name: 'John Doe',
},
{
id: 'U222222222',
name: 'jane.smith',
real_name: 'Jane Smith',
},
]);
});
it('should open conversation', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'open',
options: {
users: ['U111111111', 'U222222222'],
returnIm: true,
},
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'D123456789',
created: 1234567890,
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.open',
{
users: 'U111111111,U222222222',
return_im: true,
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should rename channel', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'rename',
channelId: 'C123456789',
name: 'new-channel-name',
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'C123456789',
name: 'new-channel-name',
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.rename',
{
channel: 'C123456789',
name: 'new-channel-name',
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should get conversation replies', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'replies',
channelId: 'C123456789',
ts: '1234567890.123456',
returnAll: false,
limit: 100,
filters: {
inclusive: true,
},
};
return params[paramName];
});
const mockResponse = {
messages: [
{ type: 'message', text: 'Original message', ts: '1234567890.123456' },
{
type: 'message',
text: 'Reply 1',
ts: '1234567891.123456',
thread_ts: '1234567890.123456',
},
],
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/conversations.replies',
{},
{
channel: 'C123456789',
ts: '1234567890.123456',
limit: 100,
inclusive: true,
},
);
expect(result).toEqual([
[
{
json: mockResponse.messages,
pairedItem: { item: 0 },
},
],
]);
});
it('should set channel purpose', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'setPurpose',
channelId: 'C123456789',
purpose: 'New channel purpose',
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'C123456789',
purpose: {
value: 'New channel purpose',
creator: 'U123456789',
last_set: 1234567890,
},
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.setPurpose',
{
channel: 'C123456789',
purpose: 'New channel purpose',
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should set channel topic', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'setTopic',
channelId: 'C123456789',
topic: 'New channel topic',
};
return params[paramName];
});
const mockResponse = {
channel: {
id: 'C123456789',
topic: {
value: 'New channel topic',
creator: 'U123456789',
last_set: 1234567890,
},
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.setTopic',
{
channel: 'C123456789',
topic: 'New channel topic',
},
{},
);
expect(result).toEqual([
[
{
json: mockResponse.channel,
pairedItem: { item: 0 },
},
],
]);
});
it('should unarchive channel', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'unarchive',
channelId: 'C123456789',
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/conversations.unarchive',
{
channel: 'C123456789',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
});
describe('Message Operations - Ephemeral Messages', () => {
beforeEach(() => {
jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('C123456789');
jest.spyOn(GenericFunctions, 'getMessageContent').mockReturnValue({
text: 'Test ephemeral message',
});
jest.spyOn(GenericFunctions, 'processThreadOptions').mockReturnValue({});
});
it('should send ephemeral message to channel', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'message',
operation: 'post',
select: 'channel',
otherOptions: {
ephemeral: {
ephemeralValues: {
user: {
mode: 'id',
value: 'U123456789',
} as INodeParameterResourceLocator,
},
},
},
};
return params[paramName];
});
const mockResponse = {
ok: true,
message: {
type: 'message',
text: 'Test ephemeral message',
user: 'U123456789',
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/chat.postEphemeral',
expect.objectContaining({
channel: 'C123456789',
text: 'Test ephemeral message',
user: 'U123456789',
}),
{},
);
expect(result).toEqual([
[
{
json: mockResponse,
pairedItem: { item: 0 },
},
],
]);
});
it('should send ephemeral message to user with username format', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'message',
operation: 'post',
select: 'channel',
otherOptions: {
ephemeral: {
ephemeralValues: {
user: {
mode: 'username',
value: 'john.doe',
} as INodeParameterResourceLocator,
},
},
},
};
return params[paramName];
});
const mockResponse = { ok: true };
slackApiRequestSpy.mockResolvedValue(mockResponse);
await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/chat.postEphemeral',
expect.objectContaining({
user: '@john.doe',
}),
{},
);
});
it('should send ephemeral message to user directly', async () => {
jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('U123456789');
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'message',
operation: 'post',
select: 'user',
otherOptions: {
ephemeral: {},
},
};
return params[paramName];
});
const mockResponse = { ok: true };
slackApiRequestSpy.mockResolvedValue(mockResponse);
await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/chat.postEphemeral',
expect.objectContaining({
channel: 'U123456789',
user: 'U123456789',
}),
{},
);
});
});
describe('Reaction and Star Operations', () => {
describe('Reaction Operations', () => {
it('should add reaction to message', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'reaction',
operation: 'add',
channelId: 'C123456789',
timestamp: '1234567890.123456',
name: 'thumbs_up',
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/reactions.add',
{
channel: 'C123456789',
name: 'thumbs_up',
timestamp: '1234567890.123456',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
it('should remove reaction from message', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'reaction',
operation: 'remove',
channelId: 'C123456789',
timestamp: '1234567890.123456',
name: 'thumbs_up',
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/reactions.remove',
{
channel: 'C123456789',
name: 'thumbs_up',
timestamp: '1234567890.123456',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
it('should get reactions for message', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'reaction',
operation: 'get',
channelId: 'C123456789',
timestamp: '1234567890.123456',
};
return params[paramName];
});
const mockResponse = {
message: {
reactions: [
{
name: 'thumbs_up',
count: 2,
users: ['U111111111', 'U222222222'],
},
],
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/reactions.get',
{},
{
channel: 'C123456789',
timestamp: '1234567890.123456',
},
);
expect(result).toEqual([
[
{
json: mockResponse,
pairedItem: { item: 0 },
},
],
]);
});
});
describe('Star Operations', () => {
it('should add star to message', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'star',
operation: 'add',
target: 'message',
channelId: 'C123456789',
timestamp: '1234567890.123456',
options: {},
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/stars.add',
{
channel: 'C123456789',
timestamp: '1234567890.123456',
},
{},
);
expect(result).toEqual([[{ json: { ok: true }, pairedItem: { item: 0 } }]]);
});
it('should add star to file', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'star',
operation: 'add',
target: 'file',
channelId: 'C123456789',
fileId: 'F123456789',
options: {
fileComment: 'FC123456789',
},
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/stars.add',
{
channel: 'C123456789',
file: 'F123456789',
file_comment: 'FC123456789',
},
{},
);
});
it('should remove star', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'star',
operation: 'delete',
options: {
channelId: 'C123456789',
timestamp: '1234567890.123456',
fileId: 'F123456789',
fileComment: 'FC123456789',
},
};
return params[paramName];
});
slackApiRequestSpy.mockResolvedValue({ ok: true });
await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/stars.remove',
{
channel: 'C123456789',
file: 'F123456789',
file_comment: 'FC123456789',
timestamp: '1234567890.123456',
},
{},
);
});
it('should get all stars', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'star',
operation: 'getAll',
returnAll: true,
};
return params[paramName];
});
const mockStars = [
{
type: 'message',
message: {
text: 'Starred message',
ts: '1234567890.123456',
},
},
];
slackApiRequestAllItemsSpy.mockResolvedValue(mockStars);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestAllItemsSpy).toHaveBeenCalledWith(
'items',
'GET',
'/stars.list',
{},
{},
);
expect(result).toEqual([
[
{
json: mockStars,
pairedItem: { item: 0 },
},
],
]);
});
it('should get stars with limit', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'star',
operation: 'getAll',
returnAll: false,
limit: 10,
};
return params[paramName];
});
const mockResponse = {
items: [
{
type: 'message',
message: {
text: 'Starred message',
ts: '1234567890.123456',
},
},
],
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'GET',
'/stars.list',
{},
{
limit: 10,
},
);
expect(result).toEqual([
[
{
json: mockResponse.items,
pairedItem: { item: 0 },
},
],
]);
});
});
});
describe('File Upload Operations - Setup', () => {
it('should set up file upload options correctly', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'upload',
options: {
channelIds: ['C111111111', 'C222222222'],
channelId: 'C123456789',
initialComment: 'File upload test',
threadTs: '1234567890.123456',
},
};
return params[paramName];
});
// We'll test the parameter extraction logic
expect(mockExecuteFunctions.getNodeParameter('options', 0)).toEqual({
channelIds: ['C111111111', 'C222222222'],
channelId: 'C123456789',
initialComment: 'File upload test',
threadTs: '1234567890.123456',
});
});
});
describe('File Upload Operations - Binary Data Handling', () => {
beforeEach(() => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'upload',
binaryData: true,
binaryPropertyName: 'data',
options: {
title: 'Test File',
channelIds: ['C123456789'],
initialComment: 'Test upload',
},
};
return params[paramName];
});
});
it('should upload file with binary data (node version <= 2.1)', async () => {
mockExecuteFunctions.getNode.mockReturnValue({
...mockNode,
typeVersion: 2.1,
});
const mockBinaryData = {
data: 'base64encodeddata', // cspell:disable-line
fileName: 'test.txt',
mimeType: 'text/plain',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
const mockResponse = {
file: {
id: 'F123456789',
name: 'test.txt',
mimetype: 'text/plain',
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/files.upload',
{},
{},
{ 'Content-Type': 'multipart/form-data' },
{
formData: expect.objectContaining({
channels: 'C123456789',
initial_comment: 'Test upload',
title: 'Test File',
file: expect.objectContaining({
value: expect.any(Buffer),
options: {
filename: 'test.txt',
contentType: 'text/plain',
},
}),
}),
},
);
expect(result).toEqual([
[
{
json: mockResponse.file,
pairedItem: { item: 0 },
},
],
]);
});
it('should upload file with external upload (node version > 2.1)', async () => {
mockExecuteFunctions.getNode.mockReturnValue({
...mockNode,
typeVersion: 2.2,
});
const mockBinaryData = {
id: 'binary-data-id',
fileName: 'test.txt',
mimeType: 'text/plain',
};
const mockStream = Buffer.from('file content');
const mockMetadata = { fileSize: 12 };
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(mockStream);
(mockExecuteFunctions.helpers.getBinaryMetadata as jest.Mock).mockResolvedValue(mockMetadata);
// Mock the upload URL response
const mockUploadUrlResponse = {
upload_url: 'https://files.slack.com/upload/v1',
file_id: 'F123456789',
};
slackApiRequestSpy
.mockResolvedValueOnce(mockUploadUrlResponse) // getUploadURLExternal
.mockResolvedValueOnce({ ok: true }) // upload to external URL
.mockResolvedValueOnce({
// completeUploadExternal
files: [
{
id: 'F123456789',
title: 'Test File',
},
],
});
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledTimes(3);
// Check getUploadURLExternal call
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
1,
'GET',
'/files.getUploadURLExternal',
{},
{
filename: 'test.txt',
length: 12,
},
);
// Check external upload call
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'https://files.slack.com/upload/v1',
{},
{},
{ 'Content-Type': 'multipart/form-data' },
{
formData: expect.objectContaining({
file: expect.objectContaining({
value: mockStream,
}),
}),
},
);
// Check completeUploadExternal call
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
3,
'POST',
'/files.completeUploadExternal',
{
channels: 'C123456789',
initial_comment: 'Test upload',
files: [
{
id: 'F123456789',
title: 'Test File',
},
],
},
);
expect(result).toEqual([
[
{
json: [
{
id: 'F123456789',
title: 'Test File',
},
],
pairedItem: { item: 0 },
},
],
]);
});
it('should upload file content as text', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'upload',
binaryData: false,
fileContent: 'This is text file content',
options: {
channelIds: ['C123456789'],
},
};
return params[paramName];
});
const mockResponse = {
file: {
id: 'F123456789',
name: 'content.txt',
},
};
slackApiRequestSpy.mockResolvedValue(mockResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenCalledWith(
'POST',
'/files.upload',
{
channels: 'C123456789',
content: 'This is text file content',
},
{},
{ 'Content-Type': 'application/x-www-form-urlencoded' },
{
form: expect.objectContaining({
channels: 'C123456789',
content: 'This is text file content',
}),
},
);
expect(result).toEqual([
[
{
json: mockResponse.file,
pairedItem: { item: 0 },
},
],
]);
});
it('should handle binary data without id (legacy format)', async () => {
mockExecuteFunctions.getNode.mockReturnValue({
...mockNode,
typeVersion: 2.2,
});
const mockBinaryData = {
data: 'YmFzZTY0ZGF0YQ==', // base64 encoded data
fileName: 'test.txt',
mimeType: 'text/plain',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
const mockUploadUrlResponse = {
upload_url: 'https://files.slack.com/upload/v1',
file_id: 'F123456789',
};
slackApiRequestSpy
.mockResolvedValueOnce(mockUploadUrlResponse)
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({
files: [{ id: 'F123456789', title: 'test.txt' }],
});
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
1,
'GET',
'/files.getUploadURLExternal',
{},
{
filename: 'test.txt',
length: 10, // length of decoded 'base64data'
},
);
expect(result).toEqual([
[
{
json: [{ id: 'F123456789', title: 'test.txt' }],
pairedItem: { item: 0 },
},
],
]);
});
it('should use custom filename when provided', async () => {
mockExecuteFunctions.getNode.mockReturnValue({
...mockNode,
typeVersion: 2.2,
});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'file',
operation: 'upload',
binaryData: true,
binaryPropertyName: 'data',
options: {
fileName: 'custom-name.txt',
title: 'Custom Title',
},
};
return params[paramName];
});
const mockBinaryData = {
id: 'binary-data-id',
fileName: 'original.txt',
mimeType: 'text/plain',
};
(mockExecuteFunctions.helpers.assertBinaryData as jest.Mock).mockReturnValue(mockBinaryData);
(mockExecuteFunctions.helpers.getBinaryStream as jest.Mock).mockResolvedValue(
Buffer.from('test'),
);
(mockExecuteFunctions.helpers.getBinaryMetadata as jest.Mock).mockResolvedValue({
fileSize: 4,
});
slackApiRequestSpy
.mockResolvedValueOnce({ upload_url: 'https://test.com', file_id: 'F123' })
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ files: [{ id: 'F123', title: 'Custom Title' }] });
await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
1,
'GET',
'/files.getUploadURLExternal',
{},
{
filename: 'custom-name.txt',
length: 4,
},
);
});
});
describe('Error Handling with continueOnFail', () => {
it('should continue execution on API error when continueOnFail is true', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'kick',
channelId: 'C123456789',
userId: 'U987654321',
};
return params[paramName];
});
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
slackApiRequestSpy.mockRejectedValue(new Error('Channel not found'));
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([
[
{
json: { error: 'Channel not found' },
},
],
]);
});
it('should throw error when continueOnFail is false', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'channel',
operation: 'kick',
channelId: 'C123456789',
userId: 'U987654321',
};
return params[paramName];
});
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
const apiError = new NodeOperationError(mockNode, 'API Error');
slackApiRequestSpy.mockRejectedValue(apiError);
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(apiError);
});
it('should handle multiple items with mixed success and failure', async () => {
const inputData: INodeExecutionData[] = [
{ json: { id: 1 } },
{ json: { id: 2 } },
{ json: { id: 3 } },
];
mockExecuteFunctions.getInputData.mockReturnValue(inputData);
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
return {
resource: 'channel',
operation: 'kick',
channelId: 'C123456789',
userId: 'U987654321',
}[paramName];
});
// First call succeeds, second fails, third succeeds
slackApiRequestSpy
.mockResolvedValueOnce({ ok: true })
.mockRejectedValueOnce(new Error('User not found'))
.mockResolvedValueOnce({ ok: true });
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([
[
{ json: { ok: true }, pairedItem: { item: 0 } },
{ json: { error: 'User not found' } },
{ json: { ok: true }, pairedItem: { item: 2 } },
],
]);
});
it('should handle JSON errors correctly', async () => {
mockExecuteFunctions.continueOnFail.mockReturnValue(true);
const jsonError = {
message: 'Invalid JSON response',
code: 'INVALID_JSON',
};
slackApiRequestSpy.mockRejectedValue(jsonError);
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([
[
{
json: { error: 'Invalid JSON response' },
},
],
]);
});
});
describe('Edge Cases and Error Scenarios', () => {
it('should handle empty input data', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([]);
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([[]]);
});
it('should handle unknown resource/operation combination', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'unknown',
operation: 'unknown',
};
return params[paramName];
});
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([
[
{
json: {
error: 'Resource unknown / operation unknown not found!',
},
pairedItem: { item: 0 },
},
],
]);
});
it('should throw error for ephemeral message with username when select is user', async () => {
jest.spyOn(GenericFunctions, 'getTarget').mockReturnValue('U123456789');
jest.spyOn(GenericFunctions, 'getMessageContent').mockReturnValue({
text: 'Test message',
});
jest.spyOn(GenericFunctions, 'processThreadOptions').mockReturnValue({});
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'resource') return 'message';
if (paramName === 'operation') return 'post';
if (paramName === 'select') return 'user';
if (paramName === 'user') {
return {
mode: 'username',
value: 'john.doe',
} as INodeParameterResourceLocator;
}
if (paramName === 'otherOptions') {
return {
ephemeral: {},
};
}
return undefined;
});
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(NodeOperationError);
await expect(node.execute.call(mockExecuteFunctions)).rejects.toThrow(
'You cannot send ephemeral messages using User type "By username". Please use "From List" or "By ID".',
);
});
});
describe('User Group Operations', () => {
describe('Add Users to User Group', () => {
it('should add users to user group successfully', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'updateUsers',
userGroupId: 'S123456789',
users: ['U111111111', 'U222222222'],
options: {
include_count: true,
},
};
return params[paramName];
});
// Mock the current user group data
const mockCurrentGroup = {
ok: true,
usergroups: [
{
id: 'S123456789',
name: 'test-group',
users: ['U000000000'], // Existing user
},
],
};
// Mock the update response
const mockUpdateResponse = {
ok: true,
usergroup: {
id: 'S123456789',
name: 'test-group',
users: ['U000000000', 'U111111111', 'U222222222'],
user_count: 3,
},
};
slackApiRequestSpy
.mockResolvedValueOnce(mockCurrentGroup) // Get current users
.mockResolvedValueOnce(mockUpdateResponse); // Update users
const result = await node.execute.call(mockExecuteFunctions);
// Verify GET request to fetch current users
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
1,
'GET',
'/usergroups.list',
{},
{
include_users: true,
usergroup: 'S123456789',
},
);
// Verify POST request to update users (should merge existing and new)
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'/usergroups.users.update',
{
usergroup: 'S123456789',
users: 'U000000000,U111111111,U222222222',
include_count: true,
},
{},
);
expect(result).toEqual([
[
{
json: mockUpdateResponse.usergroup,
pairedItem: { item: 0 },
},
],
]);
});
it('should handle adding duplicate users (deduplication)', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'updateUsers',
userGroupId: 'S123456789',
users: ['U111111111', 'U222222222'], // U111111111 already exists
options: {},
};
return params[paramName];
});
const mockCurrentGroup = {
ok: true,
usergroups: [
{
id: 'S123456789',
users: ['U111111111'], // User already in group
},
],
};
const mockUpdateResponse = {
ok: true,
usergroup: {
id: 'S123456789',
users: ['U111111111', 'U222222222'],
},
};
slackApiRequestSpy
.mockResolvedValueOnce(mockCurrentGroup)
.mockResolvedValueOnce(mockUpdateResponse);
await node.execute.call(mockExecuteFunctions);
// Verify duplicates are removed
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'/usergroups.users.update',
expect.objectContaining({
users: 'U111111111,U222222222', // No duplicates
}),
{},
);
});
it('should handle empty user group (no existing users)', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'updateUsers',
userGroupId: 'S123456789',
users: ['U111111111'],
options: {},
};
return params[paramName];
});
const mockCurrentGroup = {
ok: true,
usergroups: [
{
id: 'S123456789',
users: [], // Empty group
},
],
};
const mockUpdateResponse = {
ok: true,
usergroup: {
id: 'S123456789',
users: ['U111111111'],
},
};
slackApiRequestSpy
.mockResolvedValueOnce(mockCurrentGroup)
.mockResolvedValueOnce(mockUpdateResponse);
const result = await node.execute.call(mockExecuteFunctions);
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
2,
'POST',
'/usergroups.users.update',
expect.objectContaining({
users: 'U111111111',
}),
{},
);
expect(result).toEqual([
[
{
json: mockUpdateResponse.usergroup,
pairedItem: { item: 0 },
},
],
]);
});
});
describe('Get Users from User Group', () => {
it('should get users from user group with resolve data', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'getUsers',
userGroupId: 'S123456789',
options: {
resolveData: true,
},
};
return params[paramName];
});
const mockGroupData = {
ok: true,
usergroups: [
{
id: 'S123456789',
name: 'test-group',
users: ['U111111111', 'U222222222'],
},
],
};
// Mock user info responses
const mockUser1 = {
ok: true,
user: {
id: 'U111111111',
name: 'john.doe',
real_name: 'John Doe',
profile: {
email: 'john@example.com',
},
},
};
const mockUser2 = {
ok: true,
user: {
id: 'U222222222',
name: 'jane.smith',
real_name: 'Jane Smith',
profile: {
email: 'jane@example.com',
},
},
};
slackApiRequestSpy
.mockResolvedValueOnce(mockGroupData) // Get user group
.mockResolvedValueOnce(mockUser1) // Get user 1 info
.mockResolvedValueOnce(mockUser2); // Get user 2 info
const result = await node.execute.call(mockExecuteFunctions);
// Verify user group list call
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
1,
'GET',
'/usergroups.list',
{},
{
include_users: true,
usergroup: 'S123456789',
},
);
// Verify user info calls
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
2,
'GET',
'/users.info',
{},
{ user: 'U111111111' },
);
expect(slackApiRequestSpy).toHaveBeenNthCalledWith(
3,
'GET',
'/users.info',
{},
{ user: 'U222222222' },
);
expect(result).toEqual([
[
{
json: [mockUser1.user, mockUser2.user],
pairedItem: { item: 0 },
},
],
]);
});
it('should get users from user group without resolve data (IDs only)', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'getUsers',
userGroupId: 'S123456789',
options: {
resolveData: false,
},
};
return params[paramName];
});
const mockGroupData = {
ok: true,
usergroups: [
{
id: 'S123456789',
users: ['U111111111', 'U222222222'],
},
],
};
slackApiRequestSpy.mockResolvedValueOnce(mockGroupData);
const result = await node.execute.call(mockExecuteFunctions);
// Should only call usergroups.list, not users.info
expect(slackApiRequestSpy).toHaveBeenCalledTimes(1);
expect(result).toEqual([
[
{
json: [{ id: 'U111111111' }, { id: 'U222222222' }],
pairedItem: { item: 0 },
},
],
]);
});
it('should handle empty user group', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'getUsers',
userGroupId: 'S123456789',
options: {
resolveData: true,
},
};
return params[paramName];
});
const mockGroupData = {
ok: true,
usergroups: [
{
id: 'S123456789',
users: [], // Empty group
},
],
};
slackApiRequestSpy.mockResolvedValueOnce(mockGroupData);
const result = await node.execute.call(mockExecuteFunctions);
// Should only call usergroups.list
expect(slackApiRequestSpy).toHaveBeenCalledTimes(1);
expect(result).toEqual([
[
{
json: [],
pairedItem: { item: 0 },
},
],
]);
});
it('should throw error when user group not found', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'getUsers',
userGroupId: 'S999999999',
options: {},
};
return params[paramName];
});
const mockGroupData = {
ok: true,
usergroups: [
{
id: 'S123456789', // Different ID
users: ['U111111111'],
},
],
};
slackApiRequestSpy.mockResolvedValue(mockGroupData);
const executePromise = node.execute.call(mockExecuteFunctions);
await expect(executePromise).rejects.toThrow(NodeOperationError);
await expect(executePromise).rejects.toThrow('User group with ID "S999999999" not found');
});
it('should handle missing users field in group', async () => {
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
const params: Record<string, any> = {
resource: 'userGroup',
operation: 'getUsers',
userGroupId: 'S123456789',
options: {
resolveData: false,
},
};
return params[paramName];
});
const mockGroupData = {
ok: true,
usergroups: [
{
id: 'S123456789',
// users field missing
},
],
};
slackApiRequestSpy.mockResolvedValueOnce(mockGroupData);
const result = await node.execute.call(mockExecuteFunctions);
expect(result).toEqual([
[
{
json: [],
pairedItem: { item: 0 },
},
],
]);
});
});
});
});