diff --git a/apps/sim/app/api/auth/oauth/utils.test.ts b/apps/sim/app/api/auth/oauth/utils.test.ts index c76ed05bf..af5588626 100644 --- a/apps/sim/app/api/auth/oauth/utils.test.ts +++ b/apps/sim/app/api/auth/oauth/utils.test.ts @@ -5,9 +5,15 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -describe('OAuth Utils', () => { - const mockSession = { user: { id: 'test-user-id' } } - const mockDb = { +const mockSession = { user: { id: 'test-user-id' } } +const mockGetSession = vi.fn() + +vi.mock('@/lib/auth', () => ({ + getSession: () => mockGetSession(), +})) + +vi.mock('@sim/db', () => ({ + db: { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), @@ -15,33 +21,41 @@ describe('OAuth Utils', () => { update: vi.fn().mockReturnThis(), set: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), - } - const mockRefreshOAuthToken = vi.fn() - const mockLogger = { + }, +})) + +vi.mock('@/lib/oauth/oauth', () => ({ + refreshOAuthToken: vi.fn(), +})) + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn().mockReturnValue({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), - } + }), +})) +import { db } from '@sim/db' +import { createLogger } from '@/lib/logs/console/logger' +import { refreshOAuthToken } from '@/lib/oauth/oauth' +import { + getCredential, + getUserId, + refreshAccessTokenIfNeeded, + refreshTokenIfNeeded, +} from '@/app/api/auth/oauth/utils' + +const mockDb = db as any +const mockRefreshOAuthToken = refreshOAuthToken as any +const mockLogger = (createLogger as any)() + +describe('OAuth Utils', () => { beforeEach(() => { - vi.resetModules() - - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(mockSession), - })) - - vi.doMock('@sim/db', () => ({ - db: mockDb, - })) - - vi.doMock('@/lib/oauth/oauth', () => ({ - refreshOAuthToken: mockRefreshOAuthToken, - })) - - vi.doMock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), - })) + vi.clearAllMocks() + mockGetSession.mockResolvedValue(mockSession) + mockDb.limit.mockReturnValue([]) }) afterEach(() => { @@ -50,8 +64,6 @@ describe('OAuth Utils', () => { describe('getUserId', () => { it('should get user ID from session when no workflowId is provided', async () => { - const { getUserId } = await import('@/app/api/auth/oauth/utils') - const userId = await getUserId('request-id') expect(userId).toBe('test-user-id') @@ -60,8 +72,6 @@ describe('OAuth Utils', () => { it('should get user ID from workflow when workflowId is provided', async () => { mockDb.limit.mockReturnValueOnce([{ userId: 'workflow-owner-id' }]) - const { getUserId } = await import('@/app/api/auth/oauth/utils') - const userId = await getUserId('request-id', 'workflow-id') expect(mockDb.select).toHaveBeenCalled() @@ -72,11 +82,7 @@ describe('OAuth Utils', () => { }) it('should return undefined if no session is found', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(null), - })) - - const { getUserId } = await import('@/app/api/auth/oauth/utils') + mockGetSession.mockResolvedValueOnce(null) const userId = await getUserId('request-id') @@ -87,8 +93,6 @@ describe('OAuth Utils', () => { it('should return undefined if workflow is not found', async () => { mockDb.limit.mockReturnValueOnce([]) - const { getUserId } = await import('@/app/api/auth/oauth/utils') - const userId = await getUserId('request-id', 'nonexistent-workflow-id') expect(userId).toBeUndefined() @@ -101,8 +105,6 @@ describe('OAuth Utils', () => { const mockCredential = { id: 'credential-id', userId: 'test-user-id' } mockDb.limit.mockReturnValueOnce([mockCredential]) - const { getCredential } = await import('@/app/api/auth/oauth/utils') - const credential = await getCredential('request-id', 'credential-id', 'test-user-id') expect(mockDb.select).toHaveBeenCalled() @@ -116,8 +118,6 @@ describe('OAuth Utils', () => { it('should return undefined when credential is not found', async () => { mockDb.limit.mockReturnValueOnce([]) - const { getCredential } = await import('@/app/api/auth/oauth/utils') - const credential = await getCredential('request-id', 'nonexistent-id', 'test-user-id') expect(credential).toBeUndefined() @@ -135,8 +135,6 @@ describe('OAuth Utils', () => { providerId: 'google', } - const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id') expect(mockRefreshOAuthToken).not.toHaveBeenCalled() @@ -159,8 +157,6 @@ describe('OAuth Utils', () => { refreshToken: 'new-refresh-token', }) - const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id') expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token') @@ -183,8 +179,6 @@ describe('OAuth Utils', () => { mockRefreshOAuthToken.mockResolvedValueOnce(null) - const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - await expect( refreshTokenIfNeeded('request-id', mockCredential, 'credential-id') ).rejects.toThrow('Failed to refresh token') @@ -201,8 +195,6 @@ describe('OAuth Utils', () => { providerId: 'google', } - const { refreshTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id') expect(mockRefreshOAuthToken).not.toHaveBeenCalled() @@ -222,8 +214,6 @@ describe('OAuth Utils', () => { } mockDb.limit.mockReturnValueOnce([mockCredential]) - const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') expect(mockRefreshOAuthToken).not.toHaveBeenCalled() @@ -247,8 +237,6 @@ describe('OAuth Utils', () => { refreshToken: 'new-refresh-token', }) - const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token') @@ -260,8 +248,6 @@ describe('OAuth Utils', () => { it('should return null if credential not found', async () => { mockDb.limit.mockReturnValueOnce([]) - const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const token = await refreshAccessTokenIfNeeded('nonexistent-id', 'test-user-id', 'request-id') expect(token).toBeNull() @@ -281,8 +267,6 @@ describe('OAuth Utils', () => { mockRefreshOAuthToken.mockResolvedValueOnce(null) - const { refreshAccessTokenIfNeeded } = await import('@/app/api/auth/oauth/utils') - const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') expect(token).toBeNull() diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index a8aff5f9f..f16bc9be8 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -1,47 +1,60 @@ -import { NextRequest } from 'next/server' /** * Tests for function execution API route * * @vitest-environment node */ +import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createMockRequest } from '@/app/api/__test-utils__/utils' const mockCreateContext = vi.fn() const mockRunInContext = vi.fn() -const mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), -} +const mockScript = vi.fn() +const mockExecuteInE2B = vi.fn() + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + })), +})) + +vi.mock('vm', () => ({ + createContext: vi.fn(), + Script: vi.fn(), +})) + +vi.mock('@/lib/execution/e2b', () => ({ + executeInE2B: vi.fn(), +})) + +import { createContext, Script } from 'vm' +import { validateProxyUrl } from '@/lib/core/security/input-validation' +import { executeInE2B } from '@/lib/execution/e2b' +import { createLogger } from '@/lib/logs/console/logger' +import { POST } from './route' + +const mockedCreateContext = vi.mocked(createContext) +const mockedScript = vi.mocked(Script) +const mockedExecuteInE2B = vi.mocked(executeInE2B) +const mockedCreateLogger = vi.mocked(createLogger) describe('Function Execute API Route', () => { beforeEach(() => { - vi.resetModules() - vi.resetAllMocks() - - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: vi.fn().mockImplementation(() => ({ - runInContext: mockRunInContext, - })), - })) - - vi.doMock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), - })) - - vi.doMock('@/lib/execution/e2b', () => ({ - executeInE2B: vi.fn().mockResolvedValue({ - result: 'e2b success', - stdout: 'e2b output', - sandboxId: 'test-sandbox-id', - }), - })) + vi.clearAllMocks() + mockedCreateContext.mockReturnValue({}) mockRunInContext.mockResolvedValue('vm success') - mockCreateContext.mockReturnValue({}) + mockedScript.mockImplementation((): any => ({ + runInContext: mockRunInContext, + })) + mockedExecuteInE2B.mockResolvedValue({ + result: 'e2b success', + stdout: 'e2b output', + sandboxId: 'test-sandbox-id', + }) }) afterEach(() => { @@ -54,20 +67,17 @@ describe('Function Execute API Route', () => { code: 'return "test"', }) - const { POST } = await import('@/app/api/function/execute/route') await POST(req) - expect(mockCreateContext).toHaveBeenCalled() - const contextArgs = mockCreateContext.mock.calls[0][0] + expect(mockedCreateContext).toHaveBeenCalled() + const contextArgs = mockedCreateContext.mock.calls[0][0] expect(contextArgs).toHaveProperty('fetch') - expect(typeof contextArgs.fetch).toBe('function') + expect(typeof (contextArgs as any).fetch).toBe('function') - expect(contextArgs.fetch.name).toBe('secureFetch') + expect((contextArgs as any).fetch?.name).toBe('secureFetch') }) it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => { - const { validateProxyUrl } = await import('@/lib/core/security/input-validation') - expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false) expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false) expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false) @@ -75,16 +85,12 @@ describe('Function Execute API Route', () => { }) it.concurrent('should allow legitimate external URLs', async () => { - const { validateProxyUrl } = await import('@/lib/core/security/input-validation') - expect(validateProxyUrl('https://api.github.com/user').isValid).toBe(true) expect(validateProxyUrl('https://httpbin.org/get').isValid).toBe(true) expect(validateProxyUrl('https://example.com/api').isValid).toBe(true) }) it.concurrent('should block dangerous protocols', async () => { - const { validateProxyUrl } = await import('@/lib/core/security/input-validation') - expect(validateProxyUrl('file:///etc/passwd').isValid).toBe(false) expect(validateProxyUrl('ftp://internal.server/files').isValid).toBe(false) expect(validateProxyUrl('gopher://old.server/menu').isValid).toBe(false) @@ -98,7 +104,6 @@ describe('Function Execute API Route', () => { timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -113,7 +118,6 @@ describe('Function Execute API Route', () => { timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -127,12 +131,11 @@ describe('Function Execute API Route', () => { code: 'return "test"', }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) + const data = await response.json() expect(response.status).toBe(200) - // The logger now logs execution success, not the request details - expect(mockLogger.info).toHaveBeenCalled() + expect(data.success).toBe(true) }) }) @@ -145,11 +148,9 @@ describe('Function Execute API Route', () => { }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // The code should be resolved to: return "secret-key-123" }) it.concurrent('should resolve tag variables with syntax', async () => { @@ -160,11 +161,9 @@ describe('Function Execute API Route', () => { }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // The code should be resolved with the email object }) it.concurrent('should NOT treat email addresses as template variables', async () => { @@ -178,11 +177,9 @@ describe('Function Execute API Route', () => { }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // Should not try to replace as a template variable }) it.concurrent('should only match valid variable names in angle brackets', async () => { @@ -194,11 +191,9 @@ describe('Function Execute API Route', () => { }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // Should replace and but not }) }) @@ -230,7 +225,6 @@ describe('Function Execute API Route', () => { params: gmailData, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) @@ -255,7 +249,6 @@ describe('Function Execute API Route', () => { params: complexEmailData, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) @@ -273,11 +266,9 @@ describe('Function Execute API Route', () => { isCustomTool: true, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // For custom tools, parameters should be directly accessible as variables }) }) @@ -289,7 +280,6 @@ describe('Function Execute API Route', () => { headers: { 'Content-Type': 'application/json' }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(500) @@ -301,15 +291,11 @@ describe('Function Execute API Route', () => { timeout: 10000, }) - const { POST } = await import('@/app/api/function/execute/route') - await POST(req) + const response = await POST(req) + const data = await response.json() - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringMatching(/\[.*\] Function execution request/), - expect.objectContaining({ - timeout: 10000, - }) - ) + expect(response.status).toBe(200) + expect(data.success).toBe(true) }) it.concurrent('should handle empty parameters object', async () => { @@ -318,7 +304,6 @@ describe('Function Execute API Route', () => { params: {}, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) @@ -327,31 +312,25 @@ describe('Function Execute API Route', () => { describe('Enhanced Error Handling', () => { it('should provide detailed syntax error with line content', async () => { - // Mock VM Script to throw a syntax error - const mockScript = vi.fn().mockImplementation(() => { - const error = new Error('Invalid or unexpected token') - error.name = 'SyntaxError' - error.stack = `user-function.js:5 + const syntaxError = new Error('Invalid or unexpected token') + syntaxError.name = 'SyntaxError' + syntaxError.stack = `user-function.js:5 description: "This has a missing closing quote ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Invalid or unexpected token at new Script (node:vm:117:7) at POST (/path/to/route.ts:123:24)` - throw error - }) - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockedScript.mockImplementationOnce(() => { + throw syntaxError + }) const req = createMockRequest('POST', { code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -363,7 +342,6 @@ SyntaxError: Invalid or unexpected token expect(data.error).toContain('Invalid or unexpected token') expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)') - // Check debug information expect(data.debug).toBeDefined() expect(data.debug.line).toBe(3) expect(data.debug.errorType).toBe('SyntaxError') @@ -371,7 +349,6 @@ SyntaxError: Invalid or unexpected token }) it('should provide detailed runtime error with line and column', async () => { - // Create the error object first const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')") runtimeError.name = 'TypeError' runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod') @@ -379,22 +356,13 @@ SyntaxError: Invalid or unexpected token at user-function.js:9:3 at Script.runInContext (node:vm:147:14)` - // Mock successful script creation but runtime error - const mockScript = vi.fn().mockImplementation(() => ({ - runInContext: vi.fn().mockRejectedValue(runtimeError), - })) - - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockRunInContext.mockRejectedValueOnce(runtimeError) const req = createMockRequest('POST', { code: 'const obj = null;\nreturn obj.someMethod();', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -405,7 +373,6 @@ SyntaxError: Invalid or unexpected token expect(data.error).toContain('return obj.someMethod();') expect(data.error).toContain('Cannot read properties of null') - // Check debug information expect(data.debug).toBeDefined() expect(data.debug.line).toBe(2) expect(data.debug.column).toBe(16) @@ -414,28 +381,19 @@ SyntaxError: Invalid or unexpected token }) it('should handle ReferenceError with enhanced details', async () => { - // Create the error object first const referenceError = new Error('undefinedVariable is not defined') referenceError.name = 'ReferenceError' referenceError.stack = `ReferenceError: undefinedVariable is not defined at user-function.js:4:8 at Script.runInContext (node:vm:147:14)` - const mockScript = vi.fn().mockImplementation(() => ({ - runInContext: vi.fn().mockRejectedValue(referenceError), - })) - - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockRunInContext.mockRejectedValueOnce(referenceError) const req = createMockRequest('POST', { code: 'const x = 42;\nreturn undefinedVariable + x;', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -448,24 +406,18 @@ SyntaxError: Invalid or unexpected token }) it('should handle errors without line content gracefully', async () => { - const mockScript = vi.fn().mockImplementation(() => { - const error = new Error('Generic error without stack trace') - error.name = 'Error' - // No stack trace - throw error - }) + const genericError = new Error('Generic error without stack trace') + genericError.name = 'Error' - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockedScript.mockImplementationOnce(() => { + throw genericError + }) const req = createMockRequest('POST', { code: 'return "test";', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -473,7 +425,6 @@ SyntaxError: Invalid or unexpected token expect(data.success).toBe(false) expect(data.error).toBe('Generic error without stack trace') - // Should still have debug info, but without line details expect(data.debug).toBeDefined() expect(data.debug.errorType).toBe('Error') expect(data.debug.line).toBeUndefined() @@ -481,58 +432,47 @@ SyntaxError: Invalid or unexpected token }) it('should extract line numbers from different stack trace formats', async () => { - const mockScript = vi.fn().mockImplementation(() => { - const error = new Error('Test error') - error.name = 'Error' - error.stack = `Error: Test error + const testError = new Error('Test error') + testError.name = 'Error' + testError.stack = `Error: Test error at user-function.js:7:25 at async function at Script.runInContext (node:vm:147:14)` - throw error - }) - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockedScript.mockImplementationOnce(() => { + throw testError + }) const req = createMockRequest('POST', { code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() expect(response.status).toBe(500) expect(data.success).toBe(false) - // Line 7 in VM should map to line 5 in user code (7 - 3 + 1 = 5) expect(data.debug.line).toBe(5) expect(data.debug.column).toBe(25) expect(data.debug.lineContent).toBe('return a + b + c + d;') }) it.concurrent('should provide helpful suggestions for common syntax errors', async () => { - const mockScript = vi.fn().mockImplementation(() => { - const error = new Error('Unexpected end of input') - error.name = 'SyntaxError' - error.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input' - throw error - }) + const syntaxError = new Error('Unexpected end of input') + syntaxError.name = 'SyntaxError' + syntaxError.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input' - vi.doMock('vm', () => ({ - createContext: mockCreateContext, - Script: mockScript, - })) + mockedScript.mockImplementationOnce(() => { + throw syntaxError + }) const req = createMockRequest('POST', { code: 'const obj = {\n name: "test"\n// Missing closing brace', timeout: 5000, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) const data = await response.json() @@ -546,7 +486,6 @@ SyntaxError: Invalid or unexpected token describe('Utility Functions', () => { it.concurrent('should properly escape regex special characters', async () => { - // This tests the escapeRegExp function indirectly const req = createMockRequest('POST', { code: 'return {{special.chars+*?}}', envVars: { @@ -554,15 +493,12 @@ SyntaxError: Invalid or unexpected token }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) - // Should handle special regex characters in variable names }) it.concurrent('should handle JSON serialization edge cases', async () => { - // Test with complex but not circular data first const req = createMockRequest('POST', { code: 'return ', params: { @@ -578,7 +514,6 @@ SyntaxError: Invalid or unexpected token }, }) - const { POST } = await import('@/app/api/function/execute/route') const response = await POST(req) expect(response.status).toBe(200) diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index 650eb0017..bfc65ec1c 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -4,148 +4,207 @@ * @vitest-environment node */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createMockRequest, - mockExecutionDependencies, - sampleWorkflowState, -} from '@/app/api/__test-utils__/utils' +import { createMockRequest, mockExecutionDependencies } from '@/app/api/__test-utils__/utils' + +const { + mockGetSession, + mockGetUserEntityPermissions, + mockSelectLimit, + mockInsertValues, + mockOnConflictDoUpdate, + mockInsert, + mockUpdate, + mockDelete, + mockTransaction, + mockRandomUUID, + mockGetScheduleTimeValues, + mockGetSubBlockValue, + mockGenerateCronExpression, + mockCalculateNextRunTime, + mockValidateCronExpression, +} = vi.hoisted(() => ({ + mockGetSession: vi.fn(), + mockGetUserEntityPermissions: vi.fn(), + mockSelectLimit: vi.fn(), + mockInsertValues: vi.fn(), + mockOnConflictDoUpdate: vi.fn(), + mockInsert: vi.fn(), + mockUpdate: vi.fn(), + mockDelete: vi.fn(), + mockTransaction: vi.fn(), + mockRandomUUID: vi.fn(), + mockGetScheduleTimeValues: vi.fn(), + mockGetSubBlockValue: vi.fn(), + mockGenerateCronExpression: vi.fn(), + mockCalculateNextRunTime: vi.fn(), + mockValidateCronExpression: vi.fn(), +})) + +vi.mock('@/lib/auth', () => ({ + getSession: mockGetSession, +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + getUserEntityPermissions: mockGetUserEntityPermissions, +})) + +vi.mock('@sim/db', () => ({ + db: { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: mockSelectLimit, + }), + }), + }), + insert: mockInsert, + update: mockUpdate, + delete: mockDelete, + }, +})) + +vi.mock('@sim/db/schema', () => ({ + workflow: { + id: 'workflow_id', + userId: 'user_id', + workspaceId: 'workspace_id', + }, + workflowSchedule: { + id: 'schedule_id', + workflowId: 'workflow_id', + blockId: 'block_id', + cronExpression: 'cron_expression', + nextRunAt: 'next_run_at', + status: 'status', + }, +})) + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn((...args) => ({ type: 'eq', args })), + and: vi.fn((...args) => ({ type: 'and', args })), +})) + +vi.mock('crypto', () => ({ + randomUUID: mockRandomUUID, + default: { + randomUUID: mockRandomUUID, + }, +})) + +vi.mock('@/lib/workflows/schedules/utils', () => ({ + getScheduleTimeValues: mockGetScheduleTimeValues, + getSubBlockValue: mockGetSubBlockValue, + generateCronExpression: mockGenerateCronExpression, + calculateNextRunTime: mockCalculateNextRunTime, + validateCronExpression: mockValidateCronExpression, + BlockState: {}, +})) + +vi.mock('@/lib/core/utils/request', () => ({ + generateRequestId: vi.fn(() => 'test-request-id'), +})) + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + })), +})) + +vi.mock('@/lib/core/telemetry', () => ({ + trackPlatformEvent: vi.fn(), +})) + +import { db } from '@sim/db' +import { POST } from '@/app/api/schedules/route' describe('Schedule Configuration API Route', () => { beforeEach(() => { - vi.resetModules() + vi.clearAllMocks() + + ;(db as any).transaction = mockTransaction mockExecutionDependencies() - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { - id: 'user-id', - email: 'test@example.com', - }, - }), - })) - - vi.doMock('@/lib/workspaces/permissions/utils', () => ({ - getUserEntityPermissions: vi.fn().mockResolvedValue('admin'), // User has admin permissions - })) - - const _workflowStateWithSchedule = { - ...sampleWorkflowState, - blocks: { - ...sampleWorkflowState.blocks, - 'starter-id': { - ...sampleWorkflowState.blocks['starter-id'], - subBlocks: { - ...sampleWorkflowState.blocks['starter-id'].subBlocks, - startWorkflow: { id: 'startWorkflow', type: 'dropdown', value: 'schedule' }, - scheduleType: { id: 'scheduleType', type: 'dropdown', value: 'daily' }, - scheduleTime: { id: 'scheduleTime', type: 'time-input', value: '09:30' }, - dailyTime: { id: 'dailyTime', type: 'time-input', value: '09:30' }, - }, - }, + mockGetSession.mockResolvedValue({ + user: { + id: 'user-id', + email: 'test@example.com', }, - } - - vi.doMock('@sim/db', () => { - let callCount = 0 - const mockInsert = { - values: vi.fn().mockImplementation(() => ({ - onConflictDoUpdate: vi.fn().mockResolvedValue({}), - })), - } - - const mockDb = { - select: vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => { - callCount++ - // First call: workflow lookup for authorization - if (callCount === 1) { - return [ - { - id: 'workflow-id', - userId: 'user-id', - workspaceId: null, // User owns the workflow directly - }, - ] - } - // Second call: existing schedule lookup - return existing schedule for update test - return [ - { - id: 'existing-schedule-id', - workflowId: 'workflow-id', - blockId: 'starter-id', - cronExpression: '0 9 * * *', - nextRunAt: new Date(), - status: 'active', - }, - ] - }), - })), - })), - })), - insert: vi.fn().mockReturnValue(mockInsert), - update: vi.fn().mockImplementation(() => ({ - set: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - })), - delete: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockResolvedValue([]), - })), - transaction: vi.fn().mockImplementation(async (callback) => { - const tx = { - insert: vi.fn().mockReturnValue(mockInsert), - } - return callback(tx) - }), - } - - return { db: mockDb } }) - vi.doMock('crypto', () => ({ - randomUUID: vi.fn(() => 'test-uuid'), - default: { - randomUUID: vi.fn(() => 'test-uuid'), + mockGetUserEntityPermissions.mockResolvedValue('admin') + + mockSelectLimit.mockReturnValue([ + { + id: 'workflow-id', + userId: 'user-id', + workspaceId: null, }, + ]) + + mockInsertValues.mockImplementation(() => ({ + onConflictDoUpdate: mockOnConflictDoUpdate, + })) + mockOnConflictDoUpdate.mockResolvedValue({}) + + mockInsert.mockReturnValue({ + values: mockInsertValues, + }) + + mockUpdate.mockImplementation(() => ({ + set: vi.fn().mockImplementation(() => ({ + where: vi.fn().mockResolvedValue([]), + })), })) - vi.doMock('@/lib/workflows/schedules/utils', () => ({ - getScheduleTimeValues: vi.fn().mockReturnValue({ - scheduleTime: '09:30', - minutesInterval: 15, - hourlyMinute: 0, - dailyTime: [9, 30], - weeklyDay: 1, - weeklyTime: [9, 30], - monthlyDay: 1, - monthlyTime: [9, 30], - }), - getSubBlockValue: vi.fn().mockImplementation((block: any, id: string) => { - const subBlocks = { - startWorkflow: 'schedule', - scheduleType: 'daily', - scheduleTime: '09:30', - dailyTime: '09:30', - } - return subBlocks[id as keyof typeof subBlocks] || '' - }), - generateCronExpression: vi.fn().mockReturnValue('0 9 * * *'), - calculateNextRunTime: vi.fn().mockReturnValue(new Date()), - validateCronExpression: vi.fn().mockReturnValue({ isValid: true }), - BlockState: {}, + mockDelete.mockImplementation(() => ({ + where: vi.fn().mockResolvedValue([]), })) + + mockTransaction.mockImplementation(async (callback) => { + const tx = { + insert: vi.fn().mockReturnValue({ + values: mockInsertValues, + }), + } + return callback(tx) + }) + + mockRandomUUID.mockReturnValue('test-uuid') + + mockGetScheduleTimeValues.mockReturnValue({ + scheduleTime: '09:30', + minutesInterval: 15, + hourlyMinute: 0, + dailyTime: [9, 30], + weeklyDay: 1, + weeklyTime: [9, 30], + monthlyDay: 1, + monthlyTime: [9, 30], + }) + + mockGetSubBlockValue.mockImplementation((block: any, id: string) => { + const subBlocks = { + startWorkflow: 'schedule', + scheduleType: 'daily', + scheduleTime: '09:30', + dailyTime: '09:30', + } + return subBlocks[id as keyof typeof subBlocks] || '' + }) + + mockGenerateCronExpression.mockReturnValue('0 9 * * *') + mockCalculateNextRunTime.mockReturnValue(new Date()) + mockValidateCronExpression.mockReturnValue({ isValid: true }) }) afterEach(() => { vi.clearAllMocks() }) - /** - * Test creating a new schedule - */ it('should create a new schedule successfully', async () => { const req = createMockRequest('POST', { workflowId: 'workflow-id', @@ -166,8 +225,6 @@ describe('Schedule Configuration API Route', () => { }, }) - const { POST } = await import('@/app/api/schedules/route') - const response = await POST(req) expect(response).toBeDefined() @@ -177,38 +234,16 @@ describe('Schedule Configuration API Route', () => { expect(responseData).toHaveProperty('message', 'Schedule updated') expect(responseData).toHaveProperty('cronExpression', '0 9 * * *') expect(responseData).toHaveProperty('nextRunAt') - - // We can't verify the utility functions were called directly - // since we're mocking them at the module level - // Instead, we just verify that the response has the expected properties }) - /** - * Test error handling - */ it('should handle errors gracefully', async () => { - vi.doMock('@sim/db', () => ({ - db: { - select: vi.fn().mockImplementation(() => ({ - from: vi.fn().mockImplementation(() => ({ - where: vi.fn().mockImplementation(() => ({ - limit: vi.fn().mockImplementation(() => []), - })), - })), - })), - insert: vi.fn().mockImplementation(() => { - throw new Error('Database error') - }), - }, - })) + mockSelectLimit.mockReturnValue([]) const req = createMockRequest('POST', { workflowId: 'workflow-id', state: { blocks: {}, edges: [], loops: {} }, }) - const { POST } = await import('@/app/api/schedules/route') - const response = await POST(req) expect(response.status).toBeGreaterThanOrEqual(400) @@ -216,21 +251,14 @@ describe('Schedule Configuration API Route', () => { expect(data).toHaveProperty('error') }) - /** - * Test authentication requirement - */ it('should require authentication', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(null), - })) + mockGetSession.mockResolvedValue(null) const req = createMockRequest('POST', { workflowId: 'workflow-id', state: { blocks: {}, edges: [], loops: {} }, }) - const { POST } = await import('@/app/api/schedules/route') - const response = await POST(req) expect(response.status).toBe(401) @@ -238,16 +266,11 @@ describe('Schedule Configuration API Route', () => { expect(data).toHaveProperty('error', 'Unauthorized') }) - /** - * Test invalid data handling - */ it('should validate input data', async () => { const req = createMockRequest('POST', { workflowId: 'workflow-id', }) - const { POST } = await import('@/app/api/schedules/route') - const response = await POST(req) expect(response.status).toBe(400) diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index f2412f32e..def0eff81 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -8,45 +8,62 @@ import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -describe('Workflow By ID API Route', () => { - const mockLogger = { +const mockGetSession = vi.fn() +const mockLoadWorkflowFromNormalizedTables = vi.fn() +const mockGetWorkflowById = vi.fn() +const mockGetWorkflowAccessContext = vi.fn() +const mockDbDelete = vi.fn() +const mockDbUpdate = vi.fn() + +vi.mock('@/lib/auth', () => ({ + getSession: () => mockGetSession(), +})) + +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn(() => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), + })), +})) + +vi.mock('@/lib/workflows/persistence/utils', () => ({ + loadWorkflowFromNormalizedTables: (workflowId: string) => + mockLoadWorkflowFromNormalizedTables(workflowId), +})) + +vi.mock('@/lib/workflows/utils', async () => { + const actual = + await vi.importActual('@/lib/workflows/utils') + + return { + ...actual, + getWorkflowById: (workflowId: string) => mockGetWorkflowById(workflowId), + getWorkflowAccessContext: (workflowId: string, userId?: string) => + mockGetWorkflowAccessContext(workflowId, userId), } +}) - const mockGetWorkflowById = vi.fn() - const mockGetWorkflowAccessContext = vi.fn() +vi.mock('@sim/db', () => ({ + db: { + delete: () => mockDbDelete(), + update: () => mockDbUpdate(), + }, + workflow: {}, +})) +import { DELETE, GET, PUT } from './route' + +describe('Workflow By ID API Route', () => { beforeEach(() => { - vi.resetModules() + vi.clearAllMocks() vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('mock-request-id-12345678'), }) - vi.doMock('@/lib/logs/console/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), - })) - - vi.doMock('@/lib/workflows/persistence/utils', () => ({ - loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(null), - })) - - mockGetWorkflowById.mockReset() - mockGetWorkflowAccessContext.mockReset() - - vi.doMock('@/lib/workflows/utils', async () => { - const actual = - await vi.importActual('@/lib/workflows/utils') - - return { - ...actual, - getWorkflowById: mockGetWorkflowById, - getWorkflowAccessContext: mockGetWorkflowAccessContext, - } - }) + mockLoadWorkflowFromNormalizedTables.mockResolvedValue(null) }) afterEach(() => { @@ -55,14 +72,11 @@ describe('Workflow By ID API Route', () => { describe('GET /api/workflows/[id]', () => { it('should return 401 when user is not authenticated', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue(null), - })) + mockGetSession.mockResolvedValue(null) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(401) @@ -71,14 +85,12 @@ describe('Workflow By ID API Route', () => { }) it('should return 404 when workflow does not exist', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(null) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(null) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: null, workspaceOwnerId: null, workspacePermission: null, @@ -89,7 +101,6 @@ describe('Workflow By ID API Route', () => { const req = new NextRequest('http://localhost:3000/api/workflows/nonexistent') const params = Promise.resolve({ id: 'nonexistent' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(404) @@ -113,14 +124,12 @@ describe('Workflow By ID API Route', () => { isFromNormalizedTables: true, } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: null, workspacePermission: null, @@ -128,23 +137,11 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@/lib/workflows/persistence/utils', () => ({ - loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData), - })) - - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ - workflow: mockWorkflow, - workspaceOwnerId: null, - workspacePermission: null, - isOwner: true, - isWorkspaceOwner: false, - }) + mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(200) @@ -168,27 +165,12 @@ describe('Workflow By ID API Route', () => { isFromNormalizedTables: true, } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) - - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ - workflow: mockWorkflow, - workspaceOwnerId: 'workspace-456', - workspacePermission: 'admin', - isOwner: false, - isWorkspaceOwner: false, + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, }) - vi.doMock('@/lib/workflows/persistence/utils', () => ({ - loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData), - })) - - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: 'read', @@ -196,15 +178,11 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@/lib/workspaces/permissions/utils', () => ({ - getUserEntityPermissions: vi.fn().mockResolvedValue('read'), - hasAdminPermission: vi.fn().mockResolvedValue(false), - })) + mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(200) @@ -220,14 +198,12 @@ describe('Workflow By ID API Route', () => { workspaceId: 'workspace-456', } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: null, @@ -238,7 +214,6 @@ describe('Workflow By ID API Route', () => { const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(403) @@ -262,14 +237,12 @@ describe('Workflow By ID API Route', () => { isFromNormalizedTables: true, } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: null, workspacePermission: null, @@ -277,14 +250,11 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@/lib/workflows/persistence/utils', () => ({ - loadWorkflowFromNormalizedTables: vi.fn().mockResolvedValue(mockNormalizedData), - })) + mockLoadWorkflowFromNormalizedTables.mockResolvedValue(mockNormalizedData) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123') const params = Promise.resolve({ id: 'workflow-123' }) - const { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(200) @@ -303,14 +273,12 @@ describe('Workflow By ID API Route', () => { workspaceId: null, } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: null, workspacePermission: null, @@ -318,14 +286,9 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@sim/db', () => ({ - db: { - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), - }), - }, - workflow: {}, - })) + mockDbDelete.mockReturnValue({ + where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), + }) global.fetch = vi.fn().mockResolvedValue({ ok: true, @@ -336,7 +299,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { DELETE } = await import('@/app/api/workflows/[id]/route') const response = await DELETE(req, { params }) expect(response.status).toBe(200) @@ -352,14 +314,12 @@ describe('Workflow By ID API Route', () => { workspaceId: 'workspace-456', } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: 'admin', @@ -367,14 +327,9 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@sim/db', () => ({ - db: { - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), - }), - }, - workflow: {}, - })) + mockDbDelete.mockReturnValue({ + where: vi.fn().mockResolvedValue([{ id: 'workflow-123' }]), + }) global.fetch = vi.fn().mockResolvedValue({ ok: true, @@ -385,7 +340,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { DELETE } = await import('@/app/api/workflows/[id]/route') const response = await DELETE(req, { params }) expect(response.status).toBe(200) @@ -401,14 +355,12 @@ describe('Workflow By ID API Route', () => { workspaceId: 'workspace-456', } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: null, @@ -421,7 +373,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { DELETE } = await import('@/app/api/workflows/[id]/route') const response = await DELETE(req, { params }) expect(response.status).toBe(403) @@ -442,14 +393,12 @@ describe('Workflow By ID API Route', () => { const updateData = { name: 'Updated Workflow' } const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: null, workspacePermission: null, @@ -457,18 +406,13 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@sim/db', () => ({ - db: { - update: vi.fn().mockReturnValue({ - set: vi.fn().mockReturnValue({ - where: vi.fn().mockReturnValue({ - returning: vi.fn().mockResolvedValue([updatedWorkflow]), - }), - }), + mockDbUpdate.mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), - }, - workflow: {}, - })) + }), + }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', @@ -476,7 +420,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { PUT } = await import('@/app/api/workflows/[id]/route') const response = await PUT(req, { params }) expect(response.status).toBe(200) @@ -495,14 +438,12 @@ describe('Workflow By ID API Route', () => { const updateData = { name: 'Updated Workflow' } const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: 'write', @@ -510,18 +451,13 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - vi.doMock('@sim/db', () => ({ - db: { - update: vi.fn().mockReturnValue({ - set: vi.fn().mockReturnValue({ - where: vi.fn().mockReturnValue({ - returning: vi.fn().mockResolvedValue([updatedWorkflow]), - }), - }), + mockDbUpdate.mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([updatedWorkflow]), }), - }, - workflow: {}, - })) + }), + }) const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { method: 'PUT', @@ -529,7 +465,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { PUT } = await import('@/app/api/workflows/[id]/route') const response = await PUT(req, { params }) expect(response.status).toBe(200) @@ -547,14 +482,12 @@ describe('Workflow By ID API Route', () => { const updateData = { name: 'Updated Workflow' } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: 'workspace-456', workspacePermission: 'read', @@ -568,7 +501,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { PUT } = await import('@/app/api/workflows/[id]/route') const response = await PUT(req, { params }) expect(response.status).toBe(403) @@ -584,14 +516,12 @@ describe('Workflow By ID API Route', () => { workspaceId: null, } - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockResolvedValueOnce(mockWorkflow) - mockGetWorkflowAccessContext.mockResolvedValueOnce({ + mockGetWorkflowById.mockResolvedValue(mockWorkflow) + mockGetWorkflowAccessContext.mockResolvedValue({ workflow: mockWorkflow, workspaceOwnerId: null, workspacePermission: null, @@ -599,7 +529,6 @@ describe('Workflow By ID API Route', () => { isWorkspaceOwner: false, }) - // Invalid data - empty name const invalidData = { name: '' } const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', { @@ -608,7 +537,6 @@ describe('Workflow By ID API Route', () => { }) const params = Promise.resolve({ id: 'workflow-123' }) - const { PUT } = await import('@/app/api/workflows/[id]/route') const response = await PUT(req, { params }) expect(response.status).toBe(400) @@ -619,24 +547,20 @@ describe('Workflow By ID API Route', () => { describe('Error handling', () => { it.concurrent('should handle database errors gracefully', async () => { - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ - user: { id: 'user-123' }, - }), - })) + mockGetSession.mockResolvedValue({ + user: { id: 'user-123' }, + }) - mockGetWorkflowById.mockRejectedValueOnce(new Error('Database connection timeout')) + 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 { GET } = await import('@/app/api/workflows/[id]/route') const response = await GET(req, { params }) expect(response.status).toBe(500) const data = await response.json() expect(data.error).toBe('Internal server error') - expect(mockLogger.error).toHaveBeenCalled() }) }) }) diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts index 4616cf54b..ecbb92a97 100644 --- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts @@ -1,6 +1,5 @@ import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils' /** * Tests for workspace invitation by ID API route @@ -9,137 +8,154 @@ import { mockAuth, mockConsoleLogger } from '@/app/api/__test-utils__/utils' * @vitest-environment node */ -describe('Workspace Invitation [invitationId] API Route', () => { - const mockUser = { - id: 'user-123', - email: 'test@example.com', - name: 'Test User', - } +const mockGetSession = vi.fn() +const mockHasWorkspaceAdminAccess = vi.fn() - const mockWorkspace = { - id: 'workspace-456', - name: 'Test Workspace', - } +let dbSelectResults: any[] = [] +let dbSelectCallIndex = 0 - const mockInvitation = { - id: 'invitation-789', - workspaceId: 'workspace-456', - email: 'invited@example.com', - inviterId: 'inviter-321', - status: 'pending', - token: 'token-abc123', - permissions: 'read', - expiresAt: new Date(Date.now() + 86400000), // 1 day from now - createdAt: new Date(), - updatedAt: new Date(), - } +const mockDbSelect = vi.fn().mockImplementation(() => ({ + from: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + then: vi.fn().mockImplementation((callback: (rows: any[]) => any) => { + const result = dbSelectResults[dbSelectCallIndex] || [] + dbSelectCallIndex++ + return Promise.resolve(callback ? callback(result) : result) + }), +})) - let mockDbResults: any[] = [] - let mockGetSession: any - let mockHasWorkspaceAdminAccess: any - let mockTransaction: any +const mockDbInsert = vi.fn().mockImplementation(() => ({ + values: vi.fn().mockResolvedValue(undefined), +})) - beforeEach(async () => { - vi.resetModules() - vi.resetAllMocks() +const mockDbUpdate = vi.fn().mockImplementation(() => ({ + set: vi.fn().mockReturnThis(), + where: vi.fn().mockResolvedValue(undefined), +})) - mockDbResults = [] - mockConsoleLogger() - mockAuth(mockUser) +const mockDbDelete = vi.fn().mockImplementation(() => ({ + where: vi.fn().mockResolvedValue(undefined), +})) - vi.doMock('crypto', () => ({ - randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'), - })) - - mockGetSession = vi.fn() - vi.doMock('@/lib/auth', () => ({ - getSession: mockGetSession, - })) - - mockHasWorkspaceAdminAccess = vi.fn() - vi.doMock('@/lib/workspaces/permissions/utils', () => ({ - hasWorkspaceAdminAccess: mockHasWorkspaceAdminAccess, - })) - - vi.doMock('@/lib/core/config/env', () => { - const mockEnv = { - NEXT_PUBLIC_APP_URL: 'https://test.sim.ai', - BILLING_ENABLED: false, - } - return { - env: mockEnv, - isTruthy: (value: string | boolean | number | undefined) => - typeof value === 'string' - ? value.toLowerCase() === 'true' || value === '1' - : Boolean(value), - getEnv: (variable: string) => - mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable], - } - }) - - mockTransaction = vi.fn() - const mockDbChain = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockImplementation((callback: any) => { - const result = mockDbResults.shift() || [] - return callback ? callback(result) : Promise.resolve(result) - }), - insert: vi.fn().mockReturnThis(), +const mockDbTransaction = vi.fn().mockImplementation(async (callback: any) => { + await callback({ + insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined), - update: vi.fn().mockReturnThis(), - set: vi.fn().mockReturnThis(), - delete: vi.fn().mockReturnThis(), - transaction: mockTransaction, - } + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue(undefined), + }), + }), + }) +}) - vi.doMock('@sim/db', () => ({ - db: mockDbChain, - })) +vi.mock('@/lib/auth', () => ({ + getSession: () => mockGetSession(), +})) - vi.doMock('@sim/db/schema', () => ({ - workspaceInvitation: { - id: 'id', - workspaceId: 'workspaceId', - email: 'email', - inviterId: 'inviterId', - status: 'status', - token: 'token', - permissions: 'permissions', - expiresAt: 'expiresAt', - }, - workspace: { - id: 'id', - name: 'name', - }, - user: { - id: 'id', - email: 'email', - }, - permissions: { - id: 'id', - entityType: 'entityType', - entityId: 'entityId', - userId: 'userId', - permissionType: 'permissionType', - }, - })) +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + hasWorkspaceAdminAccess: (userId: string, workspaceId: string) => + mockHasWorkspaceAdminAccess(userId, workspaceId), +})) - vi.doMock('drizzle-orm', () => ({ - eq: vi.fn((a, b) => ({ type: 'eq', a, b })), - and: vi.fn((...args) => ({ type: 'and', args })), - })) +vi.mock('@/lib/logs/console/logger', () => ({ + createLogger: vi.fn().mockReturnValue({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), +})) + +vi.mock('@/lib/core/utils/urls', () => ({ + getBaseUrl: vi.fn().mockReturnValue('https://test.sim.ai'), +})) + +vi.mock('@sim/db', () => ({ + db: { + select: () => mockDbSelect(), + insert: (table: any) => mockDbInsert(table), + update: (table: any) => mockDbUpdate(table), + delete: (table: any) => mockDbDelete(table), + transaction: (callback: any) => mockDbTransaction(callback), + }, +})) + +vi.mock('@sim/db/schema', () => ({ + workspaceInvitation: { + id: 'id', + workspaceId: 'workspaceId', + email: 'email', + inviterId: 'inviterId', + status: 'status', + token: 'token', + permissions: 'permissions', + expiresAt: 'expiresAt', + }, + workspace: { + id: 'id', + name: 'name', + }, + user: { + id: 'id', + email: 'email', + }, + permissions: { + id: 'id', + entityType: 'entityType', + entityId: 'entityId', + userId: 'userId', + permissionType: 'permissionType', + }, +})) + +vi.mock('drizzle-orm', () => ({ + eq: vi.fn((a, b) => ({ type: 'eq', a, b })), + and: vi.fn((...args) => ({ type: 'and', args })), +})) + +vi.mock('crypto', () => ({ + randomUUID: vi.fn().mockReturnValue('mock-uuid-1234'), +})) + +import { DELETE, GET } from './route' + +const mockUser = { + id: 'user-123', + email: 'test@example.com', + name: 'Test User', +} + +const mockWorkspace = { + id: 'workspace-456', + name: 'Test Workspace', +} + +const mockInvitation = { + id: 'invitation-789', + workspaceId: 'workspace-456', + email: 'invited@example.com', + inviterId: 'inviter-321', + status: 'pending', + token: 'token-abc123', + permissions: 'read', + expiresAt: new Date(Date.now() + 86400000), // 1 day from now + createdAt: new Date(), + updatedAt: new Date(), +} + +describe('Workspace Invitation [invitationId] API Route', () => { + beforeEach(() => { + vi.clearAllMocks() + dbSelectResults = [] + dbSelectCallIndex = 0 }) describe('GET /api/workspaces/invitations/[invitationId]', () => { it('should return invitation details when called without token', async () => { - const { GET } = await import('./route') - mockGetSession.mockResolvedValue({ user: mockUser }) - - mockDbResults.push([mockInvitation]) - mockDbResults.push([mockWorkspace]) + dbSelectResults = [[mockInvitation], [mockWorkspace]] const request = new NextRequest('http://localhost/api/workspaces/invitations/invitation-789') const params = Promise.resolve({ invitationId: 'invitation-789' }) @@ -157,8 +173,6 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should redirect to login when unauthenticated with token', async () => { - const { GET } = await import('./route') - mockGetSession.mockResolvedValue(null) const request = new NextRequest( @@ -174,27 +188,30 @@ describe('Workspace Invitation [invitationId] API Route', () => { ) }) - it('should accept invitation when called with valid token', async () => { - const { GET } = await import('./route') + it('should return 401 when unauthenticated without token', async () => { + mockGetSession.mockResolvedValue(null) + const request = new NextRequest('http://localhost/api/workspaces/invitations/invitation-789') + const params = Promise.resolve({ invitationId: 'invitation-789' }) + + const response = await GET(request, { params }) + const data = await response.json() + + expect(response.status).toBe(401) + expect(data).toEqual({ error: 'Unauthorized' }) + }) + + it('should accept invitation when called with valid token', async () => { mockGetSession.mockResolvedValue({ user: { ...mockUser, email: 'invited@example.com' }, }) - mockDbResults.push([mockInvitation]) - mockDbResults.push([mockWorkspace]) - mockDbResults.push([{ ...mockUser, email: 'invited@example.com' }]) - mockDbResults.push([]) - - mockTransaction.mockImplementation(async (callback: any) => { - await callback({ - insert: vi.fn().mockReturnThis(), - values: vi.fn().mockResolvedValue(undefined), - update: vi.fn().mockReturnThis(), - set: vi.fn().mockReturnThis(), - where: vi.fn().mockResolvedValue(undefined), - }) - }) + dbSelectResults = [ + [mockInvitation], // invitation lookup + [mockWorkspace], // workspace lookup + [{ ...mockUser, email: 'invited@example.com' }], // user lookup + [], // existing permission check (empty = no existing) + ] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' @@ -208,8 +225,6 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should redirect to error page when invitation expired', async () => { - const { GET } = await import('./route') - mockGetSession.mockResolvedValue({ user: { ...mockUser, email: 'invited@example.com' }, }) @@ -219,8 +234,7 @@ describe('Workspace Invitation [invitationId] API Route', () => { expiresAt: new Date(Date.now() - 86400000), // 1 day ago } - mockDbResults.push([expiredInvitation]) - mockDbResults.push([mockWorkspace]) + dbSelectResults = [[expiredInvitation], [mockWorkspace]] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' @@ -236,15 +250,15 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should redirect to error page when email mismatch', async () => { - const { GET } = await import('./route') - mockGetSession.mockResolvedValue({ user: { ...mockUser, email: 'wrong@example.com' }, }) - mockDbResults.push([mockInvitation]) - mockDbResults.push([mockWorkspace]) - mockDbResults.push([{ ...mockUser, email: 'wrong@example.com' }]) + dbSelectResults = [ + [mockInvitation], + [mockWorkspace], + [{ ...mockUser, email: 'wrong@example.com' }], + ] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/token-abc123?token=token-abc123' @@ -258,19 +272,29 @@ describe('Workspace Invitation [invitationId] API Route', () => { 'https://test.sim.ai/invite/invitation-789?error=email-mismatch' ) }) + + it('should return 404 when invitation not found', async () => { + mockGetSession.mockResolvedValue({ user: mockUser }) + dbSelectResults = [[]] // Empty result + + const request = new NextRequest('http://localhost/api/workspaces/invitations/non-existent') + const params = Promise.resolve({ invitationId: 'non-existent' }) + + const response = await GET(request, { params }) + const data = await response.json() + + expect(response.status).toBe(404) + expect(data).toEqual({ error: 'Invitation not found or has expired' }) + }) }) describe('DELETE /api/workspaces/invitations/[invitationId]', () => { it('should return 401 when user is not authenticated', async () => { - const { DELETE } = await import('./route') - mockGetSession.mockResolvedValue(null) const request = new NextRequest( 'http://localhost/api/workspaces/invitations/invitation-789', - { - method: 'DELETE', - } + { method: 'DELETE' } ) const params = Promise.resolve({ invitationId: 'invitation-789' }) @@ -282,11 +306,8 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should return 404 when invitation does not exist', async () => { - const { DELETE } = await import('./route') - mockGetSession.mockResolvedValue({ user: mockUser }) - - mockDbResults.push([]) + dbSelectResults = [[]] const request = new NextRequest('http://localhost/api/workspaces/invitations/non-existent', { method: 'DELETE', @@ -301,18 +322,13 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should return 403 when user lacks admin access', async () => { - const { DELETE } = await import('./route') - mockGetSession.mockResolvedValue({ user: mockUser }) mockHasWorkspaceAdminAccess.mockResolvedValue(false) - - mockDbResults.push([mockInvitation]) + dbSelectResults = [[mockInvitation]] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/invitation-789', - { - method: 'DELETE', - } + { method: 'DELETE' } ) const params = Promise.resolve({ invitationId: 'invitation-789' }) @@ -325,19 +341,15 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should return 400 when trying to delete non-pending invitation', async () => { - const { DELETE } = await import('./route') - mockGetSession.mockResolvedValue({ user: mockUser }) mockHasWorkspaceAdminAccess.mockResolvedValue(true) const acceptedInvitation = { ...mockInvitation, status: 'accepted' } - mockDbResults.push([acceptedInvitation]) + dbSelectResults = [[acceptedInvitation]] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/invitation-789', - { - method: 'DELETE', - } + { method: 'DELETE' } ) const params = Promise.resolve({ invitationId: 'invitation-789' }) @@ -349,18 +361,13 @@ describe('Workspace Invitation [invitationId] API Route', () => { }) it('should successfully delete pending invitation when user has admin access', async () => { - const { DELETE } = await import('./route') - mockGetSession.mockResolvedValue({ user: mockUser }) mockHasWorkspaceAdminAccess.mockResolvedValue(true) - - mockDbResults.push([mockInvitation]) + dbSelectResults = [[mockInvitation]] const request = new NextRequest( 'http://localhost/api/workspaces/invitations/invitation-789', - { - method: 'DELETE', - } + { method: 'DELETE' } ) const params = Promise.resolve({ invitationId: 'invitation-789' }) @@ -370,61 +377,5 @@ describe('Workspace Invitation [invitationId] API Route', () => { expect(response.status).toBe(200) expect(data).toEqual({ success: true }) }) - - it('should return 500 when database error occurs', async () => { - vi.resetModules() - - const mockErrorDb = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - then: vi.fn().mockRejectedValue(new Error('Database connection failed')), - } - - vi.doMock('@sim/db', () => ({ db: mockErrorDb })) - vi.doMock('@/lib/auth', () => ({ - getSession: vi.fn().mockResolvedValue({ user: mockUser }), - })) - vi.doMock('@/lib/workspaces/permissions/utils', () => ({ - hasWorkspaceAdminAccess: vi.fn(), - })) - vi.doMock('@/lib/core/config/env', () => { - const mockEnv = { - NEXT_PUBLIC_APP_URL: 'https://test.sim.ai', - BILLING_ENABLED: false, - } - return { - env: mockEnv, - isTruthy: (value: string | boolean | number | undefined) => - typeof value === 'string' - ? value.toLowerCase() === 'true' || value === '1' - : Boolean(value), - getEnv: (variable: string) => - mockEnv[variable as keyof typeof mockEnv] ?? process.env[variable], - } - }) - vi.doMock('@sim/db/schema', () => ({ - workspaceInvitation: { id: 'id' }, - })) - vi.doMock('drizzle-orm', () => ({ - eq: vi.fn(), - })) - - const { DELETE } = await import('./route') - - const request = new NextRequest( - 'http://localhost/api/workspaces/invitations/invitation-789', - { - method: 'DELETE', - } - ) - const params = Promise.resolve({ invitationId: 'invitation-789' }) - - const response = await DELETE(request, { params }) - const data = await response.json() - - expect(response.status).toBe(500) - expect(data).toEqual({ error: 'Failed to delete invitation' }) - }) }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/templates/layout.tsx b/apps/sim/app/workspace/[workspaceId]/templates/layout.tsx index 0b6c67610..f21d673fc 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/layout.tsx @@ -1,6 +1,6 @@ export default function TemplatesLayout({ children }: { children: React.ReactNode }) { return ( -
+
{children}
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text.tsx index 06820c35e..0ef466bf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import { splitReferenceSegment } from '@/lib/workflows/sanitization/references' -import { REFERENCE } from '@/executor/consts' +import { REFERENCE } from '@/executor/constants' import { createCombinedPattern } from '@/executor/utils/reference-validation' import { normalizeBlockName } from '@/stores/workflows/utils' @@ -67,7 +67,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea if (matchText.startsWith(REFERENCE.ENV_VAR_START)) { nodes.push( - + {matchText} ) @@ -77,7 +77,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea if (split && shouldHighlightReference(split.reference)) { pushPlainText(split.leading) nodes.push( - + {split.reference} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx index 45af409b1..0e06f4535 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx @@ -16,7 +16,7 @@ import { useTriggerConfigAggregation } from '@/hooks/use-trigger-config-aggregat import { useWebhookManagement } from '@/hooks/use-webhook-management' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { getTrigger, isTriggerValid } from '@/triggers' -import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts' +import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' import { ShortInput } from '../short-input/short-input' const logger = createLogger('TriggerSave') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index bf54a375c..bdd29fc9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/emcn/components/badge/badge' import { Tooltip } from '@/components/emcn/components/tooltip/tooltip' import { getEnv, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' +import { getBaseUrl } from '@/lib/core/utils/urls' import { createLogger } from '@/lib/logs/console/logger' import { createMcpToolId } from '@/lib/mcp/utils' import { getProviderIdFromServiceId } from '@/lib/oauth' @@ -242,6 +243,7 @@ const SubBlockRow = ({ rawValue, workspaceId, workflowId, + blockId, allSubBlockValues, }: { title: string @@ -250,6 +252,7 @@ const SubBlockRow = ({ rawValue?: unknown workspaceId?: string workflowId?: string + blockId?: string allSubBlockValues?: Record }) => { const getStringValue = useCallback( @@ -353,6 +356,17 @@ const SubBlockRow = ({ return tool?.name ?? null }, [subBlock?.type, rawValue, mcpToolsData]) + const webhookUrlDisplayValue = useMemo(() => { + if (subBlock?.id !== 'webhookUrlDisplay' || !blockId) { + return null + } + const baseUrl = getBaseUrl() + const triggerPath = allSubBlockValues?.triggerPath?.value as string | undefined + return triggerPath + ? `${baseUrl}/api/webhooks/trigger/${triggerPath}` + : `${baseUrl}/api/webhooks/trigger/${blockId}` + }, [subBlock?.id, blockId, allSubBlockValues]) + const allVariables = useVariablesStore((state) => state.variables) const variablesDisplayValue = useMemo(() => { @@ -393,6 +407,7 @@ const SubBlockRow = ({ workflowSelectionName || mcpServerDisplayName || mcpToolDisplayName || + webhookUrlDisplayValue || selectorDisplayName const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) @@ -1002,6 +1017,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ rawValue={rawValue} workspaceId={workspaceId} workflowId={currentWorkflowId} + blockId={id} allSubBlockValues={subBlockState} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx index a8cbe8fe2..3f5daa765 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/layout.tsx @@ -2,7 +2,7 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp export default function WorkflowLayout({ children }: { children: React.ReactNode }) { return ( -
+
{children}
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 72f4e2b9e..f4346a73b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -40,7 +40,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' -import { isAnnotationOnlyBlock } from '@/executor/consts' +import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useStreamCleanup } from '@/hooks/use-stream-cleanup' diff --git a/apps/sim/app/workspace/[workspaceId]/w/page.tsx b/apps/sim/app/workspace/[workspaceId]/w/page.tsx index 6e50b07de..61b067f12 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/page.tsx @@ -46,7 +46,7 @@ export default function WorkflowsPage() { // Always show loading state until redirect happens // There should always be a default workflow, so we never show "no workflows found" return ( -
+
diff --git a/apps/sim/executor/consts.ts b/apps/sim/executor/constants.ts similarity index 100% rename from apps/sim/executor/consts.ts rename to apps/sim/executor/constants.ts diff --git a/apps/sim/executor/dag/builder.pause-resume.test.ts b/apps/sim/executor/dag/builder.test.ts similarity index 97% rename from apps/sim/executor/dag/builder.pause-resume.test.ts rename to apps/sim/executor/dag/builder.test.ts index bd2e60f0c..27440aadb 100644 --- a/apps/sim/executor/dag/builder.pause-resume.test.ts +++ b/apps/sim/executor/dag/builder.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { DAGBuilder } from '@/executor/dag/builder' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' diff --git a/apps/sim/executor/dag/construction/edges.ts b/apps/sim/executor/dag/construction/edges.ts index 7d442b007..821fc2952 100644 --- a/apps/sim/executor/dag/construction/edges.ts +++ b/apps/sim/executor/dag/construction/edges.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { EDGE, isConditionBlockType, isRouterBlockType } from '@/executor/consts' +import { EDGE, isConditionBlockType, isRouterBlockType } from '@/executor/constants' import type { DAG } from '@/executor/dag/builder' import { buildBranchNodeId, diff --git a/apps/sim/executor/dag/construction/loops.ts b/apps/sim/executor/dag/construction/loops.ts index ad1069083..75cf6a739 100644 --- a/apps/sim/executor/dag/construction/loops.ts +++ b/apps/sim/executor/dag/construction/loops.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType, LOOP, type SentinelType } from '@/executor/consts' +import { BlockType, LOOP, type SentinelType } from '@/executor/constants' import type { DAG, DAGNode } from '@/executor/dag/builder' import { buildSentinelEndId, buildSentinelStartId } from '@/executor/utils/subflow-utils' diff --git a/apps/sim/executor/dag/construction/nodes.ts b/apps/sim/executor/dag/construction/nodes.ts index ee5fd1cf4..b4ceeb1ae 100644 --- a/apps/sim/executor/dag/construction/nodes.ts +++ b/apps/sim/executor/dag/construction/nodes.ts @@ -1,4 +1,4 @@ -import { BlockType, isMetadataOnlyBlockType } from '@/executor/consts' +import { BlockType, isMetadataOnlyBlockType } from '@/executor/constants' import type { DAG, DAGNode } from '@/executor/dag/builder' import { buildBranchNodeId, diff --git a/apps/sim/executor/dag/construction/paths.ts b/apps/sim/executor/dag/construction/paths.ts index ec7ea4883..1d41436c7 100644 --- a/apps/sim/executor/dag/construction/paths.ts +++ b/apps/sim/executor/dag/construction/paths.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { isMetadataOnlyBlockType, isTriggerBlockType } from '@/executor/consts' +import { isMetadataOnlyBlockType, isTriggerBlockType } from '@/executor/constants' import { extractBaseBlockId } from '@/executor/utils/subflow-utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 5949b41cf..977b55cb4 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -7,7 +7,7 @@ import { DEFAULTS, EDGE, isSentinelBlockType, -} from '@/executor/consts' +} from '@/executor/constants' import type { DAGNode } from '@/executor/dag/builder' import type { BlockStateWriter, ContextExtensions } from '@/executor/execution/types' import { diff --git a/apps/sim/executor/execution/edge-manager.ts b/apps/sim/executor/execution/edge-manager.ts index 7457be8cb..0b707dacd 100644 --- a/apps/sim/executor/execution/edge-manager.ts +++ b/apps/sim/executor/execution/edge-manager.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { EDGE } from '@/executor/consts' +import { EDGE } from '@/executor/constants' import type { DAG, DAGNode } from '@/executor/dag/builder' import type { DAGEdge } from '@/executor/dag/types' import type { NormalizedBlockOutput } from '@/executor/types' diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 9deab28cc..68c471d8c 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import type { DAG } from '@/executor/dag/builder' import type { EdgeManager } from '@/executor/execution/edge-manager' import { serializePauseSnapshot } from '@/executor/execution/snapshot-serializer' diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 72df94c72..1a4becfc7 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { isHosted } from '@/lib/core/config/environment' import { getAllBlocks } from '@/blocks' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler' import type { ExecutionContext, StreamingExecution } from '@/executor/types' import { executeProviderRequest } from '@/providers' diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 32fd0a5aa..ae561ee3a 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -2,7 +2,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { createMcpToolId } from '@/lib/mcp/utils' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' -import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/consts' +import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/constants' import { memoryService } from '@/executor/handlers/agent/memory' import type { AgentInputs, diff --git a/apps/sim/executor/handlers/api/api-handler.test.ts b/apps/sim/executor/handlers/api/api-handler.test.ts index 8cec4c6e2..2a5303b9a 100644 --- a/apps/sim/executor/handlers/api/api-handler.test.ts +++ b/apps/sim/executor/handlers/api/api-handler.test.ts @@ -1,7 +1,7 @@ import '@/executor/__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { ApiBlockHandler } from '@/executor/handlers/api/api-handler' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/api/api-handler.ts b/apps/sim/executor/handlers/api/api-handler.ts index d2f0aed22..8bfbdbcd8 100644 --- a/apps/sim/executor/handlers/api/api-handler.ts +++ b/apps/sim/executor/handlers/api/api-handler.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType, HTTP } from '@/executor/consts' +import { BlockType, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' import { executeTool } from '@/tools' diff --git a/apps/sim/executor/handlers/condition/condition-handler.test.ts b/apps/sim/executor/handlers/condition/condition-handler.test.ts index b82d909d4..e6e1a3f06 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.test.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.test.ts @@ -1,7 +1,7 @@ import '@/executor/__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler' import type { BlockState, ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/condition/condition-handler.ts b/apps/sim/executor/handlers/condition/condition-handler.ts index 902c68498..5551903dd 100644 --- a/apps/sim/executor/handlers/condition/condition-handler.ts +++ b/apps/sim/executor/handlers/condition/condition-handler.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockOutput } from '@/blocks/types' -import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/consts' +import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts index b5fcfc2c3..0acdcc41c 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts @@ -1,7 +1,7 @@ import '@/executor/__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler' import type { ExecutionContext } from '@/executor/types' import { getProviderFromModel } from '@/providers/utils' diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 8beff8faa..2e9729881 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockOutput } from '@/blocks/types' -import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/consts' +import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAPIUrl, extractAPIErrorMessage } from '@/executor/utils/http' import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json' diff --git a/apps/sim/executor/handlers/function/function-handler.test.ts b/apps/sim/executor/handlers/function/function-handler.test.ts index a200211f7..f8d96c6ad 100644 --- a/apps/sim/executor/handlers/function/function-handler.test.ts +++ b/apps/sim/executor/handlers/function/function-handler.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/function/function-handler.ts b/apps/sim/executor/handlers/function/function-handler.ts index a3de3973a..2c240facd 100644 --- a/apps/sim/executor/handlers/function/function-handler.ts +++ b/apps/sim/executor/handlers/function/function-handler.ts @@ -1,6 +1,6 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants' import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/generic/generic-handler.test.ts b/apps/sim/executor/handlers/generic/generic-handler.test.ts index e1174a2af..042592b81 100644 --- a/apps/sim/executor/handlers/generic/generic-handler.test.ts +++ b/apps/sim/executor/handlers/generic/generic-handler.test.ts @@ -1,7 +1,7 @@ import '@/executor/__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts index be354f592..d79db474f 100644 --- a/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts +++ b/apps/sim/executor/handlers/human-in-the-loop/human-in-the-loop-handler.ts @@ -8,7 +8,7 @@ import { type FieldType, HTTP, PAUSE_RESUME, -} from '@/executor/consts' +} from '@/executor/constants' import { generatePauseContextId, mapNodeMetadataToPauseScopes, diff --git a/apps/sim/executor/handlers/response/response-handler.ts b/apps/sim/executor/handlers/response/response-handler.ts index db6c79b66..1af1bd9f3 100644 --- a/apps/sim/executor/handlers/response/response-handler.ts +++ b/apps/sim/executor/handlers/response/response-handler.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockOutput } from '@/blocks/types' -import { BlockType, HTTP } from '@/executor/consts' +import { BlockType, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/router/router-handler.test.ts b/apps/sim/executor/handlers/router/router-handler.test.ts index 5c517b352..eb3cc7333 100644 --- a/apps/sim/executor/handlers/router/router-handler.test.ts +++ b/apps/sim/executor/handlers/router/router-handler.test.ts @@ -2,7 +2,7 @@ import '@/executor/__test-utils__/mock-dependencies' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { generateRouterPrompt } from '@/blocks/blocks/router' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { RouterBlockHandler } from '@/executor/handlers/router/router-handler' import type { ExecutionContext } from '@/executor/types' import { getProviderFromModel } from '@/providers/utils' diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index cc21c2132..327d490f3 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -2,7 +2,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import { createLogger } from '@/lib/logs/console/logger' import { generateRouterPrompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' -import { BlockType, DEFAULTS, HTTP, isAgentBlockType, ROUTER } from '@/executor/consts' +import { BlockType, DEFAULTS, HTTP, isAgentBlockType, ROUTER } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts index 1b53cd864..599989213 100644 --- a/apps/sim/executor/handlers/trigger/trigger-handler.ts +++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/variables/variables-handler.ts b/apps/sim/executor/handlers/variables/variables-handler.ts index c5d4b59a9..8bebe483e 100644 --- a/apps/sim/executor/handlers/variables/variables-handler.ts +++ b/apps/sim/executor/handlers/variables/variables-handler.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockOutput } from '@/blocks/types' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/wait/wait-handler.ts b/apps/sim/executor/handlers/wait/wait-handler.ts index dbaf47b3b..5b7545a90 100644 --- a/apps/sim/executor/handlers/wait/wait-handler.ts +++ b/apps/sim/executor/handlers/wait/wait-handler.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index e818e38ce..bf304c786 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' -import { BlockType } from '@/executor/consts' +import { BlockType } from '@/executor/constants' import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index fa0cb6b7e..bc021d620 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -3,7 +3,7 @@ import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import type { TraceSpan } from '@/lib/logs/types' import type { BlockOutput } from '@/blocks/types' import { Executor } from '@/executor' -import { BlockType, DEFAULTS, HTTP } from '@/executor/consts' +import { BlockType, DEFAULTS, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext, diff --git a/apps/sim/executor/human-in-the-loop/utils.ts b/apps/sim/executor/human-in-the-loop/utils.ts index d2b05395d..1b060cf83 100644 --- a/apps/sim/executor/human-in-the-loop/utils.ts +++ b/apps/sim/executor/human-in-the-loop/utils.ts @@ -1,4 +1,4 @@ -import { PARALLEL } from '@/executor/consts' +import { PARALLEL } from '@/executor/constants' import type { ExecutionContext, LoopPauseScope, ParallelPauseScope } from '@/executor/types' interface NodeMetadataLike { diff --git a/apps/sim/executor/orchestrators/loop.ts b/apps/sim/executor/orchestrators/loop.ts index e7a055706..500a2ec83 100644 --- a/apps/sim/executor/orchestrators/loop.ts +++ b/apps/sim/executor/orchestrators/loop.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/consts' +import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/constants' import type { DAG } from '@/executor/dag/builder' import type { LoopScope } from '@/executor/execution/state' import type { BlockStateController } from '@/executor/execution/types' diff --git a/apps/sim/executor/orchestrators/node.ts b/apps/sim/executor/orchestrators/node.ts index 20fd27214..c3e50e957 100644 --- a/apps/sim/executor/orchestrators/node.ts +++ b/apps/sim/executor/orchestrators/node.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { EDGE } from '@/executor/consts' +import { EDGE } from '@/executor/constants' import type { DAG, DAGNode } from '@/executor/dag/builder' import type { BlockExecutor } from '@/executor/execution/block-executor' import type { BlockStateController } from '@/executor/execution/types' diff --git a/apps/sim/executor/utils/http.ts b/apps/sim/executor/utils/http.ts index d9db5a1fd..5562e4567 100644 --- a/apps/sim/executor/utils/http.ts +++ b/apps/sim/executor/utils/http.ts @@ -1,6 +1,6 @@ import { generateInternalToken } from '@/lib/auth/internal' import { getBaseUrl } from '@/lib/core/utils/urls' -import { HTTP } from '@/executor/consts' +import { HTTP } from '@/executor/constants' export async function buildAuthHeaders(): Promise> { const headers: Record = { diff --git a/apps/sim/executor/utils/json.ts b/apps/sim/executor/utils/json.ts index 9ef6d3932..d2e08136e 100644 --- a/apps/sim/executor/utils/json.ts +++ b/apps/sim/executor/utils/json.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { EVALUATOR } from '@/executor/consts' +import { EVALUATOR } from '@/executor/constants' const logger = createLogger('JSONUtils') diff --git a/apps/sim/executor/utils/reference-validation.ts b/apps/sim/executor/utils/reference-validation.ts index d1f72ce0c..ce569561c 100644 --- a/apps/sim/executor/utils/reference-validation.ts +++ b/apps/sim/executor/utils/reference-validation.ts @@ -1,5 +1,5 @@ import { isLikelyReferenceSegment } from '@/lib/workflows/sanitization/references' -import { REFERENCE } from '@/executor/consts' +import { REFERENCE } from '@/executor/constants' /** * Creates a regex pattern for matching variable references. diff --git a/apps/sim/executor/utils/subflow-utils.ts b/apps/sim/executor/utils/subflow-utils.ts index 7218654f7..c881adf26 100644 --- a/apps/sim/executor/utils/subflow-utils.ts +++ b/apps/sim/executor/utils/subflow-utils.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/consts' +import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/constants' import type { SerializedParallel } from '@/serializer/types' const logger = createLogger('SubflowUtils') @@ -10,14 +10,8 @@ const SENTINEL_START_PATTERN = new RegExp( `${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.START_SUFFIX}` ) const SENTINEL_END_PATTERN = new RegExp(`${LOOP.SENTINEL.PREFIX}(.+)${LOOP.SENTINEL.END_SUFFIX}`) -/** - * ================== - * LOOP UTILITIES - * ================== - */ -/** - * Build sentinel start node ID - */ + +/** Build sentinel start node ID */ export function buildSentinelStartId(loopId: string): string { return `${LOOP.SENTINEL.PREFIX}${loopId}${LOOP.SENTINEL.START_SUFFIX}` } @@ -41,11 +35,7 @@ export function extractLoopIdFromSentinel(sentinelId: string): string | null { if (endMatch) return endMatch[1] return null } -/** - * ================== - * PARALLEL UTILITIES - * ================== - */ + /** * Parse distribution items from parallel config * Handles: arrays, JSON strings, and references diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index d5aa1dfba..bcf34d82e 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { BlockType, REFERENCE } from '@/executor/consts' +import { BlockType, REFERENCE } from '@/executor/constants' import type { ExecutionState, LoopScope } from '@/executor/execution/state' import type { ExecutionContext } from '@/executor/types' import { replaceValidReferences } from '@/executor/utils/reference-validation' diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index f09a258e6..bc676ab78 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -1,4 +1,4 @@ -import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/consts' +import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/constants' import { navigatePath, type ResolutionContext, diff --git a/apps/sim/executor/variables/resolvers/env.ts b/apps/sim/executor/variables/resolvers/env.ts index 1c97128e2..485b23589 100644 --- a/apps/sim/executor/variables/resolvers/env.ts +++ b/apps/sim/executor/variables/resolvers/env.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { extractEnvVarName, isEnvVarReference } from '@/executor/consts' +import { extractEnvVarName, isEnvVarReference } from '@/executor/constants' import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference' const logger = createLogger('EnvResolver') diff --git a/apps/sim/executor/variables/resolvers/loop.ts b/apps/sim/executor/variables/resolvers/loop.ts index a406bab8f..416f684e3 100644 --- a/apps/sim/executor/variables/resolvers/loop.ts +++ b/apps/sim/executor/variables/resolvers/loop.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts' +import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants' import { extractBaseBlockId } from '@/executor/utils/subflow-utils' import { navigatePath, diff --git a/apps/sim/executor/variables/resolvers/parallel.ts b/apps/sim/executor/variables/resolvers/parallel.ts index ce6a5523b..447e39817 100644 --- a/apps/sim/executor/variables/resolvers/parallel.ts +++ b/apps/sim/executor/variables/resolvers/parallel.ts @@ -1,5 +1,5 @@ import { createLogger } from '@/lib/logs/console/logger' -import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts' +import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants' import { extractBaseBlockId, extractBranchIndex } from '@/executor/utils/subflow-utils' import { navigatePath, diff --git a/apps/sim/executor/variables/resolvers/workflow.ts b/apps/sim/executor/variables/resolvers/workflow.ts index e1c753682..b5ac6b988 100644 --- a/apps/sim/executor/variables/resolvers/workflow.ts +++ b/apps/sim/executor/variables/resolvers/workflow.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import { VariableManager } from '@/lib/workflows/variables/variable-manager' -import { isReference, parseReferencePath, REFERENCE } from '@/executor/consts' +import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants' import { navigatePath, type ResolutionContext, diff --git a/apps/sim/hooks/use-trigger-config-aggregation.ts b/apps/sim/hooks/use-trigger-config-aggregation.ts index 470845aed..801e2a40e 100644 --- a/apps/sim/hooks/use-trigger-config-aggregation.ts +++ b/apps/sim/hooks/use-trigger-config-aggregation.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { getTrigger, isTriggerValid } from '@/triggers' -import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts' +import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' const logger = createLogger('useTriggerConfigAggregation') diff --git a/apps/sim/lib/billing/storage/limits.ts b/apps/sim/lib/billing/storage/limits.ts index f188f8bc2..8f15ec223 100644 --- a/apps/sim/lib/billing/storage/limits.ts +++ b/apps/sim/lib/billing/storage/limits.ts @@ -9,7 +9,7 @@ import { DEFAULT_FREE_STORAGE_LIMIT_GB, DEFAULT_PRO_STORAGE_LIMIT_GB, DEFAULT_TEAM_STORAGE_LIMIT_GB, -} from '@sim/db/consts' +} from '@sim/db/constants' import { organization, subscription, userStats } from '@sim/db/schema' import { eq } from 'drizzle-orm' import { getEnv } from '@/lib/core/config/env' diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 7b9487351..984988f07 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -12,7 +12,7 @@ import { AuthMode } from '@/blocks/types' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { tools as toolsRegistry } from '@/tools/registry' import { getTrigger, isTriggerValid } from '@/triggers' -import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts' +import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' export interface CopilotSubblockMetadata { id: string diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 32567dbf0..8c3a04598 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -12,7 +12,7 @@ import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { getAllBlocks, getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/consts' +import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' /** Selector subblock types that can be validated */ const SELECTOR_TYPES = new Set([ diff --git a/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts b/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts index dbc787d86..8a671a4e9 100644 --- a/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts +++ b/apps/sim/lib/logs/execution/trace-spans/trace-spans.ts @@ -1,6 +1,6 @@ import { createLogger } from '@/lib/logs/console/logger' import type { ToolCall, TraceSpan } from '@/lib/logs/types' -import { isWorkflowBlockType } from '@/executor/consts' +import { isWorkflowBlockType } from '@/executor/constants' import type { ExecutionResult } from '@/executor/types' const logger = createLogger('TraceSpans') diff --git a/apps/sim/lib/webhooks/env-resolver.ts b/apps/sim/lib/webhooks/env-resolver.ts index 34811cc6f..74da1ce2b 100644 --- a/apps/sim/lib/webhooks/env-resolver.ts +++ b/apps/sim/lib/webhooks/env-resolver.ts @@ -1,6 +1,6 @@ import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { createLogger } from '@/lib/logs/console/logger' -import { extractEnvVarName, isEnvVarReference } from '@/executor/consts' +import { extractEnvVarName, isEnvVarReference } from '@/executor/constants' const logger = createLogger('EnvResolver') diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index 864a1b513..c3c414947 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -2,7 +2,7 @@ import type { Edge } from 'reactflow' import { sanitizeWorkflowForSharing } from '@/lib/workflows/credentials/credential-extractor' import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/consts' +import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/constants' /** * Sanitized workflow state for copilot (removes all UI-specific data) diff --git a/apps/sim/lib/workflows/training/compute-edit-sequence.ts b/apps/sim/lib/workflows/training/compute-edit-sequence.ts index bf4204376..b50ce4921 100644 --- a/apps/sim/lib/workflows/training/compute-edit-sequence.ts +++ b/apps/sim/lib/workflows/training/compute-edit-sequence.ts @@ -1,5 +1,5 @@ import type { CopilotWorkflowState } from '@/lib/workflows/sanitization/json-sanitizer' -import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/consts' +import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' export interface EditOperation { operation_type: 'add' | 'edit' | 'delete' | 'insert_into_subflow' | 'extract_from_subflow' diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index ea8d7d513..13977f37d 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -41,7 +41,6 @@ describe('getApiKey', () => { beforeEach(() => { vi.clearAllMocks() - // @ts-expect-error - mocking boolean with different value isHostedSpy.mockReturnValue(false) module.require = vi.fn(() => ({ @@ -55,7 +54,6 @@ describe('getApiKey', () => { }) it('should return user-provided key when not in hosted environment', () => { - // @ts-expect-error - mocking boolean with different value isHostedSpy.mockReturnValue(false) // For OpenAI @@ -68,7 +66,6 @@ describe('getApiKey', () => { }) it('should throw error if no key provided in non-hosted environment', () => { - // @ts-expect-error - mocking boolean with different value isHostedSpy.mockReturnValue(false) expect(() => getApiKey('openai', 'gpt-4')).toThrow('API key is required for openai gpt-4') diff --git a/apps/sim/socket-server/index.test.ts b/apps/sim/socket-server/index.test.ts index 3f006607d..d21bb9b8a 100644 --- a/apps/sim/socket-server/index.test.ts +++ b/apps/sim/socket-server/index.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { createServer } from 'http' +import { createServer, request as httpRequest } from 'http' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { createLogger } from '@/lib/logs/console/logger' import { createSocketIOServer } from '@/socket-server/config/socket' @@ -68,23 +68,17 @@ describe('Socket Server Index Integration', () => { }) beforeEach(async () => { - // Use a random port for each test to avoid conflicts PORT = 3333 + Math.floor(Math.random() * 1000) - // Create HTTP server httpServer = createServer() - // Create Socket.IO server using extracted config io = createSocketIOServer(httpServer) - // Initialize room manager after io is created roomManager = new RoomManager(io) - // Configure HTTP request handler const httpHandler = createHttpHandler(roomManager, logger) httpServer.on('request', httpHandler) - // Start server with timeout handling await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Server failed to start on port ${PORT} within 15 seconds`)) @@ -98,7 +92,6 @@ describe('Socket Server Index Integration', () => { httpServer.on('error', (err: any) => { clearTimeout(timeout) if (err.code === 'EADDRINUSE') { - // Try a different port PORT = 3333 + Math.floor(Math.random() * 1000) httpServer.listen(PORT, '0.0.0.0', () => { resolve() @@ -111,7 +104,6 @@ describe('Socket Server Index Integration', () => { }, 20000) afterEach(async () => { - // Properly close servers and wait for them to fully close if (io) { await new Promise((resolve) => { io.close(() => resolve()) @@ -132,18 +124,40 @@ describe('Socket Server Index Integration', () => { }) it('should handle health check endpoint', async () => { - try { - const response = await fetch(`http://localhost:${PORT}/health`) - expect(response.status).toBe(200) + const data = await new Promise<{ status: string; timestamp: number; connections: number }>( + (resolve, reject) => { + const req = httpRequest( + { + hostname: 'localhost', + port: PORT, + path: '/health', + method: 'GET', + }, + (res) => { + expect(res.statusCode).toBe(200) - const data = await response.json() - expect(data).toHaveProperty('status', 'ok') - expect(data).toHaveProperty('timestamp') - expect(data).toHaveProperty('connections') - } catch (error) { - // Skip this test if fetch fails (likely due to test environment) - console.warn('Health check test skipped due to fetch error:', error) - } + let body = '' + res.on('data', (chunk) => { + body += chunk + }) + res.on('end', () => { + try { + resolve(JSON.parse(body)) + } catch (e) { + reject(e) + } + }) + } + ) + + req.on('error', reject) + req.end() + } + ) + + expect(data).toHaveProperty('status', 'ok') + expect(data).toHaveProperty('timestamp') + expect(data).toHaveProperty('connections') }) }) @@ -228,7 +242,6 @@ describe('Socket Server Index Integration', () => { describe('Module Integration', () => { it.concurrent('should properly import all extracted modules', async () => { - // Test that all modules can be imported without errors const { createSocketIOServer } = await import('@/socket-server/config/socket') const { createHttpHandler } = await import('@/socket-server/routes/http') const { RoomManager } = await import('@/socket-server/rooms/manager') @@ -247,12 +260,10 @@ describe('Socket Server Index Integration', () => { }) it.concurrent('should maintain all original functionality after refactoring', () => { - // Verify that the main components are properly instantiated expect(httpServer).toBeDefined() expect(io).toBeDefined() expect(roomManager).toBeDefined() - // Verify core methods exist and are callable expect(typeof roomManager.createWorkflowRoom).toBe('function') expect(typeof roomManager.cleanupUserFromRoom).toBe('function') expect(typeof roomManager.handleWorkflowDeletion).toBe('function') diff --git a/apps/sim/socket-server/tests/socket-server.test.ts b/apps/sim/socket-server/tests/socket-server.test.ts index 241471679..706024de1 100644 --- a/apps/sim/socket-server/tests/socket-server.test.ts +++ b/apps/sim/socket-server/tests/socket-server.test.ts @@ -1,7 +1,7 @@ import { createServer } from 'http' import { Server } from 'socket.io' import { io, type Socket } from 'socket.io-client' -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' describe('Socket Server Integration Tests', () => { let httpServer: any @@ -10,7 +10,6 @@ describe('Socket Server Integration Tests', () => { let serverPort: number beforeAll(async () => { - // Create a test server instance httpServer = createServer() socketServer = new Server(httpServer, { cors: { @@ -19,7 +18,6 @@ describe('Socket Server Integration Tests', () => { }, }) - // Start server on random port await new Promise((resolve) => { httpServer.listen(() => { serverPort = httpServer.address()?.port @@ -27,7 +25,6 @@ describe('Socket Server Integration Tests', () => { }) }) - // Basic socket handlers for testing socketServer.on('connection', (socket) => { socket.on('join-workflow', ({ workflowId }) => { socket.join(workflowId) @@ -41,19 +38,7 @@ describe('Socket Server Integration Tests', () => { }) }) }) - }) - afterAll(async () => { - if (socketServer) { - socketServer.close() - } - if (httpServer) { - httpServer.close() - } - }) - - beforeEach(async () => { - // Create client socket for each test clientSocket = io(`http://localhost:${serverPort}`, { transports: ['polling', 'websocket'], }) @@ -65,10 +50,16 @@ describe('Socket Server Integration Tests', () => { }) }) - afterEach(() => { + afterAll(async () => { if (clientSocket) { clientSocket.close() } + if (socketServer) { + socketServer.close() + } + if (httpServer) { + httpServer.close() + } }) it('should connect to socket server', () => { @@ -79,7 +70,7 @@ describe('Socket Server Integration Tests', () => { const workflowId = 'test-workflow-123' const joinedPromise = new Promise((resolve) => { - clientSocket.on('joined-workflow', (data) => { + clientSocket.once('joined-workflow', (data) => { expect(data.workflowId).toBe(workflowId) resolve() }) @@ -92,39 +83,45 @@ describe('Socket Server Integration Tests', () => { it('should broadcast workflow operations', async () => { const workflowId = 'test-workflow-456' - // Create second client const client2 = io(`http://localhost:${serverPort}`) await new Promise((resolve) => { - client2.on('connect', resolve) + client2.once('connect', resolve) }) - // Both clients join the same workflow - clientSocket.emit('join-workflow', { workflowId }) - client2.emit('join-workflow', { workflowId }) - - // Wait for joins to complete - await new Promise((resolve) => setTimeout(resolve, 100)) - - const operationPromise = new Promise((resolve) => { - client2.on('workflow-operation', (data) => { - expect(data.operation).toBe('add') - expect(data.target).toBe('block') - expect(data.payload.id).toBe('block-123') - resolve() + try { + const join1Promise = new Promise((resolve) => { + clientSocket.once('joined-workflow', () => resolve()) + }) + const join2Promise = new Promise((resolve) => { + client2.once('joined-workflow', () => resolve()) }) - }) - // Client 1 sends operation - clientSocket.emit('workflow-operation', { - workflowId, - operation: 'add', - target: 'block', - payload: { id: 'block-123', type: 'action', name: 'Test Block' }, - timestamp: Date.now(), - }) + clientSocket.emit('join-workflow', { workflowId }) + client2.emit('join-workflow', { workflowId }) - await operationPromise - client2.close() + await Promise.all([join1Promise, join2Promise]) + + const operationPromise = new Promise((resolve) => { + client2.once('workflow-operation', (data) => { + expect(data.operation).toBe('add') + expect(data.target).toBe('block') + expect(data.payload.id).toBe('block-123') + resolve() + }) + }) + + clientSocket.emit('workflow-operation', { + workflowId, + operation: 'add', + target: 'block', + payload: { id: 'block-123', type: 'action', name: 'Test Block' }, + timestamp: Date.now(), + }) + + await operationPromise + } finally { + client2.close() + } }) it('should handle multiple concurrent connections', async () => { @@ -132,51 +129,58 @@ describe('Socket Server Integration Tests', () => { const clients: Socket[] = [] const workflowId = 'stress-test-workflow' - // Create multiple clients - for (let i = 0; i < numClients; i++) { - const client = io(`http://localhost:${serverPort}`) - clients.push(client) - - await new Promise((resolve) => { - client.on('connect', resolve) - }) - - client.emit('join-workflow', { workflowId }) - } - - // Wait for all joins - await new Promise((resolve) => setTimeout(resolve, 200)) - - let receivedCount = 0 - const expectedCount = numClients - 1 // All except sender - - const operationPromise = new Promise((resolve) => { - clients.forEach((client, index) => { - if (index === 0) return // Skip sender - - client.on('workflow-operation', () => { - receivedCount++ - if (receivedCount === expectedCount) { - resolve() - } + try { + const connectPromises = Array.from({ length: numClients }, () => { + const client = io(`http://localhost:${serverPort}`) + clients.push(client) + return new Promise((resolve) => { + client.once('connect', resolve) }) }) - }) - // First client sends operation - clients[0].emit('workflow-operation', { - workflowId, - operation: 'add', - target: 'block', - payload: { id: 'stress-block', type: 'action' }, - timestamp: Date.now(), - }) + await Promise.all(connectPromises) - await operationPromise - expect(receivedCount).toBe(expectedCount) + const joinPromises = clients.map((client) => { + return new Promise((resolve) => { + client.once('joined-workflow', () => resolve()) + }) + }) - // Clean up - clients.forEach((client) => client.close()) + clients.forEach((client) => { + client.emit('join-workflow', { workflowId }) + }) + + await Promise.all(joinPromises) + + let receivedCount = 0 + const expectedCount = numClients - 1 + + const operationPromise = new Promise((resolve) => { + clients.forEach((client, index) => { + if (index === 0) return + + client.once('workflow-operation', () => { + receivedCount++ + if (receivedCount === expectedCount) { + resolve() + } + }) + }) + }) + + clients[0].emit('workflow-operation', { + workflowId, + operation: 'add', + target: 'block', + payload: { id: 'stress-block', type: 'action' }, + timestamp: Date.now(), + }) + + await operationPromise + expect(receivedCount).toBe(expectedCount) + } finally { + clients.forEach((client) => client.close()) + } }) it('should handle rapid operations without loss', async () => { @@ -185,43 +189,51 @@ describe('Socket Server Integration Tests', () => { const client2 = io(`http://localhost:${serverPort}`) await new Promise((resolve) => { - client2.on('connect', resolve) + client2.once('connect', resolve) }) - clientSocket.emit('join-workflow', { workflowId }) - client2.emit('join-workflow', { workflowId }) - - await new Promise((resolve) => setTimeout(resolve, 100)) - - let receivedCount = 0 - const receivedOperations = new Set() - - const operationsPromise = new Promise((resolve) => { - client2.on('workflow-operation', (data) => { - receivedCount++ - receivedOperations.add(data.payload.id) - - if (receivedCount === numOperations) { - resolve() - } + try { + const join1Promise = new Promise((resolve) => { + clientSocket.once('joined-workflow', () => resolve()) }) - }) - - // Send rapid operations - for (let i = 0; i < numOperations; i++) { - clientSocket.emit('workflow-operation', { - workflowId, - operation: 'add', - target: 'block', - payload: { id: `rapid-block-${i}`, type: 'action' }, - timestamp: Date.now(), + const join2Promise = new Promise((resolve) => { + client2.once('joined-workflow', () => resolve()) }) + + clientSocket.emit('join-workflow', { workflowId }) + client2.emit('join-workflow', { workflowId }) + + await Promise.all([join1Promise, join2Promise]) + + let receivedCount = 0 + const receivedOperations = new Set() + + const operationsPromise = new Promise((resolve) => { + client2.on('workflow-operation', (data) => { + receivedCount++ + receivedOperations.add(data.payload.id) + + if (receivedCount === numOperations) { + resolve() + } + }) + }) + + for (let i = 0; i < numOperations; i++) { + clientSocket.emit('workflow-operation', { + workflowId, + operation: 'add', + target: 'block', + payload: { id: `rapid-block-${i}`, type: 'action' }, + timestamp: Date.now(), + }) + } + + await operationsPromise + expect(receivedCount).toBe(numOperations) + expect(receivedOperations.size).toBe(numOperations) + } finally { + client2.close() } - - await operationsPromise - expect(receivedCount).toBe(numOperations) - expect(receivedOperations.size).toBe(numOperations) - - client2.close() }) }) diff --git a/apps/sim/stores/workflows/json/importer.ts b/apps/sim/stores/workflows/json/importer.ts index 16da473f1..6157cfd3b 100644 --- a/apps/sim/stores/workflows/json/importer.ts +++ b/apps/sim/stores/workflows/json/importer.ts @@ -1,5 +1,6 @@ import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' +import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' import type { WorkflowState } from '../workflow/types' const logger = createLogger('WorkflowJsonImporter') @@ -60,10 +61,18 @@ function regenerateIds(workflowState: WorkflowState): WorkflowState { }) } - // Fifth pass: update any block references in subblock values + // Fifth pass: update any block references in subblock values and clear runtime trigger values Object.entries(newBlocks).forEach(([blockId, block]) => { if (block.subBlocks) { Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => { + if (TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subBlockId)) { + block.subBlocks[subBlockId] = { + ...subBlock, + value: null, + } + return + } + if (subBlock.value && typeof subBlock.value === 'string') { // Replace any block references in the value let updatedValue = subBlock.value diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 842a1ff40..02195ebf4 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -6,7 +6,7 @@ import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' -import { isAnnotationOnlyBlock } from '@/executor/consts' +import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { diff --git a/apps/sim/triggers/consts.ts b/apps/sim/triggers/constants.ts similarity index 100% rename from apps/sim/triggers/consts.ts rename to apps/sim/triggers/constants.ts diff --git a/apps/sim/vitest.config.ts b/apps/sim/vitest.config.ts index 8c8a21091..145943a2b 100644 --- a/apps/sim/vitest.config.ts +++ b/apps/sim/vitest.config.ts @@ -1,4 +1,4 @@ -import path, { resolve } from 'path' +import path from 'path' /// import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' @@ -18,8 +18,23 @@ export default defineConfig({ include: ['**/*.test.{ts,tsx}'], exclude: [...configDefaults.exclude, '**/node_modules/**', '**/dist/**'], setupFiles: ['./vitest.setup.ts'], - alias: { - '@sim/db': resolve(__dirname, '../../packages/db'), + pool: 'threads', + poolOptions: { + threads: { + singleThread: false, + useAtomics: true, + isolate: true, + }, + }, + fileParallelism: true, + maxConcurrency: 20, + testTimeout: 10000, + deps: { + optimizer: { + web: { + enabled: true, + }, + }, }, }, resolve: { diff --git a/packages/db/consts.ts b/packages/db/constants.ts similarity index 100% rename from packages/db/consts.ts rename to packages/db/constants.ts diff --git a/packages/db/schema.ts b/packages/db/schema.ts index ae7a7190a..cbffabe4d 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -17,7 +17,7 @@ import { uuid, vector, } from 'drizzle-orm/pg-core' -import { DEFAULT_FREE_CREDITS, TAG_SLOTS } from './consts' +import { DEFAULT_FREE_CREDITS, TAG_SLOTS } from './constants' // Custom tsvector type for full-text search export const tsvector = customType<{