Files
PrismMaze/tests/auth/auth.test.cjs

119 lines
14 KiB
JavaScript

const fs=require('fs'),vm=require('vm'),assert=require('assert').strict;
const ts=require(process.env.TYPESCRIPT_PATH || 'C:/ProgramData/cocos/editors/Creator/3.8.7/resources/app.asar.unpacked/node_modules/typescript');
function load(file,imports={}) {const exports={};vm.runInNewContext(ts.transpileModule(fs.readFileSync(file,'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2015,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText,{exports,require:k=>{if(!(k in imports))throw Error('Missing '+k);return imports[k]},console,Date,Math});return exports;}
const policy=load('assets/games/auth/scripts/AuthPolicy.ts');
const catalog=load('assets/games/auth/scripts/RechargeCatalog.ts');
const model=load('assets/games/auth/scripts/AuthService.ts',{'./AuthPolicy':policy,'./RechargeCatalog':catalog});
const {AuthService,TEST_ACCOUNTS}=model;
let time=Date.parse('2026-10-23T20:00:00+08:00');
const data=new Map();let failWrite=false;
const store={getItem:k=>data.get(k)??null,setItem:(k,v)=>{if(failWrite)throw Error('disk full');data.set(k,v)}};
const service=new AuthService(store,true,()=>time);
function wallet(name,money=100){const key='prism.auth.save.'+name;if(!data.has(key))data.set(key,JSON.stringify({settings:{sound:1,shake:1},baseData:{money,maxScore:0}}));}
function login(name, s=service){s.endSession();const a=s.authenticate(name,'123456');wallet(name);s.beginSession(a);return a;}
function buy(id,s=service,order=s.newOrderId()){return s.purchase(id,order,s.sessionId);}
function identity(birthday,seq='001'){const body='110101'+birthday+seq,w=[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2];return body+'10X98765432'[[...body].reduce((s,n,i)=>s+Number(n)*w[i],0)%11];}
// Temporary access permits off-hours and exhausted daily budgets, but retains payment restrictions.
assert.equal(policy.AUTH_POLICY.allowMinorAccessForValidation,true);
const noticeMinor=service.authenticate('wcnr004','123456');
assert.equal(service.needsMinorLoginNotice(noticeMinor),true);
assert.equal(service.needsMinorLoginNotice(service.authenticate('cnrzh001','123456')),false);
service.acknowledgeMinorLoginNotice();
service.endSession();
assert.equal(service.needsMinorLoginNotice(noticeMinor),false);
assert.equal(service.needsMinorLoginNotice(service.authenticate('wcnr007','123456')),false);
assert.equal(new AuthService(store).needsMinorLoginNotice(noticeMinor),true);
time=Date.parse('2026-10-19T12:00:00+08:00');
data.set('prism.auth.usage.wcnr004.2026-10-19','3600');
login('wcnr001');assert.throws(()=>buy('diamond_1'),/未满8/);
login('wcnr004');assert.throws(()=>buy('diamond_3'),/单次充值金额/);
for(let i=0;i<5;i++)buy('diamond_2');
for(let i=0;i<3;i++)buy('diamond_1');
assert.throws(()=>buy('diamond_1'),/每月/);
service.endSession();data.clear();
policy.AUTH_POLICY.allowMinorAccessForValidation=false;
time=Date.parse('2026-10-23T20:00:00+08:00');
assert.equal(TEST_ACCOUNTS.length,30);assert.equal(new Set(TEST_ACCOUNTS.map(a=>a.username)).size,30);
for(const fixture of TEST_ACCOUNTS){const a=service.authenticate(fixture.username,'123456');service.clockMode='blocked';assert.equal(Boolean(service.accessError(a)),a.age<18);service.clockMode='allowed';assert.equal(service.accessError(a),'');}
service.clockMode='real';
assert.throws(()=>new AuthService(store).clockMode='allowed');
assert.throws(()=>service.authenticate('cnrzh001','bad'));
assert.throws(()=>service.register('cnrzh001','123456','123456'));
assert.throws(()=>service.register('short','123456','123456'));
assert.throws(()=>service.register('tester01','123456','654321'));
const chinese=service.register(' 星河旅人 ','123456','123456');
assert.equal(chinese.username,'星河旅人');
assert.equal(new AuthService(store).authenticate('星河旅人','123456').username,'星河旅人');
assert.throws(()=>service.register('星河旅人','123456','123456'),/已存在/);
for(const name of ['一','名字 空格','名字😀','名'.repeat(21),'玩家/路径']) assert.throws(()=>service.register(name,'123456','123456'));
const a=service.register('tester01','123456','123456');assert.ok(service.accessError(a));assert.throws(()=>service.beginSession(a));
for(const bad of [identity('20000230'),'TEST20000101001','110101200001010000',identity('20271023'),identity('20000101','000')])assert.throws(()=>service.verify(a,'测试用户',bad));
const good=identity('20000101');assert.throws(()=>service.verify(a,'··',good));assert.throws(()=>service.verify(a,'测试用户',good.slice(0,17)+(good[17]==='0'?'1':'0')));
service.verify(a,'测试用户','11010519491231002x');assert.equal(a.age,76);
for(const [birth,age] of [['20181024',7],['20181023',8],['20101024',15],['20101023',16],['20081024',17],['20081023',18],['20000229',26]]){service.verify(a,'测试用户',identity(birth));assert.equal(service.ageOf(a),age);}
service.verify(a,'测试用户',good);assert.equal(new AuthService(store).authenticate('tester01','123456').verified,true);assert.ok(!data.get('prism.auth.v1').includes(good));assert.ok(!data.get('prism.auth.v1').includes('测试用户'));
const before=data.get('prism.auth.v1');failWrite=true;assert.throws(()=>service.register('failed01','123456','123456'));failWrite=false;assert.throws(()=>service.authenticate('failed01','123456'));assert.equal(data.get('prism.auth.v1'),before);
for(const [date,allow] of [['2026-10-23T19:59:59+08:00',false],['2026-10-23T20:00:00+08:00',true],['2026-10-23T21:00:00+08:00',false],['2026-10-19T20:30:00+08:00',false],['2026-10-01T20:30:00+08:00',true]]){time=Date.parse(date);assert.equal(policy.allowedTime(new Date(time)),allow);}
time=Date.parse('2026-10-23T20:00:00+08:00');
login('wcnr001');assert.throws(()=>buy('diamond_1'),/未满8/);
login('wcnr004');assert.throws(()=>buy('diamond_3'),/单次充值金额/);for(let i=0;i<5;i++)buy('diamond_2');for(let i=0;i<3;i++)buy('diamond_1');assert.throws(()=>buy('diamond_1'),/每月/);
login('wcnr007');assert.throws(()=>buy('diamond_4'),/单次充值金额/);for(let i=0;i<5;i++)buy('diamond_3');buy('diamond_2');for(let i=0;i<4;i++)buy('diamond_1');assert.throws(()=>buy('diamond_1'),/每月/);
// Inclusive monetary boundaries independently of the available fixed offers.
service.limit(service.authenticate('wcnr004','123456'),50,15000);assert.throws(()=>service.limit(service.authenticate('wcnr004','123456'),50,15001));
service.limit(service.authenticate('wcnr007','123456'),100,30000);assert.throws(()=>service.limit(service.authenticate('wcnr007','123456'),100,30001));
login('cnrzh001');const order=service.newOrderId(),res=buy('diamond_6',service,order);assert.equal(buy('diamond_6',service,order).balance,res.balance);assert.equal(buy('diamond_6',service,order).duplicate,true);assert.throws(()=>buy('diamond_1',service,order));assert.throws(()=>buy('forged_offer'));
const raw=data.get('prism.auth.save.cnrzh001');failWrite=true;assert.throws(()=>buy('diamond_1'));failWrite=false;assert.equal(data.get('prism.auth.save.cnrzh001'),raw);
const stale=service.sessionId;login('cnrzh002');assert.throws(()=>service.purchase('diamond_1',service.newOrderId(),stale),/失效/);
const restarted=new AuthService(store,true,()=>time);login('cnrzh001',restarted);assert.equal(buy('diamond_6',restarted,order).duplicate,true);
// Corrupt monthly data fails closed rather than turning into an unlimited NaN balance.
login('wcnr005');data.set('prism.auth.pay.wcnr005.2026-10','NaN');assert.throws(()=>buy('diamond_1'),/异常/);data.set('prism.auth.pay.wcnr005.2026-10','198');assert.throws(()=>buy('diamond_1'),/每月/);
// China calendar month rollover: 20:00 in November uses November's independent ledger.
service.endSession();time=Date.parse('2026-11-01T20:00:00+08:00');login('wcnr005');buy('diamond_1');assert.equal(JSON.parse(data.get('prism.auth.save.wcnr005')).payments.totals['2026-11'],600);
// Advancing clock, 21:00 forced stop, daily cumulative reconnect budget, rollback failure.
service.endSession();time=Date.parse('2026-10-23T20:59:59+08:00');login('wcnr002');time+=1000;assert.match(service.heartbeat(),/暂时无法进行游戏/);assert.throws(()=>buy('diamond_1'));service.endSession();
time=Date.parse('2026-10-24T20:00:00+08:00');login('wcnr002');time+=1800000;service.heartbeat(true);service.endSession();login('wcnr002');time+=1799000;service.heartbeat(true);service.endSession();time=Date.parse('2026-10-24T20:30:00+08:00');login('wcnr002');time+=2000;assert.match(service.heartbeat(),/1小时/);service.endSession();
time=Date.parse('2026-10-25T20:00:00+08:00');login('wcnr002');time-=1000;assert.match(service.heartbeat(),/时间异常/);service.endSession();
time=Date.parse('2026-10-23T20:00:00+08:00');service.clockMode='ending';const t0=service.now().getTime();time+=30000;assert.equal(service.now().getTime()-t0,30000);assert.equal(policy.allowedTime(service.now()),false);service.clockMode='real';
// UI flow invokes the actual service and never enters gameplay before login.
const ui=new AuthService(store,true,()=>Date.parse('2026-10-23T20:00:00+08:00'));
const {AuthPanel}=load('assets/games/auth/scripts/AuthPanel.ts',{'../../profile/GameButton':{},'./AuthRuntime':{authService:()=>ui},cc:{_decorator:{ccclass:()=>c=>c},Component:class{}}});
(async()=>{
const p=new AuthPanel();let entered=0;p.node={active:true,destroy(){}};p.message={};p.show=function(page){this.page=page;this.fields=[]};p.configure(async()=>{entered++});
p.page='register';p.fields=['uitester','123456','123456'].map(string=>({string}));await p.submit();assert.equal(p.page,'identity');
p.fields=['测试玩家',good].map(string=>({string}));await p.submit();assert.equal(p.page,'age');await p.submit();assert.equal(p.page,'notice');await p.submit();assert.equal(p.page,'login');assert.equal(entered,0);
p.fields=['uitester','123456'].map(string=>({string}));await p.submit();assert.equal(entered,1);assert.ok(ui.current);
// First minor attempt returns to login; only the next attempt may enter.
ui.endSession();policy.AUTH_POLICY.allowMinorAccessForValidation=true;
const minorPanel=new AuthPanel();let minorEntered=0;
minorPanel.node={active:true,destroy(){}};minorPanel.message={};
minorPanel.show=function(page){this.page=page;this.fields=[]};
minorPanel.configure(async()=>{minorEntered++});
minorPanel.fields=['wcnr004','123456'].map(string=>({string}));
await minorPanel.submit();assert.equal(minorPanel.page,'notice');assert.equal(minorEntered,0);assert.equal(ui.current,null);
await minorPanel.submit();assert.equal(minorPanel.page,'login');assert.equal(minorEntered,0);assert.equal(ui.current,null);
minorPanel.fields=['wcnr004','123456'].map(string=>({string}));
await minorPanel.submit();assert.equal(minorEntered,1);assert.ok(ui.current);
ui.endSession();policy.AUTH_POLICY.allowMinorAccessForValidation=false;
const defs=load('assets/common/scripts/user/def_userData.ts');
const storage=load('assets/common/scripts/user/UserStorageData.ts',{cc:{sys:{localStorage:store}},'./def_userData':defs,'../common/comp/StorageData':{StorageData:class{noticeDataChanged(){}}},'../base/base':{getGlobal:()=>({})}}).userStorageData;
data.set('userData_new',JSON.stringify({baseData:{money:77,maxScore:99}}));storage.selectAccount('cnrzh003',100);storage.setMoney(321);storage.selectAccount('cnrzh006',20000);assert.equal(storage.getData().baseData.money,20000);storage.selectAccount('cnrzh003',100);assert.equal(storage.getData().baseData.money,321);assert.equal(JSON.parse(data.get('userData_new')).baseData.money,77);
service.endSession();login('cnrzh003');const bought=buy('diamond_1');storage.refreshAccount();assert.equal(storage.getData().baseData.money,bought.balance);storage.setMoney(bought.balance-100);assert.ok(JSON.parse(data.get('prism.auth.save.cnrzh003')).payments);
// A delayed prefab must not appear after logout; loading remains accessible without auth.
let resolvePrefab,attached=0,destroyed=0;
const cc={_decorator:{ccclass:()=>c=>c},Component:class{}};
class Dict {constructor(){this.m=new Map()}has(k){return this.m.has(k)}get(k){return this.m.get(k)}set(k,v){this.m.set(k,v)}remove(k){this.m.delete(k)}}
const panels=load('assets/common/scripts/mgrs/PanelMgr.ts',{'../../../games/auth/scripts/AuthRuntime':{authService:()=>service},cc,
'../panels/abstractPanel':{abstractPanel:class{}},'../collections/Dictionary':{Dictionary:Dict},'../panels/def_panels':{enumPanel:{loadingPanel:'loading'}},
'../utils/ResLoader':{ResLoader:{preloadPanel:()=>new Promise(r=>{resolvePrefab=r})}},'./GameMgr':{GameMgr:{getRootPanelContainer:()=>({addChild(){attached++}})}}}).PanelMgr;
const pending=panels.openPanel('game');service.endSession();resolvePrefab({destroy(){destroyed++}});await assert.rejects(pending,/失效/);assert.equal(attached,0);assert.equal(destroyed,1);
const loadingNode={active:false,getComponent:()=>({node:loadingNode,OnPanelOpen(){}})};const loading=panels.openPanel('loading');resolvePrefab(loadingNode);await loading;assert.equal(attached,1);
// Actual shop handler: cancel/no confirmation, rapid double submit and failing observer never grant twice.
login('cnrzh003');const {RechargePanel}=load('assets/games/shop/scripts/RechargePanel.ts',{cc,'../../profile/GameButton':{gameArt:async()=>null},'./ShopCatalog':catalog,'../../auth/scripts/AuthRuntime':{authService:()=>service}});
const shop=new RechargePanel();shop.balance={};shop.message={};let synced=0;shop.configure({session:service.sessionId,onPurchaseCommitted(){synced++},getBalance:()=>0,onClose(){}});
const offer=catalog.SHOP_OFFERS[0],key='prism.auth.save.cnrzh003',beforeCancel=data.get(key);
shop.purchase(offer);assert.equal(data.get(key),beforeCancel);
shop.confirmation={destroy(){}};shop.orderId=service.newOrderId();shop.purchase(offer);const afterPurchase=data.get(key);shop.purchase(offer);assert.equal(data.get(key),afterPurchase);assert.equal(synced,1);
console.log('PASS: delayed panel rejected after logout; unauthenticated loading; canceled and double-click shop submissions.');
console.log('PASS: 30 fixtures; 18-digit identity and birthday boundaries; mandatory login; holidays/China-time/month rollover; advancing preview clock; daily reconnect budget; 21:00/rollback; all payment age limits; atomic failure; idempotent orders across reload; stale session; malformed ledger; registration UI; isolated saves.');
})().catch(e=>{console.error(e);process.exitCode=1});