前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、 迁移脚本、测试与自托管 docker-compose 示例。 开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读 publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为 VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注 Apache-2.0。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
poolQuery: vi.fn(),
|
|
assets: vi.fn(),
|
|
signedUrls: vi.fn(),
|
|
objectSize: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/db/pool.js', () => ({
|
|
pool: { query: mocks.poolQuery },
|
|
withTransaction: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../src/domain/assetReferences.js', () => ({
|
|
readOwnedAssetReferencesByIds: mocks.assets,
|
|
}));
|
|
|
|
vi.mock('../src/domain/assetReadUrlSigner.js', () => ({
|
|
signOwnedObjectReadUrls: mocks.signedUrls,
|
|
}));
|
|
|
|
vi.mock('../src/storage/storageService.js', () => ({
|
|
getStoredObjectSize: mocks.objectSize,
|
|
}));
|
|
|
|
import { resolveTkPromptEngineImages } from '../src/domain/tkPromptEngineService.js';
|
|
|
|
const userId = '00000000-0000-4000-8000-000000000901';
|
|
const assetId = '00000000-0000-4000-8000-000000000902';
|
|
|
|
describe('TK Prompt Engine 商品图 MIME 解析', () => {
|
|
beforeEach(() => {
|
|
mocks.poolQuery.mockReset();
|
|
mocks.assets.mockReset();
|
|
mocks.signedUrls.mockReset();
|
|
mocks.objectSize.mockReset();
|
|
const asset = imageAsset('users/user/uploads/session/product');
|
|
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
|
|
mocks.signedUrls.mockResolvedValue(new Map([[
|
|
asset.object_key,
|
|
'https://assets.example.com/product?signature=redacted',
|
|
]]));
|
|
mocks.objectSize.mockResolvedValue(2_048);
|
|
});
|
|
|
|
it('无后缀商品图必须由完成上传会话与 ready 缩略图链提供 JPEG 类型', async () => {
|
|
mocks.poolQuery.mockResolvedValue({
|
|
rows: [{ asset_id: assetId, content_type: 'image/jpeg' }],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(resolveTkPromptEngineImages(userId, [assetId])).resolves.toEqual([
|
|
expect.objectContaining({ assetId, mimeType: 'image/jpeg', sizeBytes: 2_048 }),
|
|
]);
|
|
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).toContain(
|
|
"session.status = 'completed'",
|
|
);
|
|
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).toContain(
|
|
"asset.thumbnail_status = 'ready'",
|
|
);
|
|
expect(mocks.poolQuery.mock.calls[0]?.[1]).toEqual([userId, [assetId]]);
|
|
});
|
|
|
|
it('完成会话记录伪造类型时即使对象键有图片后缀也拒绝', async () => {
|
|
const asset = imageAsset('users/user/uploads/session/product.jpg');
|
|
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
|
|
mocks.signedUrls.mockResolvedValue(new Map([[
|
|
asset.object_key,
|
|
'https://assets.example.com/product.jpg?signature=redacted',
|
|
]]));
|
|
mocks.poolQuery.mockResolvedValue({
|
|
rows: [{ asset_id: assetId, content_type: 'text/plain' }],
|
|
rowCount: 1,
|
|
});
|
|
|
|
await expect(resolveTkPromptEngineImages(userId, [assetId])).rejects.toMatchObject({
|
|
code: 'TK_PROMPT_IMAGE_MIME_INVALID',
|
|
});
|
|
});
|
|
|
|
it('没有完成会话时仅允许对象键的安全后缀回退,无后缀保持关闭', async () => {
|
|
mocks.poolQuery.mockResolvedValue({ rows: [], rowCount: 0 });
|
|
await expect(resolveTkPromptEngineImages(userId, [assetId])).rejects.toMatchObject({
|
|
code: 'TK_PROMPT_IMAGE_MIME_INVALID',
|
|
});
|
|
|
|
const asset = imageAsset('users/user/historical/product.png');
|
|
mocks.assets.mockResolvedValue(new Map([[assetId, asset]]));
|
|
mocks.signedUrls.mockResolvedValue(new Map([[
|
|
asset.object_key,
|
|
'https://assets.example.com/product.png?signature=redacted',
|
|
]]));
|
|
await expect(resolveTkPromptEngineImages(userId, [assetId])).resolves.toEqual([
|
|
expect.objectContaining({ mimeType: 'image/png' }),
|
|
]);
|
|
});
|
|
});
|
|
|
|
function imageAsset(objectKey: string) {
|
|
return {
|
|
id: assetId,
|
|
user_id: userId,
|
|
name: '商品图片',
|
|
object_key: objectKey,
|
|
thumbnail_key: 'users/user/upload-thumbnails/session.webp',
|
|
thumbnail_status: 'ready',
|
|
type: 'image',
|
|
duration_seconds: null,
|
|
size_bytes: 2_048,
|
|
width: 1_080,
|
|
height: 1_080,
|
|
};
|
|
}
|