support-requests.test.mjs 6.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import {test,before,after} from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import {readFileSync} from 'node:fs';
  4. import vm from 'node:vm';
  5. import {PGlite} from '@electric-sql/pglite';
  6. const db=new PGlite(), query=async(sql,params=[])=>(await db.query(sql,params)).rows;
  7. const ctx={Psql:{one:async(...args)=>(await query(...args))[0]},text:v=>String(v??'').trim(),
  8. number:(v,d=0)=>Number.isFinite(Number(v))?Number(v):d,companyIdOf:c=>c.company,
  9. requireRole:(c,roles)=>{if(!roles.includes(c.role))throw Error('forbidden');},fail:(_,m)=>{throw Error(m);}};
  10. vm.createContext(ctx);vm.runInContext(readFileSync(new URL('../cloud/support-requests.js',import.meta.url),'utf8'),ctx);
  11. const admin={role:'ops-staff',company:'a'};
  12. before(async()=>{
  13. await db.exec(`CREATE TABLE "Guestbook"("objectId" text,company text,userid numeric,title text,tcontent text,gdate timestamptz,"createdAt" timestamptz,parentid numeric,status numeric,cateid numeric,"sourceKey" text);
  14. CREATE TABLE "_User"("objectId" text,company text,"legacyUserId" numeric,"legacyGroupId" numeric,"identityType" text,"legacyUserData" jsonb,"realName" text,nickname text,username text,"createdAt" timestamptz);
  15. INSERT INTO "_User" VALUES('s','a',1,1,'member','{}','学生甲','','','2026-10-01'),('t','a',2,3,'coach','{}','老师乙','','','2026-10-01'),('other','b',1,3,'coach','{}','其他帐套','','','2026-10-01');`);
  16. const rows=[
  17. ['new','a',1,'【意见反馈】学生甲','希望调整课程','2026-10-14T16:00:00Z',0,0,22],
  18. ['leave','a',2,'【休假申请】老师乙','10月16日休假一天','2026-10-15T15:59:59Z',0,0,22],
  19. ['old','a',1,'学生甲 反馈的意见','旧表单的休假申请内容','2026-10-14T15:59:59Z',0,99,22],
  20. ['missing','a',9,'原账号反馈','<script>alert(1)</script>','2026-10-16T00:00:00Z',0,0,22],
  21. ['other','b',1,'别的公司','不可见','2026-10-15T00:00:00Z',0,0,22],
  22. ['reply','a',1,'回复','不应算新申请','2026-10-15T00:00:00Z',123,99,22],
  23. ['deleted','a',1,'已删除','不可见','2026-10-15T00:00:00Z',0,-2,22],
  24. ['community','a',1,'社区留言','其他栏目','2026-10-15T00:00:00Z',0,99,10],
  25. ];
  26. for(const r of rows)await query(`INSERT INTO "Guestbook" VALUES($1,$2,$3,$4,$5,$6,$6,$7,$8,$9,'legacy')`,r);
  27. });
  28. after(()=>db.close());
  29. test('includes pending and historical requests; excludes replies, recycle bin, other categories and tenants',async()=>{
  30. const d=await ctx.supportRequestsPage(admin,{});
  31. assert.equal(d.total,4);assert.equal(d.summary.feedback,1);assert.equal(d.summary.leave,1);assert.equal(d.summary.legacy,2);
  32. assert.equal(d.items.find(r=>r.id==='old').kind,'legacy');
  33. assert.equal(d.items.find(r=>r.id==='new').role,'student');
  34. assert.equal(d.items.find(r=>r.id==='leave').role,'teacher');
  35. assert.equal(d.items.find(r=>r.id==='missing').role,'other');
  36. assert.equal(d.items.find(r=>r.id==='missing').content,'<script>alert(1)</script>');
  37. });
  38. test('role, type, account, content and literal search filters',async()=>{
  39. assert.equal((await ctx.supportRequestsPage(admin,{role:'student'})).total,2);
  40. assert.equal((await ctx.supportRequestsPage(admin,{role:'teacher',kind:'leave',search:'老师乙'})).total,1);
  41. assert.equal((await ctx.supportRequestsPage(admin,{search:'旧表单'})).total,1);
  42. assert.equal((await ctx.supportRequestsPage(admin,{search:"%' OR 1=1--"})).total,0);
  43. assert.equal((await ctx.supportRequestsPage(admin,{search:'%'})).total,0);
  44. });
  45. test('Shanghai inclusive dates and stable pagination; counts survive empty pages',async()=>{
  46. const p=await ctx.supportRequestsPage(admin,{dateFrom:'2026-10-15',dateTo:'2026-10-15',pageSize:1});
  47. assert.equal(p.total,2);assert.equal(p.items[0].id,'leave');
  48. assert.equal((await ctx.supportRequestsPage(admin,{dateFrom:'2026-10-15',dateTo:'2026-10-15',pageSize:1,page:2})).items[0].id,'new');
  49. const empty=await ctx.supportRequestsPage(admin,{page:99});assert.equal(empty.total,4);assert.equal(empty.items.length,0);
  50. });
  51. test('rejects missing tenant, unauthorized roles and invalid filters',async()=>{
  52. await assert.rejects(ctx.supportRequestsPage({...admin,company:null},{}),/帐套/);
  53. await assert.rejects(ctx.supportRequestsPage({...admin,role:'student'},{}),/forbidden/);
  54. for(const input of [{kind:'approved'},{role:'admin'},{dateFrom:'2026-02-30'},{dateFrom:'2026-10-20',dateTo:'2026-10-15'}]) await assert.rejects(ctx.supportRequestsPage(admin,input));
  55. assert.equal((await ctx.supportRequestsPage({...admin,role:'ops-auditor'},{})).total,4);
  56. });
  57. test('same user ID in another tenant cannot change submitter identity',async()=>{
  58. assert.equal((await ctx.supportRequestsPage({...admin,company:'b'},{})).items[0].accountName,'其他帐套');
  59. assert.equal((await ctx.supportRequestsPage(admin,{search:'其他帐套'})).total,0);
  60. });
  61. test('existing mobile save function persists typed and legacy submissions for this board',async()=>{
  62. const source=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8');
  63. let id=0;
  64. const saveCtx={...ctx,parseObject:v=>JSON.parse(v),ownLegacyId:()=>2,firstValue:(...vs)=>vs.find(v=>v!==undefined&&v!==null&&v!==''),cleanText:(v,n)=>String(v??'').trim().slice(0,n),safe:v=>v.fields,isAdminUser:()=>false,
  65. Parse:{Object:class {fields={};set(k,v){this.fields[k]=v;}async save(){const f=this.fields;this.fields.objectId='saved'+ ++id;await query(`INSERT INTO "Guestbook" VALUES($1,$2,$3,$4,$5,$6,$6,$7,$8,$9,$10)`,[f.objectId,f.company,f.userid,f.title,f.tcontent,f.gdate.toISOString(),f.parentid,f.status,f.cateid,f.sourceKey]);}}}};
  66. vm.createContext(saveCtx);vm.runInContext(source.slice(source.indexOf('async function createGuestbook('),source.indexOf('async function authorizeRequestedUser(')),saveCtx);
  67. for(const title of ['【休假申请】老师乙','老师乙 反馈的意见'])await saveCtx.createGuestbook({model:JSON.stringify({UserID:2,Title:title,TContent:'保存链路验收',Cateid:22})},{get:()=> 'a'});
  68. const d=await ctx.supportRequestsPage(admin,{search:'保存链路验收'});assert.equal(d.total,2);
  69. assert.deepEqual(d.items.map(r=>r.kind).sort(),['leave','legacy']);
  70. });