Files
Genarrative/src/services/image-editor/editorReferenceUploadClient.test.ts
T
kdletters 9ad2152816 为图片编辑器上传和生成加自动重试
新增图片编辑器请求重试配置

生成请求遇到429等临时错误自动重试

上传凭证、资产确认和直传上传共用重试策略

补充图片编辑器重试参数和直传429测试
2026-06-23 16:30:54 +08:00

153 lines
4.7 KiB
TypeScript

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',
});
});
});