| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import crypto from 'node:crypto';
- import fs from 'node:fs';
- import { createApplication } from '../src/core.mjs';
- import { attachWorkshopHandlers } from '../src/workshop.mjs';
- import { attachAccountBindingHandlers } from '../src/account-binding.mjs';
- import { indexBoardMembers } from '../../src/core/member-groups.js';
- import { WORKSHOP_ROSTER } from '../../src/core/workshop-roster.js';
- const data=JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json',import.meta.url)));
- const audit=JSON.parse(fs.readFileSync(new URL('../../docs/board-login-20261111/account-verification.json',import.meta.url)));
- const service={id:'trusted-service',kind:'service',scopes:['ingest:write','data:read'],projectIds:['*']};
- const key='participant-account:'+'a'.repeat(64),otherKey='participant-account:'+'b'.repeat(64);
- function fixture(){
- let rows={},seq=0,clock=Date.now(),current=audit.results.find(x=>x.username==='guopeng');
- let queue=Promise.resolve();
- const store={async lock(){},async transaction(fn){const run=async()=>{const before=structuredClone(rows);try{return await fn(store);}catch(e){rows=before;throw e;}};const r=queue.then(run);queue=r.catch(()=>{});return r;},
- async list(name,{where={},limit=10000}={}){return structuredClone((rows[name]||[]).filter(r=>Object.entries(where).every(([k,v])=>JSON.stringify(r[k])===JSON.stringify(v))).slice(0,limit));},
- async get(name,id){return structuredClone((rows[name]||[]).find(r=>r.objectId===id)||null);},
- async create(name,p){const r={...structuredClone(p),objectId:'object'+ ++seq};(rows[name]||=[]).push(r);return structuredClone(r);},
- async update(name,id,p){const r=(rows[name]||[]).find(x=>x.objectId===id);assert.ok(r);Object.assign(r,structuredClone(p));return structuredClone(r);}};
- const app=createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'fixture'},files:{}});
- attachWorkshopHandlers(app,{store,data});
- attachAccountBindingHandlers(app,{store,settings:{functionsUrl:'https://kangshifu.fmode.cn/api/functions'},now:()=>clock,
- fetchImpl:async(url,options)=>{assert.equal(url,'https://server.fmode.cn/parse/users/me');return{ok:options.headers['X-Parse-Session-Token']==='fixture-session',json:async()=>({objectId:current.userId,username:current.username,company:{__type:'Pointer',className:'Company',objectId:current.companyId||'FKieRbIoGt'}})};}});
- const call=async(name,p,actor=service)=>(await app.dispatch(name,p,actor)).data;
- const account=p=>call('ksfWorkshopAccount',{requestId:crypto.randomUUID(),...p});
- const prepare=(profile=key,username='guopeng')=>account({action:'prepare',participantProfileKey:profile,username,displayName:'果鹏'});
- const confirm=async link=>call('ksfWorkshopAccount',{action:'confirm',ticket:new URL(link.confirmationUrl).hash.slice(8)},await app.authenticateWorkshopPlatform({'X-Fmode-Session-Token':'fixture-session'}));
- const start=(profile=key,workshop='topic-one',identity={})=>call('ksfStartWorkshopSession',{requestId:crypto.randomUUID(),workshopKey:workshop,participant:{key:profile,name:'果鹏',identityBasis:'existing_record',account:{platform:'fmode',tenantId:'FKieRbIoGt',accountId:current.username,accountName:current.username,...identity}},agent:{key:'agent-account:'+'c'.repeat(64),name:'助手',identityBasis:'runtime_config',role:'workshop',runtimePlatform:'hermes',channel:'matrix',account:{platform:'matrix',accountId:'fixture-agent'}},conversation:{key:'fixture-room',channel:'matrix'},dataMode:'live'});
- return{store,app,call,account,prepare,confirm,start,setUser:user=>current=user,advance:ms=>clock+=ms};
- }
- test('首次绑定必须本人平台会话,未绑定提交被拦截且不创建成员或报告',async()=>{
- const f=fixture();await assert.rejects(()=>f.start(),{code:'ACCOUNT_BINDING_REQUIRED'});
- assert.equal((await f.store.list('KfSession')).length,0);
- const prepared=await f.prepare();assert.equal(prepared.status,'awaiting_account_confirmation');
- await assert.rejects(()=>f.call('ksfWorkshopAccount',{action:'confirm',ticket:new URL(prepared.confirmationUrl).hash.slice(8)}),{code:'AUTH_REQUIRED'});
- await assert.rejects(()=>f.app.authenticateWorkshopPlatform({}),{code:'AUTH_REQUIRED'});
- assert.equal((await f.store.list('KfArtifact')).length,0);
- });
- test('同账号跨课题、会话和档案别名仍27人;报告ID、内容和历史参与键不变',async()=>{
- const f=fixture(),binding=await f.confirm(await f.prepare());
- const first=await f.start(),second=await f.start(key,'topic-two');
- await f.confirm(await f.prepare(otherKey));const third=await f.start(otherKey,'topic-three');
- assert.equal(new Set([first.memberKey,second.memberKey,third.memberKey]).size,1);
- const saved=await f.store.create('KfArtifact',{externalKey:'workshop:artifact:fixture',meta:{workshop:{visibility:'public'}},recordState:'active',title:'真实报告原题',rawText:'原文',version:1,ownerSession:first.sessionId,uploadedByKey:first.participantKey,ownerName:'果鹏'});
- const before=await f.store.get('KfArtifact',saved.objectId),board=await f.call('ksfGetPublicBoard',{},null);
- const members=indexBoardMembers({...board,member:WORKSHOP_ROSTER.members});
- assert.equal(members.members.length,27);assert.equal(members.members.filter(m=>m.username==='guopeng').length,1);
- assert.equal(members.memberId('artifact',board.artifact[0]),binding.memberKey);
- assert.deepEqual(await f.store.get('KfArtifact',saved.objectId),before);
- assert.equal(board.artifact[0].uploadedByKey,first.participantKey);
- assert.ok(!JSON.stringify(board).includes('platformUserId'));assert.ok(!JSON.stringify(board).includes('fixture-room'));
- });
- test('不存在账号、跨企业、同名不同账号及已绑定改绑均不静默合并',async()=>{
- const f=fixture();await assert.rejects(()=>f.prepare(key,'doesnotexist'),{code:'ACCOUNT_NOT_REGISTERED'});
- const p=await f.prepare();f.setUser({...audit.results.find(x=>x.username==='guopeng'),companyId:'other-company'});
- await assert.rejects(()=>f.confirm(p),{code:'COMPANY_FORBIDDEN'});
- f.setUser(audit.results.find(x=>x.username==='liuna'));await assert.rejects(()=>f.confirm(p),{code:'ACCOUNT_BINDING_CONFLICT'});
- f.setUser(audit.results.find(x=>x.username==='guopeng'));await f.confirm(p);
- await assert.rejects(()=>f.prepare(key,'liuna'),{code:'ACCOUNT_BINDING_CONFLICT'});
- const foreign=await f.account({action:'status',participantProfileKey:otherKey});assert.equal(foreign.status,'unbound');
- });
- test('过期与替换链接失效,绑定重复确认幂等且服务隔离',async()=>{
- const f=fixture(),old=await f.prepare();f.advance(1);const next=await f.prepare();
- await assert.rejects(()=>f.confirm(old),{code:'ACCOUNT_LINK_EXPIRED'});
- const bound=await f.confirm(next);assert.deepEqual(await f.confirm(next),bound);
- const status=await f.call('ksfWorkshopAccount',{action:'status',participantProfileKey:key},{...service,id:'another-service'});assert.equal(status.status,'unbound');
- const g=fixture(),expired=await g.prepare();g.advance(31*60000);await assert.rejects(()=>g.confirm(expired),{code:'ACCOUNT_LINK_EXPIRED'});
- });
- test('首次登记缺企业和平台编号时,从本人已确认别名补齐会话,不换员工键',async()=>{
- const f=fixture(),profile='participant-registration:098130da-48d3-4d1d-8a2e-a547a6693acb';
- await assert.rejects(()=>f.start(profile,'first-use',{tenantId:null,accountId:null}),{code:'ACCOUNT_BINDING_REQUIRED'});
- const bound=await f.confirm(await f.prepare(profile));
- const first=await f.start(profile,'first-use',{tenantId:null,accountId:null});
- const again=await f.start(profile,'first-use',{tenantId:null,accountId:null});
- assert.equal(again.sessionId,first.sessionId);assert.equal(first.memberKey,bound.memberKey);
- const session=await f.store.get('KfSession',first.sessionId),a=session.meta.attribution;
- assert.equal(a.accountProfileKey,profile);assert.equal(a.participantAccount.tenantId,'FKieRbIoGt');
- assert.equal(a.participantAccount.accountId,audit.results.find(x=>x.username==='guopeng').userId);
- assert.equal(a.participantAccount.accountName,'guopeng');
- const board=await f.call('ksfGetPublicBoard',{},null);
- assert.equal(board.session[0].memberKey,bound.memberKey);
- assert.equal((await f.store.list('KfSession')).length,1);assert.equal((await f.store.list('KfArtifact')).length,0);
- });
- test('已绑定别名仍拒绝显式跨企业、其他账号和其他平台编号,不回填冲突值',async()=>{
- const f=fixture();await f.confirm(await f.prepare());
- for(const identity of [{tenantId:'other-company',accountId:null},{tenantId:null,accountName:'liuna',accountId:null},{tenantId:null,accountId:'other-user'}]){
- await assert.rejects(()=>f.start(key,'conflict',identity),{code:'ACCOUNT_BINDING_CONFLICT'});
- }
- assert.equal((await f.store.list('KfSession')).length,0);
- assert.equal((await f.account({action:'status',participantProfileKey:key})).username,'guopeng');
- });
- test('既有企业明确但平台编号为空的合法会话保持原归属字节,可重复复用',async()=>{
- const f=fixture();await f.confirm(await f.prepare());
- const first=await f.start(key,'legacy-account',{accountId:null});
- const before=await f.store.get('KfSession',first.sessionId);
- assert.equal(before.meta.attribution.participantAccount.accountId,null);
- const again=await f.start(key,'legacy-account',{accountId:null});
- assert.equal(again.sessionId,first.sessionId);assert.equal(again.reused,true);
- assert.deepEqual(await f.store.get('KfSession',first.sessionId),before);
- });
- test('短确认入口不携带身份载荷;丢失、篡改引用及伪造服务确认不能绑定',async()=>{
- const f=fixture(),prepared=await f.prepare(),url=new URL(prepared.confirmationUrl),reference=new URLSearchParams(url.hash.slice(1)).get('ticket');
- assert.match(reference,/^ksfl_[A-Za-z0-9_-]{22}$/);
- assert.ok(prepared.confirmationUrl.length<110);
- assert.doesNotMatch(prepared.confirmationUrl,/guopeng|participant-|trusted-service|eyJ|\.\.\./);
- const actor=await f.app.authenticateWorkshopPlatform({'X-Fmode-Session-Token':'fixture-session'});
- const forged='ksfl_'+(reference[5]==='A'?'B':'A')+reference.slice(6);
- await assert.rejects(()=>f.call('ksfWorkshopAccount',{action:'confirm',ticket:forged},actor),{code:'ACCOUNT_LINK_EXPIRED'});
- await assert.rejects(()=>f.call('ksfWorkshopAccount',{action:'confirm',ticket:reference}),{code:'AUTH_REQUIRED'});
- assert.equal((await f.account({action:'status',participantProfileKey:key})).status,'unbound');
- assert.equal((await f.store.list('KfSession')).length,0);assert.equal((await f.store.list('KfArtifact')).length,0);
- assert.equal((await f.confirm(prepared)).status,'bound');
- });
- test('既有完整签名链接继续核验;服务端短引用的签名载荷被改动时拒绝确认',async()=>{
- const f=fixture(),prepared=await f.prepare();
- const links=(await f.store.list('KsfRuntimeConfig')).filter(row=>row.key.startsWith('workshop-account-link:'));
- assert.equal(links.length,1);
- const signed=links[0].value.ticket;
- assert.equal((await f.confirm({confirmationUrl:'https://kangshifu.fmode.cn/api/functions?path=/ksfWorkshopAccount#ticket='+encodeURIComponent(signed)})).status,'bound');
- const g=fixture(),next=await g.prepare(),row=(await g.store.list('KsfRuntimeConfig')).find(row=>row.key.startsWith('workshop-account-link:'));
- const [body,signature]=row.value.ticket.split('.'),payload=JSON.parse(Buffer.from(body,'base64url').toString('utf8'));
- payload.username='liuna';
- await g.store.update('KsfRuntimeConfig',row.objectId,{value:{...row.value,ticket:Buffer.from(JSON.stringify(payload)).toString('base64url')+'.'+signature}});
- await assert.rejects(()=>g.confirm(next),{code:'REPORT_NOT_CONFIRMED'});
- assert.equal((await g.account({action:'status',participantProfileKey:key})).status,'unbound');
- });
|