gongxiangp2/lib/__tests__/material-upload-temp-cleanup-redis.test.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

98 lines
2.9 KiB
TypeScript

/** @jest-environment node */
import { mkdir, mkdtemp, rm, stat, utimes, writeFile } from 'fs/promises'
import os from 'os'
import path from 'path'
const mockRedisGet = jest.fn()
const mockRedisDel = jest.fn()
jest.mock('@/lib/upload-sessions-redis', () => ({
redis: {
get: mockRedisGet,
del: mockRedisDel,
},
}))
import {
runCleanup,
safelyCleanupMaterialUploadSession,
} from '@/lib/temp-cleanup'
import { readUploadSessionSeal } from '@/lib/material-upload-session-seal'
describe('scheduled material upload cleanup Redis fail-closed', () => {
let uploadRoot: string
beforeEach(async () => {
jest.clearAllMocks()
uploadRoot = await mkdtemp(path.join(os.tmpdir(), 'material-cleanup-redis-'))
process.env.UPLOAD_DIR = uploadRoot
process.env.USE_REDIS = 'true'
mockRedisGet.mockRejectedValue(new Error('redis unavailable'))
const uploadId = 'expired-redis-unknown'
const tempDir = path.join(uploadRoot, 'temp', uploadId)
const oldDate = new Date(Date.now() - 10 * 60 * 60 * 1000)
await mkdir(tempDir, { recursive: true })
await writeFile(path.join(tempDir, 'meta.json'), JSON.stringify({
fileName: 'source.bin',
fileSize: 4,
totalChunks: 1,
chunkSize: 4,
userId: 'user-1',
createdAt: oldDate.getTime(),
}))
await writeFile(path.join(tempDir, 'chunk-0'), 'data')
await utimes(tempDir, oldDate, oldDate)
})
afterEach(async () => {
delete process.env.UPLOAD_DIR
delete process.env.USE_REDIS
await rm(uploadRoot, { recursive: true, force: true })
})
test('Redis read failure skips the expired directory without deleting state', async () => {
const result = await runCleanup()
const tempDir = path.join(uploadRoot, 'temp', 'expired-redis-unknown')
expect(result).toMatchObject({
cleaned: 0,
skippedUnavailable: 1,
})
await expect(stat(tempDir)).resolves.toMatchObject({})
expect(mockRedisDel).not.toHaveBeenCalled()
})
test('Redis delete failure leaves directory behind after durable cancellation tombstone', async () => {
mockRedisGet.mockResolvedValue(JSON.stringify({
fileName: 'source.bin',
fileSize: 4,
totalChunks: 1,
chunkSize: 4,
chunks: [],
createdAt: Date.now() - 10 * 60 * 60 * 1000,
userId: 'user-1',
}))
mockRedisDel.mockRejectedValue(new Error('redis delete unavailable'))
const result = await safelyCleanupMaterialUploadSession({
uploadId: 'expired-redis-unknown',
intent: 'cancel',
expectedOwnerId: 'user-1',
})
const tempDir = path.join(uploadRoot, 'temp', 'expired-redis-unknown')
expect(result).toMatchObject({
success: false,
status: 'unavailable',
code: 'UPLOAD_CLEANUP_REDIS_UNAVAILABLE',
httpStatus: 503,
})
await expect(stat(tempDir)).resolves.toMatchObject({})
await expect(readUploadSessionSeal('expired-redis-unknown')).resolves.toMatchObject({
state: 'cancelled',
})
})
})