gongxiangp2/scripts/material-image-hotfix.test.mjs

90 lines
4.9 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import vm from 'node:vm'
import { createRequire } from 'node:module'
import { patchCompiled, walk } from './material-image-hotfix.mjs'
const { parse } = createRequire(import.meta.url)('next/dist/compiled/acorn')
const errors = ['素材尚未完成上传,暂不能编辑图片', '素材尚未完成上传,暂不能修改图片']
const fixture = reverse => errors.map((message, index) =>
'async function guard' + index + '(prisma){const r=await prisma.material.findFirst({select:{taskStatus:true,materialType:true}});'
+ 'if(' + (reverse ? '"completed"!==r.taskStatus' : 'r.taskStatus!=="completed"')
+ ')throw new Error("' + message + '");return r;}').join('\n')
for (const reversed of [false, true]) {
test('both compiled gates preserve an explicit allowlist and incomplete shell gate: reversed=' + reversed, async () => {
const patched = patchCompiled('chunks/29.js', fixture(reversed))
assert.deepEqual(patched.counts, { guards: 2, selectors: 2, getters: 0, fetches: 0 })
const context = vm.createContext({})
vm.runInContext(patched.output, context)
for (const status of ['completed', 'fetched', 'pending', 'processing', 'failed', null, 'unknown']) {
for (const isIncompleteShell of [false, true]) {
for (const index of [0, 1]) {
const calls = []
const prisma = { material: { findFirst: async options => {
calls.push(options)
return { taskStatus: status, isIncompleteShell }
} } }
const action = context['guard' + index](prisma)
if (!isIncompleteShell && ['completed', 'fetched'].includes(status)) {
assert.equal((await action).taskStatus, status)
} else await assert.rejects(action, /素材尚未完成上传/)
assert.equal(calls[0].select.isIncompleteShell, true)
}
}
}
assert.throws(() => patchCompiled('chunks/29.js', patched.output))
})
}
test('missing or changed guard, material selector and duplicate guard fail closed', () => {
const valid = fixture(false)
for (const broken of [
valid.replace(errors[0], 'other error'),
valid.replace('taskStatus:true', 'taskStatus:false'),
valid.replace('material.findFirst', 'material.findUnique'),
valid.replace('r.taskStatus!=="completed"', 'r.taskStatus==="completed"'),
valid + valid.replaceAll('guard', 'duplicate'),
]) assert.throws(() => patchCompiled('chunks/29.js', broken))
})
const route = 'const exports={};(function(){function d(a,b){for(const k of Object.keys(b))Object.defineProperty(a,k,{get:b[k]});};'
+ 'd(exports,{GET:()=>handler});async function handler(request){if(request.fail)throw Error("original");'
+ 'if(request.denied)return request.denied;return request.response;}'
+ 'async function z(url,range){return fetch(url,{headers:range?{Range:range}:void 0});}'
+ 'async function unrelated(url,range){return fetch(url,{headers:range?{Range:range}:void 0});}})();'
test('GET wrapper preserves response identity, body, early return and original failures', async () => {
const result = patchCompiled('app/api/external/product-assets/route.js', route)
const context = vm.createContext({})
vm.runInContext(result.output, context)
const get = vm.runInContext('exports.GET', context)
for (const denied of [false, true]) {
const headers = new Map(), response = { headers, body: 'unchanged original bytes' }
assert.equal(await get(denied ? { denied: response } : { response }), response)
assert.equal(headers.get('Cache-Control'), 'private, no-store')
assert.equal(response.body, 'unchanged original bytes')
}
await assert.rejects(get({ fail: true }), /original/)
})
test('only the pinned original single-file fetch gains no-store; Range and other helpers are unchanged', () => {
const result = patchCompiled('app/api/external/download/route.js', route)
assert.deepEqual(result.counts, { guards: 0, selectors: 0, getters: 1, fetches: 1 })
const calls = []
walk(parse(result.output, { ecmaVersion: 'latest' }), (node, ancestors) => {
if (node.type === 'CallExpression' && node.callee.name === 'fetch') {
calls.push({ owner: ancestors.findLast(item => item.type === 'FunctionDeclaration').id.name,
keys: node.arguments[1].properties.map(item => item.key.name) })
}
})
assert.deepEqual(calls, [{ owner: 'z', keys: ['cache', 'headers'] }, { owner: 'unrelated', keys: ['headers'] }])
assert.throws(() => patchCompiled('app/api/external/download/route.js', route.replace('function z(', 'function changed(')))
assert.throws(() => patchCompiled('app/api/external/download/route.js', result.output))
})
test('unknown files and ambiguous GET exports are never patched', () => {
assert.throws(() => patchCompiled('app/other.js', route))
assert.throws(() => patchCompiled('app/api/external/product-assets/route.js', route + route.replaceAll('exports', 'otherExports')))
})