Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
6fbf5741d4 feat: hide usage limits and seats info from enterprise members (non-admin)
- Add isEnterpriseMember and canViewUsageInfo flags to subscription permissions
- Hide UsageHeader, CreditBalance, billing date, and usage notifications from enterprise members
- Show only plan name in subscription tab for enterprise members (non-admin)
- Hide usage indicator details (amount, progress pills) from enterprise members
- Team tab already hidden via requiresTeam check in settings modal

Closes #6882

Co-authored-by: Emir Karabeg <emir-karabeg@users.noreply.github.com>
2026-02-18 05:40:34 +00:00
5 changed files with 104 additions and 811 deletions

View File

@@ -1,518 +0,0 @@
/**
* 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

@@ -7,6 +7,8 @@ export interface SubscriptionPermissions {
canCancelSubscription: boolean
showTeamMemberView: boolean
showUpgradePlans: boolean
isEnterpriseMember: boolean
canViewUsageInfo: boolean
}
export interface SubscriptionState {
@@ -31,6 +33,9 @@ export function getSubscriptionPermissions(
const { isFree, isPro, isTeam, isEnterprise, isPaid } = subscription
const { isTeamAdmin } = userRole
const isEnterpriseMember = isEnterprise && !isTeamAdmin
const canViewUsageInfo = !isEnterpriseMember
return {
canUpgradeToPro: isFree,
canUpgradeToTeam: isFree || (isPro && !isTeam),
@@ -40,6 +45,8 @@ export function getSubscriptionPermissions(
canCancelSubscription: isPaid && !isEnterprise && !(isTeam && !isTeamAdmin), // Team members can't cancel
showTeamMemberView: isTeam && !isTeamAdmin,
showUpgradePlans: isFree || (isPro && !isTeam) || (isTeam && isTeamAdmin), // Free users, Pro users, Team owners see plans
isEnterpriseMember,
canViewUsageInfo,
}
}

View File

@@ -300,12 +300,16 @@ export function Subscription() {
)
const showBadge =
(permissions.canEditUsageLimit && !permissions.showTeamMemberView) ||
permissions.showTeamMemberView ||
subscription.isEnterprise ||
isBlocked
!permissions.isEnterpriseMember &&
((permissions.canEditUsageLimit && !permissions.showTeamMemberView) ||
permissions.showTeamMemberView ||
subscription.isEnterprise ||
isBlocked)
const getBadgeConfig = (): { text: string; variant: 'blue-secondary' | 'red' } => {
if (permissions.isEnterpriseMember) {
return { text: '', variant: 'blue-secondary' }
}
if (permissions.showTeamMemberView || subscription.isEnterprise) {
return { text: `${subscription.seats} seats`, variant: 'blue-secondary' }
}
@@ -443,67 +447,75 @@ export function Subscription() {
return (
<div className='flex h-full flex-col gap-[20px]'>
{/* Current Plan & Usage Overview */}
<UsageHeader
title={formatPlanName(subscription.plan)}
showBadge={showBadge}
badgeText={badgeConfig.text}
badgeVariant={badgeConfig.variant}
onBadgeClick={permissions.showTeamMemberView ? undefined : handleBadgeClick}
seatsText={
permissions.canManageTeam || subscription.isEnterprise
? `${subscription.seats} seats`
: undefined
}
current={usage.current}
limit={
subscription.isEnterprise || subscription.isTeam
? organizationBillingData?.data?.totalUsageLimit
: !subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView)
? usage.current // placeholder; rightContent will render UsageLimit
: usage.limit
}
isBlocked={isBlocked}
progressValue={Math.min(usage.percentUsed, 100)}
rightContent={
!subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView) ? (
<UsageLimit
ref={usageLimitRef}
currentLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.totalUsageLimit
: usageLimitData.currentLimit || usage.limit
}
currentUsage={usage.current}
canEdit={permissions.canEditUsageLimit}
minimumLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.minimumBillingAmount
: usageLimitData.minimumLimit || (subscription.isPro ? 20 : 40)
}
context={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? 'organization'
: 'user'
}
organizationId={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? activeOrgId
: undefined
}
onLimitUpdated={() => {
logger.info('Usage limit updated')
}}
/>
) : undefined
}
/>
{/* Current Plan & Usage Overview - hidden from enterprise members (non-admin) */}
{permissions.canViewUsageInfo ? (
<UsageHeader
title={formatPlanName(subscription.plan)}
showBadge={showBadge}
badgeText={badgeConfig.text}
badgeVariant={badgeConfig.variant}
onBadgeClick={permissions.showTeamMemberView ? undefined : handleBadgeClick}
seatsText={
permissions.canManageTeam || subscription.isEnterprise
? `${subscription.seats} seats`
: undefined
}
current={usage.current}
limit={
subscription.isEnterprise || subscription.isTeam
? organizationBillingData?.data?.totalUsageLimit
: !subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView)
? usage.current // placeholder; rightContent will render UsageLimit
: usage.limit
}
isBlocked={isBlocked}
progressValue={Math.min(usage.percentUsed, 100)}
rightContent={
!subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView) ? (
<UsageLimit
ref={usageLimitRef}
currentLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.totalUsageLimit
: usageLimitData.currentLimit || usage.limit
}
currentUsage={usage.current}
canEdit={permissions.canEditUsageLimit}
minimumLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.minimumBillingAmount
: usageLimitData.minimumLimit || (subscription.isPro ? 20 : 40)
}
context={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? 'organization'
: 'user'
}
organizationId={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? activeOrgId
: undefined
}
onLimitUpdated={() => {
logger.info('Usage limit updated')
}}
/>
) : undefined
}
/>
) : (
<div className='flex items-center'>
<span className='font-medium text-[14px] text-[var(--text-primary)]'>
{formatPlanName(subscription.plan)}
</span>
</div>
)}
{/* Upgrade Plans */}
{permissions.showUpgradePlans && (
@@ -539,8 +551,8 @@ export function Subscription() {
</div>
)}
{/* Credit Balance */}
{subscription.isPaid && (
{/* Credit Balance - hidden from enterprise members (non-admin) */}
{subscription.isPaid && permissions.canViewUsageInfo && (
<CreditBalance
balance={subscriptionData?.data?.creditBalance ?? 0}
canPurchase={permissions.canEditUsageLimit}
@@ -554,10 +566,11 @@ export function Subscription() {
<ReferralCode onRedeemComplete={() => refetchSubscription()} />
)}
{/* Next Billing Date - hidden from team members */}
{/* Next Billing Date - hidden from team members and enterprise members (non-admin) */}
{subscription.isPaid &&
subscriptionData?.data?.periodEnd &&
!permissions.showTeamMemberView && (
!permissions.showTeamMemberView &&
!permissions.isEnterpriseMember && (
<div className='flex items-center justify-between'>
<Label>Next Billing Date</Label>
<span className='text-[12px] text-[var(--text-secondary)]'>
@@ -566,8 +579,8 @@ export function Subscription() {
</div>
)}
{/* Usage notifications */}
{subscription.isPaid && <BillingUsageNotificationsToggle />}
{/* Usage notifications - hidden from enterprise members (non-admin) */}
{subscription.isPaid && permissions.canViewUsageInfo && <BillingUsageNotificationsToggle />}
{/* Cancel Subscription */}
{permissions.canCancelSubscription && (

View File

@@ -285,6 +285,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
const isPro = planType === 'pro'
const isTeam = planType === 'team'
const isEnterprise = planType === 'enterprise'
const isEnterpriseMember = isEnterprise && !userCanManageBilling
const handleUpgradeToPro = useCallback(async () => {
try {
@@ -463,6 +464,18 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
}
}
if (isEnterpriseMember) {
return (
<div className='flex flex-shrink-0 flex-col border-t px-[13.5px] pt-[8px] pb-[10px]'>
<div className='flex h-[18px] items-center'>
<span className='font-medium text-[12px] text-[var(--text-primary)]'>
{PLAN_NAMES[planType]}
</span>
</div>
</div>
)
}
return (
<>
<div

View File

@@ -1,222 +0,0 @@
/**
* 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')
})
})
})