/** * Integration tests for workflow by ID API route * Tests the new centralized permissions system * * @vitest-environment node */ import { auditMock, envMock, hybridAuthMockFns, telemetryMock, workflowAuthzMockFns, workflowsOrchestrationMock, workflowsOrchestrationMockFns, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getWorkflowResponseDataSchema } from '@/lib/api/contracts/workflows' const mockLoadWorkflowFromNormalizedTables = workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables const mockGetWorkflowById = workflowsUtilsMockFns.mockGetWorkflowById const mockAuthorizeWorkflowByWorkspacePermission = workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission const mockPerformDeleteWorkflow = workflowsOrchestrationMockFns.mockPerformDeleteWorkflow const mockPerformUpdateWorkflow = workflowsOrchestrationMockFns.mockPerformUpdateWorkflow const { mockDbUpdate, mockDbSelect, mockDbTransaction } = vi.hoisted(() => ({ mockDbUpdate: vi.fn(), mockDbSelect: vi.fn(), mockDbTransaction: vi.fn(), })) /** * Helper to set mock auth state consistently across getSession and hybrid auth. */ function mockGetSession(session: { user: { id: string } } | null) { if (session) { hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({ success: true, userId: session.user.id, }) hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ success: true, userId: session.user.id, }) } else { hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({ success: false }) hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ success: false }) } } vi.mock('@/lib/core/config/env', () => envMock) vi.mock('@/lib/core/telemetry', () => telemetryMock) vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock) vi.mock('@sim/db', () => ({ db: { update: () => mockDbUpdate(), select: () => mockDbSelect(), transaction: mockDbTransaction, }, workflow: {}, })) import { DELETE, GET, PUT } from './route' describe('Workflow By ID API Route', () => { beforeEach(() => { vi.clearAllMocks() vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('mock-request-id-12345678'), }) mockLoadWorkflowFromNormalizedTables.mockResolvedValue(null) mockPerformUpdateWorkflow.mockImplementation(async (params) => ({ success: true, workflow: { id: params.workflowId, name: params.name ?? params.currentName, description: params.description ?? null, workspaceId: params.workspaceId, folderId: params.folderId ?? params.currentFolderId ?? null, sortOrder: params.sortOrder ?? null, locked: params.locked ?? null, createdAt: new Date(), updatedAt: new Date(), archivedAt: null, }, })) mockDbTransaction.mockImplementation(async (callback) => callback({ execute: vi.fn().mockResolvedValue(undefined), select: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]), }), }), }), }) ) }) describe('GET /api/workflows/[id]', () => { it('should return 401 when user is not authenticated', async () => { mockGetSession(null) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(401) const data = await response.json() expect(data.error).toBe('Unauthorized') }) it('should return 404 when workflow does not exist', async () => { mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(null) const req = new NextRequest('http://localhost:3000/api/workflows/nonexistent') const params = Promise.resolve({ id: 'nonexistent' }) const response = await GET(req, { params }) expect(response.status).toBe(404) const data = await response.json() expect(data.error).toBe('Workflow not found') }) it.concurrent('should allow access when user has admin workspace permission', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } const mockNormalizedData = { blocks: {}, edges: [], loops: {}, parallels: {}, isFromNormalizedTables: true, } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.data.id).toBe('workflow-123') }) it('omits null workflow description from state metadata so response validates', async () => { const mockWorkflow = { id: 'workflow-null-description', userId: 'user-123', name: 'No Description Workflow', description: null, workspaceId: 'workspace-456', folderId: null, sortOrder: 0, color: '#3972F6', lastSynced: new Date(), createdAt: new Date(), updatedAt: new Date(), isDeployed: false, deployedAt: null, isPublicApi: false, locked: false, runCount: 0, lastRunAt: null, archivedAt: null, variables: {}, } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockLoadWorkflowFromNormalizedTables.mockResolvedValue({ blocks: {}, edges: [], loops: {}, parallels: {}, }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-null-description') const params = Promise.resolve({ id: 'workflow-null-description' }) const response = await GET(req, { params }) const data = await response.json() expect(response.status).toBe(200) expect(data.data.state.metadata).toEqual({ name: 'No Description Workflow' }) expect(getWorkflowResponseDataSchema.safeParse(data.data).success).toBe(true) }) it.concurrent('should allow access when user has workspace permissions', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } const mockNormalizedData = { blocks: {}, edges: [], loops: {}, parallels: {}, isFromNormalizedTables: true, } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'read', }) mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.data.id).toBe('workflow-123') }) it('should deny access when user has no workspace permissions', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, message: 'Unauthorized: Access denied to read this workflow', workflow: mockWorkflow, workspacePermission: null, }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(403) const data = await response.json() expect(data.error).toBe('Unauthorized: Access denied to read this workflow') }) it.concurrent('should use normalized tables when available', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } const mockNormalizedData = { blocks: { 'block-1': { id: 'block-1', type: 'starter' } }, edges: [{ id: 'edge-1', source: 'block-1', target: 'block-2' }], loops: {}, parallels: {}, isFromNormalizedTables: true, } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.data.state.blocks).toEqual(mockNormalizedData.blocks) expect(data.data.state.edges).toEqual(mockNormalizedData.edges) }) }) describe('DELETE /api/workflows/[id]', () => { it('should allow admin to delete workflow', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockPerformDeleteWorkflow.mockResolvedValue({ success: true }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await DELETE(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.success).toBe(true) expect(mockPerformDeleteWorkflow).toHaveBeenCalledWith( expect.objectContaining({ workflowId: 'workflow-123', userId: 'user-123', }) ) }) it('should allow admin to delete workspace workflow', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockPerformDeleteWorkflow.mockResolvedValue({ success: true }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await DELETE(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.success).toBe(true) }) it('should prevent deletion of the last workflow in workspace', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'admin', }) mockPerformDeleteWorkflow.mockResolvedValue({ success: false, error: 'Cannot delete the only workflow in the workspace', errorCode: 'validation', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await DELETE(req, { params }) expect(response.status).toBe(400) const data = await response.json() expect(data.error).toBe('Cannot delete the only workflow in the workspace') }) it('should allow user with write permission to delete workflow', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockPerformDeleteWorkflow.mockResolvedValue({ success: true }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await DELETE(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.success).toBe(true) expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( expect.objectContaining({ workflowId: 'workflow-123', action: 'write' }) ) }) it.concurrent('should deny deletion for read-only users', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, message: 'Unauthorized: Access denied to write this workflow', workflow: mockWorkflow, workspacePermission: 'read', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'DELETE', }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await DELETE(req, { params }) expect(response.status).toBe(403) const data = await response.json() expect(data.error).toBe('Unauthorized: Access denied to write this workflow') }) }) describe('PUT /api/workflows/[id]', () => { function mockDuplicateCheck(results: Array<{ id: string }> = []) { mockDbSelect.mockReturnValue({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue(results), }), }), }) } it('should allow user with write permission to update workflow', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } const updateData = { name: 'Updated Workflow' } const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockDuplicateCheck([]) mockDbUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), }), }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify(updateData), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.workflow.name).toBe('Updated Workflow') }) it('should allow users with write permission to update workflow', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } const updateData = { name: 'Updated Workflow' } const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockDuplicateCheck([]) mockDbUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), }), }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify(updateData), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.workflow.name).toBe('Updated Workflow') }) it('should deny update for users with only read permission', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'other-user', name: 'Test Workflow', workspaceId: 'workspace-456', } const updateData = { name: 'Updated Workflow' } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, message: 'Unauthorized: Access denied to write this workflow', workflow: mockWorkflow, workspacePermission: 'read', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify(updateData), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(403) const data = await response.json() expect(data.error).toBe('Unauthorized: Access denied to write this workflow') }) it.concurrent('should validate request data', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) const invalidData = { name: '' } const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify(invalidData), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(400) const data = await response.json() expect(data.error).toBe('Validation error') }) it('should reject rename when duplicate name exists in same folder', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Original Name', folderId: 'folder-1', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockPerformUpdateWorkflow.mockResolvedValueOnce({ success: false, error: 'A workflow named "Duplicate Name" already exists in this folder', errorCode: 'conflict', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ name: 'Duplicate Name' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(409) const data = await response.json() expect(data.error).toBe('A workflow named "Duplicate Name" already exists in this folder') }) it('should reject rename when duplicate name exists at root level', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Original Name', folderId: null, workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockPerformUpdateWorkflow.mockResolvedValueOnce({ success: false, error: 'A workflow named "Duplicate Name" already exists in this folder', errorCode: 'conflict', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ name: 'Duplicate Name' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(409) const data = await response.json() expect(data.error).toBe('A workflow named "Duplicate Name" already exists in this folder') }) it('should allow rename when no duplicate exists in same folder', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Original Name', folderId: 'folder-1', workspaceId: 'workspace-456', } const updatedWorkflow = { ...mockWorkflow, name: 'Unique Name', updatedAt: new Date() } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockDuplicateCheck([]) mockDbUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), }), }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ name: 'Unique Name' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.workflow.name).toBe('Unique Name') }) it('should allow same name in different folders', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'My Workflow', folderId: 'folder-1', workspaceId: 'workspace-456', } const updatedWorkflow = { ...mockWorkflow, folderId: 'folder-2', updatedAt: new Date() } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) // No duplicate in target folder mockDuplicateCheck([]) mockDbUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), }), }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ folderId: 'folder-2' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(200) const data = await response.json() expect(data.workflow.folderId).toBe('folder-2') }) it('should reject moving to a folder where same name already exists', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'My Workflow', folderId: 'folder-1', workspaceId: 'workspace-456', } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockPerformUpdateWorkflow.mockResolvedValueOnce({ success: false, error: 'A workflow named "My Workflow" already exists in this folder', errorCode: 'conflict', }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ folderId: 'folder-2' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(409) const data = await response.json() expect(data.error).toBe('A workflow named "My Workflow" already exists in this folder') }) it('should skip duplicate check when only updating non-name/non-folder fields', async () => { const mockWorkflow = { id: 'workflow-123', userId: 'user-123', name: 'Test Workflow', workspaceId: 'workspace-456', } const updatedWorkflow = { ...mockWorkflow, description: 'Updated description', updatedAt: new Date(), } mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockResolvedValue(mockWorkflow) mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: mockWorkflow, workspacePermission: 'write', }) mockDbUpdate.mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), }), }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', body: JSON.stringify({ description: 'Updated description' }), }) const params = Promise.resolve({ id: 'workflow-123' }) const response = await PUT(req, { params }) expect(response.status).toBe(200) // db.select should NOT have been called since no name/folder change expect(mockDbSelect).not.toHaveBeenCalled() }) }) describe('Error handling', () => { it.concurrent('should handle database errors gracefully', async () => { mockGetSession({ user: { id: 'user-123' } }) mockGetWorkflowById.mockRejectedValue(new Error('Database connection timeout')) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) const response = await GET(req, { params }) expect(response.status).toBe(500) const data = await response.json() expect(data.error).toBe('Internal server error') }) }) })