Files
lindyson-ai/apps/api/tests/uploadSessionService.test.ts
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(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>
2026-10-16 02:10:46 +08:00

634 lines
28 KiB
TypeScript

import { createHash } from 'node:crypto';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
poolQuery: vi.fn(),
withTransaction: vi.fn(),
createUploadSignature: vi.fn(),
copyStoredObject: vi.fn(),
readStoredObjectSize: vi.fn(),
readUploadedMediaMetadata: vi.fn(),
validateUploadedMediaDecoding: vi.fn(),
createUploadedImageThumbnail: vi.fn(),
createUploadedVideoPreview: vi.fn(),
enqueueUploadSession: vi.fn(),
resolveAssetFolderId: vi.fn(),
scheduleObjectDeletion: vi.fn(),
mapAssetRow: vi.fn(),
}));
vi.mock('../src/db/pool.js', () => ({
pool: { query: mocks.poolQuery },
withTransaction: mocks.withTransaction,
}));
vi.mock('../src/storage/storageService.js', () => ({
UPLOAD_MAX_BYTES: 1024 * 1024 * 1024,
buildPublicUrl: (key: string) => `https://storage.example.com/${key}`,
buildUploadStagingObjectKey: ({ userId, sessionId, filename }: Record<string, string>) => `users/${userId}/upload-staging/${sessionId}/${filename}`,
buildUploadFinalObjectKey: ({ userId, sessionId, filename }: Record<string, string>) => `users/${userId}/uploads/${sessionId}/${filename}`,
createUploadSignature: mocks.createUploadSignature,
copyStoredObject: mocks.copyStoredObject,
readStoredObjectSize: mocks.readStoredObjectSize,
}));
vi.mock('../src/domain/uploadedVideoPreview.js', () => ({
buildUploadedImageThumbnailKey: (_userId: string, sessionId: string) => `thumb/${sessionId}.webp`,
buildUploadedVideoThumbnailKey: (_userId: string, sessionId: string) => `thumb/${sessionId}.jpg`,
createUploadedImageThumbnail: mocks.createUploadedImageThumbnail,
createUploadedVideoPreview: mocks.createUploadedVideoPreview,
readUploadedMediaMetadata: mocks.readUploadedMediaMetadata,
validateUploadedMediaDecoding: mocks.validateUploadedMediaDecoding,
}));
vi.mock('../src/queues/uploadQueue.js', () => ({ enqueueUploadSession: mocks.enqueueUploadSession }));
vi.mock('../src/domain/assetFolders.js', () => ({ resolveAssetFolderId: mocks.resolveAssetFolderId }));
vi.mock('../src/domain/assetDeletionService.js', () => ({ scheduleObjectDeletion: mocks.scheduleObjectDeletion }));
vi.mock('../src/domain/assets.js', () => ({ mapAssetRow: mocks.mapAssetRow }));
import {
createOrReplayUploadSession,
enqueueRecoverableUploadSessions,
processUploadSession,
queueUploadCompletion,
} from '../src/domain/uploadSessionService.js';
const input = {
filename: 'clip.mp4',
contentType: 'video/mp4',
sizeBytes: 1024,
source: 'uploaded-video' as const,
folderId: null,
idempotencyKey: 'upload-session-stable-key',
};
describe('uploadSessionService', () => {
beforeEach(() => {
Object.values(mocks).forEach(mock => mock.mockReset());
mocks.withTransaction.mockImplementation(async callback => callback({ query: mocks.poolQuery }));
mocks.createUploadSignature.mockImplementation(async (request: { objectKey: string }) => ({
driver: 'local', method: 'PUT', uploadUrl: 'http://localhost/upload', headers: { 'content-type': 'video/mp4' },
objectKey: request.objectKey,
expiresAt: '2099-01-01T00:00:00.000Z', maxSizeBytes: 1024,
}));
mocks.enqueueUploadSession.mockResolvedValue(undefined);
mocks.validateUploadedMediaDecoding.mockResolvedValue(undefined);
mocks.createUploadedImageThumbnail.mockImplementation(async ({ thumbnailKey }) => thumbnailKey);
mocks.createUploadedVideoPreview.mockImplementation(async ({ thumbnailKey }) => ({
thumbnailKey, width: 16, height: 16, durationSeconds: 1,
}));
});
it('同键同请求在 waiting-upload 时重签同一 staging key,不创建新会话', async () => {
const existing = sessionRow({ request_hash: requestHash(input) });
mocks.poolQuery
.mockResolvedValueOnce(queryResult([existing]))
.mockResolvedValueOnce(queryResult([{ ...existing, expires_at: '2099-01-01T00:00:00.000Z' }]));
const result = await createOrReplayUploadSession('user-1', input);
expect(result.signature).toEqual(expect.objectContaining({
objectKey: existing.object_key,
sessionId: existing.id,
}));
expect(mocks.createUploadSignature).toHaveBeenCalledWith(expect.objectContaining({ objectKey: existing.object_key }));
expect(mocks.resolveAssetFolderId).not.toHaveBeenCalled();
});
it.each(['processing', 'completed'] as const)('%s 会话重放只返回 session,不重新签发写权限', async status => {
const existing = sessionRow({
request_hash: requestHash(input),
status,
processing_stage: status === 'completed' ? 'completed' : 'probing',
});
mocks.poolQuery.mockResolvedValueOnce(queryResult([existing]));
await expect(createOrReplayUploadSession('user-1', input)).resolves.toMatchObject({ signature: null });
expect(mocks.createUploadSignature).not.toHaveBeenCalled();
});
it('同幂等键请求事实变化时返回明确 409', async () => {
mocks.poolQuery.mockResolvedValueOnce(queryResult([sessionRow({ request_hash: 'different' })]));
await expect(createOrReplayUploadSession('user-1', input)).rejects.toMatchObject({
statusCode: 409,
code: 'UPLOAD_IDEMPOTENCY_CONFLICT',
});
});
it('complete 只把 waiting-upload 原子推进 queued,并用下一个 attempt 的稳定 job id 入队', async () => {
const pending = sessionRow();
mocks.poolQuery
.mockResolvedValueOnce(queryResult([pending]))
.mockResolvedValueOnce(queryResult([{ ...pending, processing_stage: 'queued' }]));
const result = await queueUploadCompletion('user-1', pending.id);
expect(result).toMatchObject({ status: 'pending', processingStage: 'queued' });
expect(mocks.enqueueUploadSession).toHaveBeenCalledWith(pending.id, 1);
});
it('上游交接使用同一事务,租约检查失败时不会修改上传状态或发布队列', async () => {
const leaseLost = Object.assign(new Error('导入租约失效'), { code: 'MCP_FILE_TIMEOUT' });
const beforeQueue = vi.fn(async (client) => {
expect(client.query).toBe(mocks.poolQuery);
throw leaseLost;
});
await expect(queueUploadCompletion('user-1', 'session-1', { beforeQueue })).rejects.toBe(leaseLost);
expect(beforeQueue).toHaveBeenCalledOnce();
expect(mocks.poolQuery).not.toHaveBeenCalled();
expect(mocks.enqueueUploadSession).not.toHaveBeenCalled();
});
it('已取消的交接不启动事务或发布队列', async () => {
const signal = AbortSignal.abort(new Error('worker stopping'));
await expect(queueUploadCompletion('user-1', 'session-1', { signal })).rejects.toBe(signal.reason);
expect(mocks.withTransaction).not.toHaveBeenCalled();
expect(mocks.enqueueUploadSession).not.toHaveBeenCalled();
});
it('等待会话锁期间取消,事务拒绝且不推进 queued', async () => {
const controller = new AbortController();
const reason = new Error('deadline exceeded');
const pending = sessionRow();
mocks.poolQuery.mockImplementationOnce(async () => {
controller.abort(reason);
return queryResult([pending]);
});
await expect(queueUploadCompletion('user-1', pending.id, { signal: controller.signal }))
.rejects.toBe(reason);
expect(mocks.poolQuery).toHaveBeenCalledOnce();
expect(mocks.enqueueUploadSession).not.toHaveBeenCalled();
});
it('入队状态写入期间取消仍让事务拒绝,不能在事务外发布队列', async () => {
const controller = new AbortController();
const reason = new Error('deadline exceeded');
const pending = sessionRow();
mocks.poolQuery.mockResolvedValueOnce(queryResult([pending])).mockImplementationOnce(async () => {
controller.abort(reason);
return queryResult([{ ...pending, processing_stage: 'queued' }]);
});
await expect(queueUploadCompletion('user-1', pending.id, { signal: controller.signal }))
.rejects.toBe(reason);
expect(mocks.enqueueUploadSession).not.toHaveBeenCalled();
});
it('complete 发现签名过期时先持久化 expired 与删除 Outbox,再在事务外返回 410', async () => {
const pending = sessionRow({ expires_at: '2000-01-01T00:00:00.000Z' });
const expired = sessionRow({
...pending,
status: 'expired',
processing_stage: 'expired',
error_code: 'UPLOAD_SESSION_EXPIRED',
error_message: '上传签名及宽限期已过期',
});
mocks.poolQuery
.mockResolvedValueOnce(queryResult([pending]))
.mockResolvedValueOnce(queryResult([expired]));
await expect(queueUploadCompletion('user-1', pending.id)).rejects.toMatchObject({
statusCode: 410,
code: 'UPLOAD_SESSION_EXPIRED',
});
expect(mocks.scheduleObjectDeletion).toHaveBeenCalledWith(
expect.anything(),
pending.object_key,
[],
expect.any(Date),
);
});
it('恢复器会将取消请求后崩溃且租约过期的 processing 会话终态化并安排清理', async () => {
const canceled = sessionRow({
status: 'canceled',
processing_stage: 'canceled',
cancel_requested_at: '2026-07-16T10:00:00.000Z',
error_code: 'UPLOAD_CANCELED',
error_message: '上传已取消',
});
mocks.poolQuery
.mockResolvedValueOnce(queryResult([canceled]))
.mockResolvedValueOnce(queryResult([]));
await expect(enqueueRecoverableUploadSessions(10)).resolves.toBe(0);
expect(String(mocks.poolQuery.mock.calls[0]?.[0])).toContain("SET status = 'canceled'");
expect(mocks.scheduleObjectDeletion).toHaveBeenCalledWith(
expect.anything(),
canceled.final_object_key,
[],
null,
);
});
it('完成资产与 session 的同一事务会为 staging key 写入延迟删除 Outbox', async () => {
const claimed = sessionRow({
source: 'uploaded-image', asset_type: 'image', content_type: 'image/png', filename: 'cover.png',
asset_name: 'cover', status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
folder_id: '00000000-0000-4000-8000-000000000055',
final_object_key: 'users/user-1/uploads/00000000-0000-4000-8000-000000000001/cover.png',
});
installSuccessfulProcessingQueries(claimed);
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: 1, height: 1, durationSeconds: null, hasVideo: true, hasAudio: false,
formatName: 'png_pipe', frameCount: 1, videoCodec: 'png', majorBrand: null,
});
await expect(processUploadSession(claimed.id)).resolves.toEqual({ skipped: false, assetId: 'asset-1' });
expect(mocks.resolveAssetFolderId).toHaveBeenCalledWith(expect.anything(), {
userId: claimed.user_id,
source: claimed.source,
folderId: claimed.folder_id,
});
const sessionLockIndex = mocks.poolQuery.mock.calls.findIndex(([sql]) => (
String(sql).includes("WHERE id = $1 AND status = 'processing'") && String(sql).includes('FOR UPDATE')
));
expect(sessionLockIndex).toBeGreaterThanOrEqual(0);
expect(mocks.resolveAssetFolderId.mock.invocationCallOrder[0]).toBeLessThan(
mocks.poolQuery.mock.invocationCallOrder[sessionLockIndex]!,
);
expect(mocks.validateUploadedMediaDecoding).toHaveBeenCalledWith({
objectKey: claimed.final_object_key,
assetType: 'image',
});
expect(mocks.scheduleObjectDeletion).toHaveBeenCalledWith(
expect.anything(),
claimed.object_key,
[],
expect.any(Date),
);
});
it('目录删除先提交后使用数据库最新主目录完成资产,而不使用 worker 的旧目录快照', async () => {
const oldFolderId = '00000000-0000-4000-8000-000000000055';
const rootFolderId = '00000000-0000-4000-8000-000000000066';
const claimed = audioProcessingSession({ folder_id: oldFolderId });
const locked = {
...claimed,
folder_id: rootFolderId,
asset_name: '数据库最新音频名称',
};
installSuccessfulAudioProcessingQueries(claimed, [locked], [locked]);
mocks.resolveAssetFolderId.mockResolvedValue(rootFolderId);
installSuccessfulAudioMetadata();
await expect(processUploadSession(claimed.id)).resolves.toEqual({ skipped: false, assetId: 'asset-1' });
expect(mocks.resolveAssetFolderId).toHaveBeenCalledTimes(1);
expect(mocks.resolveAssetFolderId).toHaveBeenCalledWith(expect.anything(), {
userId: claimed.user_id,
source: 'uploaded-audio',
folderId: rootFolderId,
});
const insert = mocks.poolQuery.mock.calls.find(([sql]) => String(sql).includes('INSERT INTO assets'));
expect(insert?.[1]?.slice(0, 6)).toEqual([
claimed.user_id,
rootFolderId,
'数据库最新音频名称',
'audio',
'uploaded-audio',
claimed.final_object_key,
]);
expect(insert?.[1]?.[1]).not.toBe(oldFolderId);
});
it('观察旧目录后目录已删除时跨事务重读新主目录并只插入一份资产', async () => {
const oldFolderId = '00000000-0000-4000-8000-000000000055';
const rootFolderId = '00000000-0000-4000-8000-000000000066';
const claimed = audioProcessingSession({ folder_id: oldFolderId });
const moved = { ...claimed, folder_id: rootFolderId };
installSuccessfulAudioProcessingQueries(claimed, [claimed, moved], [moved]);
mocks.resolveAssetFolderId
.mockRejectedValueOnce(Object.assign(new Error('旧目录已删除'), { code: 'ASSET_FOLDER_INVALID' }))
.mockResolvedValueOnce(rootFolderId);
installSuccessfulAudioMetadata();
await expect(processUploadSession(claimed.id)).resolves.toEqual({ skipped: false, assetId: 'asset-1' });
// claim 一次,持久化位置重试两次。
expect(mocks.withTransaction).toHaveBeenCalledTimes(3);
expect(mocks.resolveAssetFolderId).toHaveBeenCalledTimes(2);
expect(mocks.resolveAssetFolderId.mock.calls.map(([, input]) => input.folderId)).toEqual([
oldFolderId,
rootFolderId,
]);
const inserts = mocks.poolQuery.mock.calls.filter(([sql]) => String(sql).includes('INSERT INTO assets'));
expect(inserts).toHaveLength(1);
expect(inserts[0]?.[1]?.[1]).toBe(rootFolderId);
});
it('锁目录后发现会话位置变化会释放事务并有限重试', async () => {
const oldFolderId = '00000000-0000-4000-8000-000000000055';
const rootFolderId = '00000000-0000-4000-8000-000000000066';
const claimed = audioProcessingSession({ folder_id: oldFolderId });
const moved = { ...claimed, folder_id: rootFolderId };
installSuccessfulAudioProcessingQueries(claimed, [claimed, moved], [moved, moved]);
mocks.resolveAssetFolderId
.mockResolvedValueOnce(oldFolderId)
.mockResolvedValueOnce(rootFolderId);
installSuccessfulAudioMetadata();
await expect(processUploadSession(claimed.id)).resolves.toEqual({ skipped: false, assetId: 'asset-1' });
expect(mocks.withTransaction).toHaveBeenCalledTimes(3);
const inserts = mocks.poolQuery.mock.calls.filter(([sql]) => String(sql).includes('INSERT INTO assets'));
expect(inserts).toHaveLength(1);
expect(inserts[0]?.[1]?.[1]).toBe(rootFolderId);
});
it('缩略图 Put 成功但 session CAS 丢失时将 TOS 孤儿对象写入删除 Outbox', async () => {
const claimed = sessionRow({
source: 'uploaded-image', asset_type: 'image', content_type: 'image/png', filename: 'cover.png',
asset_name: 'cover', status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
final_object_key: 'users/user-1/uploads/00000000-0000-4000-8000-000000000001/cover.png',
});
const orphanThumbnailKey = `thumb/${claimed.id}.webp`;
mocks.poolQuery.mockImplementation(async (sql: unknown) => {
const query = String(sql);
if (query.includes("SET status = 'processing'")) return queryResult([claimed]);
if (query.includes('SELECT 1 FROM upload_sessions')) return queryResult([{ '?column?': 1 }]);
if (query.includes('SET sealed_at = COALESCE')) return queryResult([claimed]);
// 新 worker 已接管:旧 lease token 不得把缩略图 key 写入 session。
// 必须先匹配 SET 子句;该 SQL 的 WHERE 同样包含 processing_stage = 'thumbnailing'。
if (query.includes('SET thumbnail_key = $3')) return queryResult([]);
if (query.includes('SET processing_stage = $3') || query.includes("processing_stage = 'thumbnailing'")) {
return queryResult([claimed]);
}
if (query.includes('SET status = CASE')) return queryResult([]);
return queryResult([]);
});
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: 1, height: 1, durationSeconds: null, hasVideo: true, hasAudio: false,
formatName: 'png_pipe', frameCount: 1, videoCodec: 'png', majorBrand: null,
});
mocks.createUploadedImageThumbnail.mockResolvedValue(orphanThumbnailKey);
await expect(processUploadSession(claimed.id)).rejects.toMatchObject({ code: 'UPLOAD_LEASE_LOST' });
expect(mocks.scheduleObjectDeletion).toHaveBeenCalledWith(
expect.anything(),
orphanThumbnailKey,
[],
expect.any(Date),
);
});
it('音频会话拒绝包含视频流的伪装文件且不进入完整解码或资产事务', async () => {
const claimed = sessionRow({
filename: 'music.mp3', content_type: 'audio/mpeg', source: 'uploaded-audio', asset_type: 'audio',
status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
final_object_key: 'users/user-1/uploads/00000000-0000-4000-8000-000000000001/music.mp3',
});
installFailedProcessingQueries(claimed);
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: 16, height: 16, durationSeconds: 1, hasVideo: true, hasAudio: true,
formatName: 'mp3', frameCount: 1, videoCodec: 'h264', majorBrand: null,
});
await expect(processUploadSession(claimed.id)).rejects.toMatchObject({ code: 'UPLOAD_MEDIA_DETECTION_FAILED' });
expect(mocks.validateUploadedMediaDecoding).not.toHaveBeenCalled();
});
it('视频缩略图失败不会静默完成,且上传成功前不写入 thumbnail key', async () => {
const claimed = sessionRow({
status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
});
installFailedProcessingQueries(claimed, true);
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: 16, height: 16, durationSeconds: 1, hasVideo: true, hasAudio: false,
formatName: 'mov,mp4,m4a,3gp,3g2,mj2', frameCount: 24, videoCodec: 'h264', majorBrand: 'isom',
});
mocks.createUploadedVideoPreview.mockRejectedValue(Object.assign(new Error('thumbnail failed'), {
code: 'UPLOAD_THUMBNAIL_FAILED',
}));
await expect(processUploadSession(claimed.id)).rejects.toMatchObject({ code: 'UPLOAD_THUMBNAIL_FAILED' });
expect(mocks.poolQuery.mock.calls.some(([sql]) => (
String(sql).includes("processing_stage = 'thumbnailing'") && String(sql).includes('thumbnail_key = $3')
))).toBe(false);
expect(mocks.enqueueUploadSession).toHaveBeenCalledWith(claimed.id, 2);
});
it('媒体工具错误不把 TOS 签名 URL 写入会话或 BullMQ 失败原因', async () => {
const claimed = sessionRow({
status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
});
installFailedProcessingQueries(claimed, true);
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: 16, height: 16, durationSeconds: 1, hasVideo: true, hasAudio: false,
formatName: 'mov,mp4,m4a,3gp,3g2,mj2', frameCount: 24, videoCodec: 'h264', majorBrand: 'isom',
});
const signedUrl = 'https://tos.invalid/video.mp4?X-Tos-Signature=do-not-persist';
mocks.createUploadedVideoPreview.mockRejectedValue(Object.assign(
new Error(`ffmpeg failed for ${signedUrl}`),
{ code: 'UPLOAD_THUMBNAIL_FAILED' },
));
const failure = await processUploadSession(claimed.id).catch(error => error as Error & { code?: string });
expect(failure).toMatchObject({ code: 'UPLOAD_THUMBNAIL_FAILED', message: '缩略图生成失败,请重试' });
expect(JSON.stringify(failure)).not.toContain('X-Tos-Signature');
if (!(failure instanceof Error)) throw new Error('预期上传处理抛出安全错误');
expect(failure.message).not.toContain('tos.invalid');
const failedUpdate = mocks.poolQuery.mock.calls.find(([sql]) => String(sql).includes('SET status = CASE'));
expect(failedUpdate?.[1]?.[3]).toBe('缩略图生成失败,请重试');
expect(JSON.stringify(failedUpdate?.[1])).not.toContain('X-Tos-Signature');
expect(JSON.stringify(failedUpdate?.[1])).not.toContain('tos.invalid');
});
it('声明大小与 staging 实际大小不一致时永久失败,不复制也不自动重试', async () => {
const claimed = sessionRow({
status: 'processing', processing_stage: 'sealing', attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
});
mocks.poolQuery.mockImplementation(async (sql: unknown) => {
const query = String(sql);
if (query.includes("SET status = 'processing'")) return queryResult([claimed]);
if (query.includes('SELECT 1 FROM upload_sessions')) return queryResult([{ '?column?': 1 }]);
if (query.includes('SET status = CASE')) return queryResult([{ ...claimed, status: 'failed', processing_stage: 'failed', retryable: false }]);
return queryResult([]);
});
mocks.readStoredObjectSize.mockResolvedValueOnce(1000);
await expect(processUploadSession(claimed.id)).rejects.toMatchObject({ code: 'UPLOAD_SIZE_MISMATCH' });
expect(mocks.copyStoredObject).not.toHaveBeenCalled();
expect(mocks.enqueueUploadSession).not.toHaveBeenCalled();
});
});
function requestHash(value: typeof input) {
return createHash('sha256').update(JSON.stringify({
filename: value.filename,
contentType: value.contentType,
sizeBytes: value.sizeBytes,
source: value.source,
folderId: value.folderId,
})).digest('hex');
}
function sessionRow(patch: Record<string, unknown> = {}) {
return {
id: '00000000-0000-4000-8000-000000000001',
user_id: 'user-1',
object_key: 'users/user-1/upload-staging/00000000-0000-4000-8000-000000000001/clip.mp4',
filename: 'clip.mp4',
content_type: 'video/mp4',
max_size_bytes: 1024,
status: 'pending',
asset_id: null,
expires_at: '2099-01-01T00:00:00.000Z',
completed_at: null,
updated_at: '2026-07-16T10:00:00.000Z',
idempotency_key: input.idempotencyKey,
request_hash: requestHash(input),
source: 'uploaded-video',
asset_type: 'video',
asset_name: 'clip',
folder_id: null as string | null,
final_object_key: 'users/user-1/uploads/00000000-0000-4000-8000-000000000001/clip.mp4',
thumbnail_key: null,
actual_size_bytes: null,
sealed_at: null,
sealed_size_bytes: null,
retryable: false,
attempt: 0,
lease_token: null,
lease_expires_at: null,
heartbeat_at: null,
processing_stage: 'waiting-upload',
error_code: null,
error_message: null,
cancel_requested_at: null,
retention_until: '2099-01-02T00:00:00.000Z',
...patch,
};
}
function queryResult(rows: unknown[]) {
return { rowCount: rows.length, rows };
}
function audioProcessingSession(patch: Record<string, unknown> = {}) {
return sessionRow({
filename: 'music.mp3',
content_type: 'audio/mpeg',
source: 'uploaded-audio',
asset_type: 'audio',
asset_name: 'music',
status: 'processing',
processing_stage: 'sealing',
attempt: 1,
lease_token: '00000000-0000-4000-8000-000000000099',
lease_expires_at: '2099-01-01T00:00:00.000Z',
folder_id: '00000000-0000-4000-8000-000000000055',
final_object_key: 'users/user-1/uploads/00000000-0000-4000-8000-000000000001/music.mp3',
...patch,
});
}
function installSuccessfulAudioMetadata() {
mocks.readStoredObjectSize.mockResolvedValue(1024);
mocks.readUploadedMediaMetadata.mockResolvedValue({
width: null,
height: null,
durationSeconds: 1,
hasVideo: false,
hasAudio: true,
formatName: 'mp3',
frameCount: null,
videoCodec: null,
majorBrand: null,
});
}
function installSuccessfulAudioProcessingQueries(
claimed: ReturnType<typeof sessionRow>,
observedRows: Array<ReturnType<typeof sessionRow>>,
lockedRows: Array<ReturnType<typeof sessionRow>>,
) {
let observedIndex = 0;
let lockedIndex = 0;
mocks.poolQuery.mockImplementation(async (sql: unknown) => {
const query = String(sql);
if (query.includes("SET status = 'processing'")) return queryResult([claimed]);
if (query.includes('SELECT 1 FROM upload_sessions')) return queryResult([{ '?column?': 1 }]);
if (query.includes('SET sealed_at = COALESCE')) return queryResult([claimed]);
if (query.includes('SET processing_stage = $3')) return queryResult([claimed]);
if (query.includes('SELECT user_id, source, folder_id')) {
return queryResult([observedRows[Math.min(observedIndex++, observedRows.length - 1)]!]);
}
if (query.includes('WHERE id = $1 AND status = \'processing\'') && query.includes('FOR UPDATE')) {
return queryResult([lockedRows[Math.min(lockedIndex++, lockedRows.length - 1)]!]);
}
if (query.includes('INSERT INTO assets')) return queryResult([{ id: 'asset-1' }]);
if (query.includes("SET status = 'completed'")) return queryResult([{ ...claimed, status: 'completed' }]);
return queryResult([]);
});
}
function installSuccessfulProcessingQueries(claimed: ReturnType<typeof sessionRow>) {
const persistedThumbnailKey = claimed.asset_type === 'image'
? `thumb/${claimed.id}.webp`
: claimed.asset_type === 'video'
? `thumb/${claimed.id}.jpg`
: claimed.thumbnail_key;
mocks.poolQuery.mockImplementation(async (sql: unknown) => {
const query = String(sql);
if (query.includes("SET status = 'processing'")) return queryResult([claimed]);
if (query.includes('SELECT 1 FROM upload_sessions')) return queryResult([{ '?column?': 1 }]);
if (query.includes('SET sealed_at = COALESCE')) return queryResult([claimed]);
if (query.includes('SET processing_stage = $3')) return queryResult([claimed]);
if (query.includes("SET processing_stage = 'thumbnailing'")) return queryResult([claimed]);
if (query.includes('SET thumbnail_key = $3')) return queryResult([{ ...claimed, thumbnail_key: persistedThumbnailKey }]);
if (query.includes('SELECT user_id, source, folder_id')) return queryResult([claimed]);
if (query.includes('WHERE id = $1 AND status = \'processing\'') && query.includes('FOR UPDATE')) {
return queryResult([{
...claimed,
thumbnail_key: persistedThumbnailKey,
sealed_at: '2026-07-16T10:00:00.000Z',
sealed_size_bytes: 1024,
}]);
}
if (query.includes('INSERT INTO assets')) return queryResult([{ id: 'asset-1' }]);
if (query.includes("SET status = 'completed'")) return queryResult([{ ...claimed, status: 'completed' }]);
return queryResult([]);
});
}
function installFailedProcessingQueries(claimed: ReturnType<typeof sessionRow>, retryAutomatically = false) {
mocks.poolQuery.mockImplementation(async (sql: unknown) => {
const query = String(sql);
if (query.includes("SET status = 'processing'")) return queryResult([claimed]);
if (query.includes('SELECT 1 FROM upload_sessions')) return queryResult([{ '?column?': 1 }]);
if (query.includes('SET sealed_at = COALESCE')) return queryResult([claimed]);
if (query.includes('SET processing_stage = $3') || query.includes("processing_stage = 'thumbnailing'")) {
return queryResult([claimed]);
}
if (query.includes('SET status = CASE')) {
return queryResult([{
...claimed,
status: retryAutomatically ? 'pending' : 'failed',
processing_stage: retryAutomatically ? 'queued' : 'failed',
retryable: false,
thumbnail_key: retryAutomatically ? `thumb/${claimed.id}.jpg` : claimed.thumbnail_key,
}]);
}
return queryResult([]);
});
}