From 1b7e679d4596d8e4975d9073cbc20145338dbd8c Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 3 Sep 2026 18:59:17 +0800 Subject: [PATCH] fix(security): include csrf token in scan retry Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../stream/ScanTaskConsumerLoggingTest.java | 1 + .../security-audit/use-security-audit.test.ts | 28 +++++++++++++++++-- .../security-audit/use-security-audit.ts | 3 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java index ccdab04d..c388fcbf 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/stream/ScanTaskConsumerLoggingTest.java @@ -232,6 +232,7 @@ class ScanTaskConsumerLoggingTest { ScannerType scannerType, String reason) { } + } private static final class TestProducer implements ScanTaskProducer { diff --git a/web/src/features/security-audit/use-security-audit.test.ts b/web/src/features/security-audit/use-security-audit.test.ts index 9f0195ec..84e8566c 100644 --- a/web/src/features/security-audit/use-security-audit.test.ts +++ b/web/src/features/security-audit/use-security-audit.test.ts @@ -12,12 +12,22 @@ import { describe, expect, it, vi } from 'vitest' // Capture the options passed to useQuery so we can assert on them. let capturedOptions: Record | undefined +let capturedMutationOptions: Record | undefined +const apiMocks = vi.hoisted(() => ({ + fetchJson: vi.fn(), + getCsrfHeaders: vi.fn(() => ({ 'X-XSRF-TOKEN': 'csrf-token' })), +})) vi.mock('@tanstack/react-query', () => ({ useQuery: (options: Record) => { capturedOptions = options return { data: undefined, isLoading: false } }, + useMutation: (options: Record) => { + capturedMutationOptions = options + return { mutate: vi.fn(), isPending: false } + }, + useQueryClient: () => ({ invalidateQueries: vi.fn() }), })) // Mock fetchJson to avoid actual network calls. The hook's queryFn @@ -30,11 +40,12 @@ vi.mock('@/api/client', () => ({ this.status = status } }, - fetchJson: vi.fn(), + fetchJson: apiMocks.fetchJson, + getCsrfHeaders: apiMocks.getCsrfHeaders, })) // Dynamic import to ensure mocks are established first. -const { useSecurityAudits } = await import('./use-security-audit') +const { useRetrySecurityScan, useSecurityAudits } = await import('./use-security-audit') describe('useSecurityAudits', () => { it('uses the correct query key structure', () => { @@ -78,4 +89,17 @@ describe('useSecurityAudits', () => { expect(capturedOptions?.retry).toBe(false) }) + + it('sends the CSRF header when retrying a security scan', async () => { + apiMocks.fetchJson.mockResolvedValueOnce({ status: 'SCANNING' }) + useRetrySecurityScan(42, 100) + + await (capturedMutationOptions?.mutationFn as () => Promise)() + + expect(apiMocks.getCsrfHeaders).toHaveBeenCalledOnce() + expect(apiMocks.fetchJson).toHaveBeenCalledWith( + '/api/v1/skills/42/versions/100/security-audit/retry', + { method: 'POST', headers: { 'X-XSRF-TOKEN': 'csrf-token' } }, + ) + }) }) diff --git a/web/src/features/security-audit/use-security-audit.ts b/web/src/features/security-audit/use-security-audit.ts index 88bc3278..e1383442 100644 --- a/web/src/features/security-audit/use-security-audit.ts +++ b/web/src/features/security-audit/use-security-audit.ts @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { ApiError, fetchJson } from '@/api/client' +import { ApiError, fetchJson, getCsrfHeaders } from '@/api/client' import type { SecurityAuditRecord } from './types' async function fetchSecurityAudits( @@ -37,6 +37,7 @@ export function useRetrySecurityScan(skillId: number, versionId: number) { return useMutation({ mutationFn: () => fetchJson(`/api/v1/skills/${skillId}/versions/${versionId}/security-audit/retry`, { method: 'POST', + headers: getCsrfHeaders(), }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['security-audits', skillId, versionId] })