mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-24 00:55:35 +00:00
fix(suite): support bundle import over plain HTTP (#890)
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
342d59472d
commit
f5a58616b7
4 changed files with 84 additions and 7 deletions
|
|
@ -111,6 +111,31 @@ describe('SuiteBundleImport', () => {
|
|||
}))
|
||||
})
|
||||
|
||||
it('previews and confirms on browsers without crypto.randomUUID', async () => {
|
||||
vi.stubGlobal('crypto', {
|
||||
getRandomValues: (bytes: Uint8Array) => {
|
||||
bytes.fill(1)
|
||||
return bytes
|
||||
},
|
||||
})
|
||||
mocks.preview.mutateAsync.mockResolvedValue(preview({ members: [] }))
|
||||
mocks.confirm.mutateAsync.mockResolvedValue({ operationId: 'operation-1', status: 'RUNNING' })
|
||||
render(<SuiteBundleImport expectedMode="CREATE" />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'pick-zip' }))
|
||||
await waitFor(() => expect(
|
||||
screen.getByRole('button', { name: 'suite.bundle.confirm' }).hasAttribute('disabled')
|
||||
).toBe(false))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'suite.bundle.confirm' }))
|
||||
|
||||
await waitFor(() => expect(mocks.confirm.mutateAsync).toHaveBeenCalledWith({
|
||||
previewToken: 'preview-1',
|
||||
warningDigest: 'digest-1',
|
||||
idempotencyKey: '01010101010101010101010101010101',
|
||||
}))
|
||||
expect(mocks.toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requires warning acceptance for every affected member', async () => {
|
||||
mocks.preview.mutateAsync.mockResolvedValue(preview({
|
||||
members: [
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { Button } from '@/shared/ui/button'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { toast } from '@/shared/lib/toast'
|
||||
import { newIdempotencyKey } from '@/shared/lib/idempotency-key'
|
||||
import { validateSuiteBundleFolder, validateSuiteBundleZip } from './suite-bundle-folder'
|
||||
|
||||
type BundleMode = 'CREATE' | 'UPDATE'
|
||||
|
|
@ -136,20 +137,22 @@ export function SuiteBundleImport({ expectedMode, expectedCoordinate, returnToSu
|
|||
const controller = new AbortController()
|
||||
requestRef.current = controller
|
||||
setFileName(file.name)
|
||||
let result: SkillSuiteBundlePreview
|
||||
try {
|
||||
const result = await previewMutation.mutateAsync({ file, signal: controller.signal })
|
||||
if (!controller.signal.aborted && selectionVersion === selectionVersionRef.current) {
|
||||
idempotencyKeyRef.current = crypto.randomUUID()
|
||||
setNow(Date.now())
|
||||
setPreview(result)
|
||||
}
|
||||
result = await previewMutation.mutateAsync({ file, signal: controller.signal })
|
||||
} catch (error) {
|
||||
if (!controller.signal.aborted && selectionVersion === selectionVersionRef.current) {
|
||||
toast.error(t('suite.bundle.previewFailed'), error instanceof Error ? error.message : '')
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
if (requestRef.current === controller) requestRef.current = null
|
||||
}
|
||||
if (!controller.signal.aborted && selectionVersion === selectionVersionRef.current) {
|
||||
idempotencyKeyRef.current = newIdempotencyKey()
|
||||
setNow(Date.now())
|
||||
setPreview(result)
|
||||
}
|
||||
}
|
||||
|
||||
const previewFile = async (file: File) => {
|
||||
|
|
@ -200,7 +203,7 @@ export function SuiteBundleImport({ expectedMode, expectedCoordinate, returnToSu
|
|||
|
||||
const confirm = async () => {
|
||||
if (!preview?.previewToken || !preview.warningDigest || !canConfirm) return
|
||||
const idempotencyKey = idempotencyKeyRef.current ?? crypto.randomUUID()
|
||||
const idempotencyKey = idempotencyKeyRef.current ?? newIdempotencyKey()
|
||||
idempotencyKeyRef.current = idempotencyKey
|
||||
try {
|
||||
const result = await confirmMutation.mutateAsync({
|
||||
|
|
|
|||
35
web/src/shared/lib/idempotency-key.test.ts
Normal file
35
web/src/shared/lib/idempotency-key.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/** @vitest-environment node */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { newIdempotencyKey } from './idempotency-key'
|
||||
|
||||
describe('newIdempotencyKey', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('uses crypto.randomUUID when it is available', () => {
|
||||
vi.stubGlobal('crypto', { randomUUID: () => 'request-1' })
|
||||
|
||||
expect(newIdempotencyKey()).toBe('request-1')
|
||||
})
|
||||
|
||||
it('uses crypto.getRandomValues when randomUUID is unavailable', () => {
|
||||
vi.stubGlobal('crypto', {
|
||||
getRandomValues: (bytes: Uint8Array) => {
|
||||
bytes.fill(1)
|
||||
return bytes
|
||||
},
|
||||
})
|
||||
|
||||
expect(newIdempotencyKey()).toBe('01010101010101010101010101010101')
|
||||
})
|
||||
|
||||
it('falls back to Math.random when Web Crypto is unavailable', () => {
|
||||
vi.stubGlobal('crypto', undefined)
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0)
|
||||
|
||||
expect(newIdempotencyKey()).toBe('00000000000000000000000000000000')
|
||||
})
|
||||
})
|
||||
14
web/src/shared/lib/idempotency-key.ts
Normal file
14
web/src/shared/lib/idempotency-key.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export function newIdempotencyKey(): string {
|
||||
const cryptoApi = typeof globalThis.crypto !== 'undefined' ? globalThis.crypto : undefined
|
||||
if (typeof cryptoApi?.randomUUID === 'function') return cryptoApi.randomUUID()
|
||||
|
||||
const bytes = new Uint8Array(16)
|
||||
if (typeof cryptoApi?.getRandomValues === 'function') {
|
||||
cryptoApi.getRandomValues(bytes)
|
||||
} else {
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
bytes[index] = Math.floor(Math.random() * 256)
|
||||
}
|
||||
}
|
||||
return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue