import test, { beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { registerHooks } from 'node:module'; const NOW = Date.parse('2026-09-20T14:20:00+08:00'); const DAY = 86400000; const players = []; let unavailable = false; let enabled = true; const clone = value => structuredClone(value); globalThis.__activityListCloud = { mongo: { db: { collection(name) { if (name === 'activityConfigs') return {find(query) { assert.equal(query.activityId,'cloudRise'); return {sort(){return this;},limit(){return this;},async toArray(){ if(unavailable)throw Error('simulated database outage'); return [{configVersion:'v1',enabled,config:{unlockLevel:100,durationHours:24,pools:[10000,15000,20000]}}]; }}; }}; assert.equal(name, 'users', 'CloudRise-only list must not query gold miner or orders'); return {async findOne(query) { if (unavailable) throw Error('simulated database outage'); return clone(players.find(p => p.onlyId === query.onlyId) || null); }}; } } } }; const hooks = registerHooks({ resolve(specifier, context, next) { assert(!specifier.includes('goldMiner'), 'unpublished gold miner modules must not be loaded'); if (specifier === '@lafjs/cloud') return {url:'activitylist:cloud',shortCircuit:true}; if (specifier === '@/Utils') return {url:'activitylist:utils',shortCircuit:true}; if (specifier.startsWith('@/')) return {url:new URL('../../'+specifier.slice(2)+'.ts',import.meta.url).href,shortCircuit:true}; return next(specifier,context); }, load(url,context,next) { if(url === 'activitylist:cloud') return {format:'module',source:'export default globalThis.__activityListCloud;',shortCircuit:true}; if(url === 'activitylist:utils') return {format:'module',source:'export default {checkToken(a,b){return a===b}};',shortCircuit:true}; return next(url,context); }, }); const {default:api,activityList} = await import('../list.ts'); hooks.deregister(); const realNow = Date.now; beforeEach(() => {players.length=0;unavailable=false;enabled=true;Date.now=()=>NOW;}); afterEach(() => {Date.now=realNow;}); function personalUser(id=1001) { return {onlyId:id,token:'token-'+id,cloudRisePeriod:{periodId:'personal-'+id,configVersion:'v1', startsAt:NOW-1000,endsAt:NOW+DAY,unlockLevel:100,pools:[10000,15000,20000],durationHours:24}, cloudRiseState:JSON.stringify({version:1,playedPeriods:[],run:null})}; } test('authenticated list returns only own CloudRise period and hides private state', async () => { players.push(personalUser()); const result = await api({body:{uid:'1001',token:'token-1001'}}); assert.equal(result.code,1); assert.deepEqual(result.data.activities,[{ activityId:'cloudRise',periodId:'personal-1001',configVersion:'v1',startsAt:NOW-1000, endsAt:NOW+DAY,unlockLevel:100,phase:'active'}]); assert(!JSON.stringify(result).includes('token-')); assert(!JSON.stringify(result).includes('cloudRiseState')); }); test('anonymous list exposes neither personal CloudRise nor gold miner', async () => { players.push(personalUser()); const result = await api({}); assert.equal(result.code,1); assert.deepEqual(result.data.activities,[]); }); test('list returns opening facts without starting a countdown and respects disabled templates',async()=>{ const user={onlyId:1001,token:'token-1001'};players.push(user); const first=await api({body:{uid:1001,token:user.token}}); assert.deepEqual(first.data.activities,[]); assert.deepEqual(first.data.openingRules,[{activityId:'cloudRise',enabled:true,unlockLevel:100,cooldownMs:43200000,lastEndedAt:null,hasOpenPeriod:false}]); assert.equal(user.cloudRisePeriod,undefined); Object.assign(user,personalUser()); user.cloudRiseState=JSON.stringify({run:{periodId:user.cloudRisePeriod.periodId,status:'failed',stages:[{endedAt:NOW-60000}]}}); const failed=await api({body:{uid:1001,token:user.token}}); assert.equal(failed.data.openingRules[0].lastEndedAt,NOW-60000); enabled=false; assert.equal((await api({body:{uid:1001,token:user.token}})).data.openingRules[0].enabled,false); }); test('list validates account identity and never returns another player period', async () => { players.push(personalUser(1001),personalUser(2002)); for (const body of [{uid:'1001',token:'wrong'},{uid:'2002',token:'token-1001'},{uid:'1x',token:'token-1001'}]) { const result = await api({body});assert.equal(result.code,0);assert.equal(result.data,null); } const result = await api({body:{uid:2002,token:'token-2002'}}); assert.equal(result.data.activities[0].periodId,'personal-2002'); }); test('list neither opens nor extends personal periods and excludes ended challenges', async () => { const user=personalUser();players.push(user);const before=clone(user); await api({body:{uid:1001,token:user.token}});assert.deepEqual(user,before); for (const status of ['failed','completed','expired']) { user.cloudRiseState=JSON.stringify({run:{periodId:user.cloudRisePeriod.periodId,status}}); assert.deepEqual((await activityList(NOW,user)).activities,[]); } user.cloudRiseState=JSON.stringify({run:{periodId:'older',status:'failed'}}); assert.equal((await activityList(NOW,user)).activities.length,1); delete user.cloudRisePeriod;user.cloudRiseState='{}'; assert.deepEqual((await activityList(NOW,user)).activities,[],'polling must not create a period'); }); test('personal boundaries are inclusive at start, exclusive at end and update refreshAt', async () => { const user=personalUser();user.cloudRisePeriod.startsAt=NOW;user.cloudRisePeriod.endsAt=NOW+5000; assert.equal((await activityList(NOW,user)).refreshAt,NOW+5000); assert.equal((await activityList(NOW,user)).activities.length,1); assert.equal((await activityList(NOW-1,user)).activities.length,0); assert.equal((await activityList(NOW+5000,user)).activities.length,0); }); test('active legacy challenges remain visible without creating a new period', async () => { const user={cloudRiseState:JSON.stringify({run:{periodId:'legacy',startedAt:NOW-1000,expiresAt:NOW+5000,status:'playing'}})}; const result=await activityList(NOW,user);assert.equal(result.activities[0].periodId,'legacy'); assert.equal(result.activities[0].endsAt,NOW+5000);assert.equal(user.cloudRisePeriod,undefined); }); test('empty successful lists and user database failures have distinct responses', async () => { assert.deepEqual((await api({})).data.activities,[]);unavailable=true; const error=console.error;console.error=()=>{}; try { const result=await api({body:{uid:1001,token:'token-1001'}}); assert.equal(result.code,0);assert.equal(result.data,null); } finally {console.error=error;} }); test('existing gold miner state cannot expose gold miner even in test mode', async () => { const oldEnv=process.env.PAYMENT_APP_ENV, oldFlag=process.env.GOLD_MINER_TEST_PERIODS_ENABLED; try { process.env.PAYMENT_APP_ENV='test';process.env.GOLD_MINER_TEST_PERIODS_ENABLED='true'; const user={...personalUser(),goldMiner:{periodId:'goldMiner:test:catalog'}}; players.push(user); const result=await api({body:{uid:1001,token:user.token}}); assert.deepEqual(result.data.activities.map(a=>a.activityId),['cloudRise']); } finally { if(oldEnv===undefined) delete process.env.PAYMENT_APP_ENV; else process.env.PAYMENT_APP_ENV=oldEnv; if(oldFlag===undefined) delete process.env.GOLD_MINER_TEST_PERIODS_ENABLED; else process.env.GOLD_MINER_TEST_PERIODS_ENABLED=oldFlag; } });