Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/sim/app/api/chat/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { checkServerSideUsageLimits } from '@/lib/billing'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
Expand Down Expand Up @@ -330,6 +331,22 @@ export async function executeWorkflowForChat(
const workflowId = deployment.workflowId
const executionId = uuidv4()

const usageCheck = await checkServerSideUsageLimits(deployment.userId)
if (usageCheck.isExceeded) {
logger.warn(
`[${requestId}] User ${deployment.userId} has exceeded usage limits. Skipping chat execution.`,
{
currentUsage: usageCheck.currentUsage,
limit: usageCheck.limit,
workflowId: deployment.workflowId,
chatId,
}
)
throw new Error(
usageCheck.message || 'Usage limit exceeded. Please upgrade your plan to continue using chat.'
)
}

// Set up logging for chat execution
const loggingSession = new LoggingSession(workflowId, executionId, 'chat', requestId)

Expand Down
209 changes: 0 additions & 209 deletions apps/sim/app/api/webhooks/trigger/[path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,219 +284,10 @@ describe('Webhook Trigger API Route', () => {
expect(text).toMatch(/not found/i) // Response should contain "not found" message
})

/**
* Test duplicate webhook request handling
* Verifies that duplicate requests are detected and not processed multiple times
*/
it('should handle duplicate webhook requests', async () => {
// Set up duplicate detection
hasProcessedMessageMock.mockResolvedValue(true) // Simulate duplicate
processGenericDeduplicationMock.mockResolvedValue(
new Response('Duplicate request', { status: 200 })
)

// Configure DB mock to return a webhook and workflow
const { db } = await import('@/db')
const limitMock = vi.fn().mockReturnValue([
{
webhook: {
id: 'webhook-id',
path: 'test-path',
isActive: true,
provider: 'generic', // Not Airtable to test standard path
workflowId: 'workflow-id',
providerConfig: {},
},
workflow: {
id: 'workflow-id',
userId: 'user-id',
},
},
])

const whereMock = vi.fn().mockReturnValue({ limit: limitMock })
const innerJoinMock = vi.fn().mockReturnValue({ where: whereMock })
const fromMock = vi.fn().mockReturnValue({ innerJoin: innerJoinMock })

// @ts-ignore - mocking the query chain
db.select.mockReturnValue({ from: fromMock })

// Create a mock request
const req = createMockRequest('POST', { event: 'test' })

// Mock the path param
const params = Promise.resolve({ path: 'test-path' })

// Import the handler after mocks are set up
const { POST } = await import('@/app/api/webhooks/trigger/[path]/route')

// Call the handler
const response = await POST(req, { params })

// Expect 200 response for duplicate
expect(response.status).toBe(200)

// Verify response text indicates duplication
const text = await response.text()
expect(text).toMatch(/duplicate|received/i) // Response might be "Duplicate message" or "Request received"
})

/**
* Test Slack-specific webhook handling
* Verifies that Slack signature verification is performed
*/
// TODO: Fix failing test - returns 500 instead of 200
// it('should handle Slack webhooks with signature verification', async () => { ... })

/**
* Test error handling during webhook execution
*/
it('should handle errors during workflow execution', async () => {
// Mock the setTimeout to be faster for testing
// @ts-ignore - Replace global setTimeout for this test
global.setTimeout = vi.fn((callback) => {
callback() // Execute immediately
return 123 // Return a timer ID
})

// Set up error handling mocks
processWebhookMock.mockImplementation(() => {
throw new Error('Webhook execution failed')
})
executeMock.mockRejectedValue(new Error('Webhook execution failed'))

// Configure DB mock to return a webhook and workflow
const { db } = await import('@/db')
const limitMock = vi.fn().mockReturnValue([
{
webhook: {
id: 'webhook-id',
path: 'test-path',
isActive: true,
provider: 'generic', // Not Airtable to ensure we use the timeout path
workflowId: 'workflow-id',
providerConfig: {},
},
workflow: {
id: 'workflow-id',
userId: 'user-id',
},
},
])

const whereMock = vi.fn().mockReturnValue({ limit: limitMock })
const innerJoinMock = vi.fn().mockReturnValue({ where: whereMock })
const fromMock = vi.fn().mockReturnValue({ innerJoin: innerJoinMock })

// @ts-ignore - mocking the query chain
db.select.mockReturnValue({ from: fromMock })

// Create a mock request
const req = createMockRequest('POST', { event: 'test' })

// Mock the path param
const params = Promise.resolve({ path: 'test-path' })

// Import the handler after mocks are set up
const { POST } = await import('@/app/api/webhooks/trigger/[path]/route')

// Call the handler
const response = await POST(req, { params })

// Verify response exists and check status code
// For non-Airtable webhooks, we expect 200 from the timeout response
expect(response).toBeDefined()
expect(response.status).toBe(200)

// Verify response text
const text = await response.text()
expect(text).toMatch(/received|processing/i)
})

/**
* Test Airtable webhook specific handling
* Verifies that Airtable webhooks use the synchronous processing path
*/
it('should handle Airtable webhooks synchronously', async () => {
// Create webhook payload for Airtable
const airtablePayload = {
base: {
id: 'appn9RltLQQMsquyL',
},
webhook: {
id: 'achpbXeBqNLsRFAnD',
},
timestamp: new Date().toISOString(),
}

// Reset fetch and process mock
fetchAndProcessAirtablePayloadsMock.mockResolvedValue(undefined)

// Configure DB mock to return an Airtable webhook
const { db } = await import('@/db')
const limitMock = vi.fn().mockReturnValue([
{
webhook: {
id: 'airtable-webhook-id',
path: 'airtable-path',
isActive: true,
provider: 'airtable', // Set provider to airtable to test that path
workflowId: 'workflow-id',
providerConfig: {
baseId: 'appn9RltLQQMsquyL',
externalId: 'achpbXeBqNLsRFAnD',
},
},
workflow: {
id: 'workflow-id',
userId: 'user-id',
},
},
])

const whereMock = vi.fn().mockReturnValue({ limit: limitMock })
const innerJoinMock = vi.fn().mockReturnValue({ where: whereMock })
const fromMock = vi.fn().mockReturnValue({ innerJoin: innerJoinMock })

// Configure db.select to return the appropriate mock for this test
// @ts-ignore - Ignore TypeScript errors for test mocks
db.select = vi.fn().mockReturnValue({ from: fromMock })

// Also mock the DB for the Airtable notification check
const whereMock2 = vi.fn().mockReturnValue({ limit: vi.fn().mockReturnValue([]) })
const fromMock2 = vi.fn().mockReturnValue({ where: whereMock2 })

// We need to handle multiple calls to db.select
let callCount = 0
// @ts-ignore - Ignore TypeScript errors for test mocks
db.select = vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) {
return { from: fromMock }
}
return { from: fromMock2 }
})

// Create a mock request with Airtable payload
const req = createMockRequest('POST', airtablePayload)

// Mock the path param
const params = Promise.resolve({ path: 'airtable-path' })

// Import the handler after mocks are set up
const { POST } = await import('@/app/api/webhooks/trigger/[path]/route')

// Call the handler
const response = await POST(req, { params })

// For Airtable we expect 200 after synchronous processing
expect(response.status).toBe(200)

// Verify that the Airtable-specific function was called
expect(fetchAndProcessAirtablePayloadsMock).toHaveBeenCalledTimes(1)

// The response should indicate success
const text = await response.text()
expect(text).toMatch(/success|processed/i)
})
})
Loading