try to fix this spaghetti of a code
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
const {test} = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const {createApplication} = require('../gambling');
|
||||
const {createRepository} = require('../lib/repository');
|
||||
const {createLobbyService} = require('../lib/lobbies');
|
||||
const secret = 'test-secret-with-at-least-thirty-two-characters';
|
||||
const command = (service, username, type, fields = {}) => service.command(username, {type, requestId: crypto.randomUUID(), ...fields});
|
||||
async function setup(t, overrides = {}) {
|
||||
let now = 100000;
|
||||
const api = await createApplication({secret, databasePath: ':memory:', bcryptRounds: 4, now: () => now, intermissionMs: 60000, ...overrides});
|
||||
t.after(() => api.close());
|
||||
for (const username of ['a','b','c']) await api.sessions.register({username, password: 'password'});
|
||||
if (overrides.startingBalances) await api.repository.transaction(async tx => {
|
||||
for (const [username, money] of Object.entries(overrides.startingBalances)) await tx.run('UPDATE users SET money = ? WHERE username = ?', money, username);
|
||||
});
|
||||
const {lobbyId} = await command(api.lobbies, 'a', 'lobby:create');
|
||||
for (const username of ['b','c']) await command(api.lobbies, username, 'lobby:join', {lobbyId});
|
||||
await command(api.lobbies, 'a', 'poker:start', {lobbyId});
|
||||
const intermission = api.lobbies.get(lobbyId).game; now = intermission.nextRoundStartsAt;
|
||||
await command(api.lobbies, null, 'timer', {lobbyId, handId: intermission.id, phase: 'intermission', deadline: now});
|
||||
return {api, lobbyId};
|
||||
}
|
||||
test('accepted actions persist once; failed commits leave both memory and database unchanged', async t => {
|
||||
let fail = false;
|
||||
const {api, lobbyId} = await setup(t, {repositoryHooks: {beforeCommit: async () => {if (fail) {fail = false; throw new Error('Injected disk failure');}}}});
|
||||
const message = {type: 'poker:action', lobbyId, requestId: crypto.randomUUID(), action: 'bet', payload: {amount: 20}};
|
||||
const before = JSON.stringify(api.lobbies.get(lobbyId)); fail = true;
|
||||
await assert.rejects(api.lobbies.command('b', message), /Injected/);
|
||||
assert.equal(JSON.stringify(api.lobbies.get(lobbyId)), before);
|
||||
assert.equal((await api.repository.user('b')).money, 190);
|
||||
await Promise.all([api.lobbies.command('b', message), api.lobbies.command('b', message)]);
|
||||
assert.equal((await api.repository.user('b')).money, 170);
|
||||
assert.equal(api.lobbies.get(lobbyId).game.pot, 50);
|
||||
await assert.rejects(api.lobbies.command('b', {...message, action: 'fold'}), error => error.code === 'IDEMPOTENCY_CONFLICT');
|
||||
assert.deepEqual(await api.repository.totals(), {users: 550, escrow: 50});
|
||||
});
|
||||
test('concurrent account joins and table starts serialize; simultaneous leaves conserve chips', async t => {
|
||||
const {api, lobbyId} = await setup(t);
|
||||
const results = await Promise.allSettled([command(api.lobbies, 'a', 'lobby:create'), command(api.lobbies, 'a', 'poker:start', {lobbyId})]);
|
||||
assert.ok(results.every(result => result.status === 'rejected'));
|
||||
await Promise.all(['a', 'b', 'c'].map(username => command(api.lobbies, username, 'lobby:leave', {lobbyId})));
|
||||
assert.deepEqual(await api.repository.totals(), {users: 600, escrow: 0}); assert.equal(api.lobbies.get(lobbyId), undefined);
|
||||
});
|
||||
test('stale timer cannot act on a new turn', async t => {
|
||||
const {api, lobbyId} = await setup(t);
|
||||
const original = api.lobbies.get(lobbyId);
|
||||
const game = original.game;
|
||||
await command(api.lobbies, 'b', 'poker:action', {lobbyId, action: 'hit'});
|
||||
const before = JSON.stringify(api.lobbies.get(lobbyId));
|
||||
await command(api.lobbies, null, 'timer', {lobbyId, handId: game.id, phase: 'betting', deadline: game.turnEndsAt, revision: original.revision});
|
||||
assert.equal(JSON.stringify(api.lobbies.get(lobbyId)), before);
|
||||
});
|
||||
test('unequal all-ins settle correct side pots atomically even after a settlement write fails', async t => {
|
||||
let fail = false;
|
||||
const draws = 'AS AH KS KH QS QH 2D 3C 7S 8H 9D'.split(' ').map(value => ({rank:value.slice(0,-1),suit:value.slice(-1)}));
|
||||
const {api, lobbyId} = await setup(t, {startingBalances:{a:60,b:200,c:100}, createDeck:()=>[
|
||||
...require('../lib/poker').createDeck().filter(card=>!draws.some(d=>d.rank===card.rank && d.suit===card.suit)), ...draws.toReversed()
|
||||
], repositoryHooks:{beforeCommit:async()=>{if(fail){fail=false;throw new Error('Settlement disk failure');}}}});
|
||||
await command(api.lobbies,'b','poker:action',{lobbyId,action:'bet',payload:{amount:190}});
|
||||
await command(api.lobbies,'c','poker:action',{lobbyId,action:'hit'});
|
||||
const message={type:'poker:action',lobbyId,requestId:crypto.randomUUID(),action:'hit'};
|
||||
fail=true;
|
||||
await assert.rejects(api.lobbies.command('a',message),/Settlement disk failure/);
|
||||
assert.equal(api.lobbies.get(lobbyId).game.phase,'betting');
|
||||
assert.deepEqual(await api.repository.totals(),{users:50,escrow:310});
|
||||
await api.lobbies.command('a',message);
|
||||
await api.lobbies.command('a',message);
|
||||
const game=api.lobbies.get(lobbyId).game;
|
||||
assert.equal(game.phase,'intermission');
|
||||
assert.deepEqual({...game.payouts},{a:180,b:180});
|
||||
for(const username of ['a','b','c']) assert.equal((await api.repository.user(username)).money,game.stacks[username]);
|
||||
assert.deepEqual(await api.repository.totals(),{users:360,escrow:0});
|
||||
});
|
||||
test('reopening a database refunds interrupted escrow exactly once', async t => {
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'gambling-recovery-'));
|
||||
t.after(() => fs.rm(directory, {recursive: true, force: true}));
|
||||
const filename = path.join(directory, 'test.sqlite');
|
||||
const repo = await createRepository(filename);
|
||||
for (const username of ['a','b']) await repo.register(username, 'unused-test-hash', null);
|
||||
let now = 1;
|
||||
const service = createLobbyService(repo, {ante: 10, intermissionMs: 60000, turnMs: 30000, now: () => now});
|
||||
const {lobbyId} = await command(service, 'a', 'lobby:create');
|
||||
await command(service, 'b', 'lobby:join', {lobbyId}); await command(service, 'a', 'poker:start', {lobbyId});
|
||||
const game = service.get(lobbyId).game; now = game.nextRoundStartsAt;
|
||||
await command(service, null, 'timer', {lobbyId, handId: game.id, phase: 'intermission', deadline: now});
|
||||
await command(service, 'b', 'poker:action', {lobbyId, action: 'bet', payload: {amount: 30}});
|
||||
assert.deepEqual(await repo.totals(), {users: 350, escrow: 50});
|
||||
await service.close(); await repo.close();
|
||||
const restarted = await createRepository(filename);
|
||||
try {
|
||||
await restarted.recover(); await restarted.recover();
|
||||
assert.deepEqual(await restarted.totals(), {users: 400, escrow: 0});
|
||||
assert.equal((await restarted.user('a')).money, 200); assert.equal((await restarted.user('b')).money, 200);
|
||||
} finally {await restarted.close();}
|
||||
});
|
||||
Reference in New Issue
Block a user