| 12345678910111213141516171819202122232425262728293031323334353637383940 |
- import{test,before,after}from'node:test';import assert from'node:assert/strict';import{readFileSync}from'node:fs';import vm from'node:vm';import{PGlite}from'@electric-sql/pglite';
- const db=new PGlite(),read=p=>readFileSync(new URL(p,import.meta.url),'utf8'),q=async(s,p=[])=>(await db.query(s,p)).rows,one=async(s,p=[])=>(await q(s,p))[0],source=read('../deploy-admin-functions.mjs'),c=vm.createContext({Psql:{one,oneOrNone:one,query:q,tx:fn=>db.transaction(t=>fn({one:async(s,p=[])=>(await t.query(s,p)).rows[0]}))}}),teacher={id:'teacher',get:k=>({company:{id:'co'},legacyUserId:30,legacyGroupId:3}[k])};
- let version;before(async()=>{
- await db.exec(`CREATE TABLE "_User"("objectId" text primary key,company text,"legacyUserId" numeric,"legacyGroupId" numeric,"legacyUserData" jsonb,"isDisabled" boolean);
- CREATE TABLE "CommonModel"("objectId" text primary key,company text,"modelId" numeric,"itemId" numeric,"generalId" numeric,status numeric,"nodeId" numeric,title text,subtitle text,"teachingManaged" boolean,"updatedAt" timestamptz);
- CREATE TABLE "CourseAppointment"("objectId" text primary key,company text,id numeric,szyh text,pl text,kcid text,yykcid text,dszt text,bxrq text,yysj text,kssj text,"bindingId" text,"teachingManaged" boolean,"updatedAt" timestamptz,"creditHoldStatus" text,"creditCostsSnapshot" jsonb,"teacherPaySnapshot" numeric);
- CREATE TABLE "CourseBinding"("objectId" text primary key,company text,id numeric,yhid text,kcid text,"categoryKey" text,"categoryName" text,"enrollmentStatus" text,syjd text);
- CREATE TABLE "Node"(company text,"nodeId" numeric,"parentId" numeric,"nodeName" text,zstatus numeric);
- CREATE TABLE "CourseCreditReservation"(company text,"studentId" numeric,"appointmentId" text,state text,"creditCosts" jsonb);
- INSERT INTO "_User" VALUES('teacher','co',30,3,'{}',false),('student','co',20,1,'{"Purse":10}',false),('other-teacher','co',31,3,'{}',false);
- INSERT INTO "Node" VALUES('co',100,8,'原词表',99),('co',101,100,'Unit 1',99),('co',102,8,'新词表',99),('co',103,102,'Unit 2',99),('co',104,8,'停用',-2);
- INSERT INTO "CommonModel"("objectId",company,"modelId","generalId","nodeId",status) VALUES('w1','co',52,1001,101,99),('w2','co',52,1002,103,99);
- INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId",status) SELECT 'cb'||i,'co',58,i,5000+i,99 FROM generate_series(1,6)i;
- INSERT INTO "CourseBinding" VALUES('b1','co',1,'20','100','word','单词课','active','{}'),('b2','co',2,'20','102','trial','体验课','active','{}'),('pending','co',3,'20','100','word','单词课','pending','{}'),('recycled','co',4,'20','100','word','单词课','recycled','{}'),('foreign','co',5,'21','100','word','单词课','active','{}'),('disabled','co',6,'20','104','word','单词课','active','{}');
- INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId",status) VALUES('appointment','co',54,1,901,99);
- INSERT INTO "CourseAppointment" VALUES('addon','co',1,'20','30','100','100','0',to_char(now() AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD'),'','', 'b1',false,now(),'ready','[{"account":"Purse","amount":1}]',37);
- INSERT INTO "CourseCreditReservation" VALUES('co',20,'appointment','reserved','[{"account":"Purse","amount":1}]');`);
- const enrollment=read('../sql/member-enrollment.sql');for(const name of ['xs_appointment_day','xs_appointment_valid_at','xs_reserved','xs_course_size','xs_validate_appointment','xs_start_appointment'])await db.exec(enrollment.match(new RegExp('CREATE OR REPLACE FUNCTION '+name+'\\([\\s\\S]*?\\$\\$;'))[0]);
- await db.exec(read('../sql/lesson-workflow.sql').match(/CREATE OR REPLACE FUNCTION xs_start_appointment_for_actor\([\s\S]*?\$\$;/)[0]);
- const app=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1].replace("const DEFAULT_COMPANY_ID = '7pIbDBJmKx'","const DEFAULT_COMPANY_ID = 'co'");vm.runInContext(app+read('../cloud/lesson-workflow.js')+read('../cloud/lesson-courses.js'),c);version=(await one('SELECT "updatedAt"::text v FROM "CourseAppointment"')).v;
- });after(()=>db.close());
- test('教师按预约所属学员获取所有有效开课,包括不同课程分类,排除待确认、回收、停用和他人课程',async()=>{const r=await c.appLessonCourses({orderId:901,studentId:21},teacher);assert.deepEqual(Array.from(r.items,x=>x.bindingId).sort(),['b1','b2']);assert.equal(r.scheduledBindingId,'b1');for(const u of [{id:'other-teacher',get:k=>({company:{id:'co'},legacyUserId:31,legacyGroupId:3}[k])},{id:'student',get:k=>({company:{id:'co'},legacyUserId:20,legacyGroupId:1}[k])}])await assert.rejects(c.appLessonCourses({orderId:901},u),/仅预约老师/);});
- test('开始时验证实际选择的课程但保留排课课程、工资和扣减快照;旧版本和停用课程拒绝',async()=>{await assert.rejects(c.startLessonWithContent(teacher,'appointment','b2','2000-01-01'),/已更新/);await assert.rejects(c.startLessonWithContent(teacher,'appointment','pending',version));const before=await one('SELECT * FROM "CourseAppointment"');assert.equal((await c.startLessonWithContent(teacher,'appointment','b2',version)).bindingId,'b2');const after=await one('SELECT * FROM "CourseAppointment"');assert.equal(after.dszt,'10');for(const k of ['kcid','yykcid','bindingId','teacherPaySnapshot','creditCostsSnapshot'])assert.deepEqual(after[k],before[k]);});
- test('学习记录接受本学员所选开课而非强制排课开课;拒绝他人开课和非所选词库的单词',async()=>{const order={szyh:20,pl:30,dszt:10,bindingId:'b1'};assert.equal((await c.lessonStudyBinding({bindingId:'b2'},teacher,20,order,[{GeneralID:1002}])).objectId,'b2');await assert.rejects(c.lessonStudyBinding({bindingId:'foreign'},teacher,20,order),/未开通/);await assert.rejects(c.lessonStudyBinding({bindingId:'b2'},teacher,20,order,[{GeneralID:1001}]),/不匹配/);await assert.rejects(c.lessonStudyBinding({bindingId:'b2'},teacher,20,{...order,dszt:30}),/进行中/);});
- test('实际学习写入事务按所选开课隔离记录,重试和更新保留业务ID且不覆盖另一词库',async()=>{
- await db.exec(`CREATE TABLE "DailyStudyRecord"("objectId" text primary key,company text,id numeric,"sourceKey" text,"createdAt" timestamptz,"updatedAt" timestamptz,"userId" numeric,dsid text,pl text,"bindingId" text,xxqs text,learned numeric,ygg numeric,djq numeric,con text,dqrq text,szmdid text,fxrl text);
- ALTER TABLE "CommonModel" ADD "createdAt" timestamptz,ADD "sourceKey" text,ADD "tableName" text,ADD inputer text,ADD hits numeric;`);
- await db.exec(read('../sql/mobile-teaching.sql'));
- class Obj{constructor(name,data={}){this.className=name;this.data=data;this.id=data.objectId;}set(k,v){this.data[k]=v;}get(k){return this.data[k];}toJSON(){return{...this.data,company:this.data.company?.id?{__type:'Pointer',className:'Company',objectId:this.data.company.id}:this.data.company};}}
- c.Parse={Object:Obj,Query:class{constructor(name){this.name=name;}async get(id){return new Obj(this.name,await one('SELECT * FROM "'+this.name+'" WHERE "objectId"=$1',[id]));}}};
- vm.runInContext(read('../cloud/mobile-teaching.js')+';authorizeRequestedUser=async(current,uid)=>Number(uid);',c);
- const run=(bind,word,key,check=0)=>c.upsertStudyRecord({uid:20,orderId:901,requestKey:key,addon:JSON.stringify({bindingId:bind,xxqs:[{GeneralID:word,Title:'word',check}]})},teacher);
- const first=await run('b1',1001,'study-first-0001');await run('b2',1002,'study-second-0001');
- const updated=await run('b1',1001,'study-first-0002',1);assert.equal(first.GeneralID,updated.GeneralID);
- await run('b1',1001,'study-first-0002',1);
- const rows=await q('SELECT "bindingId",ygg,id FROM "DailyStudyRecord" ORDER BY "bindingId"');assert.equal(rows.length,2);assert.equal(Number(rows[0].ygg),1);assert.equal(Number(rows[1].ygg),0);
- assert.equal((await one('SELECT count(*)::int n FROM "CommonModel" WHERE "modelId"=56')).n,2);
- assert.equal((await one('SELECT "teacherPaySnapshot" FROM "CourseAppointment"')).teacherPaySnapshot,'37');
- });
|