import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { requestJson } from '../apiClient'; import { getSignedAssetReadUrl } from '../assetReadUrlService'; import { uploadEditorSeedanceReferenceFile } from './editorReferenceUploadClient'; vi.mock('../apiClient', () => ({ requestJson: vi.fn(), })); vi.mock('../assetReadUrlService', () => ({ getSignedAssetReadUrl: vi.fn(), })); const requestJsonMock = vi.mocked(requestJson); const getSignedAssetReadUrlMock = vi.mocked(getSignedAssetReadUrl); const editorRetryOptionsExpectation = expect.objectContaining({ maxRetries: 2, retryUnsafeMethods: true, retryableStatusCodes: expect.arrayContaining([429]), }); describe('editorReferenceUploadClient', () => { beforeEach(() => { vi.clearAllMocks(); vi.spyOn(Date, 'now').mockReturnValue(1771400000000); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(null, { status: 200 })), ); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it('uploads Seedance reference videos through direct OSS upload and returns object metadata', async () => { requestJsonMock .mockResolvedValueOnce({ upload: { bucket: 'bucket', host: 'https://oss.example.com', objectKey: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', legacyPublicPath: '/generated-character-drafts/editor/seedance-references/video/ref.mp4', formFields: { key: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', policy: 'policy', 'x-oss-signature': 'signature', }, }, }) .mockResolvedValueOnce({ assetObject: { assetObjectId: 'assetobj-video', objectKey: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', assetKind: 'editor_seedance_reference_video', }, }); getSignedAssetReadUrlMock.mockResolvedValueOnce( 'https://signed.example.com/ref.mp4', ); const result = await uploadEditorSeedanceReferenceFile( new File(['video'], 'ref.mp4', { type: 'video/mp4' }), 'video', ); expect(requestJsonMock).toHaveBeenNthCalledWith( 1, '/api/assets/direct-upload-tickets', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: expect.any(String), }), '创建参考素材上传凭证失败', expect.objectContaining({ retry: editorRetryOptionsExpectation }), ); const ticketBody = JSON.parse( (requestJsonMock.mock.calls[0]?.[1] as RequestInit).body as string, ); expect(ticketBody).toMatchObject({ legacyPrefix: 'generated-character-drafts', pathSegments: ['editor', 'seedance-references', 'video', '1771400000000'], fileName: 'ref.mp4', contentType: 'video/mp4', access: 'private', maxSizeBytes: 5, successActionStatus: 200, metadata: { asset_kind: 'editor_seedance_reference_video', editor_seedance_reference_type: 'video', }, }); expect(fetch).toHaveBeenCalledWith( 'https://oss.example.com', expect.objectContaining({ method: 'POST', body: expect.any(FormData), }), ); expect(requestJsonMock).toHaveBeenNthCalledWith( 2, '/api/assets/objects/confirm', expect.objectContaining({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: expect.any(String), }), '确认参考素材资产失败', expect.objectContaining({ retry: editorRetryOptionsExpectation }), ); const confirmBody = JSON.parse( (requestJsonMock.mock.calls[1]?.[1] as RequestInit).body as string, ); expect(confirmBody).toMatchObject({ bucket: 'bucket', objectKey: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', contentType: 'video/mp4', contentLength: 5, assetKind: 'editor_seedance_reference_video', accessPolicy: 'private', entityId: 'editor-seedance-reference-video', }); expect(getSignedAssetReadUrlMock).toHaveBeenCalledWith( { objectKey: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', expireSeconds: 3600, }, undefined, { bypassCache: true }, ); expect(result).toEqual({ src: 'https://signed.example.com/ref.mp4', objectKey: 'generated-character-drafts/editor/seedance-references/video/ref.mp4', assetObjectId: 'assetobj-video', legacyPublicPath: '/generated-character-drafts/editor/seedance-references/video/ref.mp4', }); }); });