Compare commits

...

1 Commits

Author SHA1 Message Date
Cursor Agent
bc4c42e9de test: add comprehensive tests for Slack add-reaction tool
- Add route.test.ts for the add-reaction API route
  - Tests successful reaction addition
  - Tests authentication handling
  - Tests validation of required fields (access token, channel, timestamp, emoji)
  - Tests Slack API error handling (missing_scope, channel_not_found, message_not_found, invalid_name, already_reacted)
  - Tests network error handling
  - Tests various emoji names

- Add slack.test.ts for the Slack block configuration
  - Tests block configuration and tool access
  - Tests tools.config.tool function returns correct tool names
  - Tests tools.config.params correctly maps react operation params
  - Tests subBlocks configuration for react operation
  - Tests inputs configuration

Validates that the Slack add-reaction tool works correctly.

Co-authored-by: Emir Karabeg <emir-karabeg@users.noreply.github.com>
2026-02-18 07:37:03 +00:00
2 changed files with 740 additions and 0 deletions

View File

@@ -0,0 +1,518 @@
/**
* Tests for Slack Add Reaction API route
*
* @vitest-environment node
*/
import {
createMockFetch,
createMockLogger,
createMockRequest,
createMockResponse,
} from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('Slack Add Reaction API Route', () => {
const mockLogger = createMockLogger()
const mockCheckInternalAuth = vi.fn()
beforeEach(() => {
vi.resetModules()
vi.doMock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/auth/hybrid', () => ({
checkInternalAuth: mockCheckInternalAuth,
}))
})
afterEach(() => {
vi.clearAllMocks()
vi.unstubAllGlobals()
})
it('should add reaction successfully', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: true },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.content).toBe('Successfully added :thumbsup: reaction')
expect(data.output.metadata).toEqual({
channel: 'C1234567890',
timestamp: '1405894322.002768',
reaction: 'thumbsup',
})
expect(mockFetch).toHaveBeenCalledWith(
'https://slack.com/api/reactions.add',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
Authorization: 'Bearer xoxb-test-token',
}),
body: JSON.stringify({
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
}),
})
)
})
it('should handle emoji name without colons', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: true },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'eyes',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.content).toBe('Successfully added :eyes: reaction')
})
it('should handle unauthenticated request', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: false,
error: 'Authentication required',
})
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.success).toBe(false)
expect(data.error).toBe('Authentication required')
})
it('should handle missing access token', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const req = createMockRequest(
'POST',
{
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Invalid request data')
})
it('should handle missing channel', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Invalid request data')
})
it('should handle missing timestamp', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Invalid request data')
})
it('should handle missing emoji name', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Invalid request data')
})
it('should handle Slack API missing_scope error', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: false, error: 'missing_scope' },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toBe('missing_scope')
})
it('should handle Slack API channel_not_found error', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: false, error: 'channel_not_found' },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'CINVALID',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toBe('channel_not_found')
})
it('should handle Slack API message_not_found error', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: false, error: 'message_not_found' },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '0000000000.000000',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toBe('message_not_found')
})
it('should handle Slack API invalid_name error for invalid emoji', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: false, error: 'invalid_name' },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'not_a_valid_emoji',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toBe('invalid_name')
})
it('should handle Slack API already_reacted error', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: false, error: 'already_reacted' },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toBe('already_reacted')
})
it('should handle network error when calling Slack API', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockFetch = vi.fn().mockRejectedValueOnce(new Error('Network error'))
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toBe('Network error')
})
it('should handle various emoji names correctly', async () => {
const emojiNames = ['heart', 'fire', 'rocket', '+1', '-1', 'tada', 'eyes', 'thinking_face']
for (const emojiName of emojiNames) {
vi.resetModules()
vi.doMock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/auth/hybrid', () => ({
checkInternalAuth: mockCheckInternalAuth,
}))
mockCheckInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'api-key',
})
const mockSlackResponse = createMockResponse({
status: 200,
json: { ok: true },
})
const mockFetch = vi.fn().mockResolvedValueOnce(mockSlackResponse)
vi.stubGlobal('fetch', mockFetch)
const req = createMockRequest(
'POST',
{
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: emojiName,
},
{},
'http://localhost:3000/api/tools/slack/add-reaction'
)
const { POST } = await import('@/app/api/tools/slack/add-reaction/route')
const response = await POST(req as any)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.output.metadata.reaction).toBe(emojiName)
}
})
})

View File

@@ -0,0 +1,222 @@
/**
* Tests for Slack Block configuration
*
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { SlackBlock } from './slack'
describe('SlackBlock', () => {
describe('basic configuration', () => {
it('should have correct type and name', () => {
expect(SlackBlock.type).toBe('slack')
expect(SlackBlock.name).toBe('Slack')
})
it('should have slack_add_reaction in tools access', () => {
expect(SlackBlock.tools.access).toContain('slack_add_reaction')
})
it('should have tools.config.tool function', () => {
expect(typeof SlackBlock.tools.config?.tool).toBe('function')
})
it('should have tools.config.params function', () => {
expect(typeof SlackBlock.tools.config?.params).toBe('function')
})
})
describe('tools.config.tool', () => {
const getToolName = SlackBlock.tools.config?.tool
it('should return slack_add_reaction for react operation', () => {
expect(getToolName?.({ operation: 'react' })).toBe('slack_add_reaction')
})
it('should return slack_message for send operation', () => {
expect(getToolName?.({ operation: 'send' })).toBe('slack_message')
})
it('should return slack_delete_message for delete operation', () => {
expect(getToolName?.({ operation: 'delete' })).toBe('slack_delete_message')
})
it('should return slack_update_message for update operation', () => {
expect(getToolName?.({ operation: 'update' })).toBe('slack_update_message')
})
})
describe('tools.config.params for react operation', () => {
const getParams = SlackBlock.tools.config?.params
it('should map reaction params correctly with OAuth auth', () => {
const inputParams = {
operation: 'react',
authMethod: 'oauth',
credential: 'oauth-credential-123',
channel: 'C1234567890',
reactionTimestamp: '1405894322.002768',
emojiName: 'thumbsup',
}
const result = getParams?.(inputParams)
expect(result).toEqual({
credential: 'oauth-credential-123',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'thumbsup',
})
})
it('should map reaction params correctly with bot token auth', () => {
const inputParams = {
operation: 'react',
authMethod: 'bot_token',
botToken: 'xoxb-test-token',
channel: 'C1234567890',
reactionTimestamp: '1405894322.002768',
emojiName: 'eyes',
}
const result = getParams?.(inputParams)
expect(result).toEqual({
accessToken: 'xoxb-test-token',
channel: 'C1234567890',
timestamp: '1405894322.002768',
name: 'eyes',
})
})
it('should handle various emoji names', () => {
const emojiNames = ['heart', 'fire', 'rocket', '+1', '-1', 'tada', 'thinking_face']
for (const emoji of emojiNames) {
const inputParams = {
operation: 'react',
authMethod: 'bot_token',
botToken: 'xoxb-test-token',
channel: 'C1234567890',
reactionTimestamp: '1405894322.002768',
emojiName: emoji,
}
const result = getParams?.(inputParams)
expect(result?.name).toBe(emoji)
}
})
it('should handle channel from trigger data', () => {
const inputParams = {
operation: 'react',
authMethod: 'bot_token',
botToken: 'xoxb-test-token',
channel: 'C9876543210',
reactionTimestamp: '1234567890.123456',
emojiName: 'white_check_mark',
}
const result = getParams?.(inputParams)
expect(result?.channel).toBe('C9876543210')
expect(result?.timestamp).toBe('1234567890.123456')
})
it('should trim whitespace from channel', () => {
const inputParams = {
operation: 'react',
authMethod: 'bot_token',
botToken: 'xoxb-test-token',
channel: ' C1234567890 ',
reactionTimestamp: '1405894322.002768',
emojiName: 'thumbsup',
}
const result = getParams?.(inputParams)
expect(result?.channel).toBe('C1234567890')
})
})
describe('subBlocks for react operation', () => {
it('should have reactionTimestamp subBlock with correct condition', () => {
const reactionTimestampSubBlock = SlackBlock.subBlocks.find(
(sb) => sb.id === 'reactionTimestamp'
)
expect(reactionTimestampSubBlock).toBeDefined()
expect(reactionTimestampSubBlock?.type).toBe('short-input')
expect(reactionTimestampSubBlock?.required).toBe(true)
expect(reactionTimestampSubBlock?.condition).toEqual({
field: 'operation',
value: 'react',
})
})
it('should have emojiName subBlock with correct condition', () => {
const emojiNameSubBlock = SlackBlock.subBlocks.find((sb) => sb.id === 'emojiName')
expect(emojiNameSubBlock).toBeDefined()
expect(emojiNameSubBlock?.type).toBe('short-input')
expect(emojiNameSubBlock?.required).toBe(true)
expect(emojiNameSubBlock?.condition).toEqual({
field: 'operation',
value: 'react',
})
})
it('should have channel subBlock that shows for react operation', () => {
const channelSubBlock = SlackBlock.subBlocks.find((sb) => sb.id === 'channel')
expect(channelSubBlock).toBeDefined()
const condition = channelSubBlock?.condition as {
field: string
value: string[]
not: boolean
and: { field: string; value: string; not: boolean }
}
expect(condition.field).toBe('operation')
expect(condition.value).not.toContain('react')
expect(condition.not).toBe(true)
})
})
describe('inputs configuration', () => {
it('should have timestamp input for reaction', () => {
expect(SlackBlock.inputs.timestamp).toBeDefined()
expect(SlackBlock.inputs.timestamp.type).toBe('string')
})
it('should have name input for emoji', () => {
expect(SlackBlock.inputs.name).toBeDefined()
expect(SlackBlock.inputs.name.type).toBe('string')
})
it('should have reactionTimestamp input', () => {
expect(SlackBlock.inputs.reactionTimestamp).toBeDefined()
expect(SlackBlock.inputs.reactionTimestamp.type).toBe('string')
})
it('should have emojiName input', () => {
expect(SlackBlock.inputs.emojiName).toBeDefined()
expect(SlackBlock.inputs.emojiName.type).toBe('string')
})
})
describe('operation dropdown', () => {
it('should include Add Reaction option', () => {
const operationSubBlock = SlackBlock.subBlocks.find((sb) => sb.id === 'operation')
expect(operationSubBlock?.type).toBe('dropdown')
const options = operationSubBlock?.options as Array<{ label: string; id: string }>
const reactOption = options?.find((opt) => opt.id === 'react')
expect(reactOption).toBeDefined()
expect(reactOption?.label).toBe('Add Reaction')
})
})
})