mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(workflows): disallow duplicate workflow names at the same folder level (#3260)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockLogger, createMockRequest } from '@sim/testing'
|
||||
import { createMockLogger, createMockRequest, mockHybridAuth } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('OAuth Token API Routes', () => {
|
||||
@@ -12,7 +12,7 @@ describe('OAuth Token API Routes', () => {
|
||||
const mockRefreshTokenIfNeeded = vi.fn()
|
||||
const mockGetOAuthToken = vi.fn()
|
||||
const mockAuthorizeCredentialUse = vi.fn()
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
let mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
|
||||
const mockLogger = createMockLogger()
|
||||
|
||||
@@ -41,9 +41,7 @@ describe('OAuth Token API Routes', () => {
|
||||
authorizeCredentialUse: mockAuthorizeCredentialUse,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
;({ mockCheckSessionOrInternalAuth } = mockHybridAuth())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -73,23 +71,18 @@ describe('OAuth Token API Routes', () => {
|
||||
refreshed: false,
|
||||
})
|
||||
|
||||
// Create mock request
|
||||
const req = createMockRequest('POST', {
|
||||
credentialId: 'credential-id',
|
||||
})
|
||||
|
||||
// Import handler after setting up mocks
|
||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
||||
|
||||
// Call handler
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
// Verify request was handled correctly
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
||||
|
||||
// Verify mocks were called correctly
|
||||
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
|
||||
expect(mockGetCredential).toHaveBeenCalled()
|
||||
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
@@ -94,9 +94,7 @@ vi.mock('@/lib/core/utils/sse', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/lib/core/security/encryption', () => ({
|
||||
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-password' }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { databaseMock, loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import type { NextResponse } from 'next/server'
|
||||
/**
|
||||
* Tests for chat API utils
|
||||
@@ -37,9 +37,7 @@ vi.mock('@/lib/core/security/encryption', () => ({
|
||||
decryptSecret: mockDecryptSecret,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/lib/core/config/feature-flags', () => ({
|
||||
isDev: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createMockRequest,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -28,13 +29,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth } = mockHybridAuth()
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createMockRequest,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -34,13 +35,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckInternalAuth } = mockHybridAuth()
|
||||
mockCheckInternalAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing'
|
||||
import {
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -28,13 +34,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckHybridAuth } = mockHybridAuth()
|
||||
mockCheckHybridAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
defaultMockUser,
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
@@ -54,12 +55,11 @@ describe('File Serve API Route', () => {
|
||||
withUploadUtils: true,
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -164,12 +164,11 @@ describe('File Serve API Route', () => {
|
||||
findLocalFile: vi.fn().mockReturnValue('/test/uploads/nested/path/file.txt'),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -225,12 +224,11 @@ describe('File Serve API Route', () => {
|
||||
USE_BLOB_STORAGE: false,
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
@@ -290,12 +288,11 @@ describe('File Serve API Route', () => {
|
||||
readFile: vi.fn().mockRejectedValue(new Error('ENOENT: no such file or directory')),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: serveAuthMock } = mockHybridAuth()
|
||||
serveAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(false), // File not found = no access
|
||||
@@ -349,12 +346,11 @@ describe('File Serve API Route', () => {
|
||||
|
||||
for (const test of contentTypeTests) {
|
||||
it(`should serve ${test.ext} file with correct content type`, async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: ctAuthMock } = mockHybridAuth()
|
||||
ctAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'test-user-id',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { mockAuth, mockCryptoUuid, mockUuid, setupCommonApiMocks } from '@sim/testing'
|
||||
import {
|
||||
mockAuth,
|
||||
mockCryptoUuid,
|
||||
mockHybridAuth,
|
||||
mockUuid,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -27,13 +33,12 @@ function setupFileApiMocks(
|
||||
authMocks.setUnauthenticated()
|
||||
}
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckHybridAuth } = mockHybridAuth()
|
||||
mockCheckHybridAuth.mockResolvedValue({
|
||||
success: authenticated,
|
||||
userId: authenticated ? 'test-user-id' : undefined,
|
||||
error: authenticated ? undefined : 'Unauthorized',
|
||||
})
|
||||
|
||||
vi.doMock('@/app/api/files/authorization', () => ({
|
||||
verifyFileAccess: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
createMockRequest,
|
||||
mockConsoleLogger,
|
||||
mockKnowledgeSchemas,
|
||||
requestUtilsMock,
|
||||
} from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -29,9 +30,7 @@ mockKnowledgeSchemas()
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => createEnvMock({ OPENAI_API_KEY: 'test-api-key' }))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn(() => 'test-request-id'),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/lib/documents/utils', () => ({
|
||||
retryWithExponentialBackoff: vi.fn().mockImplementation((fn) => fn()),
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { mockHybridAuth } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockCheckHybridAuth = vi.fn()
|
||||
let mockCheckHybridAuth: ReturnType<typeof vi.fn>
|
||||
const mockGetUserEntityPermissions = vi.fn()
|
||||
const mockGenerateInternalToken = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
@@ -61,9 +62,7 @@ describe('MCP Serve Route', () => {
|
||||
isDeployed: 'isDeployed',
|
||||
},
|
||||
}))
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: mockCheckHybridAuth,
|
||||
}))
|
||||
;({ mockCheckHybridAuth } = mockHybridAuth())
|
||||
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
|
||||
getUserEntityPermissions: mockGetUserEntityPermissions,
|
||||
}))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { auditMock, databaseMock, loggerMock } from '@sim/testing'
|
||||
import { auditMock, databaseMock, loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -31,9 +31,7 @@ vi.mock('drizzle-orm', () => ({
|
||||
eq: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: () => 'test-request-id',
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { databaseMock, loggerMock } from '@sim/testing'
|
||||
import { databaseMock, loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -43,9 +43,7 @@ vi.mock('drizzle-orm', () => ({
|
||||
isNull: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: () => 'test-request-id',
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest, loggerMock } from '@sim/testing'
|
||||
import { createMockRequest, loggerMock, mockHybridAuth } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -180,13 +180,12 @@ describe('Custom Tools API Routes', () => {
|
||||
getSession: vi.fn().mockResolvedValue(mockSession),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: hybridAuthMock } = mockHybridAuth()
|
||||
hybridAuthMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
authType: 'session',
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'),
|
||||
@@ -261,12 +260,11 @@ describe('Custom Tools API Routes', () => {
|
||||
'http://localhost:3000/api/tools/custom?workspaceId=workspace-123'
|
||||
)
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: unauthMock } = mockHybridAuth()
|
||||
unauthMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
})
|
||||
|
||||
const { GET } = await import('@/app/api/tools/custom/route')
|
||||
|
||||
@@ -297,12 +295,11 @@ describe('Custom Tools API Routes', () => {
|
||||
*/
|
||||
describe('POST /api/tools/custom', () => {
|
||||
it('should reject unauthorized requests', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: unauthMock } = mockHybridAuth()
|
||||
unauthMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', { tools: [], workspaceId: 'workspace-123' })
|
||||
|
||||
@@ -384,13 +381,12 @@ describe('Custom Tools API Routes', () => {
|
||||
})
|
||||
|
||||
it('should prevent unauthorized deletion of user-scoped tool', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-456',
|
||||
authType: 'session',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: diffUserMock } = mockHybridAuth()
|
||||
diffUserMock.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-456',
|
||||
authType: 'session',
|
||||
})
|
||||
|
||||
const userScopedTool = { ...sampleTools[0], workspaceId: null, userId: 'user-123' }
|
||||
const mockLimitUserScoped = vi.fn().mockResolvedValue([userScopedTool])
|
||||
@@ -408,12 +404,11 @@ describe('Custom Tools API Routes', () => {
|
||||
})
|
||||
|
||||
it('should reject unauthorized requests', async () => {
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}),
|
||||
}))
|
||||
const { mockCheckSessionOrInternalAuth: unauthMock } = mockHybridAuth()
|
||||
unauthMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/tools/custom?id=tool-1')
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createMockRequest, loggerMock } from '@sim/testing'
|
||||
import { createMockRequest, loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
/** Mock execution dependencies for webhook tests */
|
||||
@@ -348,9 +348,7 @@ vi.mock('postgres', () => vi.fn().mockReturnValue({}))
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { loggerMock, mockHybridAuth } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
let mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
const mockAuthorizeWorkflowByWorkspacePermission = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
const mockDbFrom = vi.fn()
|
||||
@@ -48,9 +48,7 @@ describe('Workflow Chat Status Route', () => {
|
||||
workflowId: 'workflowId',
|
||||
},
|
||||
}))
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
;({ mockCheckSessionOrInternalAuth } = mockHybridAuth())
|
||||
vi.doMock('@/lib/workflows/utils', () => ({
|
||||
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
|
||||
}))
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { loggerMock, mockHybridAuth } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
let mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
const mockAuthorizeWorkflowByWorkspacePermission = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
const mockDbFrom = vi.fn()
|
||||
@@ -43,9 +43,7 @@ describe('Workflow Form Status Route', () => {
|
||||
isActive: 'isActive',
|
||||
},
|
||||
}))
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
;({ mockCheckSessionOrInternalAuth } = mockHybridAuth())
|
||||
vi.doMock('@/lib/workflows/utils', () => ({
|
||||
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
|
||||
}))
|
||||
|
||||
@@ -5,11 +5,19 @@
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { auditMock, loggerMock, setupGlobalFetchMock } from '@sim/testing'
|
||||
import {
|
||||
auditMock,
|
||||
envMock,
|
||||
loggerMock,
|
||||
requestUtilsMock,
|
||||
setupGlobalFetchMock,
|
||||
telemetryMock,
|
||||
} from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockGetSession = vi.fn()
|
||||
const mockCheckHybridAuth = vi.fn()
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
const mockLoadWorkflowFromNormalizedTables = vi.fn()
|
||||
const mockGetWorkflowById = vi.fn()
|
||||
const mockAuthorizeWorkflowByWorkspacePermission = vi.fn()
|
||||
@@ -17,10 +25,34 @@ const mockDbDelete = vi.fn()
|
||||
const mockDbUpdate = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
|
||||
/**
|
||||
* Helper to set mock auth state consistently across getSession and hybrid auth.
|
||||
*/
|
||||
function mockGetSession(session: { user: { id: string } } | null) {
|
||||
if (session) {
|
||||
mockCheckHybridAuth.mockResolvedValue({ success: true, userId: session.user.id })
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({ success: true, userId: session.user.id })
|
||||
} else {
|
||||
mockCheckHybridAuth.mockResolvedValue({ success: false })
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({ success: false })
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: () => mockGetSession(),
|
||||
getSession: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: (...args: unknown[]) => mockCheckHybridAuth(...args),
|
||||
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => envMock)
|
||||
|
||||
vi.mock('@/lib/core/telemetry', () => telemetryMock)
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.mock('@/lib/audit/log', () => auditMock)
|
||||
@@ -30,20 +62,14 @@ vi.mock('@/lib/workflows/persistence/utils', () => ({
|
||||
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),
|
||||
authorizeWorkflowByWorkspacePermission: (params: {
|
||||
workflowId: string
|
||||
userId: string
|
||||
action?: 'read' | 'write' | 'admin'
|
||||
}) => mockAuthorizeWorkflowByWorkspacePermission(params),
|
||||
}
|
||||
})
|
||||
vi.mock('@/lib/workflows/utils', () => ({
|
||||
getWorkflowById: (workflowId: string) => mockGetWorkflowById(workflowId),
|
||||
authorizeWorkflowByWorkspacePermission: (params: {
|
||||
workflowId: string
|
||||
userId: string
|
||||
action?: 'read' | 'write' | 'admin'
|
||||
}) => mockAuthorizeWorkflowByWorkspacePermission(params),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: {
|
||||
@@ -73,7 +99,7 @@ describe('Workflow By ID API Route', () => {
|
||||
|
||||
describe('GET /api/workflows/[id]', () => {
|
||||
it('should return 401 when user is not authenticated', async () => {
|
||||
mockGetSession.mockResolvedValue(null)
|
||||
mockGetSession(null)
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123')
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
@@ -86,9 +112,7 @@ describe('Workflow By ID API Route', () => {
|
||||
})
|
||||
|
||||
it('should return 404 when workflow does not exist', async () => {
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(null)
|
||||
|
||||
@@ -118,9 +142,7 @@ describe('Workflow By ID API Route', () => {
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -158,9 +180,7 @@ describe('Workflow By ID API Route', () => {
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -190,9 +210,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -229,9 +247,7 @@ describe('Workflow By ID API Route', () => {
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -264,9 +280,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -308,9 +322,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -353,9 +365,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -392,9 +402,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -419,6 +427,16 @@ describe('Workflow By ID API Route', () => {
|
||||
})
|
||||
|
||||
describe('PUT /api/workflows/[id]', () => {
|
||||
function mockDuplicateCheck(results: Array<{ id: string }> = []) {
|
||||
mockDbSelect.mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(results),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
it('should allow user with write permission to update workflow', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
@@ -430,9 +448,7 @@ describe('Workflow By ID API Route', () => {
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() }
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -442,6 +458,8 @@ describe('Workflow By ID API Route', () => {
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDuplicateCheck([])
|
||||
|
||||
mockDbUpdate.mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -474,9 +492,7 @@ describe('Workflow By ID API Route', () => {
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
const updatedWorkflow = { ...mockWorkflow, ...updateData, updatedAt: new Date() }
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -486,6 +502,8 @@ describe('Workflow By ID API Route', () => {
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDuplicateCheck([])
|
||||
|
||||
mockDbUpdate.mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
@@ -517,9 +535,7 @@ describe('Workflow By ID API Route', () => {
|
||||
|
||||
const updateData = { name: 'Updated Workflow' }
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -551,9 +567,7 @@ describe('Workflow By ID API Route', () => {
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
@@ -577,13 +591,238 @@ describe('Workflow By ID API Route', () => {
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('Invalid request data')
|
||||
})
|
||||
|
||||
it('should reject rename when duplicate name exists in same folder', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Original Name',
|
||||
folderId: 'folder-1',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDuplicateCheck([{ id: 'workflow-other' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: 'Duplicate Name' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('A workflow named "Duplicate Name" already exists in this folder')
|
||||
})
|
||||
|
||||
it('should reject rename when duplicate name exists at root level', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Original Name',
|
||||
folderId: null,
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDuplicateCheck([{ id: 'workflow-other' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: 'Duplicate Name' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('A workflow named "Duplicate Name" already exists in this folder')
|
||||
})
|
||||
|
||||
it('should allow rename when no duplicate exists in same folder', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Original Name',
|
||||
folderId: 'folder-1',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
const updatedWorkflow = { ...mockWorkflow, name: 'Unique Name', updatedAt: new Date() }
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDuplicateCheck([])
|
||||
|
||||
mockDbUpdate.mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name: 'Unique Name' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.workflow.name).toBe('Unique Name')
|
||||
})
|
||||
|
||||
it('should allow same name in different folders', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'My Workflow',
|
||||
folderId: 'folder-1',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
const updatedWorkflow = { ...mockWorkflow, folderId: 'folder-2', updatedAt: new Date() }
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
// No duplicate in target folder
|
||||
mockDuplicateCheck([])
|
||||
|
||||
mockDbUpdate.mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ folderId: 'folder-2' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.workflow.folderId).toBe('folder-2')
|
||||
})
|
||||
|
||||
it('should reject moving to a folder where same name already exists', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'My Workflow',
|
||||
folderId: 'folder-1',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
// Duplicate exists in target folder
|
||||
mockDuplicateCheck([{ id: 'workflow-other' }])
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ folderId: 'folder-2' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('A workflow named "My Workflow" already exists in this folder')
|
||||
})
|
||||
|
||||
it('should skip duplicate check when only updating non-name/non-folder fields', async () => {
|
||||
const mockWorkflow = {
|
||||
id: 'workflow-123',
|
||||
userId: 'user-123',
|
||||
name: 'Test Workflow',
|
||||
workspaceId: 'workspace-456',
|
||||
}
|
||||
|
||||
const updatedWorkflow = { ...mockWorkflow, color: '#FF0000', updatedAt: new Date() }
|
||||
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
mockGetWorkflowById.mockResolvedValue(mockWorkflow)
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
allowed: true,
|
||||
status: 200,
|
||||
workflow: mockWorkflow,
|
||||
workspacePermission: 'write',
|
||||
})
|
||||
|
||||
mockDbUpdate.mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([updatedWorkflow]),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/api/workflows/workflow-123', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ color: '#FF0000' }),
|
||||
})
|
||||
const params = Promise.resolve({ id: 'workflow-123' })
|
||||
|
||||
const response = await PUT(req, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// db.select should NOT have been called since no name/folder change
|
||||
expect(mockDbSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', () => {
|
||||
it.concurrent('should handle database errors gracefully', async () => {
|
||||
mockGetSession.mockResolvedValue({
|
||||
user: { id: 'user-123' },
|
||||
})
|
||||
mockGetSession({ user: { id: 'user-123' } })
|
||||
|
||||
mockGetWorkflowById.mockRejectedValue(new Error('Database connection timeout'))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { templates, webhook, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { and, eq, isNull, ne } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
@@ -411,6 +411,45 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
if (updates.folderId !== undefined) updateData.folderId = updates.folderId
|
||||
if (updates.sortOrder !== undefined) updateData.sortOrder = updates.sortOrder
|
||||
|
||||
if (updates.name !== undefined || updates.folderId !== undefined) {
|
||||
const targetName = updates.name ?? workflowData.name
|
||||
const targetFolderId =
|
||||
updates.folderId !== undefined ? updates.folderId : workflowData.folderId
|
||||
|
||||
if (!workflowData.workspaceId) {
|
||||
logger.error(`[${requestId}] Workflow ${workflowId} has no workspaceId`)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
eq(workflow.workspaceId, workflowData.workspaceId),
|
||||
eq(workflow.name, targetName),
|
||||
ne(workflow.id, workflowId),
|
||||
]
|
||||
|
||||
if (targetFolderId) {
|
||||
conditions.push(eq(workflow.folderId, targetFolderId))
|
||||
} else {
|
||||
conditions.push(isNull(workflow.folderId))
|
||||
}
|
||||
|
||||
const [duplicate] = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(and(...conditions))
|
||||
.limit(1)
|
||||
|
||||
if (duplicate) {
|
||||
logger.warn(
|
||||
`[${requestId}] Duplicate workflow name "${targetName}" in folder ${targetFolderId ?? 'root'}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ error: `A workflow named "${targetName}" already exists in this folder` },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the workflow
|
||||
const [updatedWorkflow] = await db
|
||||
.update(workflow)
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { auditMock, createMockRequest, mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
|
||||
import {
|
||||
auditMock,
|
||||
createMockRequest,
|
||||
mockConsoleLogger,
|
||||
mockHybridAuth,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
const mockGetUserEntityPermissions = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
const mockDbInsert = vi.fn()
|
||||
@@ -30,6 +35,7 @@ describe('Workflows API Route - POST ordering', () => {
|
||||
randomUUID: vi.fn().mockReturnValue('workflow-new-id'),
|
||||
})
|
||||
|
||||
const { mockCheckSessionOrInternalAuth } = mockHybridAuth()
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
@@ -45,10 +51,6 @@ describe('Workflows API Route - POST ordering', () => {
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
|
||||
getUserEntityPermissions: (...args: unknown[]) => mockGetUserEntityPermissions(...args),
|
||||
workspaceExists: vi.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { loggerMock, requestUtilsMock } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockType } from '@/executor/constants'
|
||||
import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-handler'
|
||||
@@ -7,9 +7,7 @@ import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn(() => 'test-request-id'),
|
||||
}))
|
||||
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
vi.mock('@/tools', () => ({
|
||||
executeTool: vi.fn(),
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { loggerMock } from '@sim/testing'
|
||||
import { createEnvMock, loggerMock } from '@sim/testing'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockEnv = vi.hoisted(() => ({
|
||||
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', () => ({
|
||||
env: mockEnv,
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value),
|
||||
isFalsy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value.toLowerCase() === 'false' || value === '0' : value === false,
|
||||
}))
|
||||
vi.mock('@/lib/core/config/env', () =>
|
||||
createEnvMock({
|
||||
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { decryptSecret, encryptSecret, generatePassword } from './encryption'
|
||||
|
||||
describe('encryptSecret', () => {
|
||||
@@ -172,21 +167,21 @@ describe('generatePassword', () => {
|
||||
})
|
||||
|
||||
describe('encryption key validation', () => {
|
||||
const originalEnv = { ...mockEnv }
|
||||
const originalEncryptionKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||
|
||||
afterEach(() => {
|
||||
mockEnv.ENCRYPTION_KEY = originalEnv.ENCRYPTION_KEY
|
||||
;(env as Record<string, string>).ENCRYPTION_KEY = originalEncryptionKey
|
||||
})
|
||||
|
||||
it('should throw error when ENCRYPTION_KEY is not set', async () => {
|
||||
mockEnv.ENCRYPTION_KEY = ''
|
||||
;(env as Record<string, string>).ENCRYPTION_KEY = ''
|
||||
await expect(encryptSecret('test')).rejects.toThrow(
|
||||
'ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)'
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when ENCRYPTION_KEY is wrong length', async () => {
|
||||
mockEnv.ENCRYPTION_KEY = '0123456789abcdef'
|
||||
;(env as Record<string, string>).ENCRYPTION_KEY = '0123456789abcdef'
|
||||
await expect(encryptSecret('test')).rejects.toThrow(
|
||||
'ENCRYPTION_KEY must be set to a 64-character hex string (32 bytes)'
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createEnvMock } from '@sim/testing'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getRotatingApiKey } from '@/lib/core/config/api-keys'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
@@ -30,25 +31,20 @@ vi.mock('crypto', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/core/config/env')>()
|
||||
return {
|
||||
...actual,
|
||||
env: {
|
||||
...actual.env,
|
||||
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', // fake key for testing
|
||||
OPENAI_API_KEY_1: 'test-openai-key-1', // fake key for testing
|
||||
OPENAI_API_KEY_2: 'test-openai-key-2', // fake key for testing
|
||||
OPENAI_API_KEY_3: 'test-openai-key-3', // fake key for testing
|
||||
ANTHROPIC_API_KEY_1: 'test-anthropic-key-1', // fake key for testing
|
||||
ANTHROPIC_API_KEY_2: 'test-anthropic-key-2', // fake key for testing
|
||||
ANTHROPIC_API_KEY_3: 'test-anthropic-key-3', // fake key for testing
|
||||
GEMINI_API_KEY_1: 'test-gemini-key-1', // fake key for testing
|
||||
GEMINI_API_KEY_2: 'test-gemini-key-2', // fake key for testing
|
||||
GEMINI_API_KEY_3: 'test-gemini-key-3', // fake key for testing
|
||||
},
|
||||
}
|
||||
})
|
||||
vi.mock('@/lib/core/config/env', () =>
|
||||
createEnvMock({
|
||||
ENCRYPTION_KEY: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
OPENAI_API_KEY_1: 'test-openai-key-1',
|
||||
OPENAI_API_KEY_2: 'test-openai-key-2',
|
||||
OPENAI_API_KEY_3: 'test-openai-key-3',
|
||||
ANTHROPIC_API_KEY_1: 'test-anthropic-key-1',
|
||||
ANTHROPIC_API_KEY_2: 'test-anthropic-key-2',
|
||||
ANTHROPIC_API_KEY_3: 'test-anthropic-key-3',
|
||||
GEMINI_API_KEY_1: 'test-gemini-key-1',
|
||||
GEMINI_API_KEY_2: 'test-gemini-key-2',
|
||||
GEMINI_API_KEY_3: 'test-gemini-key-3',
|
||||
})
|
||||
)
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { createServer, request as httpRequest } from 'http'
|
||||
import { createMockLogger, databaseMock } from '@sim/testing'
|
||||
import { createEnvMock, createMockLogger, databaseMock } from '@sim/testing'
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createSocketIOServer } from '@/socket/config/socket'
|
||||
import { MemoryRoomManager } from '@/socket/rooms'
|
||||
@@ -30,19 +30,13 @@ vi.mock('redis', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock env to not have REDIS_URL (use importOriginal to get helper functions)
|
||||
vi.mock('@/lib/core/config/env', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/core/config/env')>()
|
||||
return {
|
||||
...actual,
|
||||
env: {
|
||||
...actual.env,
|
||||
DATABASE_URL: 'postgres://localhost/test',
|
||||
NODE_ENV: 'test',
|
||||
REDIS_URL: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
vi.mock('@/lib/core/config/env', () =>
|
||||
createEnvMock({
|
||||
DATABASE_URL: 'postgres://localhost/test',
|
||||
NODE_ENV: 'test',
|
||||
REDIS_URL: undefined,
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/socket/middleware/auth', () => ({
|
||||
authenticateSocket: vi.fn((socket, next) => {
|
||||
|
||||
@@ -66,6 +66,7 @@ export {
|
||||
loggerMock,
|
||||
type MockAuthResult,
|
||||
type MockFetchResponse,
|
||||
type MockHybridAuthResult,
|
||||
type MockRedis,
|
||||
type MockUser,
|
||||
mockAuth,
|
||||
@@ -73,10 +74,13 @@ export {
|
||||
mockConsoleLogger,
|
||||
mockCryptoUuid,
|
||||
mockDrizzleOrm,
|
||||
mockHybridAuth,
|
||||
mockKnowledgeSchemas,
|
||||
mockUuid,
|
||||
requestUtilsMock,
|
||||
setupCommonApiMocks,
|
||||
setupGlobalFetchMock,
|
||||
setupGlobalStorageMocks,
|
||||
telemetryMock,
|
||||
} from './mocks'
|
||||
export * from './types'
|
||||
|
||||
85
packages/testing/src/mocks/hybrid-auth.mock.ts
Normal file
85
packages/testing/src/mocks/hybrid-auth.mock.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Mock for @/lib/auth/hybrid module.
|
||||
* Provides controllable mock functions for checkHybridAuth, checkSessionOrInternalAuth, and checkInternalAuth.
|
||||
*/
|
||||
import { vi } from 'vitest'
|
||||
import type { MockUser } from './auth.mock'
|
||||
import { defaultMockUser } from './auth.mock'
|
||||
|
||||
interface HybridAuthResponse {
|
||||
success: boolean
|
||||
userId?: string
|
||||
userName?: string | null
|
||||
userEmail?: string | null
|
||||
authType?: 'session' | 'api_key' | 'internal_jwt'
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Result object returned by mockHybridAuth with helper methods
|
||||
*/
|
||||
export interface MockHybridAuthResult {
|
||||
mockCheckHybridAuth: ReturnType<typeof vi.fn>
|
||||
mockCheckSessionOrInternalAuth: ReturnType<typeof vi.fn>
|
||||
mockCheckInternalAuth: ReturnType<typeof vi.fn>
|
||||
setAuthenticated: (user?: MockUser) => void
|
||||
setUnauthenticated: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock hybrid authentication for API tests.
|
||||
* Uses vi.doMock to mock the @/lib/auth/hybrid module.
|
||||
*
|
||||
* @param user - Optional default user for authenticated state
|
||||
* @returns Object with mock functions and authentication helpers
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const hybridAuth = mockHybridAuth()
|
||||
* hybridAuth.setAuthenticated() // All hybrid auth checks succeed
|
||||
* hybridAuth.setUnauthenticated() // All hybrid auth checks fail
|
||||
* ```
|
||||
*/
|
||||
export function mockHybridAuth(user: MockUser = defaultMockUser): MockHybridAuthResult {
|
||||
const mockCheckHybridAuth = vi.fn<() => Promise<HybridAuthResponse>>()
|
||||
const mockCheckSessionOrInternalAuth = vi.fn<() => Promise<HybridAuthResponse>>()
|
||||
const mockCheckInternalAuth = vi.fn<() => Promise<HybridAuthResponse>>()
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkHybridAuth: mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: mockCheckInternalAuth,
|
||||
}))
|
||||
|
||||
const setAuthenticated = (customUser?: MockUser) => {
|
||||
const u = customUser || user
|
||||
const response: HybridAuthResponse = {
|
||||
success: true,
|
||||
userId: u.id,
|
||||
userName: u.name ?? null,
|
||||
userEmail: u.email,
|
||||
authType: 'session',
|
||||
}
|
||||
mockCheckHybridAuth.mockResolvedValue(response)
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue(response)
|
||||
mockCheckInternalAuth.mockResolvedValue(response)
|
||||
}
|
||||
|
||||
const setUnauthenticated = () => {
|
||||
const response: HybridAuthResponse = {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}
|
||||
mockCheckHybridAuth.mockResolvedValue(response)
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue(response)
|
||||
mockCheckInternalAuth.mockResolvedValue(response)
|
||||
}
|
||||
|
||||
return {
|
||||
mockCheckHybridAuth,
|
||||
mockCheckSessionOrInternalAuth,
|
||||
mockCheckInternalAuth,
|
||||
setAuthenticated,
|
||||
setUnauthenticated,
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,14 @@ export {
|
||||
mockNextFetchResponse,
|
||||
setupGlobalFetchMock,
|
||||
} from './fetch.mock'
|
||||
// Hybrid auth mocks
|
||||
export { type MockHybridAuthResult, mockHybridAuth } from './hybrid-auth.mock'
|
||||
// Logger mocks
|
||||
export { clearLoggerMocks, createMockLogger, getLoggerCalls, loggerMock } from './logger.mock'
|
||||
// Redis mocks
|
||||
export { clearRedisMocks, createMockRedis, type MockRedis } from './redis.mock'
|
||||
// Request mocks
|
||||
export { createMockFormDataRequest, createMockRequest } from './request.mock'
|
||||
export { createMockFormDataRequest, createMockRequest, requestUtilsMock } from './request.mock'
|
||||
// Socket mocks
|
||||
export {
|
||||
createMockSocket,
|
||||
@@ -78,5 +80,7 @@ export {
|
||||
} from './socket.mock'
|
||||
// Storage mocks
|
||||
export { clearStorageMocks, createMockStorage, setupGlobalStorageMocks } from './storage.mock'
|
||||
// Telemetry mocks
|
||||
export { telemetryMock } from './telemetry.mock'
|
||||
// UUID mocks
|
||||
export { mockCryptoUuid, mockUuid } from './uuid.mock'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Mock request utilities for API testing
|
||||
*/
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates a mock NextRequest for API route testing.
|
||||
@@ -57,3 +58,16 @@ export function createMockFormDataRequest(
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured mock for @/lib/core/utils/request module.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const requestUtilsMock = {
|
||||
generateRequestId: vi.fn(() => 'mock-request-id'),
|
||||
noop: vi.fn(),
|
||||
}
|
||||
|
||||
30
packages/testing/src/mocks/telemetry.mock.ts
Normal file
30
packages/testing/src/mocks/telemetry.mock.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Mock for @/lib/core/telemetry module.
|
||||
* Provides no-op implementations for telemetry functions and PlatformEvents.
|
||||
*/
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Pre-configured telemetry mock for use with vi.mock.
|
||||
* All PlatformEvents methods are no-op vi.fn() stubs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/telemetry', () => telemetryMock)
|
||||
* ```
|
||||
*/
|
||||
export const telemetryMock = {
|
||||
PlatformEvents: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (typeof prop === 'string') {
|
||||
return vi.fn()
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
),
|
||||
createWorkflowSpans: vi.fn(),
|
||||
trackPlatformEvent: vi.fn(),
|
||||
}
|
||||
Reference in New Issue
Block a user