chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* Tests for chat API utils
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import {
|
||||
dbChainMock,
|
||||
dbChainMockFns,
|
||||
encryptionMock,
|
||||
encryptionMockFns,
|
||||
loggingSessionMock,
|
||||
workflowsUtilsMock,
|
||||
} from '@sim/testing'
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockMergeSubblockStateWithValues,
|
||||
mockMergeSubBlockValues,
|
||||
mockValidateAuthToken,
|
||||
mockSetDeploymentAuthCookie,
|
||||
mockIsEmailAllowed,
|
||||
mockGetSession,
|
||||
mockCheckRateLimitDirect,
|
||||
mockIsWorkspaceApiExecutionEntitled,
|
||||
flagState,
|
||||
} = vi.hoisted(() => ({
|
||||
mockMergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
|
||||
mockMergeSubBlockValues: vi.fn().mockReturnValue({}),
|
||||
mockValidateAuthToken: vi.fn().mockReturnValue(false),
|
||||
mockSetDeploymentAuthCookie: vi.fn(),
|
||||
mockIsEmailAllowed: vi.fn(),
|
||||
mockGetSession: vi.fn(),
|
||||
mockCheckRateLimitDirect: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
mockIsWorkspaceApiExecutionEntitled: vi.fn().mockResolvedValue(true),
|
||||
flagState: { isBillingEnabled: false, isFreeApiDeploymentGateEnabled: true },
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => dbChainMock)
|
||||
|
||||
vi.mock('@/lib/billing/core/api-access', () => ({
|
||||
isWorkspaceApiExecutionEntitled: mockIsWorkspaceApiExecutionEntitled,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/rate-limiter', () => ({
|
||||
RateLimiter: class {
|
||||
checkRateLimitDirect = mockCheckRateLimitDirect
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
auth: { api: { getSession: vi.fn() } },
|
||||
getSession: mockGetSession,
|
||||
}))
|
||||
|
||||
const mockDecryptSecret = encryptionMockFns.mockDecryptSecret
|
||||
|
||||
vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)
|
||||
|
||||
vi.mock('@/executor', () => ({
|
||||
Executor: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/serializer', () => ({
|
||||
Serializer: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/workflow-persistence/subblocks', () => ({
|
||||
mergeSubblockStateWithValues: mockMergeSubblockStateWithValues,
|
||||
mergeSubBlockValues: mockMergeSubBlockValues,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
|
||||
|
||||
vi.mock('@/lib/core/security/deployment', () => ({
|
||||
validateAuthToken: mockValidateAuthToken,
|
||||
setDeploymentAuthCookie: mockSetDeploymentAuthCookie,
|
||||
isEmailAllowed: mockIsEmailAllowed,
|
||||
deploymentAuthCookieName: (prefix: string, id: string) => `${prefix}_auth_${id}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env-flags', () => ({
|
||||
isDev: true,
|
||||
isProd: false,
|
||||
get isBillingEnabled() {
|
||||
return flagState.isBillingEnabled
|
||||
},
|
||||
get isFreeApiDeploymentGateEnabled() {
|
||||
return flagState.isFreeApiDeploymentGateEnabled
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
|
||||
|
||||
import { NextRequest } from 'next/server'
|
||||
import { decryptSecret } from '@/lib/core/security/encryption'
|
||||
import { assertChatEmbedAllowed, setChatAuthCookie, validateChatAuth } from '@/app/api/chat/utils'
|
||||
|
||||
function chatRequest(origin?: string): NextRequest {
|
||||
return new NextRequest('https://www.sim.ai/api/chat/abc', {
|
||||
headers: origin ? { origin } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
describe('Chat API Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('process', {
|
||||
...process,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auth token utils', () => {
|
||||
it('should accept valid auth cookie via validateChatAuth', async () => {
|
||||
mockValidateAuthToken.mockReturnValue(true)
|
||||
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
password: 'encrypted-password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue({ value: 'valid-token' }),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest)
|
||||
expect(mockValidateAuthToken).toHaveBeenCalledWith(
|
||||
'valid-token',
|
||||
'chat-id',
|
||||
'password',
|
||||
'encrypted-password'
|
||||
)
|
||||
expect(result.authorized).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject invalid auth cookie via validateChatAuth', async () => {
|
||||
mockValidateAuthToken.mockReturnValue(false)
|
||||
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
password: 'encrypted-password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue({ value: 'invalid-token' }),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest)
|
||||
expect(result.authorized).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cookie handling', () => {
|
||||
it('should delegate to setDeploymentAuthCookie', () => {
|
||||
const mockResponse = {
|
||||
cookies: { set: vi.fn() },
|
||||
} as unknown as NextResponse
|
||||
|
||||
setChatAuthCookie(mockResponse, 'test-chat-id', 'password')
|
||||
|
||||
expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith(
|
||||
mockResponse,
|
||||
'chat',
|
||||
'test-chat-id',
|
||||
'password',
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Chat auth validation', () => {
|
||||
beforeEach(() => {
|
||||
mockDecryptSecret.mockResolvedValue({ decrypted: 'correct-password' })
|
||||
mockCheckRateLimitDirect.mockResolvedValue({ allowed: true })
|
||||
})
|
||||
|
||||
it('should allow access to public chats', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'public',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest)
|
||||
|
||||
expect(result.authorized).toBe(true)
|
||||
})
|
||||
|
||||
it('should request password auth for GET requests', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest)
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.error).toBe('auth_required_password')
|
||||
})
|
||||
|
||||
it('should validate password for POST requests', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
password: 'encrypted-password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const parsedBody = {
|
||||
password: 'correct-password',
|
||||
}
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest, parsedBody)
|
||||
|
||||
expect(decryptSecret).toHaveBeenCalledWith('encrypted-password')
|
||||
expect(result.authorized).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject incorrect password', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
password: 'encrypted-password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const parsedBody = {
|
||||
password: 'wrong-password',
|
||||
}
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest, parsedBody)
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.error).toBe('Invalid password')
|
||||
})
|
||||
|
||||
it('should return 429 when the password attempt rate limit is exceeded', async () => {
|
||||
mockCheckRateLimitDirect.mockResolvedValueOnce({ allowed: false, retryAfterMs: 60_000 })
|
||||
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'password',
|
||||
password: 'encrypted-password',
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest, {
|
||||
password: 'any-guess',
|
||||
})
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.status).toBe(429)
|
||||
expect(result.retryAfterMs).toBe(60_000)
|
||||
expect(decryptSecret).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should request email auth for email-protected chats', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'email',
|
||||
allowedEmails: ['user@example.com', '@company.com'],
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
const result = await validateChatAuth('request-id', deployment, mockRequest)
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.error).toBe('auth_required_email')
|
||||
})
|
||||
|
||||
it('should check allowed emails for email auth', async () => {
|
||||
const deployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'email',
|
||||
allowedEmails: ['user@example.com', '@company.com'],
|
||||
}
|
||||
|
||||
const mockRequest = {
|
||||
method: 'POST',
|
||||
cookies: {
|
||||
get: vi.fn().mockReturnValue(null),
|
||||
},
|
||||
} as any
|
||||
|
||||
mockIsEmailAllowed.mockReturnValue(true)
|
||||
const result1 = await validateChatAuth('request-id', deployment, mockRequest, {
|
||||
email: 'user@example.com',
|
||||
})
|
||||
expect(result1.authorized).toBe(false)
|
||||
expect(result1.error).toBe('otp_required')
|
||||
|
||||
const result2 = await validateChatAuth('request-id', deployment, mockRequest, {
|
||||
email: 'other@company.com',
|
||||
})
|
||||
expect(result2.authorized).toBe(false)
|
||||
expect(result2.error).toBe('otp_required')
|
||||
|
||||
mockIsEmailAllowed.mockReturnValue(false)
|
||||
const result3 = await validateChatAuth('request-id', deployment, mockRequest, {
|
||||
email: 'user@unknown.com',
|
||||
})
|
||||
expect(result3.authorized).toBe(false)
|
||||
expect(result3.error).toBe('Email not authorized')
|
||||
})
|
||||
|
||||
describe('SSO auth', () => {
|
||||
const ssoDeployment = {
|
||||
id: 'chat-id',
|
||||
authType: 'sso',
|
||||
allowedEmails: ['user@example.com', '@company.com'],
|
||||
}
|
||||
|
||||
const postRequest = {
|
||||
method: 'POST',
|
||||
cookies: { get: vi.fn().mockReturnValue(null) },
|
||||
} as any
|
||||
|
||||
it('rejects when no session is present', async () => {
|
||||
mockGetSession.mockResolvedValue(null)
|
||||
|
||||
const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
|
||||
input: 'hello',
|
||||
})
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.error).toBe('auth_required_sso')
|
||||
})
|
||||
|
||||
it('ignores body-supplied email and uses the session email', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { email: 'session@example.com' } })
|
||||
mockIsEmailAllowed.mockReturnValue(true)
|
||||
|
||||
await validateChatAuth('request-id', ssoDeployment, postRequest, {
|
||||
email: 'attacker@evil.com',
|
||||
input: 'hello',
|
||||
})
|
||||
|
||||
expect(mockIsEmailAllowed).toHaveBeenCalledWith(
|
||||
'session@example.com',
|
||||
ssoDeployment.allowedEmails
|
||||
)
|
||||
})
|
||||
|
||||
it('authorizes execution when session email is allowlisted', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { email: 'user@example.com' } })
|
||||
mockIsEmailAllowed.mockReturnValue(true)
|
||||
|
||||
const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
|
||||
input: 'hello',
|
||||
})
|
||||
|
||||
expect(result.authorized).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects execution when session email is not allowlisted', async () => {
|
||||
mockGetSession.mockResolvedValue({ user: { email: 'stranger@other.com' } })
|
||||
mockIsEmailAllowed.mockReturnValue(false)
|
||||
|
||||
const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
|
||||
input: 'hello',
|
||||
})
|
||||
|
||||
expect(result.authorized).toBe(false)
|
||||
expect(result.error).toBe('Your email is not authorized to access this resource')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Execution Result Processing', () => {
|
||||
it.concurrent('should process logs regardless of overall success status', () => {
|
||||
const executionResult = {
|
||||
success: false,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'agent1',
|
||||
startedAt: '2023-01-01T00:00:00Z',
|
||||
endedAt: '2023-01-01T00:00:01Z',
|
||||
durationMs: 1000,
|
||||
success: true,
|
||||
output: { content: 'Agent 1 succeeded' },
|
||||
error: undefined,
|
||||
},
|
||||
{
|
||||
blockId: 'agent2',
|
||||
startedAt: '2023-01-01T00:00:00Z',
|
||||
endedAt: '2023-01-01T00:00:01Z',
|
||||
durationMs: 500,
|
||||
success: false,
|
||||
output: null,
|
||||
error: 'Agent 2 failed',
|
||||
},
|
||||
],
|
||||
metadata: { duration: 1000 },
|
||||
}
|
||||
|
||||
expect(executionResult.success).toBe(false)
|
||||
expect(executionResult.logs).toBeDefined()
|
||||
expect(executionResult.logs).toHaveLength(2)
|
||||
|
||||
expect(executionResult.logs[0].success).toBe(true)
|
||||
expect(executionResult.logs[0].output?.content).toBe('Agent 1 succeeded')
|
||||
|
||||
expect(executionResult.logs[1].success).toBe(false)
|
||||
expect(executionResult.logs[1].error).toBe('Agent 2 failed')
|
||||
})
|
||||
|
||||
it.concurrent('should handle ExecutionResult vs StreamingExecution types correctly', () => {
|
||||
const executionResult = {
|
||||
success: true,
|
||||
output: { content: 'test' },
|
||||
logs: [],
|
||||
metadata: { duration: 100 },
|
||||
}
|
||||
|
||||
const directResult = executionResult
|
||||
const extractedDirect = directResult
|
||||
expect(extractedDirect).toBe(executionResult)
|
||||
|
||||
const streamingResult = {
|
||||
stream: new ReadableStream(),
|
||||
execution: executionResult,
|
||||
}
|
||||
|
||||
const extractedFromStreaming =
|
||||
streamingResult && typeof streamingResult === 'object' && 'execution' in streamingResult
|
||||
? streamingResult.execution
|
||||
: streamingResult
|
||||
|
||||
expect(extractedFromStreaming).toBe(executionResult)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertChatEmbedAllowed', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
flagState.isBillingEnabled = true
|
||||
flagState.isFreeApiDeploymentGateEnabled = true
|
||||
mockIsWorkspaceApiExecutionEntitled.mockResolvedValue(true)
|
||||
dbChainMockFns.limit.mockResolvedValue([{ workspaceId: 'ws-1' }])
|
||||
})
|
||||
|
||||
it('returns 403 for a cross-site origin when the owner is on the free plan', async () => {
|
||||
mockIsWorkspaceApiExecutionEntitled.mockResolvedValueOnce(false)
|
||||
const res = await assertChatEmbedAllowed(
|
||||
chatRequest('https://evil.example.com'),
|
||||
'wf-1',
|
||||
'req-1'
|
||||
)
|
||||
expect(res?.status).toBe(403)
|
||||
})
|
||||
|
||||
it('allows a cross-site origin when the owner is on a paid plan', async () => {
|
||||
const res = await assertChatEmbedAllowed(
|
||||
chatRequest('https://evil.example.com'),
|
||||
'wf-1',
|
||||
'req-1'
|
||||
)
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('returns 403 for a cross-site origin when the workflow has no active workspace', async () => {
|
||||
dbChainMockFns.limit.mockResolvedValueOnce([])
|
||||
const res = await assertChatEmbedAllowed(
|
||||
chatRequest('https://evil.example.com'),
|
||||
'wf-1',
|
||||
'req-1'
|
||||
)
|
||||
expect(res?.status).toBe(403)
|
||||
expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows a first-party *.sim.ai origin without gating', async () => {
|
||||
const res = await assertChatEmbedAllowed(chatRequest('https://chat.sim.ai'), 'wf-1', 'req-1')
|
||||
expect(res).toBeNull()
|
||||
expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('allows requests with no Origin header', async () => {
|
||||
const res = await assertChatEmbedAllowed(chatRequest(), 'wf-1', 'req-1')
|
||||
expect(res).toBeNull()
|
||||
expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is a no-op when billing is disabled', async () => {
|
||||
flagState.isBillingEnabled = false
|
||||
const res = await assertChatEmbedAllowed(
|
||||
chatRequest('https://evil.example.com'),
|
||||
'wf-1',
|
||||
'req-1'
|
||||
)
|
||||
expect(res).toBeNull()
|
||||
expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is a no-op when the gate feature flag is disabled', async () => {
|
||||
flagState.isFreeApiDeploymentGateEnabled = false
|
||||
const res = await assertChatEmbedAllowed(
|
||||
chatRequest('https://evil.example.com'),
|
||||
'wf-1',
|
||||
'req-1'
|
||||
)
|
||||
expect(res).toBeNull()
|
||||
expect(mockIsWorkspaceApiExecutionEntitled).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user