mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
95 lines
4.4 KiB
JavaScript
95 lines
4.4 KiB
JavaScript
const GameModeManager = require('../src/game/gameModeManager');
|
|
const LatePaymentReconciler = require('../src/services/latePaymentReconciler');
|
|
|
|
describe('late incoming payment reconciliation', () => {
|
|
test('bounded scanner sends an expired native receipt only to manual review', async () => {
|
|
const row = {
|
|
id: 7,
|
|
user_id: 3,
|
|
status: 'expired',
|
|
subaddress: 'late-address',
|
|
address_index: 11,
|
|
expected_amount: '100',
|
|
provider_id: 'native-monero',
|
|
provider_invoice_id: 'late-address'
|
|
};
|
|
const db = { query: jest.fn().mockResolvedValue({ rows: [row] }) };
|
|
const receipt = {
|
|
evidenceType: 'chain_output', providerId: 'native-monero',
|
|
txHash: 'a'.repeat(64), outputId: 'b'.repeat(64),
|
|
evidenceId: `${'a'.repeat(64)}:${'b'.repeat(64)}`,
|
|
addressIndex: 11, amount: '100', confirmed: true
|
|
};
|
|
const walletService = {
|
|
accountIndex: 0,
|
|
addressToUser: new Map(),
|
|
addressToSocket: new Map(),
|
|
checkPaymentStatus: jest.fn(async address => {
|
|
expect(address).toBe('late-address');
|
|
expect(walletService.addressToUser.get(address)).toEqual(expect.objectContaining({
|
|
amount: 100,
|
|
addressIndex: 11
|
|
}));
|
|
return { confirmed: true, complete: true, amount: 100, receipts: [receipt] };
|
|
}),
|
|
stopPaymentMonitoring: jest.fn()
|
|
};
|
|
const gameModeManager = {
|
|
paymentProviders: { get: jest.fn(() => null) },
|
|
reconcileLatePaymentForReview: jest.fn().mockResolvedValue({ checked: true, needsReview: true })
|
|
};
|
|
const service = new LatePaymentReconciler({
|
|
db, gameModeManager, walletService, lookbackHours: 24, batchSize: 20,
|
|
logger: { warn: jest.fn(), error: jest.fn() }
|
|
});
|
|
|
|
const result = await service.runOnce();
|
|
|
|
expect(result).toEqual({ skipped: false, checked: 1, reviews: 1, errors: 0 });
|
|
expect(db.query.mock.calls[0][1]).toEqual([24, 20]);
|
|
expect(gameModeManager.reconcileLatePaymentForReview).toHaveBeenCalledWith(7, [receipt]);
|
|
expect(walletService.addressToUser.has('late-address')).toBe(false);
|
|
expect(walletService.stopPaymentMonitoring).toHaveBeenCalledWith('late-address');
|
|
});
|
|
|
|
test('durable late receipt stays expired and creates review without any entitlement write', async () => {
|
|
const calls = [];
|
|
const client = {
|
|
query: jest.fn(async (sql, params = []) => {
|
|
calls.push({ sql, params });
|
|
if (/SELECT id, status, expected_amount/i.test(sql)) {
|
|
return { rows: [{
|
|
id: 8, status: 'expired', expected_amount: '100',
|
|
provider_id: 'native-monero', address_index: 12
|
|
}] };
|
|
}
|
|
if (/COALESCE\(SUM\(amount\)/i.test(sql)) {
|
|
return { rows: [{ total: '100', primary_tx_hash: 'c'.repeat(64), receipt_count: '1' }] };
|
|
}
|
|
return { rows: [], rowCount: 1 };
|
|
})
|
|
};
|
|
const manager = Object.create(GameModeManager.prototype);
|
|
manager.db = { withTransaction: async callback => callback(client) };
|
|
const outputId = 'd'.repeat(64);
|
|
const receipt = {
|
|
evidenceType: 'chain_output', providerId: 'native-monero',
|
|
txHash: 'c'.repeat(64), outputId,
|
|
evidenceId: `${'c'.repeat(64)}:${outputId}`,
|
|
addressIndex: 12, amount: '100', confirmed: true
|
|
};
|
|
|
|
const result = await manager.reconcileLatePaymentForReview(8, [receipt]);
|
|
|
|
expect(result).toEqual(expect.objectContaining({
|
|
checked: true,
|
|
needsReview: true,
|
|
coverageComplete: true
|
|
}));
|
|
expect(calls.some(call => /INSERT INTO payment_receipts/i.test(call.sql))).toBe(true);
|
|
expect(calls.some(call => /INSERT INTO payment_late_reviews/i.test(call.sql))).toBe(true);
|
|
expect(calls.some(call => /SET status = CASE WHEN status = 'pending' THEN 'expired'/i.test(call.sql))).toBe(true);
|
|
expect(calls.some(call => /UPDATE users|payment_entitlement_grants|credit_transactions/i.test(call.sql))).toBe(false);
|
|
});
|
|
});
|