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
2013 lines
48 KiB
TypeScript
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 },
|
|
},
|
|
],
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
});
|