112 lines
6.5 KiB
JavaScript
112 lines
6.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { registerHooks } from 'node:module';
|
|
|
|
const NOW = 1800000000000, END = NOW - 1000, PERIOD = 30 * 86400000;
|
|
let now = NOW, drop = false;
|
|
const tables = { users: [], usersAd: [], order: [], idcount: [] };
|
|
const clone = v => v == null ? v : structuredClone(v);
|
|
const get = (r, k) => k.split('.').reduce((v, key) => v?.[key], r);
|
|
const match = (r, q) => Object.entries(q).every(([k, v]) => k === '$expr'
|
|
? now < new Date(v.$lt[1]).getTime() : v === null ? get(r, k) == null : JSON.stringify(get(r, k)) === JSON.stringify(v));
|
|
const collection = name => ({
|
|
async findOne(q) { return clone(tables[name].find(r => match(r, q))); },
|
|
async updateOne(q, update) {
|
|
const row = tables[name].find(r => match(r, q));
|
|
if (!row) return { matchedCount: 0, modifiedCount: 0 };
|
|
for (const [k, v] of Object.entries(update.$set || {})) {
|
|
const keys = k.split('.'); let target = row;
|
|
for (const part of keys.slice(0, -1)) target = target[part] ||= {};
|
|
target[keys.at(-1)] = clone(v);
|
|
}
|
|
if (drop && update.$set.timestamp) { drop = false; throw Error('lost response'); }
|
|
return { matchedCount: 1, modifiedCount: 1 };
|
|
}
|
|
});
|
|
globalThis.__passTestCloud = {
|
|
mongo: { db: { collection } },
|
|
database: () => ({ collection: name => ({ where: q => ({
|
|
getOne: async () => ({ data: await collection(name).findOne(q) }),
|
|
get: async () => ({ data: clone(tables[name].filter(r => match(r, q))) }),
|
|
update: async values => ({ updated: (await collection(name).updateOne(q, { $set: values })).modifiedCount })
|
|
}) }) })
|
|
};
|
|
registerHooks({ resolve(specifier, context, next) {
|
|
if (specifier === '@lafjs/cloud') return { url: 'data:text/javascript,export default globalThis.__passTestCloud', shortCircuit: true };
|
|
if (specifier === '@/Utils') return { url: 'data:text/javascript,export default {checkToken:(a,b)=>a===b}', shortCircuit: true };
|
|
if (specifier.startsWith('@/')) return { url: new URL('../functions/' + specifier.slice(2) + '.ts', import.meta.url).href, shortCircuit: true };
|
|
return next(specifier, context);
|
|
} });
|
|
const { default: settlement, currentPassStart } = await import('../functions/passCheckSettlement.ts');
|
|
const { default: upgrade, passOrderFields } = await import('../functions/passCheckUpgrade.ts');
|
|
const realNow = Date.now;
|
|
Date.now = () => now;
|
|
test.after(() => { Date.now = realNow; });
|
|
function reset() {
|
|
now = NOW; drop = false;
|
|
for (const key of Object.keys(tables)) tables[key] = [];
|
|
process.env.PASSCHECKTIME_ID = 'clock';
|
|
tables.idcount.push({ _id: 'clock', passcheckTime: END - PERIOD });
|
|
const stage = { time: String(END), experience: 4, free: [0, 1, 1], passCheck: [1, 1, 1], activate: false };
|
|
tables.users.push({ _id: 'u', openid: 'o', token: 't', coinAmount: 10, passCheck: JSON.stringify({ 2: stage }) });
|
|
return tables.users[0];
|
|
}
|
|
const body = data => ({ body: { uid: 'u', token: 't', ...data } });
|
|
const call = data => settlement(body(data));
|
|
test('offline freeze is immutable, never credits resources, and uses millisecond periods', async () => {
|
|
const u = reset(); await call({ action: 'read' });
|
|
assert.equal(u.coinAmount, 10);
|
|
u.passCheck = JSON.stringify({ 2: { time: END, experience: 999 } });
|
|
assert.equal((await call({ action: 'read' })).data.rows[0].stage.experience, 4);
|
|
assert.equal(await currentPassStart(), END);
|
|
now = END + PERIOD; assert.equal(await currentPassStart(), now);
|
|
});
|
|
test('concurrent requests and a lost response cannot award twice', async () => {
|
|
const u = reset(), request = { action: 'claim', end: END, rewards: [{ type: 'coin', count: 15 }] };
|
|
drop = true;
|
|
const replies = await Promise.all([call(request), call(request)]);
|
|
assert.equal(u.coinAmount, 25);
|
|
assert.ok(replies.some(r => r.code === 1));
|
|
assert.equal((await call(request)).data.alreadyGranted, true);
|
|
assert.equal(u.coinAmount, 25);
|
|
});
|
|
test('offer is scoped to the rendered view and permanent abandonment revokes ordering', async () => {
|
|
const u = reset(), offerToken = 'original_render_token';
|
|
assert.equal((await call({ action: 'offer_shown', end: END, offerToken })).code, 1);
|
|
assert.equal((await call({ action: 'offer_shown', end: END, offerToken: 'another_render_token' })).code, 410);
|
|
const order = { itemid: 'battlepass', passVersion: 2, passEnd: END, passOfferToken: offerToken };
|
|
assert.equal(passOrderFields(u, order).passPostSeason, true);
|
|
await call({ action: 'decline', end: END });
|
|
assert.throws(() => passOrderFields(u, order));
|
|
});
|
|
test('paid duplicate gets a manual-review marker, not a second grant', async () => {
|
|
const u = reset(); await call({ action: 'read' });
|
|
const order = { openid: 'o', itemid: 'battlepass', passEnd: String(END), passPostSeason: true, state: 1 };
|
|
tables.order.push({ ...order, outTradeNo: 'first' }, { ...order, outTradeNo: 'second' });
|
|
const request = { action: 'purchase_claim', end: END, rewards: [{ type: 'coin', count: 300 }] };
|
|
assert.equal((await call({ ...request, outTradeNo: 'first' })).code, 1);
|
|
assert.equal((await call({ ...request, outTradeNo: 'second' })).code, 410);
|
|
assert.equal(u.coinAmount, 310);
|
|
assert.equal(tables.order[1].passReview, 'duplicate_post_season_payment');
|
|
});
|
|
test('late paid entitlement preserves prior claim masks and original verified XP', async () => {
|
|
const u = reset();
|
|
await call({ action: 'claim', end: END, rewards: [], claimFlags: { free: [0, 0, 0], passCheck: [1, 1, 1] } });
|
|
tables.order.push({ openid: 'o', itemid: 'battlepass_30', passVersion: 2, passEnd: String(END), state: 1, outTradeNo: 'late' });
|
|
const reply = await upgrade(body({ action: 'save', outTradeNo: 'late', revision: 0, coinAmount: 3010,
|
|
passCheck: JSON.stringify({ 2: { time: String(END), experience: 9999 } }), legacy: u.passCheck }));
|
|
assert.equal(reply.code, 1);
|
|
assert.equal(u.passSettlements[END].stage.experience, 24);
|
|
assert.deepEqual(u.passSettlements[END].stage.free, [0, 0, 0]);
|
|
assert.equal(u.passSettlements[END].stage.tier, 30);
|
|
assert.equal(u.passSettlements[END].claimed, false);
|
|
assert.equal((await upgrade(body({ action: 'order', outTradeNo: 'late' }))).data.alreadyGranted, true);
|
|
});
|
|
test('unauthenticated requests and expired ordinary writes cannot alter the snapshot', async () => {
|
|
const u = reset();
|
|
assert.equal((await call({ action: 'claim', end: END, token: 'wrong', rewards: [] })).code, 0);
|
|
const result = await upgrade(body({ action: 'save', revision: 0, passCheck: u.passCheck, legacy: u.passCheck }));
|
|
assert.equal(result.code, 410);
|
|
assert.equal(u.passSettlements[END].stage.experience, 4);
|
|
});
|