Cockatrice/webclient/src/websocket/events/session/sessionEvents.spec.ts
2026-04-12 15:55:00 -05:00

485 lines
19 KiB
TypeScript

// Tests for simple session events that delegate 1:1 to SessionPersistence
// or RoomPersistence with minimal logic.
jest.mock('../../persistence', () => ({
SessionPersistence: {
gameJoined: jest.fn(),
notifyUser: jest.fn(),
replayAdded: jest.fn(),
serverMessage: jest.fn(),
serverShutdown: jest.fn(),
updateUsers: jest.fn(),
updateInfo: jest.fn(),
userJoined: jest.fn(),
userLeft: jest.fn(),
userMessage: jest.fn(),
addToBuddyList: jest.fn(),
addToIgnoreList: jest.fn(),
removeFromBuddyList: jest.fn(),
removeFromIgnoreList: jest.fn(),
playerPropertiesChanged: jest.fn(),
},
RoomPersistence: {
updateRooms: jest.fn(),
},
}));
jest.mock('../../WebClient', () => ({
__esModule: true,
default: {
clientOptions: { autojoinrooms: false },
options: {},
protocolVersion: 14,
},
}));
jest.mock('../../commands/session', () => ({
joinRoom: jest.fn(),
updateStatus: jest.fn(),
disconnect: jest.fn(),
login: jest.fn(),
register: jest.fn(),
activate: jest.fn(),
requestPasswordSalt: jest.fn(),
forgotPasswordRequest: jest.fn(),
forgotPasswordChallenge: jest.fn(),
forgotPasswordReset: jest.fn(),
}));
jest.mock('../../utils', () => ({
generateSalt: jest.fn().mockReturnValue('newSalt'),
passwordSaltSupported: jest.fn().mockReturnValue(0),
}));
jest.mock('../../services/ProtoController', () => ({
ProtoController: {
root: {
Event_ConnectionClosed: {
CloseReason: {
USER_LIMIT_REACHED: 0,
TOO_MANY_CONNECTIONS: 1,
BANNED: 2,
DEMOTED: 3,
SERVER_SHUTDOWN: 4,
USERNAMEINVALID: 5,
LOGGEDINELSEWERE: 6,
OTHER: 7,
},
},
},
},
}));
import { WebSocketConnectReason } from 'types';
import { SessionPersistence, RoomPersistence } from '../../persistence';
import webClient from '../../WebClient';
import * as SessionCmds from '../../commands/session';
import * as Utils from '../../utils';
beforeEach(() => {
jest.clearAllMocks();
(Utils.generateSalt as jest.Mock).mockReturnValue('newSalt');
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
});
// ----------------------------------------------------------------
// gameJoined
// ----------------------------------------------------------------
describe('gameJoined', () => {
const { gameJoined } = jest.requireActual('./gameJoined');
it('calls SessionPersistence.gameJoined', () => {
const data = { gameId: 1 } as any;
gameJoined(data);
expect(SessionPersistence.gameJoined).toHaveBeenCalledWith(data);
});
});
// ----------------------------------------------------------------
// notifyUser
// ----------------------------------------------------------------
describe('notifyUser', () => {
const { notifyUser } = jest.requireActual('./notifyUser');
it('calls SessionPersistence.notifyUser', () => {
const data = { message: 'yo' } as any;
notifyUser(data);
expect(SessionPersistence.notifyUser).toHaveBeenCalledWith(data);
});
});
// ----------------------------------------------------------------
// replayAdded
// ----------------------------------------------------------------
describe('replayAdded', () => {
const { replayAdded } = jest.requireActual('./replayAdded');
it('calls SessionPersistence.replayAdded with matchInfo', () => {
replayAdded({ matchInfo: { id: 42 } } as any);
expect(SessionPersistence.replayAdded).toHaveBeenCalledWith({ id: 42 });
});
});
// ----------------------------------------------------------------
// serverCompleteList
// ----------------------------------------------------------------
describe('serverCompleteList', () => {
const { serverCompleteList } = jest.requireActual('./serverCompleteList');
it('calls SessionPersistence.updateUsers and RoomPersistence.updateRooms', () => {
serverCompleteList({ userList: ['u'], roomList: ['r'] } as any);
expect(SessionPersistence.updateUsers).toHaveBeenCalledWith(['u']);
expect(RoomPersistence.updateRooms).toHaveBeenCalledWith(['r']);
});
});
// ----------------------------------------------------------------
// serverMessage
// ----------------------------------------------------------------
describe('serverMessage', () => {
const { serverMessage } = jest.requireActual('./serverMessage');
it('calls SessionPersistence.serverMessage with message', () => {
serverMessage({ message: 'hello server' });
expect(SessionPersistence.serverMessage).toHaveBeenCalledWith('hello server');
});
});
// ----------------------------------------------------------------
// serverShutdown
// ----------------------------------------------------------------
describe('serverShutdown', () => {
const { serverShutdown } = jest.requireActual('./serverShutdown');
it('calls SessionPersistence.serverShutdown', () => {
const payload = { reason: 'maintenance' } as any;
serverShutdown(payload);
expect(SessionPersistence.serverShutdown).toHaveBeenCalledWith(payload);
});
});
// ----------------------------------------------------------------
// userJoined
// ----------------------------------------------------------------
describe('userJoined', () => {
const { userJoined } = jest.requireActual('./userJoined');
it('calls SessionPersistence.userJoined with userInfo', () => {
userJoined({ userInfo: { name: 'alice' } } as any);
expect(SessionPersistence.userJoined).toHaveBeenCalledWith({ name: 'alice' });
});
});
// ----------------------------------------------------------------
// userLeft
// ----------------------------------------------------------------
describe('userLeft', () => {
const { userLeft } = jest.requireActual('./userLeft');
it('calls SessionPersistence.userLeft with name', () => {
userLeft({ name: 'bob' });
expect(SessionPersistence.userLeft).toHaveBeenCalledWith('bob');
});
});
// ----------------------------------------------------------------
// userMessage
// ----------------------------------------------------------------
describe('userMessage', () => {
const { userMessage } = jest.requireActual('./userMessage');
it('calls SessionPersistence.userMessage', () => {
const payload = { userName: 'alice', message: 'hi' } as any;
userMessage(payload);
expect(SessionPersistence.userMessage).toHaveBeenCalledWith(payload);
});
});
// ----------------------------------------------------------------
// addToList
// ----------------------------------------------------------------
describe('addToList', () => {
const { addToList } = jest.requireActual('./addToList');
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
afterAll(() => logSpy.mockRestore());
it('buddy list → addToBuddyList', () => {
addToList({ listName: 'buddy', userInfo: { name: 'alice' } } as any);
expect(SessionPersistence.addToBuddyList).toHaveBeenCalledWith({ name: 'alice' });
});
it('ignore list → addToIgnoreList', () => {
addToList({ listName: 'ignore', userInfo: { name: 'bob' } } as any);
expect(SessionPersistence.addToIgnoreList).toHaveBeenCalledWith({ name: 'bob' });
});
it('unknown list → console.log', () => {
addToList({ listName: 'unknown', userInfo: {} } as any);
expect(logSpy).toHaveBeenCalled();
});
});
// ----------------------------------------------------------------
// removeFromList
// ----------------------------------------------------------------
describe('removeFromList', () => {
const { removeFromList } = jest.requireActual('./removeFromList');
it('buddy list → removeFromBuddyList', () => {
removeFromList({ listName: 'buddy', userName: 'alice' } as any);
expect(SessionPersistence.removeFromBuddyList).toHaveBeenCalledWith('alice');
});
it('ignore list → removeFromIgnoreList', () => {
removeFromList({ listName: 'ignore', userName: 'bob' } as any);
expect(SessionPersistence.removeFromIgnoreList).toHaveBeenCalledWith('bob');
});
it('unknown list → console.log', () => {
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
removeFromList({ listName: 'other', userName: 'x' } as any);
expect(logSpy).toHaveBeenCalled();
logSpy.mockRestore();
});
});
// ----------------------------------------------------------------
// listRooms
// ----------------------------------------------------------------
describe('listRooms', () => {
const { listRooms } = jest.requireActual('./listRooms');
it('calls RoomPersistence.updateRooms', () => {
listRooms({ roomList: [] });
expect(RoomPersistence.updateRooms).toHaveBeenCalledWith([]);
});
it('does not call joinRoom when autojoinrooms is false', () => {
(webClient as any).clientOptions = { autojoinrooms: false };
listRooms({ roomList: [{ autoJoin: true, roomId: 1 }] } as any);
expect(SessionCmds.joinRoom).not.toHaveBeenCalled();
});
it('calls joinRoom for autoJoin rooms when autojoinrooms is true', () => {
(webClient as any).clientOptions = { autojoinrooms: true };
listRooms({ roomList: [{ autoJoin: true, roomId: 2 }, { autoJoin: false, roomId: 3 }] } as any);
expect(SessionCmds.joinRoom).toHaveBeenCalledTimes(1);
expect(SessionCmds.joinRoom).toHaveBeenCalledWith(2);
});
});
// ----------------------------------------------------------------
// connectionClosed
// ----------------------------------------------------------------
describe('connectionClosed', () => {
const { connectionClosed } = jest.requireActual('./connectionClosed');
it('uses reasonStr when provided', () => {
connectionClosed({ reason: 0, reasonStr: 'custom' } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom');
});
it('USER_LIMIT_REACHED → specific message', () => {
connectionClosed({ reason: 0 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('maximum user capacity')
);
});
it('TOO_MANY_CONNECTIONS → specific message', () => {
connectionClosed({ reason: 1 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('too many concurrent'));
});
it('BANNED → specific message', () => {
connectionClosed({ reason: 2 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('banned'));
});
it('DEMOTED → specific message', () => {
connectionClosed({ reason: 3 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted'));
});
it('SERVER_SHUTDOWN → specific message', () => {
connectionClosed({ reason: 4 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('shutdown'));
});
it('USERNAMEINVALID → specific message', () => {
connectionClosed({ reason: 5 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username'));
});
it('LOGGEDINELSEWERE → specific message', () => {
connectionClosed({ reason: 6 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out'));
});
it('OTHER → "Unknown reason"', () => {
connectionClosed({ reason: 7 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'Unknown reason');
});
it('BANNED with valid positive endTime → shows formatted date', () => {
connectionClosed({ reason: 2, endTime: 1700000000 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(
expect.anything(),
expect.stringContaining('You are banned until')
);
});
it('BANNED with endTime = 0 → shows generic banned message', () => {
connectionClosed({ reason: 2, endTime: 0 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned');
});
it('BANNED with endTime = -1 → shows generic banned message', () => {
connectionClosed({ reason: 2, endTime: -1 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned');
});
it('BANNED with endTime = NaN → shows generic banned message', () => {
connectionClosed({ reason: 2, endTime: NaN } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned');
});
it('BANNED with endTime = Infinity → shows generic banned message', () => {
connectionClosed({ reason: 2, endTime: Infinity } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned');
});
it('BANNED with reasonStr → uses reasonStr regardless of endTime', () => {
connectionClosed({ reason: 2, endTime: 0, reasonStr: 'custom ban reason' } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason');
});
});
// ----------------------------------------------------------------
// serverIdentification
// ----------------------------------------------------------------
describe('serverIdentification', () => {
const { serverIdentification } = jest.requireActual('./serverIdentification');
beforeEach(() => {
(webClient as any).protocolVersion = 14;
(webClient as any).options = {};
});
it('disconnects when protocolVersion mismatches', () => {
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 99, serverOptions: 0 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalled();
expect(SessionCmds.disconnect).toHaveBeenCalled();
});
it('LOGIN reason without salt → calls login with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.login).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
});
it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
});
it('REGISTER reason without salt → calls register with password and null salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret',
null
);
});
it('REGISTER reason with salt → calls register with password and generated salt', () => {
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.register).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret',
'newSalt'
);
});
it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.activate).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
});
it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ password: expect.anything() }),
'secret'
);
});
it('PASSWORD_RESET_REQUEST reason → calls forgotPasswordRequest', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST };
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.forgotPasswordRequest).toHaveBeenCalled();
});
it('PASSWORD_RESET_CHALLENGE reason → calls forgotPasswordChallenge', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE };
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled();
});
it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),
'newpw'
);
});
it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => {
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' };
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
expect.not.objectContaining({ newPassword: expect.anything() }),
undefined,
'newpw'
);
});
it('unknown reason → updateStatus DISCONNECTED and disconnect', () => {
(webClient as any).options = { reason: 999 };
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionCmds.updateStatus).toHaveBeenCalled();
expect(SessionCmds.disconnect).toHaveBeenCalled();
});
it('updates webClient.options to empty and calls SessionPersistence.updateInfo', () => {
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN };
serverIdentification({ serverName: 'myServer', serverVersion: '2.0', protocolVersion: 14, serverOptions: 0 } as any);
expect(SessionPersistence.updateInfo).toHaveBeenCalledWith('myServer', '2.0');
expect((webClient as any).options).toEqual({});
});
});