mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* feat(rippling): expand Rippling integration from 16 to 86 tools * fix(rippling): add required constraints on name and data subBlocks for create operations * fix(rippling): add subblock ID migrations for removed legacy fields * fix(docs): add MANUAL-CONTENT markers to tailscale docs and regenerate * fix(rippling): add missing response fields to tool transforms Add fields found missing by validation agents: - list_companies: physical_address - list/get_supergroups: sub_group_type, read_only, parent, mutually_exclusive_key, cumulatively_exhaustive_default, include_terminated - list/get/create/update_custom_object: native_category_id, managed_package_install_id, owner_id - list/get/create/update_custom_app: icon, pages - list/get/create/update_custom_object_field: managed_package_install_id * fix(rippling): add missing block outputs and required data conditions - Add 17 missing collection output keys (titles, workLocations, supergroups, etc.) - Add delete/bulk/report output keys (deleted, results, report_id, etc.) - Mark data subBlock required for create_business_partner, create_custom_app, and create_custom_object_field (all have required params via data JSON spread) - Add optional: true to get_current_user work_email and company_id outputs * fix(rippling): add missing supergroup fields and fix validation issues - Add 5 missing supergroup fields (allow_non_employees, can_override_role_states, priority, is_invisible, ignore_prov_group_matching) to types, list, and get tools - Fix ok fallback from true to false in supergroup inclusion/exclusion member update tools - Fix truthy check to null check for description param in create_custom_object_field * fix(rippling): add missing custom page fields and structured custom setting responses - Add 5 missing CustomPage fields (components, actions, canvas_actions, variables, media) to types and all page tools - Replace opaque data blob with structured field mapping in create/update custom setting transforms - Fix secret_value type cast consistency in list_custom_settings * fix(rippling): add missing response fields, fix truthy checks, and improve UX - Add 9 missing Worker fields (location, gender, date_of_birth, race, ethnicity, citizenship, termination_details, custom_fields, country_fields) - Add 5 missing User fields (name, emails, phone_numbers, addresses, photos) - Add worker expandable field to GroupMember types and all 3 member list tools - Add 5 optional params to trigger_report_run (includeObjectIds, includeTotalRows, formatDateFields, formatCurrencyFields, outputType) - Fix truthy checks to null checks in create_department, create/update_work_location - Fix customObjectId subBlock label to say "API Name" instead of "ID" * update docs * fix(rippling): fix truthy checks, add missing fields, and regenerate docs - Replace all `if (params.x)` with `if (params.x != null)` across 30+ tool files to prevent empty string/false/zero suppression - Add expandable `parent` and `department_hierarchy` fields to department tools - Add expandable `parent` field to team tools - Add `company` expandable field to get_current_user - Add `addressType` param to create/update work location tools - Fix `secret_value` output type from 'json' to 'string' in list_custom_settings - Regenerate docs for all 86 tools from current definitions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): add all remaining spec fields and regenerate docs - Add 6 advanced params to create_custom_object_field: required, rqlDefinition, formulaAttrMetas, section, derivedFieldFormula, derivedAggregatedField - Add 6 advanced params to update_custom_object_field: required, rqlDefinition, formulaAttrMetas, section, derivedFieldFormula, nameFieldDetails - Add 4 record output fields to all custom object record tools: created_by, last_modified_by, owner_role, system_updated_at - Add cursor param to get_current_user - Add __meta response field to get_report_run - Regenerate docs for all 86 tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): align all tools with OpenAPI spec - Add __meta to 14 GET-by-ID tools (MetaResponse pattern) - Fix supergroup tools: add filter to list_supergroups, remove invalid cursor from 4 list endpoints, revert update members to PATCH with Operations body - Fix query_custom_object_records: use query/limit/cursor body params, return cursor instead of nextLink - Fix bulk_create: use rows_to_write per spec - Fix create/update record body wrappers with externalId support - Update types.ts param interfaces and block config mappings - Add limit param mapping with Number() conversion in block config - Regenerate docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): address PR review comments — add dedicated subBlocks, fix data duplication, expand externalId condition - Add dedicated apiName, businessPartnerGroupId, workerId, dataType subBlocks so required params are no longer hidden behind opaque data JSON - Narrow `data: item` in custom object record tools to only include dynamic fields, avoiding duplication of enumerated fields - Expand externalId subBlock condition to include create/update custom object record operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): remove data JSON required for ops with dedicated subBlocks create_business_partner, create_custom_app, and create_custom_object_field now have dedicated subBlocks for their required params, so the data JSON field is supplementary (not required) for those operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): use rest-destructuring for all custom object record data output The spec uses additionalProperties for custom fields at the top level, not a nested `data` sub-object. Use the same rest-destructuring pattern across all 6 custom object record tools so `data` only contains dynamic fields, not duplicates of enumerated standard fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): make update_custom_object_record data param optional in type Matches the tool's `required: false` — users may update only external_id without changing data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): add dedicated streetAddress subBlock for create_work_location streetAddress is required by the tool but had no dedicated subBlock — users had to include it in the data JSON. Now has its own required subBlock matching the pattern used by all other required params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): add allOrNothing subBlock for bulk operations The bulk create/update/delete tools accept an optional allOrNothing boolean param, but it had no subBlock and no way to be passed through the block UI. Added as an advanced-mode dropdown with boolean coercion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): derive spreadOps from DATA_OPS to prevent divergence Replace the hardcoded spreadOps array with a derivation from the file-level DATA_OPS constant minus non-spread operations. This ensures new create/update operations added to DATA_OPS automatically get spread behavior without needing a second manual update. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * updated * fix(rippling): replace generic JSON outputs with specific fields per API spec - Extract file_url, expires_at, output_type from report run result blob - Rename bulk create/update outputs to createdRecords/updatedRecords - Fix list_custom_settings output key mismatch (settings → customSettings) - Make data optional for update_custom_object_record in block - Update block outputs to match new tool output fields Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix landing * restore FF * fix(rippling): add wandConfig, clean titles, and migrate legacy operation values - Remove "(JSON)" suffix from all subBlock titles - Add wandConfig with AI prompts for filter, expand, orderBy, query, data, records, and dataType fields - Add OPERATION_VALUE_MIGRATIONS to migrate old operation values (list_employees → list_workers, etc.) preventing runtime errors on saved workflows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(rippling): fix grammar typos and revert unnecessary migration - Fix "a object" → "an object" in update/delete object category descriptions - Revert OPERATION_VALUE_MIGRATIONS (unnecessary for low-usage integration) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(landing): add interactive workspace preview tabs Adds Tables, Files, Knowledge Base, Logs, and Scheduled Tasks preview components to the landing hero, with sidebar nav items that switch to each view. * test updates * refactor(landing): clean up code quality issues in preview components - Replace widthMultiplier with explicit width on PreviewColumn - Replace key={i} with key={Icon.name} in connectorIcons - Scope --c-active CSS variable to sidebar container, eliminating hardcoded #363636 duplication - Replace '- - -' fallback with em dash - Type onSelectNav as (id: SidebarView) removing the unsafe cast * fix(landing): use stable index key in connectorIcons to avoid minification breakage --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
600 lines
17 KiB
TypeScript
600 lines
17 KiB
TypeScript
/**
|
|
* Tests for function execution API route
|
|
*
|
|
* @vitest-environment node
|
|
*/
|
|
import { createMockRequest } from '@sim/testing'
|
|
import { NextRequest } from 'next/server'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { mockCheckInternalAuth, mockExecuteInE2B, mockExecuteInIsolatedVM } = vi.hoisted(() => ({
|
|
mockCheckInternalAuth: vi.fn(),
|
|
mockExecuteInE2B: vi.fn(),
|
|
mockExecuteInIsolatedVM: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/execution/isolated-vm', () => ({
|
|
executeInIsolatedVM: mockExecuteInIsolatedVM,
|
|
}))
|
|
|
|
vi.mock('@/lib/auth/hybrid', () => ({
|
|
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
|
checkInternalAuth: mockCheckInternalAuth,
|
|
}))
|
|
|
|
vi.mock('@/lib/execution/e2b', () => ({
|
|
executeInE2B: mockExecuteInE2B,
|
|
}))
|
|
|
|
vi.mock('@/lib/core/config/feature-flags', () => ({
|
|
isHosted: false,
|
|
isE2bEnabled: false,
|
|
isProd: false,
|
|
isDev: false,
|
|
isTest: true,
|
|
}))
|
|
|
|
import { validateProxyUrl } from '@/lib/core/security/input-validation'
|
|
import { POST } from '@/app/api/function/execute/route'
|
|
|
|
/**
|
|
* Creates a fake isolated-vm execution result by evaluating code
|
|
* in a sandboxed context, mimicking the real executeInIsolatedVM behavior.
|
|
*/
|
|
function createIsolatedVmImplementation() {
|
|
return async (req: {
|
|
code: string
|
|
params: Record<string, unknown>
|
|
envVars: Record<string, unknown>
|
|
contextVariables: Record<string, unknown>
|
|
}) => {
|
|
const { code, params, envVars, contextVariables } = req
|
|
const stdoutChunks: string[] = []
|
|
|
|
const mockConsole = {
|
|
log: (...args: unknown[]) => {
|
|
stdoutChunks.push(
|
|
`${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}\n`
|
|
)
|
|
},
|
|
error: (...args: unknown[]) => {
|
|
stdoutChunks.push(
|
|
'ERROR: ' +
|
|
args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') +
|
|
'\n'
|
|
)
|
|
},
|
|
warn: (...args: unknown[]) => mockConsole.log('WARN:', ...args),
|
|
info: (...args: unknown[]) => mockConsole.log(...args),
|
|
}
|
|
|
|
try {
|
|
const escapePattern = /this\.constructor\.constructor|\.constructor\s*\(/
|
|
if (escapePattern.test(code)) {
|
|
return { result: undefined, stdout: '' }
|
|
}
|
|
|
|
const context: Record<string, unknown> = {
|
|
console: mockConsole,
|
|
params,
|
|
environmentVariables: envVars,
|
|
...contextVariables,
|
|
process: undefined,
|
|
require: undefined,
|
|
module: undefined,
|
|
exports: undefined,
|
|
__dirname: undefined,
|
|
__filename: undefined,
|
|
fetch: async () => {
|
|
throw new Error('fetch not implemented in test mock')
|
|
},
|
|
}
|
|
|
|
const paramNames = Object.keys(context)
|
|
const paramValues = Object.values(context)
|
|
|
|
const wrappedCode = `
|
|
return (async () => {
|
|
${code}
|
|
})();
|
|
`
|
|
|
|
const fn = new Function(...paramNames, wrappedCode)
|
|
const result = await fn(...paramValues)
|
|
|
|
return {
|
|
result,
|
|
stdout: stdoutChunks.join(''),
|
|
}
|
|
} catch (error: unknown) {
|
|
const err = error as Error
|
|
return {
|
|
result: null,
|
|
stdout: stdoutChunks.join(''),
|
|
error: {
|
|
message: err.message || String(error),
|
|
name: err.name || 'Error',
|
|
stack: err.stack,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('Function Execute API Route', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
mockCheckInternalAuth.mockResolvedValue({
|
|
success: true,
|
|
userId: 'user-123',
|
|
authType: 'internal_jwt',
|
|
})
|
|
|
|
mockExecuteInIsolatedVM.mockImplementation(createIsolatedVmImplementation())
|
|
|
|
mockExecuteInE2B.mockResolvedValue({
|
|
result: 'e2b success',
|
|
stdout: 'e2b output',
|
|
sandboxId: 'test-sandbox-id',
|
|
})
|
|
})
|
|
|
|
describe('Security Tests', () => {
|
|
it('should reject unauthorized requests', async () => {
|
|
mockCheckInternalAuth.mockResolvedValueOnce({
|
|
success: false,
|
|
error: 'Unauthorized',
|
|
})
|
|
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "test"',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(401)
|
|
expect(data).toHaveProperty('error', 'Unauthorized')
|
|
})
|
|
|
|
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "test"',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.output.result).toBe('test')
|
|
})
|
|
|
|
it.concurrent('should prevent VM escape via constructor chain', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return this.constructor.constructor("return process")().env',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
if (response.status === 500) {
|
|
expect(data.success).toBe(false)
|
|
} else {
|
|
const result = data.output?.result
|
|
expect(result === undefined || result === null).toBe(true)
|
|
}
|
|
})
|
|
|
|
it.concurrent('should prevent access to require via constructor chain', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: `
|
|
const proc = this.constructor.constructor("return process")();
|
|
const fs = proc.mainModule.require("fs");
|
|
return fs.readFileSync("/etc/passwd", "utf8");
|
|
`,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
if (response.status === 200) {
|
|
const result = data.output?.result
|
|
if (result !== undefined && result !== null && typeof result === 'string') {
|
|
expect(result).not.toContain('root:')
|
|
}
|
|
}
|
|
})
|
|
|
|
it.concurrent('should not expose process object', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return typeof process',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.output.result).toBe('undefined')
|
|
})
|
|
|
|
it.concurrent('should not expose require function', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return typeof require',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.output.result).toBe('undefined')
|
|
})
|
|
|
|
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
|
|
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
|
|
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(true)
|
|
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
|
|
expect(validateProxyUrl('http://10.0.0.1/internal').isValid).toBe(false)
|
|
})
|
|
|
|
it.concurrent('should allow legitimate external URLs', async () => {
|
|
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 () => {
|
|
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)
|
|
})
|
|
})
|
|
|
|
describe('Basic Function Execution', () => {
|
|
it.concurrent('should execute simple JavaScript code successfully', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "Hello World"',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.output).toHaveProperty('result')
|
|
expect(data.output).toHaveProperty('executionTime')
|
|
})
|
|
|
|
it.concurrent('should return computed result for multi-line code', async () => {
|
|
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 response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
expect(data.output.result).toBe(10)
|
|
})
|
|
|
|
it.concurrent('should handle missing code parameter', async () => {
|
|
const req = createMockRequest('POST', {
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data).toHaveProperty('error')
|
|
})
|
|
|
|
it.concurrent('should use default timeout when not provided', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "test"',
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Template Variable Resolution', () => {
|
|
it.concurrent('should resolve environment variables with {{var_name}} syntax', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return {{API_KEY}}',
|
|
envVars: {
|
|
API_KEY: 'secret-key-123',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
|
|
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return <email>',
|
|
blockData: {
|
|
'block-123': { id: '123', subject: 'Test Email' },
|
|
},
|
|
blockNameMapping: {
|
|
email: 'block-123',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
|
|
it.concurrent('should NOT treat email addresses as template variables', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "Email sent to user"',
|
|
params: {
|
|
email: {
|
|
from: 'Dr. Shaw <shaw@high-flying.ai>',
|
|
to: 'User <user@example.com>',
|
|
},
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
|
|
it.concurrent('should only match valid variable names in angle brackets', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
|
blockData: {
|
|
'block-1': 'hello',
|
|
'block-2': 'world',
|
|
},
|
|
blockNameMapping: {
|
|
validvar: 'block-1',
|
|
another_valid: 'block-2',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
})
|
|
|
|
describe('Gmail Email Data Handling', () => {
|
|
it.concurrent(
|
|
'should handle Gmail webhook data with email addresses containing angle brackets',
|
|
async () => {
|
|
const emailData = {
|
|
id: '123',
|
|
from: 'Dr. Shaw <shaw@high-flying.ai>',
|
|
to: 'User <user@example.com>',
|
|
subject: 'Test Email',
|
|
bodyText: 'Hello world',
|
|
}
|
|
|
|
const req = createMockRequest('POST', {
|
|
code: 'return <email>',
|
|
blockData: {
|
|
'block-email': emailData,
|
|
},
|
|
blockNameMapping: {
|
|
email: 'block-email',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const data = await response.json()
|
|
expect(data.success).toBe(true)
|
|
}
|
|
)
|
|
|
|
it.concurrent(
|
|
'should properly serialize complex email objects with special characters',
|
|
async () => {
|
|
const emailData = {
|
|
from: 'Test User <test@example.com>',
|
|
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
|
bodyText: 'Text with\nnewlines\tand\ttabs',
|
|
}
|
|
|
|
const req = createMockRequest('POST', {
|
|
code: 'return <email>',
|
|
blockData: {
|
|
'block-email': emailData,
|
|
},
|
|
blockNameMapping: {
|
|
email: 'block-email',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('Custom Tools', () => {
|
|
it.concurrent('should handle custom tool execution with direct parameter access', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return location + " weather is sunny"',
|
|
params: {
|
|
location: 'San Francisco',
|
|
},
|
|
isCustomTool: true,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
})
|
|
|
|
describe('Security and Edge Cases', () => {
|
|
it.concurrent('should handle malformed JSON in request body', async () => {
|
|
const req = new NextRequest('http://localhost:3000/api/function/execute', {
|
|
method: 'POST',
|
|
body: 'invalid json{',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(500)
|
|
})
|
|
|
|
it.concurrent('should handle timeout parameter', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "test"',
|
|
timeout: 10000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(data.success).toBe(true)
|
|
})
|
|
|
|
it.concurrent('should handle empty parameters object', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return "no params"',
|
|
params: {},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
})
|
|
|
|
describe('Enhanced Error Handling', () => {
|
|
it('should provide detailed syntax error with line content', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data.error).toBeTruthy()
|
|
})
|
|
|
|
it('should provide detailed runtime error with line and column', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'const obj = null;\nreturn obj.someMethod();',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data.error).toContain('Type Error')
|
|
expect(data.error).toContain('Cannot read properties of null')
|
|
})
|
|
|
|
it('should handle ReferenceError with enhanced details', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'const x = 42;\nreturn undefinedVariable + x;',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data.error).toContain('Reference Error')
|
|
expect(data.error).toContain('undefinedVariable is not defined')
|
|
})
|
|
|
|
it('should handle thrown errors gracefully', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'throw new Error("Custom error message");',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data.error).toContain('Custom error message')
|
|
})
|
|
|
|
it.concurrent('should provide helpful suggestions for common syntax errors', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'const obj = {\n name: "test"\n// Missing closing brace',
|
|
timeout: 5000,
|
|
})
|
|
|
|
const response = await POST(req)
|
|
const data = await response.json()
|
|
|
|
expect(response.status).toBe(500)
|
|
expect(data.success).toBe(false)
|
|
expect(data.error).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('Utility Functions', () => {
|
|
it.concurrent('should properly escape regex special characters', async () => {
|
|
const req = createMockRequest('POST', {
|
|
code: 'return {{special.chars+*?}}',
|
|
envVars: {
|
|
'special.chars+*?': 'escaped-value',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
|
|
it.concurrent('should handle JSON serialization edge cases', async () => {
|
|
const complexData = {
|
|
special: 'chars"with\'quotes',
|
|
unicode: '🎉 Unicode content',
|
|
nested: {
|
|
deep: {
|
|
value: 'test',
|
|
},
|
|
},
|
|
}
|
|
|
|
const req = createMockRequest('POST', {
|
|
code: 'return <complexData>',
|
|
blockData: {
|
|
'block-complex': complexData,
|
|
},
|
|
blockNameMapping: {
|
|
complexdata: 'block-complex',
|
|
},
|
|
})
|
|
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
})
|
|
})
|
|
})
|