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'))) })