mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-11 07:04:58 -05:00
fix(import): fixed trigger save on export/import flow (#2239)
* fix(import): fixed trigger save on export/import flow * optimized test runners * ack PR comments
This commit is contained in:
@@ -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 <tag_name> 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 <waleed@sim.ai> 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 <validVar> and <another_valid> but not <invalid@email.com>
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 <complexData>',
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user