223 lines
12 KiB
JavaScript
223 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, UNLOCK_LEVEL } = 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: UNLOCK_LEVEL, 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();
|
|
user.levelAmount = UNLOCK_LEVEL;
|
|
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);
|
|
});
|