Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
117 lines
4.0 KiB
TypeScript
117 lines
4.0 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import { mkdtemp, mkdir, readFile, rename, rm, writeFile } from 'fs/promises'
|
||
import os from 'os'
|
||
import path from 'path'
|
||
|
||
import {
|
||
recoverMaterialUploadConsumptionJournal,
|
||
removeMaterialUploadConsumptionJournal,
|
||
writeMaterialUploadConsumptionJournal,
|
||
} from '@/lib/material-upload-consumption-journal'
|
||
|
||
describe('普通素材上传消费 journal', () => {
|
||
let uploadRoot = ''
|
||
const previousUploadDir = process.env.UPLOAD_DIR
|
||
|
||
beforeEach(async () => {
|
||
uploadRoot = await mkdtemp(path.join(os.tmpdir(), 'material-upload-journal-'))
|
||
process.env.UPLOAD_DIR = uploadRoot
|
||
})
|
||
|
||
afterEach(async () => {
|
||
if (previousUploadDir === undefined) {
|
||
delete process.env.UPLOAD_DIR
|
||
} else {
|
||
process.env.UPLOAD_DIR = previousUploadDir
|
||
}
|
||
await rm(uploadRoot, { recursive: true, force: true })
|
||
})
|
||
|
||
test('rename 后模拟进程退出,重启读取 journal 能找回同一正式源', async () => {
|
||
const uploadId = 'upload-crash-recovery'
|
||
const sessionDirectory = path.join(uploadRoot, 'temp', uploadId)
|
||
const mergedPath = path.join(sessionDirectory, 'merged-source.mp4')
|
||
const finalPath = path.join(uploadRoot, 'video-crash-recovery.mp4')
|
||
await mkdir(sessionDirectory, { recursive: true })
|
||
await writeFile(mergedPath, Buffer.from('video-source'))
|
||
|
||
const record = await writeMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
mergedPath,
|
||
finalPath,
|
||
storedFinalPath: finalPath,
|
||
fileName: 'video-crash-recovery.mp4',
|
||
size: Buffer.byteLength('video-source'),
|
||
})
|
||
// 故障注入点:rename 已成功,但还没有任何 Material 数据库记录。
|
||
await rename(mergedPath, finalPath)
|
||
|
||
const recovered = await recoverMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
})
|
||
|
||
expect(recovered).toEqual({
|
||
record,
|
||
sourcePath: finalPath,
|
||
sourceLocation: 'final',
|
||
})
|
||
expect(await readFile(recovered!.sourcePath, 'utf8')).toBe('video-source')
|
||
})
|
||
|
||
test('owner 不匹配时不能发现或消费其他用户的恢复记录', async () => {
|
||
const uploadId = 'upload-owner-scope'
|
||
const sessionDirectory = path.join(uploadRoot, 'temp', uploadId)
|
||
const mergedPath = path.join(sessionDirectory, 'merged-source.zip')
|
||
const finalPath = path.join(uploadRoot, 'material-owner-scope.zip')
|
||
await mkdir(sessionDirectory, { recursive: true })
|
||
await writeFile(mergedPath, Buffer.from('archive'))
|
||
await writeMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
mergedPath,
|
||
finalPath,
|
||
storedFinalPath: finalPath,
|
||
fileName: 'material-owner-scope.zip',
|
||
size: Buffer.byteLength('archive'),
|
||
})
|
||
|
||
await expect(recoverMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-2',
|
||
})).resolves.toBeNull()
|
||
})
|
||
|
||
test('正式源大小变化时拒绝恢复并保留 journal 供人工核验', async () => {
|
||
const uploadId = 'upload-size-mismatch'
|
||
const sessionDirectory = path.join(uploadRoot, 'temp', uploadId)
|
||
const mergedPath = path.join(sessionDirectory, 'merged-source.zip')
|
||
const finalPath = path.join(uploadRoot, 'material-size-mismatch.zip')
|
||
await mkdir(sessionDirectory, { recursive: true })
|
||
await writeFile(mergedPath, Buffer.from('archive'))
|
||
await writeMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
mergedPath,
|
||
finalPath,
|
||
storedFinalPath: finalPath,
|
||
fileName: 'material-size-mismatch.zip',
|
||
size: Buffer.byteLength('archive'),
|
||
})
|
||
await rename(mergedPath, finalPath)
|
||
await writeFile(finalPath, Buffer.from('changed-size'))
|
||
|
||
await expect(recoverMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
})).rejects.toThrow('上传源文件恢复记录异常')
|
||
await expect(removeMaterialUploadConsumptionJournal({
|
||
uploadId,
|
||
ownerId: 'user-1',
|
||
expectedFinalPath: finalPath,
|
||
})).resolves.toBe(true)
|
||
})
|
||
})
|