| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 |
- 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);
- });
|