| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import crypto from 'node:crypto';
- import { createApplication } from '../src/core.mjs';
- import { attachMemberGroupHandlers, readMemberGroupBoard } from '../src/member-groups.mjs';
- const data = JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json',import.meta.url)));
- function fixture() {
- let rows={KsfRuntimeConfig:[{objectId:'runtime',key:'runtime',value:{signingSecret:'PRIVATE',wecomDirectory:{secret:'PRIVATE'}}}]},seq=0,queue=Promise.resolve();
- const clone=structuredClone;
- const store={
- async transaction(fn) { const exec=async()=>{const before=clone(rows);try{return await fn(store);}catch(e){rows=before;throw e;}};const result=queue.then(exec);queue=result.catch(()=>{});return result; },
- async lock(){},
- async list(name,{where={}}={}) {return clone((rows[name]||[]).filter((r)=>Object.entries(where).every(([k,v])=>r[k]===v)));},
- async create(name,value){const row={...clone(value),objectId:'object'+ ++seq};(rows[name]||=[]).push(row);return clone(row);},
- async update(name,id,changes){const row=rows[name].find((r)=>r.objectId===id);Object.assign(row,clone(changes));return clone(row);},
- };
- const board={session:[{objectId:'s1',studentName:'同名',groupId:'G1',meta:{participantKey:'participant:one'}},{objectId:'s2',studentName:'同名',groupId:'G2',meta:{participantKey:'participant:two'}}],artifact:[],project:[],opportunity:[]};
- const app=createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'test'},files:{}});
- attachMemberGroupHandlers(app,{store,loadBoard:async()=>board});
- const call=async(name,input={})=>(await app.dispatch(name,input,null)).data;
- const change=(revision,command,requestId=crypto.randomUUID())=>call('ksfManagePublicMemberGroups',{requestId,expectedRevision:revision,...command});
- return {store,board,call,change};
- }
- test('公开分组只读不会写库或泄露运行配置,首次沿用现有组号',async()=>{
- const f=fixture(),r=await f.call('ksfGetPublicMemberGroups');
- assert.equal(r.revision,0);assert.equal(r.groups.length,2);assert.equal(r.memberCount,2);
- assert.equal((await f.store.list('KsfRuntimeConfig')).length,1);assert.ok(!JSON.stringify(r).includes('PRIVATE'));
- });
- test('共享分组创建/批量分配/重命名/删除持久化且不改业务会话',async()=>{
- const f=fixture(),before=JSON.stringify(f.board);
- let r=await f.change(0,{action:'create',name:'研发组'});const id=r.groups.at(-1).id;
- r=await f.change(r.revision,{action:'assign',groupId:id,memberIds:['participant:one','participant:two']});
- assert.equal(Object.values(r.assignments).filter((v)=>v===id).length,2);
- r=await f.change(r.revision,{action:'rename',groupId:id,name:'创新组'});assert.equal(r.groups.at(-1).name,'创新组');
- r=await f.change(r.revision,{action:'delete',groupId:id});assert.deepEqual(r.assignments,{});
- assert.equal(JSON.stringify(f.board),before);assert.equal((await f.call('ksfGetPublicMemberGroups')).revision,4);
- assert.equal((await f.store.list('KsfOperation')).length,4);assert.equal((await f.store.list('KsfRuntimeConfig')).find((x)=>x.key==='runtime').value.signingSecret,'PRIVATE');
- });
- test('同请求幂等、旧版本和并发更新不会覆盖其他人的更改',async()=>{
- const f=fixture(),command={action:'create',name:'研发组'};
- const first=await f.change(0,command,'same');assert.deepEqual(await f.change(0,command,'same'),first);
- await assert.rejects(()=>f.change(0,{...command,name:'另一组'},'same'),{code:'IDEMPOTENCY_CONFLICT'});
- const results=await Promise.allSettled([f.change(1,{action:'create',name:'A组'}),f.change(1,{action:'create',name:'B组'})]);
- assert.equal(results.filter((r)=>r.status==='fulfilled').length,1);assert.equal(results.find((r)=>r.status==='rejected').reason.code,'VERSION_CONFLICT');
- assert.equal((await f.store.list('KsfOperation')).length,2);
- });
- test('隐藏成员、越权参数、重复名称和部分无效批次全部拒绝且不留半批结果',async()=>{
- const f=fixture();
- for(const command of [{action:'create',name:'G1'},{action:'create',name:'G1',groupId:'legacy-group:G1'},
- {action:'assign',groupId:'legacy-group:G1',memberIds:['participant:one','private:unknown']},
- {action:'assign',groupId:null,memberIds:['participant:one'],role:'admin'}]) await assert.rejects(()=>f.change(0,command),{code:'VALIDATION_FAILED'});
- assert.equal((await f.call('ksfGetPublicMemberGroups')).revision,0);assert.equal((await f.store.list('KsfOperation')).length,0);
- });
- test('公开成员读取完整分页且不带服务凭据,读取失败不当空成员',async()=>{
- let count=0;
- const board=await readMemberGroupBoard({url:'/fixture',appId:'public',fetchImpl:async(url,options)=>{
- count++;assert.deepEqual(Object.keys(options.headers).sort(),['Content-Type','X-Parse-Application-Id']);
- const cursor=JSON.parse(options.body).params.cursor;
- return {ok:true,json:async()=>({result:{ok:true,data:{session:[{objectId:cursor?'s2':'s1'}],project:[],artifact:[],opportunity:[],nextCursor:cursor?null:'next'}}})};
- }});assert.equal(count,2);assert.equal(board.session.length,2);
- await assert.rejects(()=>readMemberGroupBoard({url:'/fixture',appId:'public',fetchImpl:async()=>({ok:false,json:async()=>({})})}),{code:'MEMBER_READ_FAILED'});
- });
|