Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
76 lines
3.3 KiB
TypeScript
76 lines
3.3 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
import { readFile } from 'fs/promises'
|
|
import path from 'path'
|
|
|
|
const guardedRoutes = [
|
|
'app/api/upload/s3-original/route.ts',
|
|
'app/api/upload/s3/route.ts',
|
|
'app/api/upload/preview/route.ts',
|
|
'app/api/materials/[id]/skus/upload/route.ts',
|
|
'app/api/user/avatar/route.ts',
|
|
'app/api/materials/[id]/route.ts',
|
|
'app/api/materials/update/route.ts',
|
|
'app/api/system-settings/upload-logo/route.ts',
|
|
] as const
|
|
|
|
const preBoundedLegacyRoutes = [
|
|
'app/api/upload/psd/route.ts',
|
|
'app/api/materials/[id]/white-background/route.ts',
|
|
'app/api/external/standard-model-white-background/by-barcode/route.ts',
|
|
'app/api/external/standard-model-white-background/by-spu/route.ts',
|
|
] as const
|
|
|
|
describe('multipart routes are bounded before parsing', () => {
|
|
test.each(guardedRoutes)('%s checks headers and acquires a slot before formData', async (routePath) => {
|
|
const source = await readFile(path.join(process.cwd(), routePath), 'utf8')
|
|
const headerGuard = source.indexOf('assertBoundedMultipartHeaders(')
|
|
const acquireSlot = source.indexOf('boundedMultipartRequestLimiter.acquire()')
|
|
const parseBody = source.indexOf('request.formData()')
|
|
const parsedFiles = source.indexOf('assertMultipartFiles(', parseBody)
|
|
|
|
expect(headerGuard).toBeGreaterThan(0)
|
|
expect(acquireSlot).toBeGreaterThan(headerGuard)
|
|
expect(parseBody).toBeGreaterThan(acquireSlot)
|
|
expect(parsedFiles).toBeGreaterThan(parseBody)
|
|
expect(source).toContain('releaseRequestSlot?.()')
|
|
})
|
|
|
|
test.each(preBoundedLegacyRoutes)('%s acquires a slot after its existing size check', async (routePath) => {
|
|
const source = await readFile(path.join(process.cwd(), routePath), 'utf8')
|
|
const headerGuard = source.indexOf("headers.get('content-length')")
|
|
const acquireSlot = source.indexOf('boundedMultipartRequestLimiter.acquire()')
|
|
const parseBody = source.indexOf('request.formData()')
|
|
|
|
expect(headerGuard).toBeGreaterThan(0)
|
|
expect(acquireSlot).toBeGreaterThan(headerGuard)
|
|
expect(parseBody).toBeGreaterThan(acquireSlot)
|
|
expect(source).toContain('releaseRequestSlot?.()')
|
|
})
|
|
|
|
test('stable proxy uses a small fallback and explicit upload route ceilings', async () => {
|
|
const source = (await readFile(
|
|
path.join(process.cwd(), 'scripts/git-incremental-deploy.ps1'),
|
|
'utf8',
|
|
)).replace(/\r\n/g, '\n')
|
|
|
|
expect(source).toContain('client_max_body_size 16m;')
|
|
expect(source).not.toContain('client_max_body_size 1000m;')
|
|
|
|
const requiredFragments = [
|
|
'location = /api/upload/preview {\n client_max_body_size 53m;',
|
|
'location ~ "^/api/upload/s3(?:-original)?\\$" {\n client_max_body_size 103m;',
|
|
'location = /api/materials/update {\n client_max_body_size 11m;',
|
|
'location ~ "^/api/(?:user/avatar|system-settings/upload-logo)\\$" {\n client_max_body_size 7m;',
|
|
'/skus/upload)\\$" {\n client_max_body_size 131m;',
|
|
'/white-background|api/external/standard-model-white-background/(?:by-spu|by-barcode))\\$" {\n client_max_body_size 106m;',
|
|
]
|
|
for (const fragment of requiredFragments) {
|
|
expect(source).toContain(fragment)
|
|
}
|
|
|
|
const boundedLocations = source.match(/client_max_body_size (?:7|11|53|103|106|131)m;[\s\S]{0,180}?proxy_request_buffering off;/g) || []
|
|
expect(boundedLocations.length).toBeGreaterThanOrEqual(7)
|
|
})
|
|
})
|