server/laf-cloud/functions/activityConfig/tests/list.test.mjs
2026-09-24 17:59:52 +08:00

141 lines
7.3 KiB
JavaScript

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;
}
});