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; 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 === 'goldMiner') return { sort() { return this; }, limit() { return this; }, async toArray() { if (goldUnavailable) throw Error('simulated gold miner database outage'); return clone(goldConfigs.filter(row => row.recordType === query.recordType && row.status === query.status && row.effectiveFrom <= query.effectiveFrom.$lte && row.publishedAt <= query.publishedAt.$lte) .sort((a, b) => b.effectiveFrom - a.effectiveFrom || b.publishedAt - a.publishedAt).slice(0, 2)); } }; 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] } }]; } }; }, async findOne(query) { if (goldUnavailable) throw Error('simulated gold miner database outage'); return clone(goldConfigs.find(row => Object.entries(query).every(([key, value]) => row[key] === value)) || null); } }; if (name === 'goldMinerPlayerPeriods') return { async findOne(query) { return clone(goldPlayers.find(row => row._id === query._id) || null); } }; assert.equal(name, 'users', 'listing must not query orders or rewards'); 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) { 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 G = await import('../../goldMiner/config.ts'); const S = await import('../../goldMiner/service.ts'); hooks.deregister(); const realNow = Date.now; const oldEnv = process.env.PAYMENT_APP_ENV, oldFlag = process.env.GOLD_MINER_TEST_PERIODS_ENABLED; beforeEach(() => { players.length = 0; goldConfigs.length = 0; goldPlayers.length = 0; goldUnavailable = false; unavailable = false; enabled = true; Date.now = () => NOW; process.env.PAYMENT_APP_ENV = 'production'; delete process.env.GOLD_MINER_TEST_PERIODS_ENABLED; }); afterEach(() => { Date.now = realNow; 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 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 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; } }); function publishGold(enabled = true) { const effectiveFrom = Date.parse('2026-09-17T00:00:00+08:00'); goldConfigs.push({ _id: 'gold-v1', activityId: 'goldMiner', recordType: 'version', status: 'published', configVersion: 'goldV1', effectiveFrom, publishedAt: effectiveFrom - 1000, enabled, config: { effectiveFromPeriodId: 'goldMiner:2026-09-17', unlockPassedLevel: 41, timezone: 'Asia/Shanghai', productId: 'gold_miner', priceFen: 300, currency: 'CNY', tasks: [{ taskId: 'task1', sequence: 1, targetWins: 3, items: [{ type: 'coin', count: 100 }] }] } }); } function temporary(startsAt, endsAt) { const periodId = 'goldMiner:test:catalog'; goldConfigs.push({ _id: G.key('goldMinerTestSchedule'), activityId: 'goldMiner', recordType: 'testSchedule', periods: [{ periodId, startsAt, endsAt, enabled: true, configVersion: 'temporaryV1', configSnapshot: { unlockPassedLevel: 41 } }] }); return periodId; } test('registered gold miner is public and coexists with personal CloudRise without writes or private fields', async () => { publishGold(); const user = { ...personalUser(), _id: 'u1' }; players.push(user); const before = clone({ players, goldConfigs, goldPlayers }); const anonymous = await api({}); assert.deepEqual(anonymous.data.activities, [{ activityId: 'goldMiner', periodId: 'goldMiner:2026-09-17', configVersion: 'goldV1', startsAt: G.calendar(NOW).startsAt, endsAt: G.calendar(NOW).endsAt, unlockLevel: 41, phase: 'active', purchaseEnabled: true }]); const loggedIn = await api({ body: { uid: 1001, token: user.token } }); assert.deepEqual(loggedIn.data.activities.map(a => a.activityId), ['cloudRise', 'goldMiner']); assert.equal(loggedIn.data.openingRules.length, 1); assert.deepEqual({ players, goldConfigs, goldPlayers }, before); }); test('gold miner uses seven open days per fortnight and nearest upcoming period during cooldown', async () => { publishGold(); const start = G.FORTNIGHT_START; for (const [now, id, phase] of [[start, '2026-10-01', 'active'], [start + 7 * DAY - 1, '2026-10-01', 'active'], [start + 7 * DAY + 1, '2026-10-15', 'upcoming'], [start + 14 * DAY, '2026-10-15', 'active']]) { const result = await activityList(now); assert.equal(result.activities.length, 1); assert.equal(result.activities[0].periodId, 'goldMiner:' + id); assert.equal(result.activities[0].phase, phase); assert.equal(result.activities[0].endsAt - result.activities[0].startsAt, 7 * DAY); } assert.deepEqual((await activityList(start + 7 * DAY)).activities, [], 'next start exactly at lookahead right edge is excluded'); assert.equal((await activityList(start + 7 * DAY - 1)).refreshAt, start + 7 * DAY); }); test('closed gold miner remains visible only to existing participants or assigned entitlements', async () => { publishGold(false); const user = { ...personalUser(), _id: 'u1' }; players.push(user); assert.deepEqual((await api({})).data.activities, []); assert.deepEqual((await activityList(NOW, user)).activities.map(a => a.activityId), ['cloudRise']); const row = { _id: S.playerId(user._id, 'goldMiner:2026-09-17'), tasks: [], configVersion: 'goldV1', configSnapshot: { unlockPassedLevel: 41 } }; goldPlayers.push(row); assert.equal((await api({ body: { uid: 1001, token: user.token } })).data.activities[1].activityId, 'goldMiner'); delete row.tasks; row.entitlement = { state: 'reserved' }; assert.equal((await activityList(NOW, user)).activities.length, 2); row.entitlement.state = 'manual_review'; assert.equal((await activityList(NOW, user)).activities.length, 1); }); test('gold miner purchase control does not hide its activity', async () => { publishGold(); goldConfigs.push({ _id: S.controlId('goldMiner:2026-09-17'), activityId: 'goldMiner', recordType: 'control', purchaseEnabled: false }); const activity = (await activityList(NOW)).activities[0]; assert.equal(activity.purchaseEnabled, false); }); test('test periods take precedence only in enabled test environments and refresh at their boundaries', async () => { publishGold(); const id = temporary(NOW + 5000, NOW + 15000); process.env.PAYMENT_APP_ENV = 'test'; process.env.GOLD_MINER_TEST_PERIODS_ENABLED = 'true'; const before = await activityList(NOW); assert.equal(before.activities[0].periodId, 'goldMiner:2026-09-17'); assert.equal(before.refreshAt, NOW + 5000); const active = await activityList(NOW + 5000); assert.equal(active.activities[0].periodId, id); assert.equal(active.refreshAt, NOW + 15000); assert.equal((await activityList(NOW + 15000)).activities[0].periodId, 'goldMiner:2026-09-17'); process.env.PAYMENT_APP_ENV = 'production'; assert.equal((await activityList(NOW + 5000)).activities[0].periodId, 'goldMiner:2026-09-17'); }); test('gold miner query failures return failure instead of a misleading partial successful list', async () => { goldUnavailable = true; const error = console.error; console.error = () => { }; try { const result = await api({}); assert.equal(result.code, 0); assert.equal(result.data, null); } finally { console.error = error; } });