import './pass-check-loader.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import { isDeepStrictEqual } from 'node:util'; import { createHmac } from 'node:crypto'; const { handlePassRequest, preparePassOrder, acceptPassPayment, reconcilePassOrder, verifyPassCallback, PERIOD, config } = await import('../functions/passCheckService.ts'); const A = Date.UTC(2026, 8, 1), realNow = Date.now; let now, tables, beforeUpdate, failFinalize, failAdd; const copy = x => x == null ? x : structuredClone(x); function matches(row, query) { return Object.entries(query).every(([k,v]) => v === null ? row[k] == null : isDeepStrictEqual(row[k], v)); } const db = { collection(name) { return { async add(value) { if (failAdd) { failAdd=false; throw Error('add interrupted'); } if (tables[name].some(r=>r._id===value._id)) throw Error('duplicate id'); tables[name].push(copy(value)); return {id:value._id}; }, where(query) { return { async getOne() { return {data:copy(tables[name].find(r=>matches(r,query))||null)}; }, async get() { return {data:copy(tables[name].filter(r=>matches(r,query)))}; }, async update(update) { if (beforeUpdate) { const hook=beforeUpdate; beforeUpdate=null; await hook(name,update); } if(name==='order' && update.passGranted && failFinalize) { failFinalize=false; throw Error('finalize interrupted'); } let found=tables[name].filter(r=>matches(r,query)); found.forEach(r=>Object.assign(r,copy(update))); return {updated:found.length}; } }; } }; }}; const fixture = { version:'test', premiumProduct:'premium-test', upgradeProduct:'upgrade-test', loopPoints:3, loopCoins:10, loopPowerSeconds:60, gift:[{field:'coinAmount',amount:7}] }; function reset(extra={}) { now=A+1000; Date.now=()=>now; process.env.PASSCHECKTIME_ID='anchor'; process.env.PASSCHECK_PAY_CALLBACK_KEY='test-key'; process.env.PASSCHECK_V2_ENABLED='true'; delete process.env.PASSCHECK_V2_USERS; process.env.PASSCHECK_V2_CONFIG=JSON.stringify(fixture); tables={users:[{_id:'u',openid:'o',token:'t',coinAmount:0,userPowerTime:0,...extra}],order:[],idcount:[{_id:'anchor',passcheckTime:A}]};beforeUpdate=null;failFinalize=false;failAdd=false; } test.beforeEach(()=>reset()); test.after(()=>{Date.now=realNow;}); const request = (action, extra={})=>handlePassRequest(db,{uid:'u',token:'t',action,...extra}); const state = ()=>JSON.parse(tables.users[0].passCheckV2); function change(fn) { const s=state();fn(s);tables.users[0].passCheckV2=JSON.stringify(s); } async function win() { const r=(await request('begin_round')).data.round; return request('complete_round',{roundId:r.id,roundSeq:r.seq,won:true}); } async function buy(product='premium-test', number='order-1') { await preparePassOrder(db,{uid:'u',token:'t',itemid:product,passChannel:'and'},number); const o=tables.order.find(o=>o.outTradeNo===number); return acceptPassPayment(db,o,{outTradeNo:number,openid:'o',price:o.goodsPrice,quantity:1,productId:product}); } const rejects = (fn,reason)=>assert.rejects(fn,e=>e.reason===reason); test('disabled, authentication and configuration fail closed',async()=>{ process.env.PASSCHECK_V2_ENABLED='false';await rejects(()=>request('read'),'FEATURE_DISABLED'); process.env.PASSCHECK_V2_ENABLED='true';await rejects(()=>request('read',{token:'bad'}),'TOKEN_INVALID'); process.env.PASSCHECK_V2_CONFIG='bad';await rejects(()=>request('read'),'CONFIG_INVALID'); }); test('missing gift blocks purchase until gift configuration is filled',async()=>{ process.env.PASSCHECK_V2_CONFIG='{}';const r=await request('read');assert.deepEqual(r.data.purchaseOptions,[]); await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'and'},'missing-gift'),'CONFIG_INCOMPLETE'); process.env.PASSCHECK_V2_CONFIG=JSON.stringify(fixture);const next=await request('read');assert.equal(next.data.purchaseOptions.length,2); }); test('free and paid gain one point, premium gains two without retroactive credit',async()=>{ await request('read');await win();assert.equal(state().seasons[0].points,1);await buy('battlepass');await win();assert.equal(state().seasons[0].points,2); await buy('upgrade-test','order-2');await win();assert.equal(state().seasons[0].points,4);assert.equal(tables.users[0].coinAmount,7); assert.deepEqual(tables.order.map(o=>o.goodsPrice),[1800,1200]); }); test('direct premium equals upgrade and callback retries do not duplicate gift',async()=>{ await request('read');await buy();await reconcilePassOrder(db,'order-1','u');assert.equal(state().seasons[0].tier,3);assert.equal(tables.users[0].coinAmount,7); assert.equal(tables.order[0].goodsPrice,3000);await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'and'},'x'),'TIER_CONFLICT'); }); test('illegal upgrade, tampered amounts and owner mismatch rejected',async()=>{ await request('read');await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'upgrade-test',passChannel:'and'},'x'),'TIER_CONFLICT'); const p=await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',itemPrice:1,itemCount:20,passChannel:'and'},'x');assert.equal(p.price,3000); await rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'x',openid:'o',price:1,productId:'premium-test',quantity:1}),'PAYMENT_MISMATCH'); await rejects(()=>reconcilePassOrder(db,'x','someone'),'ORDER_OWNER_MISMATCH'); }); test('concurrent purchases reserve one order and retry returns original number',async()=>{ await request('read');const bodies={uid:'u',token:'t',itemid:'premium-test',passChannel:'and'}; const r=await Promise.all([preparePassOrder(db,bodies,'a'),preparePassOrder(db,bodies,'b')]);assert.equal(r[0].outTradeNo,r[1].outTradeNo);assert.equal(tables.order.length,1); await rejects(()=>preparePassOrder(db,{...bodies,itemid:'battlepass'},'c'),'ORDER_PENDING'); }); test('reservation survives interrupted order insert',async()=>{ await request('read');failAdd=true;await assert.rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a')); const p=await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'b');assert.equal(p.outTradeNo,'a');assert.equal(tables.order.length,1); }); test('only provider-confirmed closure permits changing product',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'and'},'a');tables.order[0].state=3; await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'b'),'ORDER_PENDING'); tables.order[0].passClosedVerified=true;await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'b');assert.equal(tables.order.length,2); }); test('duplicate round and concurrent settlement count one win',async()=>{ await request('read');const r=(await request('begin_round')).data.round;const r2=(await request('begin_round')).data.round;assert.equal(r.id,r2.id); await Promise.all([request('complete_round',{roundId:r.id,roundSeq:r.seq,won:true}),request('complete_round',{roundId:r.id,roundSeq:r.seq,won:true})]);assert.equal(state().seasons[0].points,1); await rejects(()=>request('complete_round',{roundId:'fake',roundSeq:r.seq,won:true}),'ROUND_INVALID'); }); test('two-point win crosses threshold and preserves overflow at cap',async()=>{ await request('read');await buy();const total=config().levels.reduce((n,r)=>n+r.token,0);change(s=>s.seasons[0].points=total-1);const r=await win();assert.equal(r.data.current.unlocked,30);assert.equal(r.data.current.overflow,1); }); test('lower tiers bank overflow; upgrade unlocks alternating batch and keeps remainder',async()=>{ await request('read');const total=config().levels.reduce((n,r)=>n+r.token,0);change(s=>s.seasons[0].points=total+10); await rejects(()=>request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0}),'TIER_REQUIRED');await buy(); const r=await request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0});assert.equal(r.data.current.loopClaimed,3);assert.equal(r.data.current.loopRemainder,1);assert.equal(tables.users[0].coinAmount,27);assert.equal(tables.users[0].userPowerTime,Math.floor(now/1000)+60); await request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0});assert.equal(tables.users[0].coinAmount,27); await win();await request('claim_loop',{seasonId:String(A),expectedLoopClaimed:3});assert.equal(tables.users[0].userPowerTime,Math.floor(now/1000)+120); }); test('concurrent level claims grant once, reject locked track and level',async()=>{ await request('read');await rejects(()=>request('claim_level',{seasonId:String(A),track:'paid',level:1}),'TIER_REQUIRED'); await rejects(()=>request('claim_level',{seasonId:String(A),track:'free',level:30}),'LEVEL_LOCKED'); const b={seasonId:String(A),track:'free',level:1};await Promise.all([request('claim_level',b),request('claim_level',b)]);assert.equal(tables.users[0].userPowerTime,Math.floor(now/1000)+900); }); test('concurrent external resource grant is retained by CAS retry',async()=>{ await request('read');await buy();beforeUpdate=async(name)=>{if(name==='users')tables.users[0].coinAmount+=100;}; await request('claim_level',{seasonId:String(A),track:'paid',level:1});assert.equal(tables.users[0].coinAmount,1607); }); test('old rewards block display and purchase while current accumulates at free speed',async()=>{ await request('read');await buy();now=A+PERIOD+1000;const r=await request('read');assert.equal(r.data.displaySeasonId,String(A));assert.equal(r.data.current.tier,1); await win();assert.equal(state().seasons.find(p=>p.id===String(A+PERIOD)).points,1); await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'b'),'OLD_REWARDS_PENDING'); await request('claim_level',{seasonId:String(A),track:'free',level:1});await request('claim_level',{seasonId:String(A),track:'paid',level:1});assert.equal((await request('read')).data.displaySeasonId,String(A+PERIOD)); }); test('expired period discards remainder and rejects old round and rewards',async()=>{ await request('read');const r=(await request('begin_round')).data.round;now=A+PERIOD; await rejects(()=>request('complete_round',{roundId:r.id,roundSeq:r.seq,won:true}),'ROUND_EXPIRED'); now=A+PERIOD*2;await request('read');await rejects(()=>request('claim_level',{seasonId:String(A),track:'free',level:1}),'SEASON_EXPIRED'); }); test('late paid order grants original season only; too late is retained for review',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a');now=A+PERIOD+1; await acceptPassPayment(db,tables.order[0],{outTradeNo:'a',openid:'o',price:3000,quantity:1,productId:'premium-test'});assert.equal(state().seasons.find(p=>p.id===String(A)).tier,3);assert.equal(state().seasons.find(p=>p.id===String(A+PERIOD)).tier,1); reset();await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a');now=A+PERIOD*2; await rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'a',openid:'o',price:3000,quantity:1,productId:'premium-test'}),'SEASON_EXPIRED');assert.equal(tables.order[0].passReviewReason,'SEASON_EXPIRED');assert.equal(tables.order[0].passGranted,undefined); }); test('interrupted order completion retries without gifting twice',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a');failFinalize=true; await assert.rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'a',openid:'o',price:3000,quantity:1,productId:'premium-test'}));assert.equal(tables.users[0].coinAmount,7); await reconcilePassOrder(db,'a','u');assert.equal(tables.users[0].coinAmount,7);assert.equal(tables.order[0].passGranted,true); }); test('legacy migration preserves claimed rewards and snapshot, clips unknown overflow',async()=>{ const legacy=JSON.stringify({1:{time:'0'},2:{time:String(A+PERIOD),progressLevel:31,progress:999,activate:true,free:[0,1],passCheck:[1,1]}});tables.users[0].passCheck=legacy; const r=await request('read');assert.equal(r.data.current.tier,2);assert.equal(r.data.current.overflow,0);assert.deepEqual(r.data.current.freeClaimed,[1]);assert.equal(state().legacySnapshot,legacy); }); test('malformed legacy and unresolved legacy payments block migration',async()=>{ tables.users[0].passCheck='invalid';await rejects(()=>request('read'),'LEGACY_STATE_INVALID');tables.users[0].passCheck=null; tables.order.push({openid:'o',itemid:'battlepass',state:1});await rejects(()=>request('read'),'LEGACY_ORDER_PENDING'); }); test('immutable anchor survives changes by legacy time API',async()=>{ await request('read');tables.idcount[0].passcheckTime=A+10*PERIOD;now=A+PERIOD+1;assert.equal((await request('read')).data.current.startsAt,A+PERIOD); }); test('signed callback authenticates event plus payload and rejects mock/tamper',()=>{ process.env.PASSCHECK_PAY_CALLBACK_KEY='test-key';const body={Event:'minigame_game_pay_goods_deliver_notify',MiniGame:{Payload:'{"Env":0}',IsMock:false}}; body.MiniGame.PayEventSig=createHmac('sha256','test-key').update(body.Event+'&'+body.MiniGame.Payload).digest('hex');verifyPassCallback(body); body.MiniGame.Payload='{"changed":true}';assert.throws(()=>verifyPassCallback(body),e=>e.reason==='PAYMENT_SIGNATURE_INVALID'); }); // Exercise the real cloud-function adapters, with provider transport and storage stubbed. const { registerHooks } = await import('node:module'); const { generateKeyPairSync } = await import('node:crypto'); const { privateKey } = generateKeyPairSync('rsa',{modulusLength:2048,privateKeyEncoding:{type:'pkcs8',format:'pem'},publicKeyEncoding:{type:'spki',format:'pem'}}); globalThis.__passCloud = { database:()=>db, shared:{get:()=>null,set(){}}, storage:{bucket:()=>({readFile:async()=>({Body:{transformToString:async()=>privateKey}})})} }; globalThis.require=()=>({initWithBatchMode:()=>({track(){},close(){}})}); registerHooks({ resolve(specifier,context,next) { if(specifier==='@lafjs/cloud')return {url:'data:text/javascript,export default globalThis.__passCloud',shortCircuit:true}; if(specifier==='@/jungleConfig')return {url:'data:text/javascript,export function getJunglePaidTierByProductId(){return null}',shortCircuit:true}; if(specifier==='@/Utils')return {url:'data:text/javascript,export default {checkToken:(a,b)=>a===b}',shortCircuit:true}; return next(specifier,context); }}); const {default: androidSign}=await import('../functions/wx/orderPaySig.ts'); const {default: iosSign}=await import('../functions/wx/iosorderPaySig.ts'); const {default: callback}=await import('../functions/wx/payCallBack.ts'); const {default: iosPoll}=await import('../functions/wx/iosgetPayInfo.ts'); const {default: legacyPass}=await import('../functions/passCheck.ts'); const {default: legacyLv}=await import('../functions/passCheckLv.ts'); const {default: acknowledge}=await import('../functions/wx/getOrderReward.ts'); const {default: api}=await import('../functions/passCheckV2.ts'); const originalFetch=globalThis.fetch; test.after(()=>{globalThis.fetch=originalFetch;}); test('Android and iOS adapters sign server price and create exactly one bound order',async()=>{ for(const sign of [androidSign,iosSign]) { reset();tables.users[0].session_key='test-session';await request('read'); const ctx={body:{uid:'u',token:'t',itemid:'premium-test',itemCount:999,itemPrice:1,passCheckVersion:2}}; const first=await sign(ctx);assert.equal(first.code,1);const data=JSON.parse(first.data.signData);assert.equal(data.goodsPrice,3000);assert.equal(data.buyQuantity,1); const again=await sign(ctx);assert.equal(again.data.outTradeNo,first.data.outTradeNo);assert.equal(tables.order.length,1);assert.equal(tables.order[0].passOrder.seasonId,String(A)); } }); test('callback rejects forged signatures then grants a valid signed payment once',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a'); const data={OutTradeNo:'a',OpenId:'o',Env:0,GoodsInfo:{ProductId:'premium-test',Quantity:1,OrigPrice:3000}}; const body={Event:'minigame_game_pay_goods_deliver_notify',MiniGame:{Payload:JSON.stringify(data),PayEventSig:'0'.repeat(64),IsMock:false}}; process.env.PASSCHECK_PAY_CALLBACK_KEY='test-key';assert.equal((await callback({body})).ErrCode,-1);assert.equal(state().seasons[0].tier,1); body.MiniGame.PayEventSig=createHmac('sha256','test-key').update(body.Event+'&'+body.MiniGame.Payload).digest('hex'); assert.equal((await callback({body})).ErrCode,0);assert.equal((await callback({body})).ErrCode,0);assert.equal(tables.users[0].coinAmount,7); }); test('iOS provider query validates payer and amount, including customer service orders',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'ios'},'a'); let payer='other';globalThis.fetch=async()=>({json:async()=>({trade_state:'SUCCESS',out_trade_no:'a',payer:{openid:payer},amount:{total:3000}})}); const ctx={body:{uid:'u',token:'t',outTradeNo:'a'}};assert.equal((await iosPoll(ctx)).data.reason,'PAYMENT_MISMATCH'); payer='o';assert.equal((await iosPoll(ctx)).code,1);assert.equal(state().seasons[0].tier,3); }); test('legacy saves and generic delivery cannot overwrite migrated users or unpaid orders',async()=>{ await request('read');const body={uid:'u',token:'t',action:'save',passCheck:'{}',passCheckLv:'{}'}; assert.equal((await legacyPass({body})).data.reason,'CLIENT_UPGRADE_REQUIRED');assert.equal((await legacyLv({body})).data.reason,'CLIENT_UPGRADE_REQUIRED'); await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a'); assert.equal((await acknowledge({body:{uid:'u',token:'t',outTradeNo:'a'}})).data.reason,'ORDER_UNPAID');assert.equal(tables.order[0].state,0); }); test('API returns stable error envelope for malformed requests',async()=>{ const r=await api({body:{uid:'u',token:'wrong',action:'read'}});assert.equal(r.code,0);assert.equal(r.data.reason,'TOKEN_INVALID'); }); test('grant receipt survives period cleanup when order finalize was interrupted',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a');failFinalize=true; await assert.rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'a',openid:'o',price:3000,quantity:1,productId:'premium-test'})); now=A+PERIOD*3;await reconcilePassOrder(db,'a','u');assert.equal(tables.users[0].coinAmount,7);assert.equal(tables.order[0].passGranted,true); }); test('provider-confirmed iOS closure releases a pending purchase without granting',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'ios'},'a'); globalThis.fetch=async()=>({json:async()=>({trade_state:'CLOSED',out_trade_no:'a'})}); assert.equal((await iosPoll({body:{uid:'u',token:'t',outTradeNo:'a'}})).data.reason,'ORDER_CLOSED'); await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'ios'},'b');assert.equal(state().seasons[0].tier,1); }); test('failed gift leaves paid order recoverable and no partial balance update',async()=>{ process.env.PASSCHECK_V2_CONFIG=JSON.stringify({...fixture,gift:[{field:'coinAmount',amount:7},{field:'userPowerTime',amount:60}]}); await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'and'},'a');tables.users[0].userPowerTime=Number.MAX_SAFE_INTEGER; await rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'a',openid:'o',price:3000,quantity:1,productId:'premium-test'}),'RESOURCE_OVERFLOW'); assert.equal(tables.users[0].coinAmount,0);assert.equal(state().seasons[0].tier,1);assert.equal(tables.order[0].passPaymentVerified,true); tables.users[0].userPowerTime=0;await reconcilePassOrder(db,'a','u');assert.equal(tables.users[0].coinAmount,7);assert.equal(state().seasons[0].tier,3); }); test('old lower-tier overflow and locked rewards never hide the new period',async()=>{ await request('read');change(s=>{const p=s.seasons[0];p.points=p.config.levels.reduce((a,r)=>a+r.token,0)+100;p.freeClaimed=Array.from({length:30},(_,i)=>i+1);}); now=A+PERIOD+1;const r=await request('read');assert.equal(r.data.displaySeasonId,String(A+PERIOD));assert.equal(r.data.previous,null); }); test('concurrent loop claims only consume one cursor even when both arrive together',async()=>{ await request('read');await buy();change(s=>s.seasons[0].points=s.seasons[0].config.levels.reduce((a,r)=>a+r.token,0)+6); await Promise.all([request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0}),request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0})]); assert.equal(state().seasons[0].loopClaimed,2);assert.equal(tables.users[0].coinAmount,17); }); test('customer service payment uses the reserved price and does not create a second order',async()=>{ const {default: kefu}=await import('../functions/wx/KeFuInfo.ts'); await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'premium-test',passChannel:'ios'},'a'); const sent=[];globalThis.__passCloud.shared.get=()=> 'test-access-token'; globalThis.fetch=async(url,options)=>{sent.push({url,body:JSON.parse(options.body)});return {ok:true,json:async()=>({prepay_id:'test-prepay'})};}; await kefu({body:{FromUserName:'o',SessionFrom:JSON.stringify({tpye:'ios',outTradeNo:'a',propName:'premium-test',count:50,price:1})}}); const charge=sent.find(r=>r.url.includes('/pay/transactions/jsapi'));assert.equal(charge.body.amount.total,3000);assert.equal(tables.order.length,1); assert.equal(sent.filter(r=>r.body.msgtype==='link').length,1);globalThis.__passCloud.shared.get=()=>null; }); test('missing callback verifier disables purchase before a user can pay',async()=>{ delete process.env.PASSCHECK_PAY_CALLBACK_KEY;assert.equal((await request('read')).data.purchaseOptions.length,0); await rejects(()=>preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'and'},'a'),'PAYMENT_VERIFIER_UNCONFIGURED');assert.equal(tables.order.length,0); }); test('level display preserves initial level and exact-boundary progress',async()=>{ const fresh=(await request('read')).data.current;assert.equal(fresh.level,1);assert.equal(fresh.levelProgress,0);assert.equal(fresh.unlocked,1); let p=(await win()).data.current;assert.equal(p.level,1);assert.equal(p.levelProgress,1); p=(await win()).data.current;assert.equal(p.level,2);assert.equal(p.levelProgress,1); }); test('second tier receives the shared gift once and never unlocks loops or double progress',async()=>{ await request('read');await buy('battlepass','base'); assert.equal(tables.users[0].coinAmount,7);assert.equal(state().seasons[0].giftGranted,true); await reconcilePassOrder(db,'base','u');assert.equal(tables.users[0].coinAmount,7); const p=(await win()).data.current;assert.equal(p.multiplier,1);assert.equal(p.points,1); change(s=>s.seasons[0].points=s.seasons[0].config.levels.reduce((sum,row)=>sum+row.token,0)+9); assert.equal((await request('read')).data.current.loopAvailable,0); await rejects(()=>request('claim_loop',{seasonId:String(A),expectedLoopClaimed:0}),'TIER_REQUIRED'); await buy('upgrade-test','upgrade');assert.equal(tables.users[0].coinAmount,7); const upgraded=(await win()).data.current;assert.equal(upgraded.multiplier,2);assert.equal(upgraded.overflow,11);assert.equal(upgraded.loopAvailable,3); }); test('base gift can be enabled before premium config, and stays fixed when premium opens',async()=>{ process.env.PASSCHECK_V2_CONFIG=JSON.stringify({gift:fixture.gift}); const r=await request('read');assert.deepEqual(r.data.purchaseOptions,[{productId:'battlepass',price:1800}]); await buy('battlepass','base');assert.equal(tables.users[0].coinAmount,7); process.env.PASSCHECK_V2_CONFIG=JSON.stringify({...fixture,gift:[{field:'coinAmount',amount:999}]}); await request('read');assert.deepEqual(state().seasons[0].config.gift,fixture.gift); await buy('upgrade-test','upgrade');assert.equal(tables.users[0].coinAmount,7); }); test('second-tier payment recovers after interrupted finalization without repeating the gift',async()=>{ await request('read');await preparePassOrder(db,{uid:'u',token:'t',itemid:'battlepass',passChannel:'and'},'base');failFinalize=true; await assert.rejects(()=>acceptPassPayment(db,tables.order[0],{outTradeNo:'base',openid:'o',price:1800,quantity:1,productId:'battlepass'})); assert.equal(tables.users[0].coinAmount,7);await reconcilePassOrder(db,'base','u');assert.equal(tables.users[0].coinAmount,7);assert.equal(state().seasons[0].tier,2); }); test('migration preserves explicit legacy gift receipt on premium upgrade',async()=>{ tables.users[0].passCheck=JSON.stringify({2:{time:String(A+PERIOD),progressLevel:1,progress:0,activate:true,free:[1],passCheck:[1],giftGranted:true}}); await request('read');await buy('upgrade-test','upgrade');assert.equal(tables.users[0].coinAmount,0);assert.equal(state().seasons[0].giftGranted,true); }); const oldPeriod = (end, overrides={}) => ({time:String(end),progressLevel:1,progress:1,activate:true,free:[1],passCheck:[1],...overrides}); test('migration keeps both old periods and all unpaid rewards claimable exactly once',async()=>{ const legacy=JSON.stringify({1:oldPeriod(A,{free:[0],passCheck:[1]}),2:oldPeriod(A+PERIOD)}); tables.users[0].passCheck=legacy; const r=await request('read');assert.equal(r.data.displaySeasonId,String(A-PERIOD));assert.equal(r.data.current.tier,2); assert.deepEqual(r.data.previous.freeClaimed,[1]);assert.deepEqual(r.data.previous.paidClaimed,[]); const claim={seasonId:r.data.previous.id,level:1,track:'paid'}; await Promise.all([request('claim_level',claim),request('claim_level',claim)]); assert.equal(tables.users[0].coinAmount,1500); const next=await request('read');assert.equal(next.data.displaySeasonId,String(A)); // Consuming the initial token also unlocks level 1 only; no unclaimed old rewards remain. assert.equal(next.data.previous,null); assert.equal(state().legacySnapshot,legacy);assert.equal(tables.users[0].passCheck,legacy); await request('claim_level',{seasonId:r.data.current.id,level:1,track:'paid'}); assert.equal(tables.users[0].coinAmount,3000); await request('claim_level',{seasonId:r.data.current.id,level:1,track:'free'}); assert.equal(tables.users[0].userPowerTime,Math.floor(now/1000)+900); }); test('expired legacy unpaid rewards block migration without changing the user or resources',async()=>{ for(const track of ['free','passCheck']) { reset();tables.users[0].passCheck=JSON.stringify({1:oldPeriod(A-PERIOD,{free:[0],passCheck:[0],[track]:[1]}),2:oldPeriod(A+PERIOD)}); const before=copy(tables.users[0]);await rejects(()=>request('read'),'LEGACY_REWARDS_PENDING');assert.deepEqual(tables.users[0],before); } }); test('expired fully claimed or unpurchased paid rewards do not block migration',async()=>{ for(const activate of [true,false]) { reset();tables.users[0].passCheck={1:oldPeriod(A-PERIOD,{activate,free:[0],passCheck:activate?[0]:[1]}),2:oldPeriod(A+PERIOD)}; const r=await request('read');assert.equal(r.data.previous,null);assert.equal(state().seasons.length,1); } }); test('legacy reward amounts and progress use the original table despite V2 overrides',async()=>{ const levels=copy(config().levels);levels[0]={token:100,free:[{field:'coinAmount',amount:1}],paid:[{field:'coinAmount',amount:1}]}; process.env.PASSCHECK_V2_CONFIG=JSON.stringify({...fixture,levels}); tables.users[0].passCheck={2:oldPeriod(A+PERIOD,{progressLevel:2,progress:1,free:[0,1],passCheck:[1,1]})}; const r=await request('read');assert.equal(r.data.current.points,2);assert.equal(r.data.current.level,2); await request('claim_level',{seasonId:r.data.current.id,level:1,track:'paid'});assert.equal(tables.users[0].coinAmount,1500); }); test('drifting periods and ambiguous entitlement flags cannot silently lose old rewards',async()=>{ for(const overrides of [{time:String(A+PERIOD+3600000)},{activate:'true'},{time:'0'}]) { reset();tables.users[0].passCheck={2:oldPeriod(A+PERIOD,overrides)};const before=copy(tables.users[0]); await assert.rejects(()=>request('read'),e=>['LEGACY_PERIOD_INVALID','LEGACY_STATE_INVALID'].includes(e.reason));assert.deepEqual(tables.users[0],before); } }); test('legacy concurrent claim during migration is re-read instead of granting it twice',async()=>{ tables.users[0].passCheck=JSON.stringify({2:oldPeriod(A+PERIOD)}); // Initialize only the shared anchor so the hook targets the user CAS. tables.idcount[0].passcheckV2Anchor=A; beforeUpdate=async(name)=>{assert.equal(name,'users');tables.users[0].passCheck=JSON.stringify({2:oldPeriod(A+PERIOD,{passCheck:[0]})});tables.users[0].coinAmount=1500;}; const r=await request('read');assert.deepEqual(r.data.current.paidClaimed,[1]); await request('claim_level',{seasonId:r.data.current.id,level:1,track:'paid'});assert.equal(tables.users[0].coinAmount,1500); }); test('unmigrated users keep the legacy interface while rollout is disabled',async()=>{ const legacy=JSON.stringify({2:oldPeriod(A+PERIOD)});tables.users[0].passCheck=legacy; process.env.PASSCHECK_V2_ENABLED='false';await rejects(()=>request('read'),'FEATURE_DISABLED'); const r=await legacyPass({body:{uid:'u',token:'t',action:'read'}}); assert.equal(r.code,1);assert.equal(r.data.passCheck,legacy);assert.equal(tables.users[0].passCheckV2,undefined); });