mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04: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:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<typeof import('@/lib/workflows/utils')>('@/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<typeof import('@/lib/workflows/utils')>('@/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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
<span key={key++} className='text-[#34B5FF] dark:text-[#34B5FF]'>
|
||||
<span key={key++} className='text-[#34B5FF]'>
|
||||
{matchText}
|
||||
</span>
|
||||
)
|
||||
@@ -77,7 +77,7 @@ export function formatDisplayText(text: string, context?: HighlightContext): Rea
|
||||
if (split && shouldHighlightReference(split.reference)) {
|
||||
pushPlainText(split.leading)
|
||||
nodes.push(
|
||||
<span key={key++} className='text-[#34B5FF] dark:text-[#34B5FF]'>
|
||||
<span key={key++} className='text-[#34B5FF]'>
|
||||
{split.reference}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<string, { value: unknown }>
|
||||
}) => {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
|
||||
|
||||
export default function WorkflowLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
<main className='flex flex-1 flex-col h-full overflow-hidden bg-muted/40'>
|
||||
<main className='flex h-full flex-1 flex-col overflow-hidden bg-muted/40'>
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<div className='text-center'>
|
||||
<div className='mx-auto mb-4'>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type FieldType,
|
||||
HTTP,
|
||||
PAUSE_RESUME,
|
||||
} from '@/executor/consts'
|
||||
} from '@/executor/constants'
|
||||
import {
|
||||
generatePauseContextId,
|
||||
mapNodeMetadataToPauseScopes,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PARALLEL } from '@/executor/consts'
|
||||
import { PARALLEL } from '@/executor/constants'
|
||||
import type { ExecutionContext, LoopPauseScope, ParallelPauseScope } from '@/executor/types'
|
||||
|
||||
interface NodeMetadataLike {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<Record<string, string>> {
|
||||
const headers: Record<string, string> = {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<void>((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<void>((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')
|
||||
|
||||
@@ -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<void>((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<void>((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<void>((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<void>((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<void>((resolve) => {
|
||||
clientSocket.once('joined-workflow', () => resolve())
|
||||
})
|
||||
const join2Promise = new Promise<void>((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<void>((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<void>((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<void>((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<void>((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<void>((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<void>((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<void>((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<string>()
|
||||
|
||||
const operationsPromise = new Promise<void>((resolve) => {
|
||||
client2.on('workflow-operation', (data) => {
|
||||
receivedCount++
|
||||
receivedOperations.add(data.payload.id)
|
||||
|
||||
if (receivedCount === numOperations) {
|
||||
resolve()
|
||||
}
|
||||
try {
|
||||
const join1Promise = new Promise<void>((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<void>((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<string>()
|
||||
|
||||
const operationsPromise = new Promise<void>((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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path, { resolve } from 'path'
|
||||
import path from 'path'
|
||||
/// <reference types="vitest" />
|
||||
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: {
|
||||
|
||||
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user