fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

This commit is contained in:
Waleed
2026-02-19 14:12:43 -08:00
committed by GitHub
parent 2b5e436a2a
commit 3c470ab0f8
28 changed files with 639 additions and 261 deletions

View File

@@ -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()

View File

@@ -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' }),

View File

@@ -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,

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),

View File

@@ -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()),

View File

@@ -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,
}))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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'

View File

@@ -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,
}))

View File

@@ -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,
}))

View File

@@ -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'))

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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)'
)

View File

@@ -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()

View File

@@ -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) => {

View File

@@ -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'

View 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,
}
}

View File

@@ -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'

View File

@@ -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(),
}

View 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(),
}