git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
574 lines
14 KiB
TypeScript
574 lines
14 KiB
TypeScript
/**
|
|
* Slack Integration - Integration Tests
|
|
*
|
|
* Tests for Slack message handling, events, and API interactions
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import {
|
|
MockSlackWebClient,
|
|
MockSlackBoltApp,
|
|
createMockSlackClient,
|
|
createMockSlackApp
|
|
} from '../../mocks/slack.mock';
|
|
import { slackFixtures } from '../../fixtures';
|
|
|
|
describe('Slack Web Client', () => {
|
|
let client: MockSlackWebClient;
|
|
|
|
beforeEach(() => {
|
|
client = createMockSlackClient();
|
|
});
|
|
|
|
afterEach(() => {
|
|
client.reset();
|
|
});
|
|
|
|
describe('Chat API', () => {
|
|
it('should post message', async () => {
|
|
const response = await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Hello, world!'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.ts).toBeDefined();
|
|
expect(response.channel).toBe('C12345678');
|
|
});
|
|
|
|
it('should post message with blocks', async () => {
|
|
const blocks = [
|
|
{
|
|
type: 'section',
|
|
text: { type: 'mrkdwn', text: '*Bold text*' }
|
|
}
|
|
];
|
|
|
|
const response = await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Fallback text',
|
|
blocks
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(client.getMessageLog()).toHaveLength(1);
|
|
expect(client.getMessageLog()[0].blocks).toEqual(blocks);
|
|
});
|
|
|
|
it('should post thread reply', async () => {
|
|
const parentTs = '1234567890.123456';
|
|
|
|
const response = await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Thread reply',
|
|
thread_ts: parentTs
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(client.getMessageLog()[0].thread_ts).toBe(parentTs);
|
|
});
|
|
|
|
it('should update message', async () => {
|
|
const postResponse = await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Original'
|
|
});
|
|
|
|
const updateResponse = await client.chat.update({
|
|
channel: 'C12345678',
|
|
ts: postResponse.ts!,
|
|
text: 'Updated'
|
|
});
|
|
|
|
expect(updateResponse.ok).toBe(true);
|
|
});
|
|
|
|
it('should delete message', async () => {
|
|
const postResponse = await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'To delete'
|
|
});
|
|
|
|
const deleteResponse = await client.chat.delete({
|
|
channel: 'C12345678',
|
|
ts: postResponse.ts!
|
|
});
|
|
|
|
expect(deleteResponse.ok).toBe(true);
|
|
});
|
|
|
|
it('should post ephemeral message', async () => {
|
|
const response = await client.chat.postEphemeral({
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
text: 'Only you can see this'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Conversations API', () => {
|
|
it('should get channel info', async () => {
|
|
const response = await client.conversations.info({
|
|
channel: 'C12345678'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.channel?.id).toBe('C12345678');
|
|
expect(response.channel?.name).toBe('general');
|
|
});
|
|
|
|
it('should list channel members', async () => {
|
|
const response = await client.conversations.members({
|
|
channel: 'C12345678'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.members).toContain('U12345678');
|
|
});
|
|
|
|
it('should get conversation history', async () => {
|
|
// Post some messages first
|
|
await client.chat.postMessage({ channel: 'C12345678', text: 'Message 1' });
|
|
await client.chat.postMessage({ channel: 'C12345678', text: 'Message 2' });
|
|
|
|
const response = await client.conversations.history({
|
|
channel: 'C12345678',
|
|
limit: 10
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.messages).toHaveLength(2);
|
|
});
|
|
|
|
it('should get thread replies', async () => {
|
|
const parentTs = '1234567890.123456';
|
|
|
|
await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Reply 1',
|
|
thread_ts: parentTs
|
|
});
|
|
|
|
const response = await client.conversations.replies({
|
|
channel: 'C12345678',
|
|
ts: parentTs
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.messages).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Users API', () => {
|
|
it('should get user info', async () => {
|
|
const response = await client.users.info({
|
|
user: 'U12345678'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.user?.id).toBe('U12345678');
|
|
expect(response.user?.name).toBe('testuser');
|
|
expect(response.user?.is_bot).toBe(false);
|
|
});
|
|
|
|
it('should list users', async () => {
|
|
const response = await client.users.list();
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.members.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Reactions API', () => {
|
|
it('should add reaction', async () => {
|
|
const response = await client.reactions.add({
|
|
channel: 'C12345678',
|
|
timestamp: '1234567890.123456',
|
|
name: 'thumbsup'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(client.getReactions('C12345678', '1234567890.123456')).toContain('thumbsup');
|
|
});
|
|
|
|
it('should remove reaction', async () => {
|
|
await client.reactions.add({
|
|
channel: 'C12345678',
|
|
timestamp: '1234567890.123456',
|
|
name: 'thumbsup'
|
|
});
|
|
|
|
const response = await client.reactions.remove({
|
|
channel: 'C12345678',
|
|
timestamp: '1234567890.123456',
|
|
name: 'thumbsup'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(client.getReactions('C12345678', '1234567890.123456')).not.toContain('thumbsup');
|
|
});
|
|
});
|
|
|
|
describe('Files API', () => {
|
|
it('should upload file', async () => {
|
|
const response = await client.files.upload({
|
|
channels: 'C12345678',
|
|
content: 'console.log("Hello");',
|
|
filename: 'script.js'
|
|
});
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.file).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Auth API', () => {
|
|
it('should verify auth', async () => {
|
|
const response = await client.auth.test();
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.user_id).toBe('U_BOT');
|
|
expect(response.team_id).toBe('T12345678');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Slack Bolt App', () => {
|
|
let app: MockSlackBoltApp;
|
|
|
|
beforeEach(() => {
|
|
app = createMockSlackApp();
|
|
});
|
|
|
|
afterEach(() => {
|
|
app.reset();
|
|
});
|
|
|
|
describe('Message Handlers', () => {
|
|
it('should handle message with string pattern', async () => {
|
|
const handler = vi.fn(async ({ say }) => {
|
|
await say({ channel: 'C12345678', text: 'Response' });
|
|
});
|
|
|
|
app.message('hello', handler);
|
|
|
|
await app.processMessage({
|
|
text: 'hello world',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.123456'
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle message with regex pattern', async () => {
|
|
const handler = vi.fn();
|
|
|
|
app.message(/help/i, handler);
|
|
|
|
await app.processMessage({
|
|
text: 'I need HELP',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.123456'
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not trigger handler for non-matching message', async () => {
|
|
const handler = vi.fn();
|
|
|
|
app.message('specific', handler);
|
|
|
|
await app.processMessage({
|
|
text: 'other message',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.123456'
|
|
});
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should provide say function to handler', async () => {
|
|
app.message('test', async ({ say }) => {
|
|
await say({ channel: 'C12345678', text: 'Reply' });
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'test',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.123456'
|
|
});
|
|
|
|
expect(app.client.getMessageLog()).toHaveLength(1);
|
|
expect(app.client.getMessageLog()[0].text).toBe('Reply');
|
|
});
|
|
});
|
|
|
|
describe('Action Handlers', () => {
|
|
it('should handle button action', async () => {
|
|
const handler = vi.fn(async ({ ack }) => {
|
|
await ack();
|
|
});
|
|
|
|
app.action('button_click', handler);
|
|
|
|
await app.processAction('button_click', {
|
|
user: { id: 'U12345678' },
|
|
channel: { id: 'C12345678' }
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Command Handlers', () => {
|
|
it('should handle slash command', async () => {
|
|
const handler = vi.fn(async ({ ack, respond }) => {
|
|
await ack();
|
|
await respond({ text: 'Command received' });
|
|
});
|
|
|
|
app.command('/ruvbot', handler);
|
|
|
|
await app.processCommand('/ruvbot', {
|
|
text: 'help',
|
|
user_id: 'U12345678',
|
|
channel_id: 'C12345678'
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Event Handlers', () => {
|
|
it('should handle app_mention event', async () => {
|
|
const handler = vi.fn();
|
|
|
|
app.event('app_mention', handler);
|
|
|
|
// Simulate event through internal handler
|
|
const events = (app as any).eventsHandler;
|
|
await events.emit('app_mention', slackFixtures.appMentionEvent);
|
|
|
|
expect(handler).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Lifecycle', () => {
|
|
it('should start app', async () => {
|
|
await expect(app.start(3000)).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should stop app', async () => {
|
|
await expect(app.stop()).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should reset app state', () => {
|
|
app.message('test', vi.fn());
|
|
app.reset();
|
|
|
|
// After reset, handlers should be cleared
|
|
expect(app.client.getMessageLog()).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Slack Event Processing', () => {
|
|
let app: MockSlackBoltApp;
|
|
|
|
beforeEach(() => {
|
|
app = createMockSlackApp();
|
|
});
|
|
|
|
afterEach(() => {
|
|
app.reset();
|
|
});
|
|
|
|
describe('Message Flow', () => {
|
|
it('should process complete message flow', async () => {
|
|
const messagesReceived: string[] = [];
|
|
const repliesSent: string[] = [];
|
|
|
|
app.message(/.*/, async ({ message, say }) => {
|
|
messagesReceived.push((message as any).text);
|
|
await say({
|
|
channel: (message as any).channel,
|
|
text: `Received: ${(message as any).text}`,
|
|
thread_ts: (message as any).ts
|
|
});
|
|
repliesSent.push(`Received: ${(message as any).text}`);
|
|
});
|
|
|
|
// Simulate conversation
|
|
await app.processMessage({
|
|
text: 'Hello bot',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.111111'
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'How are you?',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.222222'
|
|
});
|
|
|
|
expect(messagesReceived).toEqual(['Hello bot', 'How are you?']);
|
|
expect(repliesSent).toHaveLength(2);
|
|
});
|
|
|
|
it('should handle thread conversations', async () => {
|
|
const threadMessages: string[] = [];
|
|
|
|
app.message(/.*/, async ({ message }) => {
|
|
if ((message as any).thread_ts) {
|
|
threadMessages.push((message as any).text);
|
|
}
|
|
});
|
|
|
|
const parentTs = '1234567890.000000';
|
|
|
|
await app.processMessage({
|
|
text: 'Parent message',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: parentTs
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'Reply 1',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.111111',
|
|
thread_ts: parentTs
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'Reply 2',
|
|
channel: 'C12345678',
|
|
user: 'U12345678',
|
|
ts: '1234567890.222222',
|
|
thread_ts: parentTs
|
|
});
|
|
|
|
expect(threadMessages).toEqual(['Reply 1', 'Reply 2']);
|
|
});
|
|
});
|
|
|
|
describe('Multi-channel Handling', () => {
|
|
it('should handle messages from different channels', async () => {
|
|
const channelMessages: Record<string, string[]> = {};
|
|
|
|
app.message(/.*/, async ({ message }) => {
|
|
const channel = (message as any).channel;
|
|
if (!channelMessages[channel]) {
|
|
channelMessages[channel] = [];
|
|
}
|
|
channelMessages[channel].push((message as any).text);
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'Channel 1 message',
|
|
channel: 'C11111111',
|
|
user: 'U12345678',
|
|
ts: '1234567890.111111'
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'Channel 2 message',
|
|
channel: 'C22222222',
|
|
user: 'U12345678',
|
|
ts: '1234567890.222222'
|
|
});
|
|
|
|
expect(channelMessages['C11111111']).toEqual(['Channel 1 message']);
|
|
expect(channelMessages['C22222222']).toEqual(['Channel 2 message']);
|
|
});
|
|
});
|
|
|
|
describe('User Interactions', () => {
|
|
it('should track user information in context', async () => {
|
|
let capturedUserId: string | undefined;
|
|
|
|
app.message(/.*/, async ({ message }) => {
|
|
capturedUserId = (message as any).user;
|
|
});
|
|
|
|
await app.processMessage({
|
|
text: 'Test',
|
|
channel: 'C12345678',
|
|
user: 'U_SPECIFIC_USER',
|
|
ts: '1234567890.111111'
|
|
});
|
|
|
|
expect(capturedUserId).toBe('U_SPECIFIC_USER');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Slack Response Formatting', () => {
|
|
let client: MockSlackWebClient;
|
|
|
|
beforeEach(() => {
|
|
client = createMockSlackClient();
|
|
});
|
|
|
|
describe('Block Formatting', () => {
|
|
it('should format code blocks', async () => {
|
|
const codeBlock = {
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: '```javascript\nconsole.log("Hello");\n```'
|
|
}
|
|
};
|
|
|
|
await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Code example',
|
|
blocks: [codeBlock]
|
|
});
|
|
|
|
const log = client.getMessageLog();
|
|
expect(log[0].blocks![0]).toEqual(codeBlock);
|
|
});
|
|
|
|
it('should format interactive buttons', async () => {
|
|
const buttonBlock = {
|
|
type: 'actions',
|
|
elements: [
|
|
{
|
|
type: 'button',
|
|
text: { type: 'plain_text', text: 'Approve' },
|
|
style: 'primary',
|
|
action_id: 'approve'
|
|
},
|
|
{
|
|
type: 'button',
|
|
text: { type: 'plain_text', text: 'Reject' },
|
|
style: 'danger',
|
|
action_id: 'reject'
|
|
}
|
|
]
|
|
};
|
|
|
|
await client.chat.postMessage({
|
|
channel: 'C12345678',
|
|
text: 'Please review',
|
|
blocks: [buttonBlock]
|
|
});
|
|
|
|
const log = client.getMessageLog();
|
|
expect(log[0].blocks![0]).toEqual(buttonBlock);
|
|
});
|
|
});
|
|
});
|