diff --git a/eslint-suppressions.json b/eslint-suppressions.json index ba5b7366185..796b8166c73 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -474,11 +474,6 @@ "count": 1 } }, - "web/app/components/app/configuration/base/var-highlight/index.tsx": { - "react-refresh/only-export-components": { - "count": 1 - } - }, "web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -1068,34 +1063,6 @@ "count": 1 } }, - "web/app/components/base/block-input/index.stories.tsx": { - "no-console": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/base/block-input/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, - "react-refresh/only-export-components": { - "count": 1 - }, - "react/no-nested-component-definitions": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - }, - "react/static-components": { - "count": 2 - } - }, "web/app/components/base/carousel/index.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -1387,7 +1354,7 @@ }, "web/app/components/base/error-boundary/index.tsx": { "react-refresh/only-export-components": { - "count": 3 + "count": 1 }, "react/jsx-no-key-after-spread": { "count": 1 @@ -2610,11 +2577,6 @@ "count": 3 } }, - "web/app/components/datasets/common/retrieval-method-info/index.tsx": { - "react-refresh/only-export-components": { - "count": 1 - } - }, "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -3089,19 +3051,6 @@ "count": 1 } }, - "web/app/components/datasets/documents/detail/metadata/components/metadata-field-list.tsx": { - "ts/no-non-null-asserted-optional-chain": { - "count": 1 - } - }, - "web/app/components/datasets/documents/detail/metadata/hooks/use-metadata-state.ts": { - "react-hooks-extra/no-direct-set-state-in-use-effect": { - "count": 6 - }, - "react/set-state-in-effect": { - "count": 6 - } - }, "web/app/components/datasets/documents/detail/metadata/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 1 @@ -5121,10 +5070,10 @@ }, "web/app/components/workflow/nodes/_base/components/layout/index.tsx": { "no-barrel-files/no-barrel-files": { - "count": 7 + "count": 6 }, "react-refresh/only-export-components": { - "count": 7 + "count": 6 } }, "web/app/components/workflow/nodes/_base/components/mcp-tool-availability.tsx": { @@ -5486,9 +5435,6 @@ "erasable-syntax-only/enums": { "count": 1 }, - "no-barrel-files/no-barrel-files": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -7177,11 +7123,6 @@ "count": 1 } }, - "web/service/__tests__/use-snippet-workflows.spec.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/__tests__/use-tools.spec.tsx": { "no-restricted-imports": { "count": 1 @@ -7286,7 +7227,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 26 + "count": 13 } }, "web/service/datasets.ts": { @@ -7294,7 +7235,7 @@ "count": 1 }, "ts/no-explicit-any": { - "count": 6 + "count": 5 } }, "web/service/debug.ts": { diff --git a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts deleted file mode 100644 index cc97065d8f8..00000000000 --- a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Integration test: DSL export/import flow - * - * Validates DSL export logic (sync draft → check secrets → download) - * and DSL import modal state management. - */ -import { act, renderHook } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' - -const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined) -const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' }) -const mockNotify = vi.fn() -const mockToast = { - success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), - error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), - warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), - info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), - dismiss: vi.fn(), - update: vi.fn(), - promise: vi.fn(), -} - -vi.mock('@langgenius/dify-ui/toast', () => ({ - toast: mockToast, -})) -const mockEventEmitter = { emit: vi.fn() } -const mockDownloadBlob = vi.fn() - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => key, - }), -})) - -vi.mock('@/app/components/workflow/constants', () => ({ - DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK', -})) - -vi.mock('@/app/components/workflow/store', () => ({ - useWorkflowStore: () => ({ - getState: () => ({ - pipelineId: 'pipeline-abc', - knowledgeName: 'My Pipeline', - }), - }), -})) - -vi.mock('@/context/event-emitter', () => ({ - useEventEmitterContextContext: () => ({ - eventEmitter: mockEventEmitter, - }), -})) - -vi.mock('@/service/use-pipeline', () => ({ - useExportPipelineDSL: () => ({ - mutateAsync: mockExportPipelineConfig, - }), -})) - -vi.mock('@/service/workflow', () => ({ - fetchWorkflowDraft: vi.fn(), -})) - -vi.mock('@/utils/download', () => ({ - downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args), -})) - -vi.mock('@/app/components/rag-pipeline/hooks/use-nodes-sync-draft', () => ({ - useNodesSyncDraft: () => ({ - doSyncWorkflowDraft: mockDoSyncWorkflowDraft, - }), -})) - -describe('DSL Export/Import Flow', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Export Flow', () => { - it('should sync draft then export then download', async () => { - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.handleExportDSL() - }) - - expect(mockDoSyncWorkflowDraft).toHaveBeenCalled() - expect(mockExportPipelineConfig).toHaveBeenCalledWith({ - pipelineId: 'pipeline-abc', - include: false, - }) - expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({ - fileName: 'My Pipeline.pipeline', - })) - }) - - it('should export with include flag when specified', async () => { - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.handleExportDSL(true) - }) - - expect(mockExportPipelineConfig).toHaveBeenCalledWith({ - pipelineId: 'pipeline-abc', - include: true, - }) - }) - - it('should notify on export error', async () => { - mockDoSyncWorkflowDraft.mockRejectedValueOnce(new Error('sync failed')) - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.handleExportDSL() - }) - - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ - type: 'error', - })) - }) - }) - - describe('Export Check Flow', () => { - it('should export directly when no secret environment variables', async () => { - const { fetchWorkflowDraft } = await import('@/service/workflow') - vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({ - environment_variables: [ - { value_type: 'string', key: 'API_URL', value: 'https://api.example.com' }, - ], - } as unknown as Awaited>) - - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.exportCheck() - }) - - // Should proceed to export directly (no secret vars) - expect(mockDoSyncWorkflowDraft).toHaveBeenCalled() - }) - - it('should emit DSL_EXPORT_CHECK event when secret variables exist', async () => { - const { fetchWorkflowDraft } = await import('@/service/workflow') - vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({ - environment_variables: [ - { value_type: 'secret', key: 'API_KEY', value: '***' }, - ], - } as unknown as Awaited>) - - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.exportCheck() - }) - - expect(mockEventEmitter.emit).toHaveBeenCalledWith(expect.objectContaining({ - type: 'DSL_EXPORT_CHECK', - payload: expect.objectContaining({ - data: expect.arrayContaining([ - expect.objectContaining({ value_type: 'secret' }), - ]), - }), - })) - }) - - it('should notify on export check error', async () => { - const { fetchWorkflowDraft } = await import('@/service/workflow') - vi.mocked(fetchWorkflowDraft).mockRejectedValueOnce(new Error('fetch failed')) - - const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL') - const { result } = renderHook(() => useDSL()) - - await act(async () => { - await result.current.exportCheck() - }) - - expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ - type: 'error', - })) - }) - }) -}) diff --git a/web/__tests__/xss-prevention.test.tsx b/web/__tests__/xss-prevention.test.tsx deleted file mode 100644 index 233cbebf0ee..00000000000 --- a/web/__tests__/xss-prevention.test.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * XSS Prevention Test Suite - * - * This test verifies that the XSS vulnerability in block-input has been properly - * fixed by replacing dangerouslySetInnerHTML with safe React rendering. - */ - -import { cleanup, render } from '@testing-library/react' -import * as React from 'react' -import BlockInput from '../app/components/base/block-input' - -// Mock styles -vi.mock('../app/components/app/configuration/base/var-highlight/style.module.css', () => ({ - default: { - item: 'mock-item-class', - }, -})) - -describe('XSS Prevention - Block Input Security', () => { - afterEach(() => { - cleanup() - }) - - describe('BlockInput Component Security', () => { - it('should safely render malicious variable names without executing scripts', () => { - const testInput = 'user@test.com{{}}' - const { container } = render() - - const scriptElements = container.querySelectorAll('script') - expect(scriptElements).toHaveLength(0) - - const textContent = container.textContent - expect(textContent).toContain(''} - const { container } = render() - - const spanElement = container.querySelector('span') - const scriptElements = container.querySelectorAll('script') - - expect(spanElement?.textContent).toBe('') - expect(scriptElements).toHaveLength(0) - }) - }) -}) - -export {} diff --git a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx deleted file mode 100644 index 573cdf02334..00000000000 --- a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import type { App, AppSSO } from '@/types/app' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import * as React from 'react' -import { AppModeEnum } from '@/types/app' -import AppInfo from '..' - -const mockDetailPanel = vi.hoisted(() => vi.fn()) -const mockModals = vi.hoisted(() => vi.fn()) - -let mockAppPermissionKeys = ['app.acl.view_layout'] -const mockSetPanelOpen = vi.fn() - -vi.mock('@/context/app-context', () => ({ - useAppContext: () => ({ - workspacePermissionKeys: ['app.create_and_management'], - }), - useSelector: (selector: (state: { userProfile: { id: string }, workspacePermissionKeys: string[] }) => unknown) => selector({ - userProfile: { id: 'user-1' }, - workspacePermissionKeys: ['app.create_and_management'], - }), -})) - -vi.mock('../app-info-trigger', () => ({ - default: React.memo(({ appDetail, expand, onClick }: { - appDetail: App & Partial - expand: boolean - onClick: () => void - }) => ( - - )), -})) - -vi.mock('../app-info-detail-panel', () => ({ - default: React.memo((props: { show: boolean, onClose: () => void }) => { - mockDetailPanel(props) - return props.show ?
: null - }), -})) - -vi.mock('../app-info-modals', () => ({ - default: React.memo((props: { activeModal: string | null }) => { - mockModals(props) - return props.activeModal ?
: null - }), -})) - -const mockAppDetail: App & Partial = { - id: 'app-1', - name: 'Test App', - mode: AppModeEnum.CHAT, - icon: '🤖', - icon_type: 'emoji', - icon_background: '#FFEAD5', - icon_url: '', - description: '', - use_icon_as_answer_icon: false, - permission_keys: mockAppPermissionKeys, -} as App & Partial - -const mockUseAppInfoActions = { - appDetail: mockAppDetail, - panelOpen: false, - setPanelOpen: mockSetPanelOpen, - closePanel: vi.fn(), - activeModal: null as string | null, - openModal: vi.fn(), - closeModal: vi.fn(), - secretEnvList: [], - setSecretEnvList: vi.fn(), - onEdit: vi.fn(), - onCopy: vi.fn(), - onExport: vi.fn(), - exportCheck: vi.fn(), - handleConfirmExport: vi.fn(), - onConfirmDelete: vi.fn(), -} - -vi.mock('../use-app-info-actions', () => ({ - useAppInfoActions: () => mockUseAppInfoActions, -})) - -describe('AppInfo', () => { - beforeEach(() => { - vi.clearAllMocks() - mockAppPermissionKeys = ['app.acl.view_layout'] - mockUseAppInfoActions.appDetail = mockAppDetail - mockUseAppInfoActions.appDetail.permission_keys = mockAppPermissionKeys - mockUseAppInfoActions.panelOpen = false - mockUseAppInfoActions.activeModal = null - }) - - it('should return null when appDetail is not available', () => { - mockUseAppInfoActions.appDetail = undefined as unknown as App & Partial - const { container } = render() - expect(container.innerHTML).toBe('') - }) - - it('should render trigger when not onlyShowDetail', () => { - render() - expect(screen.getByTestId('trigger'))!.toBeInTheDocument() - }) - - it('should not mount detail layer while the app info panel is closed', () => { - render() - expect(mockDetailPanel).not.toHaveBeenCalled() - expect(mockModals).not.toHaveBeenCalled() - }) - - it('should not render trigger when onlyShowDetail is true', () => { - render() - expect(screen.queryByTestId('trigger')).not.toBeInTheDocument() - }) - - it('should pass expand prop to trigger', () => { - render() - expect(screen.getByTestId('trigger'))!.toHaveAttribute('data-expand', 'true') - - const { unmount } = render() - const triggers = screen.getAllByTestId('trigger') - expect(triggers[triggers.length - 1])!.toHaveAttribute('data-expand', 'false') - unmount() - }) - - it('should toggle panel when trigger is clicked and user is editor', async () => { - const user = userEvent.setup() - render() - - await user.click(screen.getByTestId('trigger')) - - expect(mockSetPanelOpen).toHaveBeenCalled() - const updater = mockSetPanelOpen.mock.calls[0]![0] as (v: boolean) => boolean - expect(updater(false)).toBe(true) - expect(updater(true)).toBe(false) - }) - - it('should not toggle panel when app ACL does not allow layout access', async () => { - const user = userEvent.setup() - mockAppPermissionKeys = [] - mockUseAppInfoActions.appDetail.permission_keys = mockAppPermissionKeys - render() - - await user.click(screen.getByTestId('trigger')) - - expect(mockSetPanelOpen).not.toHaveBeenCalled() - }) - - it('should show detail panel based on panelOpen when not onlyShowDetail', () => { - mockUseAppInfoActions.panelOpen = true - render() - expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument() - expect(mockDetailPanel).toHaveBeenCalled() - }) - - it('should show detail panel based on openState when onlyShowDetail', () => { - render() - expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument() - }) - - it('should hide detail panel when openState is false and onlyShowDetail', () => { - render() - expect(screen.queryByTestId('detail-panel')).not.toBeInTheDocument() - expect(mockDetailPanel).not.toHaveBeenCalled() - expect(mockModals).not.toHaveBeenCalled() - }) -}) diff --git a/web/app/components/app-sidebar/app-info/index.tsx b/web/app/components/app-sidebar/app-info/index.tsx index f08c1f12ed8..ea1665d0dbd 100644 --- a/web/app/components/app-sidebar/app-info/index.tsx +++ b/web/app/components/app-sidebar/app-info/index.tsx @@ -5,7 +5,6 @@ import { getAppACLCapabilities } from '@/utils/permission' import AppInfoDetailPanel from './app-info-detail-panel' import AppInfoModals from './app-info-modals' import AppInfoTrigger from './app-info-trigger' -import { useAppInfoActions } from './use-app-info-actions' type IAppInfoProps = { expand: boolean @@ -122,16 +121,3 @@ export const AppInfoView = ({
) } - -const AppInfo = ({ onDetailExpand, ...props }: IAppInfoProps) => { - const actions = useAppInfoActions({ onDetailExpand }) - - return ( - - ) -} - -export default React.memo(AppInfo) diff --git a/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx index 1add8601c40..e60576004b3 100644 --- a/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/base/var-highlight/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import VarHighlight, { varHighlightHTML } from '../index' +import VarHighlight from '../index' describe('VarHighlight', () => { beforeEach(() => { @@ -35,32 +35,4 @@ describe('VarHighlight', () => { expect(container.firstChild).toHaveClass('mt-2') }) }) - - // Escaping HTML via helper - describe('varHighlightHTML', () => { - it('should escape dangerous characters before returning HTML string', () => { - // Arrange - const props = { name: '' } - - // Act - const html = varHighlightHTML(props) - - // Assert - expect(html).toContain('<script>alert('xss')</script>') - expect(html).not.toContain('')).not.toContain('