server/laf-cloud/functions/goldMiner/tests/gold-miner-postman.test.mjs

435 lines
27 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { Script, createContext } from 'node:vm';
const read = name => JSON.parse(readFileSync(new URL(`../postman/${name}`, import.meta.url), 'utf8'));
const collection = read('gold-miner.postman_collection.json');
const template = read('gold-miner.postman_environment.json');
const requests = collection.item.flatMap(folder => folder.item);
const request = (folder, prefix) => {
const value = collection.item[folder - 1].item.find(item => item.name.startsWith(prefix));
assert.ok(value, `${folder}: ${prefix}`);
return value;
};
const script = (item, listen) => item.event.find(event => event.listen === listen)?.script.exec.join('\n') || '';
const copy = value => value === undefined ? value : JSON.parse(JSON.stringify(value));
const SKIP = Symbol('skip request');
// Executes the exported scripts without HTTP, Postman packages or real credentials.
function sandbox(values = {}) {
const env = new Map(template.values.map(({ key, value }) => [key, value]));
Object.entries({ baseUrl: 'https://test.invalid', uid: 'test-user', token: 'test-token', ...values })
.forEach(([key, value]) => env.set(key, String(value)));
let local = new Map(), counter = 0, response = {}, responseStatus = 200, failures = [];
const replace = value => value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
if (key === '$guid') return `00000000-0000-4000-8000-${String(++counter).padStart(12, '0')}`;
assert.ok(local.has(key) || env.has(key), `unresolved variable: ${key}`);
return local.has(key) ? local.get(key) : env.get(key);
});
const pm = {
environment: { get: key => env.get(key), set: (key, value) => env.set(key, String(value)), unset: key => env.delete(key) },
variables: { get: key => local.get(key) ?? env.get(key), set: (key, value) => local.set(key, String(value)), replaceIn: replace },
execution: { skipRequest() { throw SKIP; } },
response: { json: () => copy(response), to: { have: { status: status => assert.equal(status, responseStatus) } } },
expect: actual => ({ to: { eql: expected => assert.deepEqual(copy(actual), copy(expected)) } }),
test(name, callback) { try { callback(); } catch (error) { failures.push(new Error(`${name}: ${error.message}`)); } },
};
function run(code) { new Script(code).runInContext(createContext({ pm, console: { error() { } } })); }
return {
env,
pre(item) {
local = new Map();
try {
run(script(collection, 'prerequest'));
run(script(item, 'prerequest'));
return { body: item.request.body ? JSON.parse(replace(item.request.body.raw)) : undefined };
} catch (error) { if (error === SKIP) return { skipped: true }; throw error; }
},
post(item, payload, status = 200) {
response = payload; responseStatus = status; failures = [];
run(script(item, 'test'));
if (failures.length) throw new AggregateError(failures, failures.map(error => error.message).join('\n'));
},
};
}
const periodId = 'goldMiner:2026-09-17';
const startsAt = Date.parse('2026-09-17T00:00:00+08:00');
function info(overrides = {}) {
const d = {
status: 'unlocked', periodId, startsAt, endsAt: startsAt + 4 * 86400000,
serverTime: startsAt + 1000, productId: 'gold_miner', priceFen: 123,
progressWins: 5, maxTarget: 5, ...overrides,
};
d.tasks = [3, 5].map((targetWins, index) => ({
taskId: `task_${index + 1}`, targetWins, itemsSnapshot: [{ type: 'coin', count: 50 }],
progress: Math.min(d.progressWins, targetWins), completed: d.progressWins >= targetWins,
claimStatus: d.progressWins >= targetWins ? d.status === 'unlocked' ? 'claimable' : 'pending_unlock' : 'locked',
}));
return d;
}
const success = data => ({ code: 1, data, msg: '成功' });
const grant = () => ({ grantId: 'grant-1', taskId: 'task_1', items: [{ type: 'coin', count: 50 }], claimStatus: 'issuing', claimedThrough: 0, requiresClientDelivery: true });
test('collection, environment and every script are well formed', () => {
assert.equal(collection.info.schema, 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json');
assert.equal(requests.length, 48);
assert.equal(new Set(template.values.map(value => value.key)).size, template.values.length);
for (const key of ['token', 'adminToken', 'gmWinBody', 'legacySessionFrom']) {
const value = template.values.find(value => value.key === key);
assert.equal(value.type, 'secret'); assert.equal(value.value, '');
}
for (const item of [collection, ...requests]) {
for (const event of item.event) new Script(event.script.exec.join('\n'), { filename: `${item.name || 'collection'}:${event.listen}` });
}
for (const item of requests) {
assert.ok(['POST', 'GET'].includes(item.request.method));
if (item.request.method === 'POST') {
assert.equal(item.request.body.mode, 'raw');
assert.equal(item.request.header[0].value, 'application/json');
} else {
assert.equal(item.request.url.raw, '{{baseUrl}}/activityConfig/list');
assert.equal(item.request.body, undefined);
}
assert.equal(item.request.url.raw, `{{baseUrl}}/${item.request.url.path.join('/')}`);
const functionName = item.request.url.path.join('/');
assert.ok(readFileSync(new URL(`../../${functionName}.ts`, import.meta.url), 'utf8').length > 0);
if (functionName.startsWith('goldMiner')) {
assert.ok(['goldMiner/index', 'goldMiner/admin', 'goldMiner/merchantNotify'].includes(functionName), 'activity requests must use the current public routes');
const yaml = readFileSync(new URL(`../../${functionName}.yaml`, import.meta.url), 'utf8');
assert.match(yaml, /^name: (.+)$/m);
assert.equal(yaml.match(/^name: (.+)$/m)[1].trim(), functionName);
assert.match(yaml, /^\s+- POST\s*$/m);
}
assert.ok(script(item, 'test').includes('pm.test('));
if (item.request.body && !item.request.body.raw.startsWith('{{')) JSON.parse(item.request.body.raw);
}
});
test('unconfigured application is skipped without producing a request', () => {
const s = sandbox({ baseUrl: 'https://<APPID>.laf.run' });
assert.deepEqual(s.pre(request(1, '01')), { skipped: true });
});
test('changing account clears grants, progress payload and selected targets', () => {
const s = sandbox(); s.pre(request(1, '01'));
for (const key of ['gmWinBody', 'gmGrant', 'grantId', 'gmSavedGrantId', 'claimPeriodId', 'claimTaskId', 'outTradeNo']) s.env.set(key, 'stale');
s.env.set('uid', 'another-user');
assert.deepEqual(s.pre(request(1, '01')), { skipped: true });
for (const key of ['gmWinBody', 'gmGrant', 'grantId', 'gmSavedGrantId', 'claimPeriodId', 'claimTaskId', 'outTradeNo']) assert.equal(s.env.get(key), undefined);
assert.equal(s.pre(request(1, '01')).body.uid, 'another-user');
});
test('active info enforces shared progress and independent task claimability', () => {
const s = sandbox(); const item = request(1, '01');
s.post(item, success(info()));
assert.equal(s.env.get('periodId'), periodId);
assert.equal(s.env.get('secondTaskId'), 'task_2');
const broken = info(); broken.tasks[1].claimStatus = 'locked';
assert.throws(() => s.post(item, success(broken)), /活动状态结构/);
});
test('locked and unavailable responses accept missing task/product fields', () => {
const s = sandbox(); const item = request(1, '01');
s.post(item, success(info()));
s.post(item, success({ status: 'locked', serverTime: startsAt, periodId, tasks: [] }));
assert.equal(s.env.get('productId'), undefined);
assert.equal(s.env.get('periodId'), periodId);
s.post(item, success({ status: 'unavailable', serverTime: startsAt, tasks: [] }));
assert.equal(s.env.get('periodId'), undefined);
});
test('carry opening assertions check unlocked status and reset progress without exposing source', () => {
const s = sandbox({ expectedStatus: 'unlocked', expectedProgressWins: '0' });
const d = info({ progressWins: 0 });
s.post(request(1, '01'), success(d));
d.status = 'purchasable';
assert.throws(() => s.post(request(1, '01'), success(d)), /活动状态结构/);
});
test('info scripts reject leaked fields and preserve issuing as the only pending task', () => {
const s = sandbox(); const item = request(1, '01');
const d = info(); d.tasks[0].claimStatus = 'issuing';
s.post(item, success(d));
d.tasks[0].grantId = 'internal';
assert.throws(() => s.post(item, success(d)), /活动状态结构/);
delete d.tasks[0].grantId; d.entitlement = { status: 'unlocked' };
assert.throws(() => s.post(item, success(d)), /活动状态结构/);
});
test('purchase disabled info permits progress but blocks native and customer preorders', () => {
const s = sandbox({ currentMainLevel: 41, mainLevelAfterWin: 42 });
s.post(request(1, '01'), success(info({ status: 'purchase_disabled', progressWins: 0 })));
assert.equal(s.pre(request(2, '01')).body.levelAmount, 42);
assert.deepEqual(s.pre(request(3, '01')), { skipped: true });
assert.deepEqual(s.pre(request(7, '01')), { skipped: true });
});
test('new win creates one payload, retry reuses it even after current period changes', () => {
const s = sandbox({ periodId, currentMainLevel: 41, mainLevelAfterWin: 42, gmInfo: JSON.stringify(info({ progressWins: 0 })) });
const first = s.pre(request(2, '01')).body;
assert.equal(first.levelAmount, 42); assert.equal(first.goldMiner.clearedMainLevel, 42);
s.post(request(2, '01'), success({ levelAmount: 42, goldMiner: { code: 1, activityCounted: true, duplicate: false, progressWins: 1 } }));
s.env.set('periodId', 'goldMiner:2026-09-24'); s.env.set('token', 'renewed-token');
const retried = s.pre(request(2, '03')).body;
assert.deepEqual(retried.goldMiner, first.goldMiner);
assert.equal(retried.token, 'renewed-token');
s.post(request(2, '03'), success({ goldMiner: { code: 1, activityCounted: false, duplicate: true, progressWins: 1 } }));
const conflict = s.pre(request(2, '04')).body;
assert.equal(conflict.goldMiner.eventId, first.goldMiner.eventId);
assert.equal(conflict.levelAmount, first.levelAmount);
assert.equal(conflict.goldMiner.clearedMainLevel, 43);
assert.deepEqual(JSON.parse(s.env.get('gmWinBody')), first);
});
test('progress tests inspect nested failure, not only successful save envelope', () => {
const s = sandbox({ gmWinBefore: 0 });
assert.throws(() => s.post(request(2, '01'), success({ goldMiner: { code: 0, errorCode: 'PERIOD_ENDED' } })), /活动子结果与计数/);
s.post(request(2, '05'), success({ goldMiner: { code: 0, errorCode: 'PERIOD_ENDED' } }));
assert.throws(() => s.post(request(2, '05'), success({ goldMiner: { code: 1, progressWins: 1 } })), /不补记过期进度/);
});
test('qualification and capped events assert no progress, endless preserves main save', () => {
const s = sandbox({ periodId, currentMainLevel: 40, mainLevelAfterWin: 41, expectedProgressResult: 'qualification', gmInfo: JSON.stringify({ status: 'locked', periodId }) });
assert.equal(s.pre(request(2, '01')).body.levelAmount, 41);
s.post(request(2, '01'), success({ levelAmount: 41, goldMiner: { code: 1, activityCounted: false, reason: 'qualification_level', progressWins: 0 } }));
s.env.set('expectedProgressResult', 'capped'); s.env.set('endlessSequence', '999');
s.env.set('gmInfo', JSON.stringify(info()));
const endless = s.pre(request(2, '02')).body;
assert.equal(endless.isWuXian, 'true'); assert.equal(endless.levelAmount, 41);
assert.equal(endless.goldMiner.endlessSequence, 999);
s.post(request(2, '02'), success({ levelAmount: 41, goldMiner: { code: 1, activityCounted: false, duplicate: false, progressWins: 5 } }));
s.env.set('mainLevelAfterWin', '42');
assert.deepEqual(s.pre(request(2, '02')), { skipped: true });
});
test('order creation captures snapshot pricing and reuses original order identity', () => {
const s = sandbox();
s.post(request(1, '01'), success(info({ status: 'purchasable' })));
const first = s.pre(request(3, '01')).body;
assert.equal(first.itemPrice, 1); assert.equal(first.itemCount, 999);
s.post(request(3, '01'), success({ outTradeNo: 'order-1', signData: JSON.stringify({ productId: 'gold_miner', buyQuantity: 1, goodsPrice: 123, outTradeNo: 'order-1' }), signature: 'signature', paySig: 'paySig' }));
const retry = s.pre(request(3, '03')).body;
assert.notEqual(first.createRequestId, retry.createRequestId);
s.post(request(3, '03'), success({ outTradeNo: 'order-1' }));
assert.throws(() => s.post(request(3, '03'), success({ outTradeNo: 'order-2' })), /复用原订单/);
});
test('payment assertions distinguish pending, paid and late-payment routes', () => {
const s = sandbox({ outTradeNo: 'order-1' });
const items = collection.item[2].item.filter(item => item.name.startsWith('查单'));
for (const item of items) {
assert.equal(s.pre(item).body.outTradeNo, 'order-1');
s.post(item, { code: 0, errorCode: 'PAYMENT_PENDING' });
assert.throws(() => s.post(item, success({ pay_state: 2 })), /查单匹配指定状态/);
}
s.env.set('expectedPaymentState', 'paid');
assert.deepEqual(s.pre(items[0]), { skipped: true });
s.env.set('expectedTargetPeriodId', 'goldMiner:2026-09-24'); s.env.set('expectedFulfillmentRoute', 'carry_next');
const paid = success({ pay_state: 2, rewardDelivery: 'goldMiner.claim', goldMiner: { fulfillmentStatus: 'fulfilled', fulfillmentRoute: 'carry_next', targetPeriodId: 'goldMiner:2026-09-24' } });
for (const item of items) s.post(item, paid);
paid.data.goldMiner.targetPeriodId = periodId;
assert.throws(() => s.post(items[0], paid), /查单匹配指定状态/);
});
test('claim-save-confirm chain forbids premature confirmation and duplicate credit', () => {
const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' });
s.pre(request(4, '01')); s.post(request(4, '01'), success(grant()));
assert.deepEqual(s.pre(request(4, '05')), { skipped: true });
s.post(request(4, '02'), success(grant()));
assert.throws(() => s.post(request(4, '02'), success({ ...grant(), grantId: 'grant-2' })), /重复领取无新奖励凭证/);
s.post(request(4, '03'), success({ coinAmount: 100 }));
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
s.env.set('coinAfterDelivery', '149');
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
s.env.set('coinAfterDelivery', '150');
assert.equal(s.pre(request(4, '04')).body.coinAmount, '150');
s.post(request(4, '04'), success({ coinAmount: 150 }));
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
assert.equal(s.pre(request(4, '05')).body.grantId, 'grant-1');
s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 1 }));
s.pre(request(4, '05')); s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 1 }));
assert.throws(() => s.post(request(4, '05'), success({ grantId: 'wrong-grant', claimStatus: 'claimed', claimedThrough: 2 })), /确认同一凭证/);
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
});
test('failed coin save cannot enable delivery confirmation', () => {
const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' });
s.pre(request(4, '01')); s.post(request(4, '01'), success(grant()));
assert.throws(() => s.post(request(4, '04'), { code: 0, data: null }), /业务成功/);
assert.equal(s.env.get('gmSavedGrantId'), undefined);
assert.deepEqual(s.pre(request(4, '05')), { skipped: true });
});
test('selected historical task cannot use another task grant', () => {
const s = sandbox({ claimPeriodId: periodId, claimTaskId: 'task_1' });
s.pre(request(4, '01')); s.post(request(4, '01'), success(grant()));
s.env.set('claimTaskId', 'task_2');
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
assert.deepEqual(s.pre(request(4, '05')), { skipped: true });
});
test('empty settlement page retains cursor; terminal page blocks further requests', () => {
const s = sandbox();
s.post(request(5, '01'), success({ status: 'pending_delivery', items: [], nextCursor: 'history-cursor' }));
assert.equal(s.pre(request(5, '02')).body.afterId, 'history-cursor');
s.post(request(5, '02'), success({ status: 'pending_delivery', items: [], nextCursor: null }));
assert.deepEqual(s.pre(request(5, '02')), { skipped: true });
});
test('admin publication blocks blank commercial values; purchase flag is a JSON boolean', () => {
const s = sandbox({ adminToken: 'test-admin', adminPeriodId: periodId });
assert.deepEqual(s.pre(request(6, '02')), { skipped: true });
s.env.set('configJson', JSON.stringify({ priceFen: null, tasks: [] }));
assert.deepEqual(s.pre(request(6, '02')), { skipped: true });
s.env.set('configJson', JSON.stringify({ configVersion: 'test-v1', priceFen: 123, tasks: [{ taskId: 'test' }] }));
assert.equal(s.pre(request(6, '02')).body.config.priceFen, 123);
assert.deepEqual(s.pre(request(6, '03')), { skipped: true });
s.env.set('purchaseEnabled', 'false');
assert.equal(s.pre(request(6, '03')).body.purchaseEnabled, false);
});
test('legacy collection preserves scoped session ticket and gates real customer messages', () => {
const s = sandbox({ openid: 'openid' });
s.pre(request(1, '01'));
s.post(request(1, '01'), success(info({ status: 'purchasable' })));
assert.equal(s.pre(request(7, '01')).body.channel, 'legacy_ios');
const data = {
outTradeNo: 'order-1', periodId, priceFen: 123, productId: 'gold_miner', quantity: 1, channel: 'legacy_ios',
sessionFrom: JSON.stringify({ outTradeNo: 'order-1', propName: 'gold_miner', goldMinerTicket: 'scoped-ticket' })
};
s.post(request(7, '01'), success(data));
s.pre(request(7, '02')); s.post(request(7, '02'), success(data));
assert.throws(() => s.post(request(7, '02'), success({ ...data, outTradeNo: 'different' })), /复用原订单/);
assert.deepEqual(s.pre(request(7, '03')), { skipped: true });
s.env.set('allowCustomerMessage', 'true');
const sent = s.pre(request(7, '03')).body;
assert.equal(sent.SessionFrom, data.sessionFrom);
assert.equal(sent.FromUserName, 'openid'); assert.equal(sent.token, undefined);
s.post(request(7, '04'), true); assert.throws(() => s.post(request(7, '04'), { code: 0 }));
s.post(request(7, '05'), false); s.post(request(7, '06'), { code: 'FAIL' }, 500);
s.post(request(7, '07'), { code: 0, errorCode: 'PAYMENT_CHANNEL_CONFLICT' });
s.env.set('uid', 'other'); assert.deepEqual(s.pre(request(1, '01')), { skipped: true });
assert.equal(s.env.get('legacySessionFrom'), undefined); assert.equal(s.env.get('allowCustomerMessage'), undefined);
});
test('settling pages advance the cursor and settlement delivery uses original grant without claim', () => {
const s = sandbox({ settlementCursor: 'history-cursor' });
s.post(request(5, '02'), success({ status: 'settling', items: [], nextCursor: 'next-history-cursor' }));
assert.equal(s.pre(request(5, '02')).body.afterId, 'next-history-cursor');
s.post(request(5, '02'), success({ status: 'settling', items: [], nextCursor: null }));
assert.deepEqual(s.pre(request(5, '02')), { skipped: true });
assert.equal(s.pre(request(5, '01')).body.afterId, undefined);
s.post(request(5, '05'), success({ status: 'pending_delivery', items: [{ settlementId: 'settlement-1', periodId, rewards: [{ taskId: 'task_1', grantId: 'grant-1', items: [{ type: 'coin', count: 50 }] }] }], nextCursor: null }));
assert.equal(s.env.get('grantId'), 'grant-1');
assert.deepEqual(s.pre(request(5, '06')), { skipped: true });
s.post(request(4, '03'), success({ coinAmount: 100 })); s.env.set('coinAfterDelivery', '150');
assert.equal(s.pre(request(4, '04')).body.coinAmount, '150');
s.post(request(4, '04'), success({ coinAmount: 150 }));
const body = s.pre(request(5, '06')).body; assert.equal(body.action, 'confirm_settlement_delivery');
assert.deepEqual(body.grantIds, ['grant-1']); assert.equal(body.settlementId, 'settlement-1');
s.post(request(5, '06'), success({ settlementId: 'settlement-1', confirmedGrantIds: ['grant-1'], deliveryStatus: 'client_saved' }));
assert.deepEqual(s.pre(request(4, '04')), { skipped: true });
});
test('settlement collection omits client limits and accepts only globally empty no_pending_rewards responses', () => {
const s = sandbox();
for (const prefix of ['01', '05']) {
assert.equal(s.pre(request(5, prefix)).body.limit, undefined);
s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [], nextCursor: null }));
assert.equal(s.env.get('settlementCursor'), '');
assert.throws(() => s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [], nextCursor: 'next' })));
assert.throws(() => s.post(request(5, prefix), success({ status: 'no_pending_rewards', items: [{}], nextCursor: null })));
}
assert.deepEqual(s.pre(request(5, '02')), { skipped: true });
const negative = s.pre(request(1, '06')).body;
assert.equal(negative.limit, undefined); assert.equal(negative.afterId, 42);
});
test('temporary info accepts custom duration while weekly info still enforces the normal calendar', () => {
const s = sandbox(), d = info({ periodId: 'goldMiner:test:short', endsAt: startsAt + 1800000 });
s.post(request(1, '01'), success(d)); assert.equal(s.env.get('periodId'), d.periodId);
assert.throws(() => s.post(request(1, '01'), success({ ...d, periodId })), /活动状态结构/);
assert.throws(() => s.post(request(1, '01'), success({ ...d, serverTime: d.endsAt })), /活动状态结构/);
});
const schedule = (activities = []) => ({
schemaVersion: 1, serverNow: startsAt + 1000, windowEndsAt: startsAt + 1000 + 7 * 86400000,
refreshAt: Math.min(startsAt + 31000, ...activities.flatMap(a => [a.startsAt, a.endsAt]).filter(t => t > startsAt + 1000)), activities
});
const scheduleEntry = (extra = {}) => ({
activityId: 'goldMiner', periodId: 'goldMiner:test:short', configVersion: 'v1', startsAt,
endsAt: startsAt + 10000, unlockLevel: 41, phase: 'active', purchaseEnabled: true, ...extra
});
test('public schedule GET and POST require no login and do not overwrite player period or grant', () => {
const s = sandbox({ uid: '', token: '', adminToken: '', periodId: 'player-period', grantId: 'original-grant' });
assert.deepEqual(s.pre(request(8, '01')), { body: undefined }); assert.deepEqual(s.pre(request(8, '02')), { body: {} });
for (const item of collection.item[7].item) {
s.post(item, success(schedule([scheduleEntry()])));
assert.equal(s.env.get('periodId'), 'player-period'); assert.equal(s.env.get('grantId'), 'original-grant');
assert.equal(JSON.parse(s.env.get('gmActivitySchedule')).activities[0].periodId, 'goldMiner:test:short');
}
const yaml = readFileSync(new URL('../../activityConfig/list.yaml', import.meta.url), 'utf8');
assert.match(yaml, /^\s+- GET\s*$/m); assert.match(yaml, /^\s+- POST\s*$/m);
});
test('schedule accepts empty lists and cloudRise upcoming entries; errors preserve cached success', () => {
const s = sandbox(), item = request(8, '01'); s.post(item, success(schedule()));
assert.deepEqual(JSON.parse(s.env.get('gmActivitySchedule')).activities, []);
const cloud = { activityId: 'cloudRise', periodId: 'race', configVersion: 'v1', startsAt: startsAt + 5000, endsAt: startsAt + 60000, unlockLevel: 100, phase: 'upcoming' };
const d = schedule([cloud, scheduleEntry()]); s.post(item, success(d));
const cached = s.env.get('gmActivitySchedule');
assert.throws(() => s.post(item, { code: 0, data: null, msg: '数据库暂不可用' }), /公共日程查询成功/);
assert.equal(s.env.get('gmActivitySchedule'), cached);
assert.throws(() => s.post(item, success({ ...d, refreshAt: d.serverNow + 30000 })), /公共日程字段/);
assert.equal(s.env.get('gmActivitySchedule'), cached);
assert.throws(() => s.post(item, success(schedule([scheduleEntry({ tasks: [] })]))), /公共日程字段/);
assert.throws(() => s.post(item, success(schedule([scheduleEntry(), cloud]))), /公共日程字段/);
s.env.set('expectedListedPeriodId', 'missing'); assert.throws(() => s.post(item, success(d)), /公共日程字段/);
s.env.set('expectedListedPeriodId', 'goldMiner:test:short'); s.post(item, success(d));
});
test('integrated temporary publisher reuses the same draft and validates immutable response', () => {
const s = sandbox({ adminToken: 'admin-test' }), item = request(6, '04'); const first = s.pre(item).body;
assert.equal(first.action, 'publish_test_period'); assert.equal(first.endsAt - first.startsAt, 1800000);
assert.match(first.periodId, /^goldMiner:test:/); assert.deepEqual(s.pre(item).body, first);
assert.equal(s.env.get('tempPeriodId'), first.periodId); assert.equal(s.env.get('testPeriodRequestBody'), undefined);
const result = {
periodId: first.periodId, startsAt: first.startsAt, endsAt: first.endsAt, nextStartsAt: first.endsAt + 1000,
configHash: 'hash', configSnapshot: { ...first.config, effectiveFromPeriodId: first.periodId }
};
s.post(item, success(result)); assert.throws(() => s.post(item, success({ ...result, endsAt: first.endsAt + 1 })), /时间与请求一致/);
const standalone = read('gold-miner-test-period.postman_collection.json').item[0];
assert.equal(script(item, 'prerequest'), script(standalone, 'prerequest')); assert.equal(script(item, 'test'), script(standalone, 'test'));
});
test('public cached schedule is cleared when switching deployment context', () => {
const s = sandbox(); s.pre(request(8, '01')); s.post(request(8, '01'), success(schedule()));
s.env.set('baseUrl', 'https://another.invalid'); assert.deepEqual(s.pre(request(8, '01')), { skipped: true });
assert.equal(s.env.get('gmActivitySchedule'), undefined); assert.deepEqual(s.pre(request(8, '01')), { body: undefined });
});
test('VIP Postman publication and pending-unlock display match the six-tier protocol', () => {
const config = JSON.parse(readFileSync(new URL('../vip-tiers.example.json', import.meta.url), 'utf8')).config;
const s = sandbox({ adminToken: 'admin-test', configJson: JSON.stringify(config) });
assert.deepEqual(s.pre(request(6, '02')).body.config, config);
s.post(request(6, '02'), success(config));
const unpaid = info({ status: 'purchasable', vipLevel: 0, priceFen: 300, productId: 'gold_miner_vip0' });
s.post(request(1, '01'), success(unpaid));
assert.ok(unpaid.tasks.every(t => t.claimStatus === 'pending_unlock'));
assert.equal(s.env.get('productId'), 'gold_miner_vip0');
const broken = copy(unpaid); broken.tasks[0].claimStatus = 'claimable';
assert.throws(() => s.post(request(1, '01'), success(broken)), /活动状态结构/);
// Independent tasks can all be ready, including with progress beyond the selected tier's cap.
s.post(request(1, '01'), success(info({ vipLevel: 1, progressWins: 61 })));
s.env.set('claimPeriodId', periodId); s.env.set('claimTaskId', 'task_2');
s.pre(request(4, '08')); s.post(request(4, '08'), success({ ...grant(), taskId: 'task_2' }));
assert.equal(s.env.get('claimTaskId'), 'task_2');
s.post(request(4, '05'), success({ grantId: 'grant-1', claimStatus: 'claimed', claimedThrough: 0 }));
});