mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-25 01:58:33 +02:00
fix(showcase): harden Smirk checkout and multiplayer presentation
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
const MatchQueue = require('../src/network/matchQueue');
|
||||
const MatchManager = require('../src/network/matchManager');
|
||||
const MatchRoom = require('../src/multiplayer/MatchRoom');
|
||||
const SocketHandlers = require('../src/network/socketHandlers');
|
||||
|
||||
describe('MatchQueue', () => {
|
||||
@@ -238,4 +240,179 @@ describe('SocketHandlers match queue acknowledgements', () => {
|
||||
payload: { success: true, economy: 'free' }
|
||||
});
|
||||
});
|
||||
|
||||
test('reconnect restores owned queued entries without joining or charging again', async () => {
|
||||
const emitted = [];
|
||||
const socket = { id: 'new-socket', emit: (event, payload) => emitted.push({ event, payload }) };
|
||||
const reattach = jest.fn(async (userId, economy, socketId, token) => {
|
||||
expect(userId).toBe(7);
|
||||
expect(socketId).toBe(socket.id);
|
||||
expect(token).toBe('current-session');
|
||||
return economy === 'free' || economy === 'credits_prestige'
|
||||
? { inQueue: true, position: economy === 'free' ? 2 : 1 }
|
||||
: { inQueue: false };
|
||||
});
|
||||
const enqueue = jest.fn();
|
||||
const handleReconnect = jest.fn(() => false);
|
||||
const context = {
|
||||
matchManager: { enabled: true, handleReconnect },
|
||||
matchQueue: { reattach, enqueue },
|
||||
_resolveMatchSession: jest.fn(async () => ({ userId: 7, sessionToken: 'current-session' })),
|
||||
debugManager: { CONSOLE_LOGGING: false }
|
||||
};
|
||||
|
||||
await SocketHandlers.prototype._handleMatchReconnect.call(context, socket, {
|
||||
userId: 999, economy: 'crypto_race', sessionToken: 'untrusted'
|
||||
});
|
||||
|
||||
expect(handleReconnect).toHaveBeenCalledWith(socket, expect.objectContaining({ userId: 7 }));
|
||||
expect(reattach).toHaveBeenCalledWith(7, 'free', socket.id, 'current-session');
|
||||
expect(reattach).toHaveBeenCalledWith(7, 'credits_prestige', socket.id, 'current-session');
|
||||
expect(reattach).toHaveBeenCalledWith(7, 'crypto_race', socket.id, 'current-session');
|
||||
expect(enqueue).not.toHaveBeenCalled();
|
||||
expect(emitted.filter(item => item.event === 'match_queue_joined')).toEqual([
|
||||
{ event: 'match_queue_joined', payload: {
|
||||
success: true, economy: 'free', position: 2, alreadyQueued: true, restored: true
|
||||
} },
|
||||
{ event: 'match_queue_joined', payload: {
|
||||
success: true, economy: 'credits_prestige', position: 1, alreadyQueued: true, restored: true
|
||||
} }
|
||||
]);
|
||||
});
|
||||
|
||||
test('reconnect does not reclaim a queue row while its active match owns the player', async () => {
|
||||
const socket = { id: 'new-socket', emit: jest.fn() };
|
||||
const reattach = jest.fn();
|
||||
const context = {
|
||||
matchManager: { enabled: true, handleReconnect: jest.fn(() => true) },
|
||||
matchQueue: { reattach },
|
||||
_resolveMatchSession: jest.fn(async () => ({ userId: 7, sessionToken: 'current-session' }))
|
||||
};
|
||||
|
||||
await SocketHandlers.prototype._handleMatchReconnect.call(context, socket);
|
||||
|
||||
expect(reattach).not.toHaveBeenCalled();
|
||||
expect(socket.emit).not.toHaveBeenCalledWith('match_queue_joined', expect.anything());
|
||||
});
|
||||
|
||||
test('reconnect does not use a client claimed user when session resolution fails', async () => {
|
||||
const socket = { id: 'new-socket', emit: jest.fn() };
|
||||
const handleReconnect = jest.fn();
|
||||
const reattach = jest.fn();
|
||||
const context = {
|
||||
matchManager: { enabled: true, handleReconnect },
|
||||
matchQueue: { reattach },
|
||||
_resolveMatchSession: jest.fn(async () => null)
|
||||
};
|
||||
|
||||
await SocketHandlers.prototype._handleMatchReconnect.call(context, socket, { userId: 7 });
|
||||
|
||||
expect(handleReconnect).not.toHaveBeenCalled();
|
||||
expect(reattach).not.toHaveBeenCalled();
|
||||
expect(socket.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MatchQueue queued reconnect', () => {
|
||||
test('updates the durable queued row and memory socket while preserving its place', async () => {
|
||||
process.env.MATCH_ENABLED = 'true';
|
||||
const statements = [];
|
||||
const queuedRow = { id: 31, created_at: new Date('2026-10-21T10:00:00Z') };
|
||||
const db = { query: jest.fn(async (sql, params) => {
|
||||
statements.push(sql);
|
||||
expect(params).toEqual(['new-socket', 'current-session', 7, 'free']);
|
||||
return { rowCount: 1, rows: [queuedRow] };
|
||||
}) };
|
||||
const queue = new MatchQueue({ db });
|
||||
queue._queues.free.push({
|
||||
queueEntryId: queuedRow.id, userId: 7, socketId: 'old-socket',
|
||||
sessionToken: 'old-session', createdAt: queuedRow.created_at.getTime()
|
||||
});
|
||||
try {
|
||||
const restored = await queue.reattach(7, 'free', 'new-socket', 'current-session');
|
||||
expect(restored).toEqual({ inQueue: true, position: 1 });
|
||||
expect(queue._queues.free[0]).toMatchObject({
|
||||
queueEntryId: queuedRow.id, userId: 7, socketId: 'new-socket',
|
||||
sessionToken: 'current-session', createdAt: queuedRow.created_at.getTime()
|
||||
});
|
||||
expect(statements).toHaveLength(1);
|
||||
} finally {
|
||||
delete process.env.MATCH_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
test('a matched or absent durable row cannot be restored from stale memory', async () => {
|
||||
process.env.MATCH_ENABLED = 'true';
|
||||
const db = { query: jest.fn(async () => ({ rowCount: 0, rows: [] })) };
|
||||
const queue = new MatchQueue({ db });
|
||||
queue._queues.free.push({ userId: 7, socketId: 'old-socket' });
|
||||
try {
|
||||
expect(await queue.reattach(7, 'free', 'new-socket', 'current-session'))
|
||||
.toEqual({ inQueue: false });
|
||||
expect(queue._queues.free[0].socketId).toBe('old-socket');
|
||||
} finally {
|
||||
delete process.env.MATCH_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
test('a restored entrant receives the countdown and start on the new socket', async () => {
|
||||
jest.useFakeTimers();
|
||||
process.env.MATCH_ENABLED = 'true';
|
||||
const db = { query: jest.fn(async () => ({
|
||||
rowCount: 1, rows: [{ id: 31, created_at: new Date('2026-10-21T10:00:00Z') }]
|
||||
})) };
|
||||
const queue = new MatchQueue({ db });
|
||||
queue._queues.free.push({
|
||||
queueEntryId: 31, userId: 7, socketId: 'old-socket',
|
||||
sessionToken: 'old-session', createdAt: Date.parse('2026-10-21T10:00:00Z')
|
||||
});
|
||||
const makeSocket = id => ({
|
||||
id, channels: new Set(), events: [],
|
||||
join(channel) { this.channels.add(channel); },
|
||||
emit(event, payload) { this.events.push({ event, payload }); }
|
||||
});
|
||||
const oldSocket = makeSocket('old-socket');
|
||||
const newSocket = makeSocket('new-socket');
|
||||
const otherSocket = makeSocket('other-socket');
|
||||
const sockets = new Map([[newSocket.id, newSocket], [otherSocket.id, otherSocket]]);
|
||||
const io = {
|
||||
sockets: { sockets },
|
||||
to(channel) {
|
||||
return { emit(event, payload) {
|
||||
for (const socket of sockets.values()) {
|
||||
if (socket.channels.has(channel)) socket.emit(event, payload);
|
||||
}
|
||||
} };
|
||||
}
|
||||
};
|
||||
let manager;
|
||||
try {
|
||||
await queue.reattach(7, 'free', newSocket.id, 'current-session');
|
||||
const entrant = queue._queues.free[0];
|
||||
const room = new MatchRoom({
|
||||
economy: 'free',
|
||||
entrants: {
|
||||
[entrant.socketId]: { userId: entrant.userId },
|
||||
[otherSocket.id]: { userId: 8 }
|
||||
},
|
||||
seed: '0'.repeat(64)
|
||||
});
|
||||
manager = new MatchManager({ io, db: null });
|
||||
manager.setEngine(room.id, { start: () => room.start(), stop: () => {} });
|
||||
manager.attach(room, [entrant, { userId: 8, socketId: otherSocket.id }], {
|
||||
countdownMs: 20, minDurationMs: 20000, hardCeilingMs: 60000
|
||||
});
|
||||
|
||||
expect(newSocket.events.some(item => item.event === 'match_joined')).toBe(true);
|
||||
expect(oldSocket.events).toHaveLength(0);
|
||||
jest.advanceTimersByTime(20);
|
||||
expect(room.status).toBe('active');
|
||||
expect(newSocket.events.some(item => item.event === 'match_start')).toBe(true);
|
||||
expect(oldSocket.events).toHaveLength(0);
|
||||
} finally {
|
||||
manager?.shutdown();
|
||||
jest.useRealTimers();
|
||||
delete process.env.MATCH_ENABLED;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user