mirror of
https://github.com/langgenius/dify.git
synced 2026-08-29 19:54:46 +08:00
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
import { canEmbedPath, proxy } from '@/proxy'
|
|
|
|
const mockEnv = vi.hoisted(() => ({
|
|
NEXT_PUBLIC_ALLOW_EMBED: false,
|
|
NEXT_PUBLIC_CSP_WHITELIST: 'https://example.com',
|
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: '',
|
|
}))
|
|
|
|
vi.mock('@/env', () => ({
|
|
env: mockEnv,
|
|
}))
|
|
|
|
const createRequest = (url: string) => {
|
|
const nextUrl = new URL(url) as URL & { clone: () => URL }
|
|
nextUrl.clone = () => new URL(nextUrl)
|
|
|
|
return {
|
|
headers: new Headers(),
|
|
nextUrl,
|
|
} as Parameters<typeof proxy>[0]
|
|
}
|
|
|
|
describe('proxy frame options', () => {
|
|
afterEach(() => {
|
|
mockEnv.NEXT_PUBLIC_ALLOW_EMBED = false
|
|
mockEnv.NEXT_PUBLIC_TURNSTILE_SITE_KEY = ''
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('should allow embedded share routes', () => {
|
|
expect(canEmbedPath('/chatbot/token')).toBe(true)
|
|
expect(canEmbedPath('/workflow/token')).toBe(true)
|
|
expect(canEmbedPath('/completion/token')).toBe(true)
|
|
expect(canEmbedPath('/webapp-signin')).toBe(true)
|
|
expect(canEmbedPath('/agent/token')).toBe(true)
|
|
})
|
|
|
|
it('should deny non-embedded console routes by default', () => {
|
|
expect(canEmbedPath('/chatty')).toBe(false)
|
|
expect(canEmbedPath('/workflowish')).toBe(false)
|
|
expect(canEmbedPath('/completionist')).toBe(false)
|
|
expect(canEmbedPath('/webapp-signing')).toBe(false)
|
|
expect(canEmbedPath('/agents')).toBe(false)
|
|
expect(canEmbedPath('/agent-settings')).toBe(false)
|
|
expect(canEmbedPath('/agentic')).toBe(false)
|
|
expect(canEmbedPath('/agents/agent-1/access')).toBe(false)
|
|
expect(canEmbedPath('/apps')).toBe(false)
|
|
})
|
|
|
|
it('should enforce frame ancestors on protected document routes', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
const response = proxy(createRequest('https://cloud.dify.ai/device'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy')
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(contentSecurityPolicy).toContain("script-src 'self'")
|
|
expect(contentSecurityPolicy).toContain("frame-ancestors 'none'")
|
|
})
|
|
|
|
it('should keep published app routes embeddable', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
const response = proxy(createRequest('https://udify.app/chat/test-token'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy')
|
|
|
|
expect(response.headers.get('x-frame-options')).toBeNull()
|
|
expect(contentSecurityPolicy).toContain("script-src 'self'")
|
|
expect(contentSecurityPolicy).not.toContain('frame-ancestors')
|
|
})
|
|
|
|
it('should allow Cloudflare Turnstile resources when its site key is configured', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
mockEnv.NEXT_PUBLIC_TURNSTILE_SITE_KEY = 'site-key-for-tests'
|
|
|
|
const response = proxy(createRequest('https://cloud.dify.ai/signin'))
|
|
|
|
expect(response.headers.get('content-security-policy')).toContain(
|
|
'https://challenges.cloudflare.com',
|
|
)
|
|
})
|
|
|
|
it('should protect device routes when global embedding is enabled', () => {
|
|
mockEnv.NEXT_PUBLIC_ALLOW_EMBED = true
|
|
const response = proxy(createRequest('https://cloud.dify.ai/device/code'))
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(response.headers.get('content-security-policy')).toContain("frame-ancestors 'none'")
|
|
})
|
|
})
|
|
|
|
describe('proxy education entry normalization', () => {
|
|
it('redirects the legacy education action without leaking it into the canonical URL', () => {
|
|
const response = proxy(
|
|
createRequest('https://cloud.dify.ai/?action=getEducationVerify&utm_source=education-site'),
|
|
)
|
|
|
|
expect(response.status).toBe(308)
|
|
expect(response.headers.get('location')).toBe(
|
|
'https://cloud.dify.ai/education/verify?utm_source=education-site',
|
|
)
|
|
})
|
|
|
|
it('does not redirect unrelated actions or paths', () => {
|
|
const unrelatedAction = proxy(createRequest('https://cloud.dify.ai/?action=showSettings'))
|
|
const unrelatedPath = proxy(
|
|
createRequest('https://cloud.dify.ai/apps?action=getEducationVerify'),
|
|
)
|
|
|
|
expect(unrelatedAction.status).toBe(200)
|
|
expect(unrelatedPath.status).toBe(200)
|
|
})
|
|
})
|