/** * @vitest-environment node */ import { createMockRequest, executionPreprocessingMock, executionPreprocessingMockFns, hybridAuthMockFns, loggingSessionMock, requestUtilsMockFns, workflowAuthzMockFns, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockEnqueue, mockExecuteWorkflowCore, mockHandlePostExecutionPauseState, mockIsWorkspaceApiExecutionEntitled, } = vi.hoisted(() => ({ mockEnqueue: vi.fn().mockResolvedValue('job-123'), mockExecuteWorkflowCore: vi.fn(), mockHandlePostExecutionPauseState: vi.fn(), mockIsWorkspaceApiExecutionEntitled: vi.fn().mockResolvedValue(true), })) vi.mock('@/lib/billing/core/api-access', () => ({ API_EXECUTION_REQUIRES_PAID_PLAN_MESSAGE: 'paid plan required', isWorkspaceApiExecutionEntitled: mockIsWorkspaceApiExecutionEntitled, })) const mockCheckHybridAuth = hybridAuthMockFns.mockCheckHybridAuth const mockPreprocessExecution = executionPreprocessingMockFns.mockPreprocessExecution const mockAuthorizeWorkflowByWorkspacePermission = workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock) vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) vi.mock('@/lib/workflows/executor/execution-core', () => ({ executeWorkflowCore: mockExecuteWorkflowCore, })) vi.mock('@/lib/workflows/executor/pause-persistence', () => ({ handlePostExecutionPauseState: mockHandlePostExecutionPauseState, })) vi.mock('@/lib/execution/payloads/store', () => ({ storeLargeValue: vi.fn(async (_value, _json, size: number) => ({ __simLargeValueRef: true, version: 1, id: 'lv_abcdefghijkl', kind: 'string', size, })), })) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn().mockResolvedValue({ enqueue: mockEnqueue, startJob: vi.fn(), completeJob: vi.fn(), markJobFailed: vi.fn(), }), shouldExecuteInline: vi.fn().mockReturnValue(false), })) vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'), getOllamaUrl: vi.fn().mockReturnValue('http://localhost:11434'), })) vi.mock('@/lib/execution/call-chain', () => ({ SIM_VIA_HEADER: 'x-sim-via', parseCallChain: vi.fn().mockReturnValue([]), validateCallChain: vi.fn().mockReturnValue(null), buildNextCallChain: vi.fn().mockReturnValue(['workflow-1']), })) vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock) vi.mock('@/background/workflow-execution', () => ({ executeWorkflowJob: vi.fn(), })) vi.mock('@sim/utils/id', () => ({ generateId: vi.fn(() => 'execution-123'), generateShortId: vi.fn(() => 'mock-short-id'), isValidUuid: vi.fn((v: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v) ), })) import { storeLargeValue } from '@/lib/execution/payloads/store' import { POST } from './route' describe('workflow execute async route', () => { beforeEach(() => { vi.clearAllMocks() requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-12345678') workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValue(false) hybridAuthMockFns.mockHasExternalApiCredentials.mockReturnValue(true) mockCheckHybridAuth.mockResolvedValue({ success: true, userId: 'session-user-1', authType: 'session', }) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, workflow: { id: 'workflow-1', userId: 'owner-1', workspaceId: 'workspace-1', }, }) mockPreprocessExecution.mockResolvedValue({ success: true, actorUserId: 'actor-1', workflowRecord: { id: 'workflow-1', userId: 'owner-1', workspaceId: 'workspace-1', }, }) workflowsPersistenceUtilsMockFns.mockLoadDeployedWorkflowState.mockResolvedValue(null) workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables.mockResolvedValue(null) mockExecuteWorkflowCore.mockResolvedValue({ success: true, status: 'completed', output: { ok: true }, metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, }) mockHandlePostExecutionPauseState.mockResolvedValue(undefined) }) it('queues async execution with matching correlation metadata', async () => { const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-Execution-Mode': 'async', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(202) expect(body.executionId).toBe('execution-123') expect(body.jobId).toBe('job-123') expect(mockEnqueue).toHaveBeenCalledWith( 'workflow-execution', expect.objectContaining({ workflowId: 'workflow-1', userId: 'actor-1', workspaceId: 'workspace-1', executionId: 'execution-123', executionMode: 'async', }), expect.objectContaining({ metadata: expect.objectContaining({ workflowId: 'workflow-1', userId: 'actor-1', workspaceId: 'workspace-1', correlation: expect.objectContaining({ executionId: 'execution-123', requestId: 'req-12345678', source: 'workflow', workflowId: 'workflow-1', triggerType: 'manual', }), }), }) ) }) it('rejects cross-site session requests before authorization work', async () => { const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'Sec-Fetch-Site': 'cross-site', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(403) expect(body.error).toBe('Access denied') expect(mockAuthorizeWorkflowByWorkspacePermission).not.toHaveBeenCalled() expect(mockEnqueue).not.toHaveBeenCalled() }) it('allows same-site session requests (multi-subdomain Run, e.g. www.)', async () => { const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-Execution-Mode': 'async', 'Sec-Fetch-Site': 'same-site', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) expect(response.status).toBe(202) expect(mockEnqueue).toHaveBeenCalled() }) it('rejects oversized request bodies before authorization work', async () => { const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'Content-Length': String(10 * 1024 * 1024 + 1), } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(413) expect(body.error).toContain('Workflow execution request body') expect(mockAuthorizeWorkflowByWorkspacePermission).not.toHaveBeenCalled() }) it('authenticates before rejecting oversized request bodies', async () => { mockCheckHybridAuth.mockResolvedValueOnce({ success: false, error: 'Unauthorized', authType: 'api_key', }) const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'Content-Length': String(10 * 1024 * 1024 + 1), 'X-API-Key': 'invalid', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(401) expect(body.error).toBe('Unauthorized') expect(mockCheckHybridAuth).toHaveBeenCalled() }) it('returns 499 when a non-SSE execution is cancelled by client disconnect', async () => { const abortController = new AbortController() mockExecuteWorkflowCore.mockImplementationOnce( async ({ abortSignal }: { abortSignal: AbortSignal }) => { abortController.abort() expect(abortSignal.aborted).toBe(true) return { success: false, status: 'cancelled', output: { partial: true }, metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, } } ) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-1/execute', { method: 'POST', body: JSON.stringify({ input: { hello: 'world' } }), signal: abortController.signal, }) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(499) expect(body.error).toBe('Client cancelled request') }) it('rejects large MCP bridge outputs instead of returning large-value refs', async () => { mockCheckHybridAuth.mockResolvedValueOnce({ success: true, userId: 'internal-user-1', authType: 'internal_jwt', }) mockExecuteWorkflowCore.mockResolvedValueOnce({ success: true, status: 'completed', output: 'x'.repeat(10 * 1024 * 1024 + 1), metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, }) const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-Sim-MCP-Tool-Call': 'true', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(413) expect(body.error).toContain('Workflow execution response') expect(storeLargeValue).not.toHaveBeenCalled() }) it('does not trust client-spoofed MCP bridge headers on API key executions', async () => { mockCheckHybridAuth.mockResolvedValueOnce({ success: true, userId: 'api-user-1', authType: 'api_key', apiKeyType: 'personal', }) workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true) workflowsUtilsMockFns.mockCreateHttpResponseFromBlock.mockResolvedValueOnce( Response.json({ response: 'plain text body' }) ) mockExecuteWorkflowCore.mockResolvedValueOnce({ success: true, status: 'completed', output: { response: 'plain text body' }, metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, }) const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-API-Key': 'valid', 'X-Sim-MCP-Tool-Call': 'true', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(200) expect(body).toEqual({ response: 'plain text body' }) expect(workflowsUtilsMockFns.mockCreateHttpResponseFromBlock).toHaveBeenCalled() }) it('keeps trusted internal MCP bridge executions on the JSON envelope path', async () => { mockCheckHybridAuth.mockResolvedValueOnce({ success: true, userId: 'internal-user-1', authType: 'internal_jwt', }) workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValueOnce(true) mockExecuteWorkflowCore.mockResolvedValueOnce({ success: true, status: 'completed', output: { response: 'plain text body' }, metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, }) const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-Sim-MCP-Tool-Call': 'true', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) const body = await response.json() expect(response.status).toBe(200) expect(body).toMatchObject({ success: true, output: { response: 'plain text body' }, }) expect(workflowsUtilsMockFns.mockCreateHttpResponseFromBlock).not.toHaveBeenCalled() expect(mockExecuteWorkflowCore).toHaveBeenCalledWith( expect.objectContaining({ snapshot: expect.objectContaining({ input: { hello: 'world' }, }), }) ) }) it('preserves authenticated-user actor semantics for trusted MCP bridge calls', async () => { mockCheckHybridAuth.mockResolvedValueOnce({ success: true, userId: 'api-user-1', authType: 'internal_jwt', }) mockExecuteWorkflowCore.mockResolvedValueOnce({ success: true, status: 'completed', output: { ok: true }, metadata: { duration: 100, startTime: '2026-01-01T00:00:00Z', endTime: '2026-01-01T00:00:01Z', }, }) const req = createMockRequest( 'POST', { input: { hello: 'world' } }, { 'Content-Type': 'application/json', 'X-Sim-MCP-Tool-Call': 'true', 'X-Sim-MCP-Tool-Actor': 'authenticated-user', } ) const params = Promise.resolve({ id: 'workflow-1' }) const response = await POST(req, { params }) expect(response.status).toBe(200) expect(mockPreprocessExecution).toHaveBeenCalledWith( expect.objectContaining({ userId: 'api-user-1', useAuthenticatedUserAsActor: true, }) ) const executionCall = mockExecuteWorkflowCore.mock.calls[0][0] const snapshot = typeof executionCall.snapshot === 'string' ? JSON.parse(executionCall.snapshot) : executionCall.snapshot expect(snapshot.metadata.enforceCredentialAccess).toBe(true) }) })