mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
Backend (Tier 0/1/3): single-payout double-pay -> needs_review; admin-retry tx_hash guard; UNIQUE index on games.payment_id (migration 023) + atomic claim; reconnect no longer resets block-timeout; completeGame keys on user_id+dungeon_seed (not socket_id); token-gated /api/user/:socketId/* endpoints; payout min/max enforcement; orphaned-active-game boot recovery; sessionManager cleanup FK fix; XFF rightmost-hop; global uncaughtException/unhandledRejection guard; server binds 127.0.0.1; provably-fair client-seed (HMAC) + RNG [0,1) bound; difficulty preset ranges reach the digger; chat no longer leaks full socket.id; removed dangerous-keyword SQL theater. Match mode (Tier 2, still MATCH_ENABLED-gated): fixed match_queue/match_move crash paths + scheduler dead-code + leaked ceiling timer; pot collected before start with abort+refund; BigInt/basis-point fee math, capped payout; no-address winner claimable; winnerId==placement[0]; finalize schema + payout-first isolation; reconnect re-key with ownership; refunds on all cancel paths; real countdown; map/timer cleanup. +16 tests. Graphics: new overlay FX layer (torch light, vignette, embers, sparkle/burst/flash/shake, self-suspending rAF); gameplay juice; atmospheric screens; CSS design-token chrome with framed modals; richer Kenney tile renderer; imported Kenney dungeon/interior sheets + fx particles. Verification: 45 suites / 312 tests pass; 40 changed JS files parse; 20 server modules load clean; no regressions.
70 lines
3.5 KiB
JavaScript
70 lines
3.5 KiB
JavaScript
const SocketChatProvider = require('../src/network/chat/SocketChatProvider');
|
|
|
|
function makeFakeIo() {
|
|
const io = {
|
|
emitted: [], // { channel|null, event, payload }
|
|
to(channel) {
|
|
return { emit: (event, payload) => io.emitted.push({ channel, event, payload }) };
|
|
},
|
|
emit(event, payload) { io.emitted.push({ channel: null, event, payload }); }
|
|
};
|
|
return io;
|
|
}
|
|
|
|
describe('SocketChatProvider', () => {
|
|
test('global publish persists via history and broadcasts via broadcastManager', async () => {
|
|
const io = makeFakeIo();
|
|
const saved = [];
|
|
const broadcast = [];
|
|
const history = { saveMessage: (m) => { saved.push(m); return Promise.resolve(); } };
|
|
const broadcastManager = {
|
|
// The 4th arg is the sanitized public id (never the raw socket.id — S1 defense in depth).
|
|
broadcastChatMessage: (username, text, ts, publicId) => broadcast.push({ username, text, ts, publicId })
|
|
};
|
|
const provider = new SocketChatProvider({ io, broadcastManager, historyManager: history });
|
|
|
|
await provider.publish({ scope: 'global', username: 'abc123', text: 'hi', ts: 42, socketId: 'sock', userId: 7 });
|
|
|
|
expect(saved).toHaveLength(1);
|
|
expect(saved[0]).toMatchObject({ socketId: 'sock', username: 'abc123', message: 'hi', userId: 7 });
|
|
expect(broadcast).toHaveLength(1);
|
|
// userId is present, so the broadcast is attributed by the public user id ('7'), not the socket id.
|
|
expect(broadcast[0]).toEqual({ username: 'abc123', text: 'hi', ts: 42, publicId: '7' });
|
|
});
|
|
|
|
test('global publish falls back to io.emit when no broadcastManager is given', async () => {
|
|
const io = makeFakeIo();
|
|
const provider = new SocketChatProvider({ io });
|
|
await provider.publish({ scope: 'global', username: 'x', text: 'yo', ts: 1 });
|
|
const ev = io.emitted.find(e => e.event === 'chat_broadcast' && e.channel === null);
|
|
expect(ev).toBeTruthy();
|
|
expect(ev.payload).toMatchObject({ username: 'x', message: 'yo', timestamp: 1 });
|
|
});
|
|
|
|
test('room-scoped publish delivers only to that room and tags the scope', async () => {
|
|
const io = makeFakeIo();
|
|
const provider = new SocketChatProvider({ io });
|
|
await provider.publish({ scope: 'tavern:main', username: 'bob', text: 'hey', ts: 5 });
|
|
const ev = io.emitted.find(e => e.event === 'chat_broadcast');
|
|
expect(ev.channel).toBe('tavern:main');
|
|
expect(ev.payload).toMatchObject({ username: 'bob', message: 'hey', timestamp: 5, scope: 'tavern:main' });
|
|
});
|
|
|
|
test('getHistory: global reads the history manager; room scopes are ephemeral', async () => {
|
|
const io = makeFakeIo();
|
|
const history = { getRecentMessages: (n) => Promise.resolve([{ message: 'old', n }]) };
|
|
const provider = new SocketChatProvider({ io, historyManager: history });
|
|
await expect(provider.getHistory({ scope: 'global', limit: 10 })).resolves.toEqual([{ message: 'old', n: 10 }]);
|
|
await expect(provider.getHistory({ scope: 'tavern:main' })).resolves.toEqual([]);
|
|
});
|
|
|
|
test('an injected history manager is not shut down by the provider (caller owns it)', async () => {
|
|
const io = makeFakeIo();
|
|
let shut = false;
|
|
const history = { shutdown: () => { shut = true; } };
|
|
const provider = new SocketChatProvider({ io, historyManager: history });
|
|
await provider.shutdown();
|
|
expect(shut).toBe(false);
|
|
});
|
|
});
|