/** @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 } 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 }) }) })