Files
server/laf-cloud/functions/activityConfig/tests/list.test.mjs
T

222 lines
12 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 = [];
const goldConfigs = [], goldPlayers = [];
let goldUnavailable = false;
const pyramidConfigs = [], pyramidPlayers = [];
function cursor(rows, query) {
const get = (row, path) => path.split('.').reduce((value, key) => value?.[key], row);
let selected = rows.filter(row => Object.entries(query).every(([key, value]) => {
const current = get(row, key);
return value && typeof value === 'object' ? Object.entries(value).every(([op, v]) =>
op === '$lte' ? current <= v : op === '$gt' ? current > v : false) : current === value;
}));
return {
sort(order) {
selected.sort((a, b) => {
for (const [key, dir] of Object.entries(order)) {
if (get(a, key) !== get(b, key)) return (get(a, key) < get(b, key) ? -1 : 1) * dir;
} return 0;
}); return this;
}, limit(n) { selected = selected.slice(0, n); return this; }, async toArray() { return clone(selected); }
};
}
let unavailable = false;
let enabled = true;
const clone = value => structuredClone(value);
globalThis.__activityListCloud = {
mongo: {
db: {
collection(name) {
if (name === 'activityConfigs') return {
find(query) {
if (query.activityId === 'pyramid') return cursor(pyramidConfigs, 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] } }];
}
};
}
};
if (name === 'pyramid_players') return {
find(query) { return cursor(pyramidPlayers, query); },
async findOne(query) { return clone(pyramidPlayers.find(p => p._id === query._id) || null); }
};
assert.equal(name, 'users', 'list must not query gold miner or orders');
return {
async findOne(query) {
if (unavailable) throw Error('simulated database outage');
return clone(players.find(p => query._id ? p._id === query._id : p.onlyId === query.onlyId) || null);
}
};
}
}
}
};
const hooks = registerHooks({
resolve(specifier, context, next) {
// These cases exercise CloudRise and Pyramid; no GoldMiner config is installed.
if (specifier === '@/goldMiner/service') return { url: 'data:text/javascript,export const periodById=async()=>null,periodForPlayer=async()=>null;', shortCircuit: true };
if (specifier === '@/goldMiner/testPeriods') return { url: 'data:text/javascript,export const testPeriods=async()=>[];', shortCircuit: true };
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');
const { initial } = await import('../../pyramid/api.ts');
hooks.deregister();
const realNow = Date.now;
beforeEach(() => { players.length = 0; pyramidConfigs.length = 0; pyramidPlayers.length = 0; unavailable = false; enabled = true; Date.now = () => NOW; });
afterEach(() => { Date.now = realNow; });
function personalUser(id = 1001) {
return {
_id: 'user-' + id, onlyId: id, levelAmount: 50, 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 hides personal CloudRise and gold miner without configuration', 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;
}
});
function pyramidConfig(startAt = NOW - 1000, endAt = NOW + DAY) {
return {
periodId: 'pyramid:scheduled', configVersion: 'pyramid-v1', environment: process.env.PAYMENT_APP_ENV || 'production', startAt, endAt,
levels: Array.from({ length: 6 }, (_, i) => ({
level: i + 1, itemId: i < 5 ? 'pyramid_one' : null, itemPrice: i < 5 ? 100 : null,
rewards: [{ rewardId: 'r' + i, weight: 1, up: i < 5, goods: [{ id: 1001, count: 100 }] }]
}))
};
}
function publishPyramid(config = pyramidConfig()) {
const row = {
_id: 'pyramid-v1', activityId: 'pyramid', recordType: 'version', status: 'published', configVersion: config.configVersion,
enabled: true, effectiveFrom: config.startAt, publishedAt: NOW - 2000, config
}; pyramidConfigs.push(row); return row;
}
test('public list includes pyramid schedule but does not create player progress', async () => {
const row = publishPyramid(pyramidConfig(NOW + 5000, NOW + DAY));
let result = await api({}); assert.equal(result.code, 1); assert.deepEqual(result.data.activities, [{
activityId: 'pyramid', periodId: row.config.periodId, configVersion: row.configVersion, startsAt: NOW + 5000, endsAt: NOW + DAY, unlockLevel: 50, phase: 'upcoming'
}]);
assert.equal(result.data.refreshAt, NOW + 5000); assert.equal(pyramidPlayers.length, 0);
row.enabled = false; assert.deepEqual((await api({})).data.activities, []);
});
test('disabled pyramid is visible only to its existing participant identified by users._id', async () => {
const user = personalUser(); players.push(user); const row = publishPyramid();
pyramidPlayers.push(initial(user._id, row.config));
row.enabled = false; assert.deepEqual((await api({})).data.activities, []);
const own = await api({ body: { uid: user.onlyId, token: user.token } });
assert.deepEqual(own.data.activities.map(a => a.activityId), ['cloudRise', 'pyramid']);
const other = personalUser(2002); players.push(other);
assert.deepEqual((await api({ body: { uid: other.onlyId, token: other.token } })).data.activities.map(a => a.activityId), ['cloudRise']);
assert.equal(pyramidPlayers.length, 1);
});