gongxiangp2/lib/__tests__/material-upload-request-guard.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

134 lines
4.0 KiB
TypeScript

/** @jest-environment node */
import {
MATERIAL_UPLOAD_REQUEST_LIMITS,
MaterialUploadRequestError,
MaterialUploadRequestLimiter,
validateMaterialUploadFormData,
validateMaterialUploadRequestHeaders,
} from '@/lib/material-upload-request-guard'
type FakeFile = {
name: string
size: number
arrayBuffer: () => Promise<ArrayBuffer>
}
function fakeFile(name: string, size: number): FakeFile {
return {
name,
size,
arrayBuffer: jest.fn(async () => new ArrayBuffer(0)),
}
}
function fakeFormData(entries: Array<[string, string | FakeFile]>): FormData {
return {
entries: () => entries[Symbol.iterator](),
get: (field: string) => entries.find(([key]) => key === field)?.[1] ?? null,
} as unknown as FormData
}
function expectGuardError(action: () => unknown, status: number, code: string) {
try {
action()
throw new Error('expected guard error')
} catch (error) {
expect(error).toBeInstanceOf(MaterialUploadRequestError)
expect(error).toMatchObject({ status, code })
}
}
describe('material upload request guard', () => {
test('requires a bounded multipart Content-Length before parsing', () => {
expectGuardError(
() => validateMaterialUploadRequestHeaders(new Headers({
'content-type': 'multipart/form-data; boundary=test',
})),
411,
'CONTENT_LENGTH_REQUIRED',
)
expect(validateMaterialUploadRequestHeaders(new Headers({
'content-length': String(MATERIAL_UPLOAD_REQUEST_LIMITS.maxRequestBytes),
'content-type': 'multipart/form-data; boundary=test',
}))).toBe(MATERIAL_UPLOAD_REQUEST_LIMITS.maxRequestBytes)
expectGuardError(
() => validateMaterialUploadRequestHeaders(new Headers({
'content-length': String(MATERIAL_UPLOAD_REQUEST_LIMITS.maxRequestBytes + 1),
'content-type': 'multipart/form-data; boundary=test',
})),
413,
'UPLOAD_REQUEST_TOO_LARGE',
)
})
test('forces a 5MB direct source file onto the chunk upload path', () => {
const source = fakeFile('source.zip', 5 * 1024 * 1024)
expectGuardError(
() => validateMaterialUploadFormData(fakeFormData([['zipFile', source]])),
413,
'CHUNK_UPLOAD_REQUIRED',
)
expect(source.arrayBuffer).not.toHaveBeenCalled()
})
test('rejects excessive file counts and image field limits before decoding', () => {
const tooMany = Array.from({ length: 51 }, (_, index) => (
['modelImages', fakeFile(`${index}.jpg`, 1)] as [string, FakeFile]
))
expectGuardError(
() => validateMaterialUploadFormData(fakeFormData(tooMany)),
413,
'TOO_MANY_UPLOAD_FILES',
)
expectGuardError(
() => validateMaterialUploadFormData(fakeFormData([
['previewImage', fakeFile('cover.png', 8 * 1024 * 1024 + 1)],
])),
413,
'UPLOAD_FILE_TOO_LARGE',
)
expectGuardError(
() => validateMaterialUploadFormData(fakeFormData([
['modelImages', fakeFile('gallery.jpg', 16 * 1024 * 1024 + 1)],
])),
413,
'UPLOAD_FILE_TOO_LARGE',
)
})
test('allows an existing merged source with bounded image metadata request', () => {
expect(() => validateMaterialUploadFormData(fakeFormData([
['existingFilePath', 'uploads/temp/upload-1/merged.mp4'],
['previewImage', fakeFile('cover.jpg', 1024)],
]))).not.toThrow()
})
})
describe('material upload request limiter', () => {
test('hands the only slot to one waiter and releases it on every completion', async () => {
const limiter = new MaterialUploadRequestLimiter(1, 1)
const releaseFirst = await limiter.acquire()
const secondAcquire = limiter.acquire()
expect(limiter.snapshot()).toEqual({ active: 1, waiting: 1 })
await expect(limiter.acquire()).rejects.toMatchObject({
status: 503,
code: 'UPLOAD_BUSY',
retryAfterSeconds: 3,
})
releaseFirst()
const releaseSecond = await secondAcquire
expect(limiter.snapshot()).toEqual({ active: 1, waiting: 0 })
releaseSecond()
releaseSecond()
expect(limiter.snapshot()).toEqual({ active: 0, waiting: 0 })
})
})