import {test} from 'node:test'; import assert from 'node:assert/strict'; import vm from 'node:vm'; import {readFileSync} from 'node:fs'; const read=path=>readFileSync(new URL(path,import.meta.url),'utf8'); const gateway=read('../deploy-admin-functions.mjs').match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1]; const feedback={contentSummary:'阅读短文',trainingSummary:'写作练习',noHomework:true}; const teacher={id:'teacher',get:key=>({company:{id:'7pIbDBJmKx'},legacyUserId:30,legacyGroupId:3}[key])}; const pdf={fileName:'知识点.pdf',contentType:'application/pdf',base64:Buffer.from('%PDF-1.4\nnotes').toString('base64'),orderId:'901'}; function setup(){ let order={GeneralID:901,pl:30,courseCategoryKey:'primary_writing'},actions=['editFeedback'],uploads=0; const receipts=new Map(),requests=[]; const c=vm.createContext({atob,Blob,FormData,AbortSignal,fetch:async(url,request)=>{ uploads++;assert.equal(request.body.get('file').type,requests.at(-1)?.contentType||pdf.contentType); return {ok:true,text:async()=>JSON.stringify({retcode:1,result:'/UploadFiles/notes.pdf'})}; }}); vm.runInContext(gateway+'\n'+read('../cloud/lesson-workflow.js')+'\n'+read('../cloud/lesson-attachments.js'),c); c.orderDetailRow=async()=>order;c.lessonOrderCapabilities=async()=>({allowedActions:actions});c.appLegacyConfig=async()=>({url:'https://media.example/api',apiId:'test',apiKey:'test'}); c.randomObjectId=()=>String(receipts.size+1).padStart(10,'0'); c.Psql={none:async(sql,args)=>{const[id,company,appointment,coach,name,url,contentType,size]=args;receipts.set(id,{id,company,appointment,coach,name,url,contentType,size});requests.push({contentType});}, query:async(sql,[company,appointment,coach,ids])=>ids.map(id=>receipts.get(id)).filter(row=>row&&row.company===company&&row.appointment===appointment&&row.coach===coach).map(({id,name,url,contentType,size})=>({id,name,url,contentType,size}))}; return {c,receipts,requests,setOrder:value=>order={...order,...value},setActions:value=>actions=value,uploads:()=>uploads}; } test('本节语法写作老师上传常用文档和照片,服务端保存原始中文名及课次归属',async()=>{ const {c,receipts,setOrder}=setup(); for(const category of ['primary_writing','middle_writing','high_writing']){ setOrder({courseCategoryKey:category});const file=await c.uploadLessonAttachment(pdf,teacher); assert.equal(file.name,'知识点.pdf');assert.equal(file.contentType,'application/pdf');assert.equal(file.url,'https://media.example/UploadFiles/notes.pdf');assert.equal(receipts.get(file.id).appointment,'901');assert.equal(receipts.get(file.id).coach,30); } const png={...pdf,fileName:'笔记.png',contentType:'image/png',base64:Buffer.from([137,80,78,71,13,10,26,10]).toString('base64')}; // Verify transport type independently of the previous document upload. c.fetch=async(_url,request)=>{assert.equal(request.body.get('file').type,'image/png');return{ok:true,text:async()=>JSON.stringify({retcode:1,result:'https://media.example/note.png'})};}; assert.equal((await c.uploadLessonAttachment(png,teacher)).name,'笔记.png'); }); test('学生、其他老师、外帐套、非写作课和已锁定反馈不能上传,拦截发生在媒体请求之前',async()=>{ const {c,setOrder,setActions,uploads}=setup(); for(const user of [{get:key=>({company:{id:'7pIbDBJmKx'},legacyUserId:20,legacyGroupId:1}[key])},{get:key=>({company:{id:'7pIbDBJmKx'},legacyUserId:31,legacyGroupId:3}[key])},{get:key=>key==='company'?{id:'foreign'}:null}])await assert.rejects(c.uploadLessonAttachment(pdf,user),/仅本节课|无权/); setOrder({courseCategoryKey:'word'});await assert.rejects(c.uploadLessonAttachment(pdf,teacher),/语法写作课/); setOrder({courseCategoryKey:'primary_writing'});setActions([]);await assert.rejects(c.uploadLessonAttachment(pdf,teacher),/不可编辑/);assert.equal(uploads(),0); }); test('拒绝伪造文件类型、超限、无效编码及不支持的文件,原通用上传仍仅允许图片',async()=>{ const {c,uploads}=setup(); for(const patch of [{fileName:'test.exe'},{base64:Buffer.from('not a PDF').toString('base64')},{base64:'!!!'},{base64:Buffer.alloc(5*1024*1024+1).toString('base64')},{fileName:'../note.pdf'}])await assert.rejects(c.uploadLessonAttachment({...pdf,...patch},teacher),/请选择|内容|编码/); await assert.rejects(c.uploadAppMedia(pdf),/文件类型/);assert.equal(uploads(),0); }); test('反馈只接受本节课的已上传附件编号,客户端伪造的名称和地址会被覆盖',async()=>{ const {c,setOrder}=setup();const file=await c.uploadLessonAttachment(pdf,teacher); const parsed=c.lessonFeedbackInput({feedback:JSON.stringify({...feedback,attachments:[{id:file.id,name:'伪造',url:'javascript:alert(1)'}]})}); await c.resolveLessonFeedbackAttachments({orderId:'901'},teacher,parsed);assert.equal(parsed.attachments[0].name,'知识点.pdf');assert.equal(parsed.attachments[0].url,file.url); setOrder({GeneralID:902});await assert.rejects(c.resolveLessonFeedbackAttachments({orderId:'902'},teacher,{attachments:[{id:file.id}]}),/不属于本节课/); for(const attachments of [[{id:''}],Array.from({length:11},(_,i)=>({id:String(i)})),[{id:'same'},{id:'same'}],'invalid'])assert.throws(()=>c.lessonFeedbackInput({feedback:JSON.stringify({...feedback,attachments})}),/附件/); }); test('附件归属检查不会拦截成功提交后学生评价触发的幂等重试,锁定由SQL事务判断',async()=>{ const {c,setActions}=setup(),file=await c.uploadLessonAttachment(pdf,teacher); setActions([]); let commands=0;c.Psql.one=async(sql,args)=>{commands++;assert.match(sql,/xs_lesson_feedback_update/);assert.equal(JSON.parse(args[6]).attachments[0].id,file.id);return{result:{saved:true}};}; const result=await c.appLessonFeedbackUpdate({orderId:'901',requestKey:'attachment-retry-001',expectedUpdatedAt:'2026-10-29T00:00:00Z',feedback:JSON.stringify({...feedback,attachments:[file]})},teacher); assert.equal(result.saved,true);assert.equal(commands,1); });