mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-18 10:22:00 -05:00
Compare commits
1 Commits
cursor/ent
...
cursor/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc4c42e9de |
518
apps/sim/app/api/tools/slack/add-reaction/route.test.ts
Normal file
518
apps/sim/app/api/tools/slack/add-reaction/route.test.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -7,8 +7,6 @@ export interface SubscriptionPermissions {
|
||||
canCancelSubscription: boolean
|
||||
showTeamMemberView: boolean
|
||||
showUpgradePlans: boolean
|
||||
isEnterpriseMember: boolean
|
||||
canViewUsageInfo: boolean
|
||||
}
|
||||
|
||||
export interface SubscriptionState {
|
||||
@@ -33,9 +31,6 @@ 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),
|
||||
@@ -45,8 +40,6 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,16 +300,12 @@ export function Subscription() {
|
||||
)
|
||||
|
||||
const showBadge =
|
||||
!permissions.isEnterpriseMember &&
|
||||
((permissions.canEditUsageLimit && !permissions.showTeamMemberView) ||
|
||||
permissions.showTeamMemberView ||
|
||||
subscription.isEnterprise ||
|
||||
isBlocked)
|
||||
(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' }
|
||||
}
|
||||
@@ -447,75 +443,67 @@ export function Subscription() {
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-[20px]'>
|
||||
{/* 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>
|
||||
)}
|
||||
{/* 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
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Upgrade Plans */}
|
||||
{permissions.showUpgradePlans && (
|
||||
@@ -551,8 +539,8 @@ export function Subscription() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Credit Balance - hidden from enterprise members (non-admin) */}
|
||||
{subscription.isPaid && permissions.canViewUsageInfo && (
|
||||
{/* Credit Balance */}
|
||||
{subscription.isPaid && (
|
||||
<CreditBalance
|
||||
balance={subscriptionData?.data?.creditBalance ?? 0}
|
||||
canPurchase={permissions.canEditUsageLimit}
|
||||
@@ -566,11 +554,10 @@ export function Subscription() {
|
||||
<ReferralCode onRedeemComplete={() => refetchSubscription()} />
|
||||
)}
|
||||
|
||||
{/* Next Billing Date - hidden from team members and enterprise members (non-admin) */}
|
||||
{/* Next Billing Date - hidden from team members */}
|
||||
{subscription.isPaid &&
|
||||
subscriptionData?.data?.periodEnd &&
|
||||
!permissions.showTeamMemberView &&
|
||||
!permissions.isEnterpriseMember && (
|
||||
!permissions.showTeamMemberView && (
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label>Next Billing Date</Label>
|
||||
<span className='text-[12px] text-[var(--text-secondary)]'>
|
||||
@@ -579,8 +566,8 @@ export function Subscription() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage notifications - hidden from enterprise members (non-admin) */}
|
||||
{subscription.isPaid && permissions.canViewUsageInfo && <BillingUsageNotificationsToggle />}
|
||||
{/* Usage notifications */}
|
||||
{subscription.isPaid && <BillingUsageNotificationsToggle />}
|
||||
|
||||
{/* Cancel Subscription */}
|
||||
{permissions.canCancelSubscription && (
|
||||
|
||||
@@ -285,7 +285,6 @@ 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 {
|
||||
@@ -464,18 +463,6 @@ 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
|
||||
|
||||
222
apps/sim/blocks/blocks/slack.test.ts
Normal file
222
apps/sim/blocks/blocks/slack.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user