mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
Add near 100% unit test coverage for webclient websocket layer
This commit is contained in:
parent
8cc65b8967
commit
35be723ebf
26 changed files with 3932 additions and 0 deletions
122
webclient/src/websocket/WebClient.spec.ts
Normal file
122
webclient/src/websocket/WebClient.spec.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
jest.mock('./services/WebSocketService', () => ({
|
||||
WebSocketService: jest.fn().mockImplementation(() => ({
|
||||
message$: { subscribe: jest.fn() },
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('./services/ProtobufService', () => ({
|
||||
ProtobufService: jest.fn().mockImplementation(() => ({
|
||||
handleMessageEvent: jest.fn(),
|
||||
sendKeepAliveCommand: jest.fn(),
|
||||
resetCommands: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('./persistence', () => ({
|
||||
RoomPersistence: { clearStore: jest.fn() },
|
||||
SessionPersistence: { clearStore: jest.fn() },
|
||||
}));
|
||||
|
||||
import { WebClient } from './WebClient';
|
||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||
import { StatusEnum } from 'types';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
describe('WebClient', () => {
|
||||
let client: WebClient;
|
||||
let messageSubject: Subject<MessageEvent>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const { ProtobufService } = require('./services/ProtobufService');
|
||||
ProtobufService.mockImplementation(() => ({
|
||||
handleMessageEvent: jest.fn(),
|
||||
sendKeepAliveCommand: jest.fn(),
|
||||
resetCommands: jest.fn(),
|
||||
}));
|
||||
messageSubject = new Subject<MessageEvent>();
|
||||
const { WebSocketService } = require('./services/WebSocketService');
|
||||
WebSocketService.mockImplementation(() => ({
|
||||
message$: messageSubject,
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
// suppress console.log from constructor in non-test-env check
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
client = new WebClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('subscribes socket.message$ to protobuf.handleMessageEvent', () => {
|
||||
const event = { data: new ArrayBuffer(0) } as MessageEvent;
|
||||
messageSubject.next(event);
|
||||
expect(client.protobuf.handleMessageEvent).toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect', () => {
|
||||
it('sets connectionAttemptMade to true', () => {
|
||||
const opts: any = { host: 'h', port: 1 };
|
||||
client.connect(opts);
|
||||
expect(client.connectionAttemptMade).toBe(true);
|
||||
});
|
||||
|
||||
it('stores options and calls socket.connect', () => {
|
||||
const opts: any = { host: 'h', port: 1 };
|
||||
client.connect(opts);
|
||||
expect(client.options).toBe(opts);
|
||||
expect(client.socket.connect).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testConnect', () => {
|
||||
it('delegates to socket.testConnect', () => {
|
||||
const opts: any = { host: 'h', port: 1 };
|
||||
client.testConnect(opts);
|
||||
expect(client.socket.testConnect).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('delegates to socket.disconnect', () => {
|
||||
client.disconnect();
|
||||
expect(client.socket.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keepAlive', () => {
|
||||
it('delegates to protobuf.sendKeepAliveCommand', () => {
|
||||
const pingCb = jest.fn();
|
||||
client.keepAlive(pingCb);
|
||||
expect(client.protobuf.sendKeepAliveCommand).toHaveBeenCalledWith(pingCb);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStatus', () => {
|
||||
it('sets the status', () => {
|
||||
client.updateStatus(StatusEnum.CONNECTED);
|
||||
expect(client.status).toBe(StatusEnum.CONNECTED);
|
||||
});
|
||||
|
||||
it('calls protobuf.resetCommands and clears stores on DISCONNECTED', () => {
|
||||
client.updateStatus(StatusEnum.DISCONNECTED);
|
||||
expect(client.protobuf.resetCommands).toHaveBeenCalled();
|
||||
expect(RoomPersistence.clearStore).toHaveBeenCalled();
|
||||
expect(SessionPersistence.clearStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not clear stores when status is not DISCONNECTED', () => {
|
||||
client.updateStatus(StatusEnum.CONNECTED);
|
||||
expect(client.protobuf.resetCommands).not.toHaveBeenCalled();
|
||||
expect(RoomPersistence.clearStore).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
35
webclient/src/websocket/__mocks__/callbackHelpers.ts
Normal file
35
webclient/src/websocket/__mocks__/callbackHelpers.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Factory for invoking BackendService command callbacks in unit tests.
|
||||
*
|
||||
* @param mockFn - The jest.Mock for the BackendService send method
|
||||
* (e.g. BackendService.sendSessionCommand as jest.Mock).
|
||||
* @param optsArgIndex - Index of the options argument in the mock call.
|
||||
* Defaults to 2 (commandName, params, options).
|
||||
* Use 3 for sendRoomCommand (roomId, commandName, params, options).
|
||||
*/
|
||||
export function makeCallbackHelpers(mockFn: jest.Mock, optsArgIndex = 2) {
|
||||
function getLastSendOpts() {
|
||||
const calls = mockFn.mock.calls;
|
||||
return calls[calls.length - 1]?.[optsArgIndex];
|
||||
}
|
||||
|
||||
function invokeOnSuccess(response: any = {}, raw?: any) {
|
||||
getLastSendOpts()?.onSuccess?.(response, raw ?? response);
|
||||
}
|
||||
|
||||
function invokeResponseCode(code: number, raw: any = { responseCode: code }) {
|
||||
const opts = getLastSendOpts();
|
||||
if (opts?.onResponseCode?.[code]) opts.onResponseCode[code](raw);
|
||||
}
|
||||
|
||||
function invokeOnError(code: number = 99, raw: any = {}) {
|
||||
getLastSendOpts()?.onError?.(code, raw);
|
||||
}
|
||||
|
||||
function invokeCallback(callbackName: string, ...args: any[]) {
|
||||
const opts = getLastSendOpts();
|
||||
if (opts?.[callbackName]) opts[callbackName](...args);
|
||||
}
|
||||
|
||||
return { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError, invokeCallback };
|
||||
}
|
||||
74
webclient/src/websocket/__mocks__/helpers.ts
Normal file
74
webclient/src/websocket/__mocks__/helpers.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Shared mock factories for websocket layer unit tests.
|
||||
* Import the helpers you need in each spec file via:
|
||||
* import { makeMockProtoRoot, makeMockWebSocket } from '../__mocks__/helpers';
|
||||
*/
|
||||
|
||||
/** Builds a minimal mock of ProtoController.root */
|
||||
export function makeMockProtoRoot() {
|
||||
const encode = { finish: jest.fn().mockReturnValue(new Uint8Array()) };
|
||||
return {
|
||||
CommandContainer: {
|
||||
create: jest.fn(args => ({ ...args })),
|
||||
encode: jest.fn().mockReturnValue(encode),
|
||||
},
|
||||
SessionCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
RoomCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
ModeratorCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
AdminCommand: { create: jest.fn(args => ({ ...args })) },
|
||||
ServerMessage: {
|
||||
decode: jest.fn(),
|
||||
MessageType: {
|
||||
RESPONSE: 'RESPONSE',
|
||||
ROOM_EVENT: 'ROOM_EVENT',
|
||||
SESSION_EVENT: 'SESSION_EVENT',
|
||||
GAME_EVENT_CONTAINER: 'GAME_EVENT_CONTAINER',
|
||||
},
|
||||
},
|
||||
Response: {
|
||||
ResponseCode: {
|
||||
RespOk: 0,
|
||||
RespRegistrationRequired: 1,
|
||||
},
|
||||
},
|
||||
Event_ServerIdentification: {
|
||||
ServerOptions: { SupportsPasswordHash: 2 },
|
||||
},
|
||||
Event_ConnectionClosed: {
|
||||
CloseReason: {
|
||||
USER_LIMIT_REACHED: 1,
|
||||
TOO_MANY_CONNECTIONS: 2,
|
||||
BANNED: 3,
|
||||
DEMOTED: 4,
|
||||
SERVER_SHUTDOWN: 5,
|
||||
USERNAMEINVALID: 6,
|
||||
LOGGEDINELSEWERE: 7,
|
||||
OTHER: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Builds a mock WebSocket instance */
|
||||
export function makeMockWebSocketInstance() {
|
||||
return {
|
||||
send: jest.fn(),
|
||||
close: jest.fn(),
|
||||
readyState: WebSocket.OPEN,
|
||||
binaryType: '' as BinaryType,
|
||||
onopen: null as any,
|
||||
onclose: null as any,
|
||||
onerror: null as any,
|
||||
onmessage: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
/** Installs a mock WebSocket constructor on global. Returns the mock instance. */
|
||||
export function installMockWebSocket() {
|
||||
const mockInstance = makeMockWebSocketInstance();
|
||||
const MockWS = jest.fn(() => mockInstance) as any;
|
||||
MockWS.OPEN = 1;
|
||||
MockWS.CLOSED = 3;
|
||||
(global as any).WebSocket = MockWS;
|
||||
return { MockWS, mockInstance };
|
||||
}
|
||||
132
webclient/src/websocket/__mocks__/sessionCommandMocks.ts
Normal file
132
webclient/src/websocket/__mocks__/sessionCommandMocks.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Shared mock shape factories for session command specs.
|
||||
*
|
||||
* Usage inside jest.mock() factory callbacks (require is used because
|
||||
* jest.mock() is hoisted above imports):
|
||||
*
|
||||
* jest.mock('../../WebClient', () => {
|
||||
* const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
* return { __esModule: true, default: makeWebClientMock() };
|
||||
* });
|
||||
*/
|
||||
|
||||
/** Superset WebClient mock — covers all properties used across both session spec files. */
|
||||
export function makeWebClientMock() {
|
||||
return {
|
||||
connect: jest.fn(),
|
||||
testConnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
clientConfig: { clientid: 'webatrice', clientver: '1.0', clientfeatures: [] },
|
||||
options: {},
|
||||
protocolVersion: 14,
|
||||
status: 0,
|
||||
connectionAttemptMade: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Superset ProtoController.root mock — includes all ResponseCode values and Event_ServerIdentification. */
|
||||
export function makeProtoControllerRootMock() {
|
||||
return {
|
||||
Response: {
|
||||
ResponseCode: {
|
||||
RespOk: 0,
|
||||
RespClientUpdateRequired: 1,
|
||||
RespWrongPassword: 2,
|
||||
RespUsernameInvalid: 3,
|
||||
RespWouldOverwriteOldSession: 4,
|
||||
RespUserIsBanned: 5,
|
||||
RespRegistrationRequired: 6,
|
||||
RespClientIdRequired: 7,
|
||||
RespContextError: 8,
|
||||
RespAccountNotActivated: 9,
|
||||
RespRegistrationAccepted: 10,
|
||||
RespRegistrationAcceptedNeedsActivation: 11,
|
||||
RespUserAlreadyExists: 12,
|
||||
RespPasswordTooShort: 13,
|
||||
RespEmailRequiredToRegister: 14,
|
||||
RespEmailBlackListed: 15,
|
||||
RespTooManyRequests: 16,
|
||||
RespRegistrationDisabled: 17,
|
||||
RespActivationAccepted: 18,
|
||||
},
|
||||
},
|
||||
Event_ServerIdentification: {
|
||||
ServerOptions: { SupportsPasswordHash: 2 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Utils mock with unified return values. */
|
||||
export function makeUtilsMock() {
|
||||
return {
|
||||
hashPassword: jest.fn().mockReturnValue('hashed_pw'),
|
||||
generateSalt: jest.fn().mockReturnValue('randSalt'),
|
||||
passwordSaltSupported: jest.fn().mockReturnValue(0),
|
||||
};
|
||||
}
|
||||
|
||||
/** Superset SessionPersistence mock — covers all methods used across both session spec files. */
|
||||
export function makeSessionPersistenceMock() {
|
||||
return {
|
||||
loginSuccessful: jest.fn(),
|
||||
loginFailed: jest.fn(),
|
||||
updateBuddyList: jest.fn(),
|
||||
updateIgnoreList: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
updateUsers: jest.fn(),
|
||||
accountAwaitingActivation: jest.fn(),
|
||||
accountActivationSuccess: jest.fn(),
|
||||
accountActivationFailed: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
directMessageSent: jest.fn(),
|
||||
addToList: jest.fn(),
|
||||
removeFromList: jest.fn(),
|
||||
deleteServerDeck: jest.fn(),
|
||||
deleteServerDeckDir: jest.fn(),
|
||||
updateServerDecks: jest.fn(),
|
||||
uploadServerDeck: jest.fn(),
|
||||
createServerDeckDir: jest.fn(),
|
||||
getGamesOfUser: jest.fn(),
|
||||
getUserInfo: jest.fn(),
|
||||
accountPasswordChange: jest.fn(),
|
||||
accountEditChanged: jest.fn(),
|
||||
accountImageChanged: jest.fn(),
|
||||
replayList: jest.fn(),
|
||||
replayAdded: jest.fn(),
|
||||
replayModifyMatch: jest.fn(),
|
||||
replayDeleteMatch: jest.fn(),
|
||||
resetPasswordChallenge: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
resetPasswordFailed: jest.fn(),
|
||||
resetPasswordSuccess: jest.fn(),
|
||||
registrationFailed: jest.fn(),
|
||||
registrationSuccess: jest.fn(),
|
||||
registrationUserNameError: jest.fn(),
|
||||
registrationPasswordError: jest.fn(),
|
||||
registrationEmailError: jest.fn(),
|
||||
registrationRequiresEmail: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Session barrel mock — pure jest.fn() map for all cross-command calls.
|
||||
* Used as-is by sessionCommands-complex.spec.ts, or spread over jest.requireActual
|
||||
* by sessionCommands-simple.spec.ts to preserve real implementations for
|
||||
* the commands under test.
|
||||
*/
|
||||
export function makeSessionBarrelMock() {
|
||||
return {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
activate: jest.fn(),
|
||||
forgotPasswordReset: jest.fn(),
|
||||
forgotPasswordRequest: jest.fn(),
|
||||
forgotPasswordChallenge: jest.fn(),
|
||||
requestPasswordSalt: jest.fn(),
|
||||
listUsers: jest.fn(),
|
||||
listRooms: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
}
|
||||
104
webclient/src/websocket/commands/admin/adminCommands.spec.ts
Normal file
104
webclient/src/websocket/commands/admin/adminCommands.spec.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
jest.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendAdminCommand: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => ({
|
||||
AdminPersistence: {
|
||||
adjustMod: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
shutdownServer: jest.fn(),
|
||||
updateServerMessage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { AdminPersistence } from '../../persistence';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
|
||||
BackendService.sendAdminCommand as jest.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// adjustMod
|
||||
// ----------------------------------------------------------------
|
||||
describe('adjustMod', () => {
|
||||
const { adjustMod } = jest.requireActual('./adjustMod');
|
||||
|
||||
it('calls sendAdminCommand with Command_AdjustMod', () => {
|
||||
adjustMod('alice', true, false);
|
||||
expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(
|
||||
'Command_AdjustMod',
|
||||
expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls AdminPersistence.adjustMod', () => {
|
||||
adjustMod('alice', true, false);
|
||||
invokeOnSuccess();
|
||||
expect(AdminPersistence.adjustMod).toHaveBeenCalledWith('alice', true, false);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// reloadConfig
|
||||
// ----------------------------------------------------------------
|
||||
describe('reloadConfig', () => {
|
||||
const { reloadConfig } = jest.requireActual('./reloadConfig');
|
||||
|
||||
it('calls sendAdminCommand with Command_ReloadConfig', () => {
|
||||
reloadConfig();
|
||||
expect(BackendService.sendAdminCommand).toHaveBeenCalledWith('Command_ReloadConfig', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls AdminPersistence.reloadConfig', () => {
|
||||
reloadConfig();
|
||||
invokeOnSuccess();
|
||||
expect(AdminPersistence.reloadConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// shutdownServer
|
||||
// ----------------------------------------------------------------
|
||||
describe('shutdownServer', () => {
|
||||
const { shutdownServer } = jest.requireActual('./shutdownServer');
|
||||
|
||||
it('calls sendAdminCommand with Command_ShutdownServer', () => {
|
||||
shutdownServer('maintenance', 10);
|
||||
expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(
|
||||
'Command_ShutdownServer',
|
||||
{ reason: 'maintenance', minutes: 10 },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls AdminPersistence.shutdownServer', () => {
|
||||
shutdownServer('maintenance', 10);
|
||||
invokeOnSuccess();
|
||||
expect(AdminPersistence.shutdownServer).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// updateServerMessage
|
||||
// ----------------------------------------------------------------
|
||||
describe('updateServerMessage', () => {
|
||||
const { updateServerMessage } = jest.requireActual('./updateServerMessage');
|
||||
|
||||
it('calls sendAdminCommand with Command_UpdateServerMessage', () => {
|
||||
updateServerMessage();
|
||||
expect(BackendService.sendAdminCommand).toHaveBeenCalledWith('Command_UpdateServerMessage', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls AdminPersistence.updateServerMessage', () => {
|
||||
updateServerMessage();
|
||||
invokeOnSuccess();
|
||||
expect(AdminPersistence.updateServerMessage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
jest.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendModeratorCommand: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => ({
|
||||
ModeratorPersistence: {
|
||||
banFromServer: jest.fn(),
|
||||
forceActivateUser: jest.fn(),
|
||||
getAdminNotes: jest.fn(),
|
||||
banHistory: jest.fn(),
|
||||
warnHistory: jest.fn(),
|
||||
warnListOptions: jest.fn(),
|
||||
grantReplayAccess: jest.fn(),
|
||||
updateAdminNotes: jest.fn(),
|
||||
viewLogs: jest.fn(),
|
||||
warnUser: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { ModeratorPersistence } from '../../persistence';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
|
||||
BackendService.sendModeratorCommand as jest.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// banFromServer
|
||||
// ----------------------------------------------------------------
|
||||
describe('banFromServer', () => {
|
||||
const { banFromServer } = jest.requireActual('./banFromServer');
|
||||
|
||||
it('calls sendModeratorCommand with Command_BanFromServer', () => {
|
||||
banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1);
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_BanFromServer',
|
||||
expect.objectContaining({ minutes: 30, userName: 'alice' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.banFromServer', () => {
|
||||
banFromServer(30, 'alice');
|
||||
invokeOnSuccess();
|
||||
expect(ModeratorPersistence.banFromServer).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// forceActivateUser
|
||||
// ----------------------------------------------------------------
|
||||
describe('forceActivateUser', () => {
|
||||
const { forceActivateUser } = jest.requireActual('./forceActivateUser');
|
||||
|
||||
it('calls sendModeratorCommand with Command_ForceActivateUser', () => {
|
||||
forceActivateUser('alice', 'mod1');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_ForceActivateUser', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.forceActivateUser', () => {
|
||||
forceActivateUser('alice', 'mod1');
|
||||
invokeOnSuccess();
|
||||
expect(ModeratorPersistence.forceActivateUser).toHaveBeenCalledWith('alice', 'mod1');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// getAdminNotes
|
||||
// ----------------------------------------------------------------
|
||||
describe('getAdminNotes', () => {
|
||||
const { getAdminNotes } = jest.requireActual('./getAdminNotes');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetAdminNotes', () => {
|
||||
getAdminNotes('alice');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_GetAdminNotes',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ responseName: 'Response_GetAdminNotes' })
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.getAdminNotes with notes', () => {
|
||||
getAdminNotes('alice');
|
||||
const resp = { notes: 'some notes' };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_GetAdminNotes.ext': resp });
|
||||
expect(ModeratorPersistence.getAdminNotes).toHaveBeenCalledWith('alice', 'some notes');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// getBanHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('getBanHistory', () => {
|
||||
const { getBanHistory } = jest.requireActual('./getBanHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetBanHistory', () => {
|
||||
getBanHistory('alice');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_GetBanHistory',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ responseName: 'Response_BanHistory' })
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.banHistory with banList', () => {
|
||||
getBanHistory('alice');
|
||||
const resp = { banList: [{ id: 1 }] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_BanHistory.ext': resp });
|
||||
expect(ModeratorPersistence.banHistory).toHaveBeenCalledWith('alice', [{ id: 1 }]);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// getWarnHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('getWarnHistory', () => {
|
||||
const { getWarnHistory } = jest.requireActual('./getWarnHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetWarnHistory', () => {
|
||||
getWarnHistory('alice');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_GetWarnHistory',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ responseName: 'Response_WarnHistory' })
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.warnHistory with warnList', () => {
|
||||
getWarnHistory('alice');
|
||||
const resp = { warnList: [{ id: 2 }] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_WarnHistory.ext': resp });
|
||||
expect(ModeratorPersistence.warnHistory).toHaveBeenCalledWith('alice', [{ id: 2 }]);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// getWarnList
|
||||
// ----------------------------------------------------------------
|
||||
describe('getWarnList', () => {
|
||||
const { getWarnList } = jest.requireActual('./getWarnList');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GetWarnList', () => {
|
||||
getWarnList('mod1', 'alice', 'US');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_GetWarnList',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ responseName: 'Response_WarnList' })
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.warnListOptions with warning', () => {
|
||||
getWarnList('mod1', 'alice', 'US');
|
||||
const resp = { warning: ['w1', 'w2'] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_WarnList.ext': resp });
|
||||
expect(ModeratorPersistence.warnListOptions).toHaveBeenCalledWith(['w1', 'w2']);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// grantReplayAccess
|
||||
// ----------------------------------------------------------------
|
||||
describe('grantReplayAccess', () => {
|
||||
const { grantReplayAccess } = jest.requireActual('./grantReplayAccess');
|
||||
|
||||
it('calls sendModeratorCommand with Command_GrantReplayAccess', () => {
|
||||
grantReplayAccess(10, 'mod1');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_GrantReplayAccess', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.grantReplayAccess', () => {
|
||||
grantReplayAccess(10, 'mod1');
|
||||
invokeOnSuccess();
|
||||
expect(ModeratorPersistence.grantReplayAccess).toHaveBeenCalledWith(10, 'mod1');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// updateAdminNotes
|
||||
// ----------------------------------------------------------------
|
||||
describe('updateAdminNotes', () => {
|
||||
const { updateAdminNotes } = jest.requireActual('./updateAdminNotes');
|
||||
|
||||
it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => {
|
||||
updateAdminNotes('alice', 'new notes');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_UpdateAdminNotes', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.updateAdminNotes', () => {
|
||||
updateAdminNotes('alice', 'new notes');
|
||||
invokeOnSuccess();
|
||||
expect(ModeratorPersistence.updateAdminNotes).toHaveBeenCalledWith('alice', 'new notes');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// viewLogHistory
|
||||
// ----------------------------------------------------------------
|
||||
describe('viewLogHistory', () => {
|
||||
const { viewLogHistory } = jest.requireActual('./viewLogHistory');
|
||||
|
||||
it('calls sendModeratorCommand with Command_ViewLogHistory', () => {
|
||||
viewLogHistory({ filters: 'all' } as any);
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(
|
||||
'Command_ViewLogHistory',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ responseName: 'Response_ViewLogHistory' })
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.viewLogs with logMessage', () => {
|
||||
viewLogHistory({ filters: 'all' } as any);
|
||||
const resp = { logMessage: ['log1'] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_ViewLogHistory.ext': resp });
|
||||
expect(ModeratorPersistence.viewLogs).toHaveBeenCalledWith(['log1']);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// warnUser
|
||||
// ----------------------------------------------------------------
|
||||
describe('warnUser', () => {
|
||||
const { warnUser } = jest.requireActual('./warnUser');
|
||||
|
||||
it('calls sendModeratorCommand with Command_WarnUser', () => {
|
||||
warnUser('alice', 'bad behavior', 'cid');
|
||||
expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_WarnUser', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls ModeratorPersistence.warnUser', () => {
|
||||
warnUser('alice', 'bad behavior', 'cid');
|
||||
invokeOnSuccess();
|
||||
expect(ModeratorPersistence.warnUser).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
});
|
||||
100
webclient/src/websocket/commands/room/roomCommands.spec.ts
Normal file
100
webclient/src/websocket/commands/room/roomCommands.spec.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
jest.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendRoomCommand: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => ({
|
||||
RoomPersistence: {
|
||||
gameCreated: jest.fn(),
|
||||
joinedGame: jest.fn(),
|
||||
leaveRoom: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { RoomPersistence } from '../../persistence';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers(
|
||||
BackendService.sendRoomCommand as jest.Mock,
|
||||
3 // sendRoomCommand(roomId, commandName, params, options) — options at index 3
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// createGame
|
||||
// ----------------------------------------------------------------
|
||||
describe('createGame', () => {
|
||||
const { createGame } = jest.requireActual('./createGame');
|
||||
|
||||
it('calls sendRoomCommand with Command_CreateGame', () => {
|
||||
createGame(5, { maxPlayers: 4 } as any);
|
||||
expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(5, 'Command_CreateGame', { maxPlayers: 4 }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls RoomPersistence.gameCreated with roomId', () => {
|
||||
createGame(5, {} as any);
|
||||
invokeOnSuccess();
|
||||
expect(RoomPersistence.gameCreated).toHaveBeenCalledWith(5);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// joinGame
|
||||
// ----------------------------------------------------------------
|
||||
describe('joinGame', () => {
|
||||
const { joinGame } = jest.requireActual('./joinGame');
|
||||
|
||||
it('calls sendRoomCommand with Command_JoinGame', () => {
|
||||
joinGame(7, { gameId: 42, password: '' } as any);
|
||||
expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(7, 'Command_JoinGame', { gameId: 42, password: '' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls RoomPersistence.joinedGame with roomId and gameId', () => {
|
||||
joinGame(7, { gameId: 42 } as any);
|
||||
invokeOnSuccess();
|
||||
expect(RoomPersistence.joinedGame).toHaveBeenCalledWith(7, 42);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// leaveRoom
|
||||
// ----------------------------------------------------------------
|
||||
describe('leaveRoom', () => {
|
||||
const { leaveRoom } = jest.requireActual('./leaveRoom');
|
||||
|
||||
it('calls sendRoomCommand with Command_LeaveRoom', () => {
|
||||
leaveRoom(3);
|
||||
expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(3, 'Command_LeaveRoom', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls RoomPersistence.leaveRoom with roomId', () => {
|
||||
leaveRoom(3);
|
||||
invokeOnSuccess();
|
||||
expect(RoomPersistence.leaveRoom).toHaveBeenCalledWith(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// roomSay
|
||||
// ----------------------------------------------------------------
|
||||
describe('roomSay', () => {
|
||||
const { roomSay } = jest.requireActual('./roomSay');
|
||||
|
||||
it('calls sendRoomCommand with trimmed message', () => {
|
||||
roomSay(2, ' hello ');
|
||||
expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(2, 'Command_RoomSay', { message: 'hello' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('does not call sendRoomCommand when message is blank', () => {
|
||||
roomSay(2, ' ');
|
||||
expect(BackendService.sendRoomCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call sendRoomCommand when message is empty string', () => {
|
||||
roomSay(2, '');
|
||||
expect(BackendService.sendRoomCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,512 @@
|
|||
// Tests for complex session commands that call webClient directly
|
||||
// or have multiple branching callbacks.
|
||||
|
||||
jest.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendSessionCommand: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => {
|
||||
const { makeSessionPersistenceMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return {
|
||||
SessionPersistence: makeSessionPersistenceMock(),
|
||||
RoomPersistence: {},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../WebClient', () => {
|
||||
const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return { __esModule: true, default: makeWebClientMock() };
|
||||
});
|
||||
|
||||
jest.mock('../../services/ProtoController', () => {
|
||||
const { makeProtoControllerRootMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return { ProtoController: { root: makeProtoControllerRootMock() } };
|
||||
});
|
||||
|
||||
jest.mock('../../utils', () => {
|
||||
const { makeUtilsMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return makeUtilsMock();
|
||||
});
|
||||
|
||||
// Intercept all re-exported commands to avoid recursive real invocations
|
||||
jest.mock('./', () => {
|
||||
const { makeSessionBarrelMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return makeSessionBarrelMock();
|
||||
});
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { SessionPersistence } from '../../persistence';
|
||||
import webClient from '../../WebClient';
|
||||
import * as SessionIndexMocks from './';
|
||||
import { StatusEnum, WebSocketConnectReason } from 'types';
|
||||
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
|
||||
|
||||
const { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
|
||||
BackendService.sendSessionCommand as jest.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(hashPassword as jest.Mock).mockReturnValue('hashed_pw');
|
||||
(generateSalt as jest.Mock).mockReturnValue('randSalt');
|
||||
(passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// connect.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('connect', () => {
|
||||
const { connect } = jest.requireActual('./connect');
|
||||
|
||||
it('calls updateStatus CONNECTING for LOGIN reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.LOGIN);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
expect(webClient.connect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTING for REGISTER reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.REGISTER);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTING for ACTIVATE_ACCOUNT reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.ACTIVATE_ACCOUNT);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTING for PASSWORD_RESET_REQUEST reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET_REQUEST);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTING for PASSWORD_RESET_CHALLENGE reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTING for PASSWORD_RESET reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...');
|
||||
});
|
||||
|
||||
it('calls testConnect for TEST_CONNECTION reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.TEST_CONNECTION);
|
||||
expect(webClient.testConnect).toHaveBeenCalled();
|
||||
expect(webClient.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls updateStatus DISCONNECTED for unknown reason', () => {
|
||||
connect({ host: 'h', port: 1 } as any, 999 as WebSocketConnectReason);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.stringContaining('Unknown'));
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// updateStatus.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('updateStatus', () => {
|
||||
const { updateStatus } = jest.requireActual('./updateStatus');
|
||||
|
||||
it('calls SessionPersistence.updateStatus and webClient.updateStatus', () => {
|
||||
updateStatus(StatusEnum.CONNECTED, 'OK');
|
||||
expect(SessionPersistence.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'OK');
|
||||
expect(webClient.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED);
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// login.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('login', () => {
|
||||
const { login } = jest.requireActual('./login');
|
||||
|
||||
it('sends Command_Login with plain password when no salt', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_Login',
|
||||
expect.objectContaining({ userName: 'alice', password: 'pw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('sends Command_Login with hashedPassword when salt is given', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any, 'salt');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_Login',
|
||||
expect.objectContaining({ hashedPassword: 'hashed_pw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('uses options.hashedPassword if provided', () => {
|
||||
login({ userName: 'alice', password: 'pw', hashedPassword: 'pre_hashed' } as any, 'salt');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_Login',
|
||||
expect.objectContaining({ hashedPassword: 'pre_hashed' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
|
||||
invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp });
|
||||
expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]);
|
||||
expect(SessionPersistence.updateIgnoreList).toHaveBeenCalledWith([]);
|
||||
expect(SessionPersistence.updateUser).toHaveBeenCalledWith({ name: 'alice' });
|
||||
expect(SessionPersistence.loginSuccessful).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.listUsers).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.listRooms).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.');
|
||||
});
|
||||
|
||||
it('onResponseCode RespClientUpdateRequired calls onLoginError', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(1);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespWrongPassword', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(2);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespUsernameInvalid', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(3);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespWouldOverwriteOldSession', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(4);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespUserIsBanned', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(5);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespRegistrationRequired', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(6);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespClientIdRequired', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(7);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespContextError', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(8);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(9);
|
||||
expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled();
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError calls onLoginError with unknown error message', () => {
|
||||
login({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeOnError(999);
|
||||
expect(SessionPersistence.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// register.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('register', () => {
|
||||
const { register } = jest.requireActual('./register');
|
||||
|
||||
it('sends Command_Register with plain password when no salt', () => {
|
||||
register({ userName: 'alice', password: 'pw', email: 'a@b.com', country: 'US', realName: 'Al' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_Register',
|
||||
expect.objectContaining({ userName: 'alice', password: 'pw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('uses hashedPassword when salt is provided', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any, 'salt');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_Register',
|
||||
expect.objectContaining({ hashedPassword: 'hashed_pw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(10);
|
||||
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), undefined);
|
||||
expect(SessionPersistence.registrationSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespRegistrationAccepted forwards salt to login', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any, 'mySalt');
|
||||
invokeResponseCode(10);
|
||||
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'mySalt');
|
||||
expect(SessionPersistence.registrationSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(11);
|
||||
expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespUserAlreadyExists calls registrationUserNameError', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(12);
|
||||
expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespUsernameInvalid calls registrationUserNameError', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(3);
|
||||
expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespPasswordTooShort calls registrationPasswordError', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(13);
|
||||
expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(14);
|
||||
expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespEmailBlackListed calls registrationEmailError', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(15);
|
||||
expect(SessionPersistence.registrationEmailError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespTooManyRequests calls registrationEmailError', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(16);
|
||||
expect(SessionPersistence.registrationEmailError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespRegistrationDisabled calls registrationFailed', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(17);
|
||||
expect(SessionPersistence.registrationFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('RespUserIsBanned calls registrationFailed with raw.reasonStr and raw.endTime', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeResponseCode(5, { reasonStr: 'bad user', endTime: 9999 });
|
||||
expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999);
|
||||
});
|
||||
|
||||
it('onError calls registrationFailed', () => {
|
||||
register({ userName: 'alice', password: 'pw' } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.registrationFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// activate.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('activate', () => {
|
||||
const { activate } = jest.requireActual('./activate');
|
||||
|
||||
it('sends Command_Activate', () => {
|
||||
activate({ userName: 'alice', token: 'tok' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Activate', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('RespActivationAccepted calls accountActivationSuccess and login with salt', () => {
|
||||
activate({ userName: 'alice', token: 'tok' } as any, 'salt');
|
||||
invokeResponseCode(18);
|
||||
expect(SessionPersistence.accountActivationSuccess).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'salt');
|
||||
});
|
||||
|
||||
it('onError calls accountActivationFailed and disconnect', () => {
|
||||
activate({ userName: 'alice', token: 'tok' } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// forgotPasswordChallenge.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('forgotPasswordChallenge', () => {
|
||||
const { forgotPasswordChallenge } = jest.requireActual('./forgotPasswordChallenge');
|
||||
|
||||
it('sends Command_ForgotPasswordChallenge', () => {
|
||||
forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ForgotPasswordChallenge', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls resetPassword and disconnect', () => {
|
||||
forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.resetPassword).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError calls resetPasswordFailed and disconnect', () => {
|
||||
forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// forgotPasswordRequest.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('forgotPasswordRequest', () => {
|
||||
const { forgotPasswordRequest } = jest.requireActual('./forgotPasswordRequest');
|
||||
|
||||
it('sends Command_ForgotPasswordRequest', () => {
|
||||
forgotPasswordRequest({ userName: 'alice' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ForgotPasswordRequest', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess with challengeEmail calls resetPasswordChallenge', () => {
|
||||
forgotPasswordRequest({ userName: 'alice' } as any);
|
||||
const resp = { challengeEmail: true };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_ForgotPasswordRequest.ext': resp });
|
||||
expect(SessionPersistence.resetPasswordChallenge).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onSuccess without challengeEmail calls resetPassword', () => {
|
||||
forgotPasswordRequest({ userName: 'alice' } as any);
|
||||
const resp = { challengeEmail: false };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_ForgotPasswordRequest.ext': resp });
|
||||
expect(SessionPersistence.resetPassword).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError calls resetPasswordFailed and disconnect', () => {
|
||||
forgotPasswordRequest({ userName: 'alice' } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// forgotPasswordReset.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('forgotPasswordReset', () => {
|
||||
const { forgotPasswordReset } = jest.requireActual('./forgotPasswordReset');
|
||||
|
||||
it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => {
|
||||
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_ForgotPasswordReset',
|
||||
expect.objectContaining({ newPassword: 'newpw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('sends hashed new password when salt provided', () => {
|
||||
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any, 'salt');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_ForgotPasswordReset',
|
||||
expect.objectContaining({ hashedNewPassword: 'hashed_pw' }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls resetPasswordSuccess and disconnect', () => {
|
||||
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.resetPasswordSuccess).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError calls resetPasswordFailed and disconnect', () => {
|
||||
forgotPasswordReset({ userName: 'alice', token: 'tok', newPassword: 'newpw' } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// requestPasswordSalt.ts
|
||||
// ----------------------------------------------------------------
|
||||
describe('requestPasswordSalt', () => {
|
||||
const { requestPasswordSalt } = jest.requireActual('./requestPasswordSalt');
|
||||
|
||||
it('sends Command_RequestPasswordSalt', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RequestPasswordSalt', expect.any(Object), expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess with LOGIN reason calls login', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any);
|
||||
const resp = { passwordSalt: 'salt123' };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
|
||||
expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'salt123');
|
||||
});
|
||||
|
||||
it('onSuccess with ACTIVATE_ACCOUNT reason calls activate', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any);
|
||||
const resp = { passwordSalt: 'salt123' };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
|
||||
expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'salt123');
|
||||
});
|
||||
|
||||
it('onSuccess with PASSWORD_RESET reason calls forgotPasswordReset', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any);
|
||||
const resp = { passwordSalt: 'salt123' };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp });
|
||||
expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any);
|
||||
invokeResponseCode(6);
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String));
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any);
|
||||
invokeResponseCode(6);
|
||||
expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError calls updateStatus DISCONNECTED and disconnect', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any);
|
||||
invokeOnError();
|
||||
expect(SessionIndexMocks.updateStatus).toHaveBeenCalled();
|
||||
expect(SessionIndexMocks.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onError with PASSWORD_RESET reason calls resetPasswordFailed', () => {
|
||||
requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any);
|
||||
invokeOnError();
|
||||
expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
// Shared mock setup for session command tests
|
||||
|
||||
jest.mock('../../services/BackendService', () => ({
|
||||
BackendService: {
|
||||
sendSessionCommand: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../persistence', () => {
|
||||
const { makeSessionPersistenceMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return {
|
||||
SessionPersistence: makeSessionPersistenceMock(),
|
||||
RoomPersistence: { joinRoom: jest.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../WebClient', () => {
|
||||
const { makeWebClientMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return { __esModule: true, default: makeWebClientMock() };
|
||||
});
|
||||
|
||||
jest.mock('../../services/ProtoController', () => {
|
||||
const { makeProtoControllerRootMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return { ProtoController: { root: makeProtoControllerRootMock() } };
|
||||
});
|
||||
|
||||
jest.mock('../../utils', () => {
|
||||
const { makeUtilsMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return makeUtilsMock();
|
||||
});
|
||||
|
||||
// Mock session commands barrel to allow cross-command calls while keeping real implementations
|
||||
jest.mock('./', () => {
|
||||
const actual = jest.requireActual('./');
|
||||
const { makeSessionBarrelMock } = require('../../__mocks__/sessionCommandMocks');
|
||||
return { ...actual, ...makeSessionBarrelMock() };
|
||||
});
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { BackendService } from '../../services/BackendService';
|
||||
import { SessionPersistence } from '../../persistence';
|
||||
import { RoomPersistence } from '../../persistence';
|
||||
import webClient from '../../WebClient';
|
||||
import * as SessionCommands from './';
|
||||
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
|
||||
|
||||
const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers(
|
||||
BackendService.sendSessionCommand as jest.Mock
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(hashPassword as jest.Mock).mockReturnValue('hashed_pw');
|
||||
(generateSalt as jest.Mock).mockReturnValue('randSalt');
|
||||
(passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe('accountEdit', () => {
|
||||
const { accountEdit } = jest.requireActual('./accountEdit');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_AccountEdit with correct params', () => {
|
||||
accountEdit('pw', 'Alice', 'a@b.com', 'US');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_AccountEdit',
|
||||
{ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.accountEditChanged on success', () => {
|
||||
accountEdit('pw', 'Alice', 'a@b.com', 'US');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.accountEditChanged).toHaveBeenCalledWith('Alice', 'a@b.com', 'US');
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountImage', () => {
|
||||
const { accountImage } = jest.requireActual('./accountImage');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_AccountImage', () => {
|
||||
const img = new Uint8Array([1, 2]);
|
||||
accountImage(img);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_AccountImage', { image: img }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.accountImageChanged on success', () => {
|
||||
const img = new Uint8Array([1, 2]);
|
||||
accountImage(img);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.accountImageChanged).toHaveBeenCalledWith(img);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accountPassword', () => {
|
||||
const { accountPassword } = jest.requireActual('./accountPassword');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_AccountPassword', () => {
|
||||
accountPassword('old', 'new', 'hashed');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_AccountPassword',
|
||||
{ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.accountPasswordChange on success', () => {
|
||||
accountPassword('old', 'new', 'hashed');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.accountPasswordChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deckDel', () => {
|
||||
const { deckDel } = jest.requireActual('./deckDel');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_DeckDel', () => {
|
||||
deckDel(42);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckDel', { deckId: 42 }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls deleteServerDeck on success', () => {
|
||||
deckDel(42);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.deleteServerDeck).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deckDelDir', () => {
|
||||
const { deckDelDir } = jest.requireActual('./deckDelDir');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_DeckDelDir', () => {
|
||||
deckDelDir('/path');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckDelDir', { path: '/path' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls deleteServerDeckDir on success', () => {
|
||||
deckDelDir('/path');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.deleteServerDeckDir).toHaveBeenCalledWith('/path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deckList', () => {
|
||||
const { deckList } = jest.requireActual('./deckList');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_DeckList', () => {
|
||||
deckList();
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckList', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls updateServerDecks on success', () => {
|
||||
deckList();
|
||||
const resp = { folders: [] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_DeckList.ext': resp });
|
||||
expect(SessionPersistence.updateServerDecks).toHaveBeenCalledWith(resp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deckNewDir', () => {
|
||||
const { deckNewDir } = jest.requireActual('./deckNewDir');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_DeckNewDir', () => {
|
||||
deckNewDir('/path', 'dir');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckNewDir', { path: '/path', dirName: 'dir' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls createServerDeckDir on success', () => {
|
||||
deckNewDir('/path', 'dir');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.createServerDeckDir).toHaveBeenCalledWith('/path', 'dir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deckUpload', () => {
|
||||
const { deckUpload } = jest.requireActual('./deckUpload');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_DeckUpload', () => {
|
||||
deckUpload('/path', 1, 'content');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(
|
||||
'Command_DeckUpload',
|
||||
{ path: '/path', deckId: 1, deckList: 'content' },
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('calls uploadServerDeck on success', () => {
|
||||
deckUpload('/path', 1, 'content');
|
||||
const resp = { newFile: { id: 1 } };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_DeckUpload.ext': resp });
|
||||
expect(SessionPersistence.uploadServerDeck).toHaveBeenCalledWith('/path', resp.newFile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
const { disconnect } = jest.requireActual('./disconnect');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('calls webClient.disconnect', () => {
|
||||
disconnect();
|
||||
expect(webClient.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGamesOfUser', () => {
|
||||
const { getGamesOfUser } = jest.requireActual('./getGamesOfUser');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_GetGamesOfUser', () => {
|
||||
getGamesOfUser('alice');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_GetGamesOfUser', { userName: 'alice' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls getGamesOfUser on success', () => {
|
||||
getGamesOfUser('alice');
|
||||
const resp = { gameList: [] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_GetGamesOfUser.ext': resp });
|
||||
expect(SessionPersistence.getGamesOfUser).toHaveBeenCalledWith('alice', resp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
const { getUserInfo } = jest.requireActual('./getUserInfo');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_GetUserInfo', () => {
|
||||
getUserInfo('alice');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_GetUserInfo', { userName: 'alice' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls getUserInfo on success', () => {
|
||||
getUserInfo('alice');
|
||||
const resp = { userInfo: { name: 'alice' } };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_GetUserInfo.ext': resp });
|
||||
expect(SessionPersistence.getUserInfo).toHaveBeenCalledWith(resp.userInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('joinRoom', () => {
|
||||
const { joinRoom } = jest.requireActual('./joinRoom');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_JoinRoom', () => {
|
||||
joinRoom(5);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_JoinRoom', { roomId: 5 }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls RoomPersistence.joinRoom on success', () => {
|
||||
joinRoom(5);
|
||||
const resp = { roomInfo: { roomId: 5 } };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_JoinRoom.ext': resp });
|
||||
expect(RoomPersistence.joinRoom).toHaveBeenCalledWith(resp.roomInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listRooms (command)', () => {
|
||||
const { listRooms } = jest.requireActual('./listRooms');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_ListRooms', () => {
|
||||
listRooms();
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ListRooms', {}, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listUsers', () => {
|
||||
const { listUsers } = jest.requireActual('./listUsers');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_ListUsers', () => {
|
||||
listUsers();
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ListUsers', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.updateUsers with the user list on success', () => {
|
||||
listUsers();
|
||||
const resp = { userList: [{ name: 'Alice' }] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_ListUsers.ext': resp });
|
||||
expect(SessionPersistence.updateUsers).toHaveBeenCalledWith([{ name: 'Alice' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message', () => {
|
||||
const { message } = jest.requireActual('./message');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_Message', () => {
|
||||
message('bob', 'hi');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Message', { userName: 'bob', message: 'hi' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls directMessageSent on success', () => {
|
||||
message('bob', 'hi');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.directMessageSent).toHaveBeenCalledWith('bob', 'hi');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ping', () => {
|
||||
const { ping } = jest.requireActual('./ping');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_Ping', () => {
|
||||
const pingReceived = jest.fn();
|
||||
ping(pingReceived);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Ping', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls pingReceived via onResponse', () => {
|
||||
const pingReceived = jest.fn();
|
||||
ping(pingReceived);
|
||||
const raw = {};
|
||||
invokeCallback('onResponse', raw);
|
||||
expect(pingReceived).toHaveBeenCalledWith(raw);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replayDeleteMatch', () => {
|
||||
const { replayDeleteMatch } = jest.requireActual('./replayDeleteMatch');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_ReplayDeleteMatch', () => {
|
||||
replayDeleteMatch(7);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ReplayDeleteMatch', { gameId: 7 }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls replayDeleteMatch on success', () => {
|
||||
replayDeleteMatch(7);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.replayDeleteMatch).toHaveBeenCalledWith(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replayList', () => {
|
||||
const { replayList } = jest.requireActual('./replayList');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_ReplayList', () => {
|
||||
replayList();
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ReplayList', {}, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls replayList on success', () => {
|
||||
replayList();
|
||||
const resp = { matchList: [] };
|
||||
invokeOnSuccess(resp, { responseCode: 0, '.Response_ReplayList.ext': resp });
|
||||
expect(SessionPersistence.replayList).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replayModifyMatch', () => {
|
||||
const { replayModifyMatch } = jest.requireActual('./replayModifyMatch');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('sends Command_ReplayModifyMatch', () => {
|
||||
replayModifyMatch(7, true);
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ReplayModifyMatch', { gameId: 7, doNotHide: true }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls replayModifyMatch on success', () => {
|
||||
replayModifyMatch(7, true);
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.replayModifyMatch).toHaveBeenCalledWith(7, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToList / addToBuddyList / addToIgnoreList', () => {
|
||||
const { addToList, addToBuddyList, addToIgnoreList } = jest.requireActual('./addToList');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('addToBuddyList sends Command_AddToList with list=buddy', () => {
|
||||
addToBuddyList('alice');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_AddToList', { list: 'buddy', userName: 'alice' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('addToIgnoreList sends Command_AddToList with list=ignore', () => {
|
||||
addToIgnoreList('bob');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_AddToList', { list: 'ignore', userName: 'bob' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls SessionPersistence.addToList', () => {
|
||||
addToList('buddy', 'alice');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.addToList).toHaveBeenCalledWith('buddy', 'alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => {
|
||||
const { removeFromList, removeFromBuddyList, removeFromIgnoreList } = jest.requireActual('./removeFromList');
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => {
|
||||
removeFromBuddyList('alice');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RemoveFromList', { list: 'buddy', userName: 'alice' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => {
|
||||
removeFromIgnoreList('bob');
|
||||
expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RemoveFromList', { list: 'ignore', userName: 'bob' }, expect.any(Object));
|
||||
});
|
||||
|
||||
it('onSuccess calls SessionPersistence.removeFromList', () => {
|
||||
removeFromList('buddy', 'alice');
|
||||
invokeOnSuccess();
|
||||
expect(SessionPersistence.removeFromList).toHaveBeenCalledWith('buddy', 'alice');
|
||||
});
|
||||
});
|
||||
19
webclient/src/websocket/events/common/commonEvents.spec.ts
Normal file
19
webclient/src/websocket/events/common/commonEvents.spec.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
jest.mock('../../persistence', () => ({
|
||||
SessionPersistence: {
|
||||
playerPropertiesChanged: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { SessionPersistence } from '../../persistence';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('playerPropertiesChanged', () => {
|
||||
const { playerPropertiesChanged } = jest.requireActual('./playerPropertiesChanged');
|
||||
|
||||
it('delegates to SessionPersistence.playerPropertiesChanged', () => {
|
||||
const payload = { gameId: 1, player: { playerId: 2 } } as any;
|
||||
playerPropertiesChanged(payload);
|
||||
expect(SessionPersistence.playerPropertiesChanged).toHaveBeenCalledWith(payload);
|
||||
});
|
||||
});
|
||||
29
webclient/src/websocket/events/game/gameEvents.spec.ts
Normal file
29
webclient/src/websocket/events/game/gameEvents.spec.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
jest.mock('../../persistence', () => ({
|
||||
GamePersistence: {
|
||||
joinGame: jest.fn(),
|
||||
leaveGame: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { GamePersistence } from '../../persistence';
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('joinGame event', () => {
|
||||
const { joinGame } = jest.requireActual('./joinGame');
|
||||
|
||||
it('delegates to GamePersistence.joinGame', () => {
|
||||
const data = { gameId: 5, player: { playerId: 1 } } as any;
|
||||
joinGame(data);
|
||||
expect(GamePersistence.joinGame).toHaveBeenCalledWith(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaveGame event', () => {
|
||||
const { leaveGame } = jest.requireActual('./leaveGame');
|
||||
|
||||
it('delegates to GamePersistence.leaveGame', () => {
|
||||
leaveGame(42 as any);
|
||||
expect(GamePersistence.leaveGame).toHaveBeenCalledWith(42);
|
||||
});
|
||||
});
|
||||
63
webclient/src/websocket/events/room/roomEvents.spec.ts
Normal file
63
webclient/src/websocket/events/room/roomEvents.spec.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
jest.mock('../../persistence', () => ({
|
||||
RoomPersistence: {
|
||||
userJoined: jest.fn(),
|
||||
userLeft: jest.fn(),
|
||||
updateGames: jest.fn(),
|
||||
removeMessages: jest.fn(),
|
||||
addMessage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { RoomPersistence } from '../../persistence';
|
||||
|
||||
const makeRoomEvent = (roomId: number) => ({ roomEvent: { roomId } });
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('joinRoom room event', () => {
|
||||
const { joinRoom } = jest.requireActual('./joinRoom');
|
||||
|
||||
it('calls RoomPersistence.userJoined with roomId and userInfo', () => {
|
||||
const userInfo = { name: 'alice' } as any;
|
||||
joinRoom({ userInfo }, makeRoomEvent(3));
|
||||
expect(RoomPersistence.userJoined).toHaveBeenCalledWith(3, userInfo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaveRoom room event', () => {
|
||||
const { leaveRoom } = jest.requireActual('./leaveRoom');
|
||||
|
||||
it('calls RoomPersistence.userLeft with roomId and name', () => {
|
||||
leaveRoom({ name: 'alice' }, makeRoomEvent(4));
|
||||
expect(RoomPersistence.userLeft).toHaveBeenCalledWith(4, 'alice');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listGames room event', () => {
|
||||
const { listGames } = jest.requireActual('./listGames');
|
||||
|
||||
it('calls RoomPersistence.updateGames with roomId and gameList', () => {
|
||||
const gameList = [{ gameId: 1 }] as any;
|
||||
listGames({ gameList }, makeRoomEvent(5));
|
||||
expect(RoomPersistence.updateGames).toHaveBeenCalledWith(5, gameList);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMessages room event', () => {
|
||||
const { removeMessages } = jest.requireActual('./removeMessages');
|
||||
|
||||
it('calls RoomPersistence.removeMessages with roomId, name, amount', () => {
|
||||
removeMessages({ name: 'bob', amount: 10 }, makeRoomEvent(6));
|
||||
expect(RoomPersistence.removeMessages).toHaveBeenCalledWith(6, 'bob', 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roomSay room event', () => {
|
||||
const { roomSay } = jest.requireActual('./roomSay');
|
||||
|
||||
it('calls RoomPersistence.addMessage with roomId and message', () => {
|
||||
const msg = { text: 'hello' } as any;
|
||||
roomSay(msg, makeRoomEvent(7));
|
||||
expect(RoomPersistence.addMessage).toHaveBeenCalledWith(7, msg);
|
||||
});
|
||||
});
|
||||
425
webclient/src/websocket/events/session/sessionEvents.spec.ts
Normal file
425
webclient/src/websocket/events/session/sessionEvents.spec.ts
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 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', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
|
||||
expect(SessionCmds.login).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('LOGIN reason with salt → calls requestPasswordSalt', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.LOGIN };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
|
||||
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('REGISTER reason without salt → calls register with null salt', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
|
||||
expect(SessionCmds.register).toHaveBeenCalledWith(expect.any(Object), null);
|
||||
});
|
||||
|
||||
it('REGISTER reason with salt → calls register with generated salt', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.REGISTER };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
|
||||
expect(SessionCmds.register).toHaveBeenCalledWith(expect.any(Object), 'newSalt');
|
||||
});
|
||||
|
||||
it('ACTIVATE_ACCOUNT reason without salt → calls activate', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
|
||||
expect(SessionCmds.activate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
|
||||
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(0);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 } as any);
|
||||
expect(SessionCmds.forgotPasswordReset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('PASSWORD_RESET reason with salt → calls requestPasswordSalt', () => {
|
||||
(webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET };
|
||||
(Utils.passwordSaltSupported as jest.Mock).mockReturnValue(1);
|
||||
serverIdentification({ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 } as any);
|
||||
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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({});
|
||||
});
|
||||
});
|
||||
37
webclient/src/websocket/persistence/AdminPersistence.spec.ts
Normal file
37
webclient/src/websocket/persistence/AdminPersistence.spec.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
jest.mock('store', () => ({
|
||||
ServerDispatch: {
|
||||
adjustMod: jest.fn(),
|
||||
reloadConfig: jest.fn(),
|
||||
shutdownServer: jest.fn(),
|
||||
updateServerMessage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { AdminPersistence } from './AdminPersistence';
|
||||
import { ServerDispatch } from 'store';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('AdminPersistence', () => {
|
||||
it('adjustMod passes userName, shouldBeMod, shouldBeJudge', () => {
|
||||
AdminPersistence.adjustMod('alice', true, false);
|
||||
expect(ServerDispatch.adjustMod).toHaveBeenCalledWith('alice', true, false);
|
||||
});
|
||||
|
||||
it('reloadConfig -> ServerDispatch.reloadConfig', () => {
|
||||
AdminPersistence.reloadConfig();
|
||||
expect(ServerDispatch.reloadConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shutdownServer -> ServerDispatch.shutdownServer', () => {
|
||||
AdminPersistence.shutdownServer();
|
||||
expect(ServerDispatch.shutdownServer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateServerMessage -> ServerDispatch.updateServerMessage', () => {
|
||||
AdminPersistence.updateServerMessage();
|
||||
expect(ServerDispatch.updateServerMessage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
18
webclient/src/websocket/persistence/GamePersistence.spec.ts
Normal file
18
webclient/src/websocket/persistence/GamePersistence.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { GamePersistence } from './GamePersistence';
|
||||
|
||||
describe('GamePersistence', () => {
|
||||
it('joinGame logs to console', () => {
|
||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const data = { playerId: 1 } as any;
|
||||
GamePersistence.joinGame(data);
|
||||
expect(spy).toHaveBeenCalledWith('joinGame', data);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('leaveGame logs to console', () => {
|
||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
GamePersistence.leaveGame(0 as any);
|
||||
expect(spy).toHaveBeenCalledWith('leaveGame', 0);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
jest.mock('store', () => ({
|
||||
ServerDispatch: {
|
||||
banFromServer: jest.fn(),
|
||||
banHistory: jest.fn(),
|
||||
viewLogs: jest.fn(),
|
||||
warnHistory: jest.fn(),
|
||||
warnListOptions: jest.fn(),
|
||||
warnUser: jest.fn(),
|
||||
grantReplayAccess: jest.fn(),
|
||||
forceActivateUser: jest.fn(),
|
||||
getAdminNotes: jest.fn(),
|
||||
updateAdminNotes: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../utils/NormalizeService', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
normalizeLogs: jest.fn((logs: any) => ({ normalized: logs })),
|
||||
},
|
||||
}));
|
||||
|
||||
import { ModeratorPersistence } from './ModeratorPersistence';
|
||||
import { ServerDispatch } from 'store';
|
||||
import NormalizeService from '../utils/NormalizeService';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(NormalizeService.normalizeLogs as jest.Mock).mockImplementation((logs: any) => ({ normalized: logs }));
|
||||
});
|
||||
|
||||
describe('ModeratorPersistence', () => {
|
||||
it('banFromServer passes userName', () => {
|
||||
ModeratorPersistence.banFromServer('alice');
|
||||
expect(ServerDispatch.banFromServer).toHaveBeenCalledWith('alice');
|
||||
});
|
||||
|
||||
it('banHistory passes userName and banHistory', () => {
|
||||
ModeratorPersistence.banHistory('alice', []);
|
||||
expect(ServerDispatch.banHistory).toHaveBeenCalledWith('alice', []);
|
||||
});
|
||||
|
||||
it('viewLogs normalizes logs and dispatches', () => {
|
||||
const logs = [{ targetType: 'room' }] as any;
|
||||
ModeratorPersistence.viewLogs(logs);
|
||||
expect(NormalizeService.normalizeLogs).toHaveBeenCalledWith(logs);
|
||||
expect(ServerDispatch.viewLogs).toHaveBeenCalledWith({ normalized: logs });
|
||||
});
|
||||
|
||||
it('warnHistory passes userName and warnHistory', () => {
|
||||
ModeratorPersistence.warnHistory('bob', []);
|
||||
expect(ServerDispatch.warnHistory).toHaveBeenCalledWith('bob', []);
|
||||
});
|
||||
|
||||
it('warnListOptions passes warnList', () => {
|
||||
ModeratorPersistence.warnListOptions([]);
|
||||
expect(ServerDispatch.warnListOptions).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('warnUser passes userName', () => {
|
||||
ModeratorPersistence.warnUser('carol');
|
||||
expect(ServerDispatch.warnUser).toHaveBeenCalledWith('carol');
|
||||
});
|
||||
|
||||
it('grantReplayAccess passes replayId and moderatorName', () => {
|
||||
ModeratorPersistence.grantReplayAccess(10, 'mod1');
|
||||
expect(ServerDispatch.grantReplayAccess).toHaveBeenCalledWith(10, 'mod1');
|
||||
});
|
||||
|
||||
it('forceActivateUser passes usernameToActivate and moderatorName', () => {
|
||||
ModeratorPersistence.forceActivateUser('user1', 'mod1');
|
||||
expect(ServerDispatch.forceActivateUser).toHaveBeenCalledWith('user1', 'mod1');
|
||||
});
|
||||
|
||||
it('getAdminNotes passes userName and notes', () => {
|
||||
ModeratorPersistence.getAdminNotes('alice', 'some notes');
|
||||
expect(ServerDispatch.getAdminNotes).toHaveBeenCalledWith('alice', 'some notes');
|
||||
});
|
||||
|
||||
it('updateAdminNotes passes userName and notes', () => {
|
||||
ModeratorPersistence.updateAdminNotes('alice', 'new notes');
|
||||
expect(ServerDispatch.updateAdminNotes).toHaveBeenCalledWith('alice', 'new notes');
|
||||
});
|
||||
});
|
||||
117
webclient/src/websocket/persistence/RoomPersistence.spec.ts
Normal file
117
webclient/src/websocket/persistence/RoomPersistence.spec.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
jest.mock('store', () => ({
|
||||
store: { getState: jest.fn().mockReturnValue({}) },
|
||||
RoomsDispatch: {
|
||||
clearStore: jest.fn(),
|
||||
joinRoom: jest.fn(),
|
||||
leaveRoom: jest.fn(),
|
||||
updateRooms: jest.fn(),
|
||||
updateGames: jest.fn(),
|
||||
addMessage: jest.fn(),
|
||||
userJoined: jest.fn(),
|
||||
userLeft: jest.fn(),
|
||||
removeMessages: jest.fn(),
|
||||
gameCreated: jest.fn(),
|
||||
joinedGame: jest.fn(),
|
||||
},
|
||||
RoomsSelectors: {
|
||||
getRoom: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../utils/NormalizeService', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
normalizeRoomInfo: jest.fn(),
|
||||
normalizeGameObject: jest.fn(),
|
||||
normalizeUserMessage: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { RoomPersistence } from './RoomPersistence';
|
||||
import { store, RoomsDispatch, RoomsSelectors } from 'store';
|
||||
import NormalizeService from '../utils/NormalizeService';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('RoomPersistence', () => {
|
||||
it('clearStore -> RoomsDispatch.clearStore', () => {
|
||||
RoomPersistence.clearStore();
|
||||
expect(RoomsDispatch.clearStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('joinRoom normalizes and dispatches', () => {
|
||||
const room = { roomId: 1 } as any;
|
||||
RoomPersistence.joinRoom(room);
|
||||
expect(NormalizeService.normalizeRoomInfo).toHaveBeenCalledWith(room);
|
||||
expect(RoomsDispatch.joinRoom).toHaveBeenCalledWith(room);
|
||||
});
|
||||
|
||||
it('leaveRoom -> RoomsDispatch.leaveRoom', () => {
|
||||
RoomPersistence.leaveRoom(5);
|
||||
expect(RoomsDispatch.leaveRoom).toHaveBeenCalledWith(5);
|
||||
});
|
||||
|
||||
it('updateRooms -> RoomsDispatch.updateRooms', () => {
|
||||
RoomPersistence.updateRooms([]);
|
||||
expect(RoomsDispatch.updateRooms).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
describe('updateGames', () => {
|
||||
it('normalizes game when gameType is missing and room exists', () => {
|
||||
const game = { gameType: null, gameTypes: [1] } as any;
|
||||
const room = { gametypeMap: { 1: 'Standard' } } as any;
|
||||
(RoomsSelectors.getRoom as jest.Mock).mockReturnValue(room);
|
||||
RoomPersistence.updateGames(1, [game]);
|
||||
expect(NormalizeService.normalizeGameObject).toHaveBeenCalledWith(game, room.gametypeMap);
|
||||
expect(RoomsDispatch.updateGames).toHaveBeenCalledWith(1, [game]);
|
||||
});
|
||||
|
||||
it('does not normalize when game already has gameType', () => {
|
||||
const game = { gameType: 'Standard' } as any;
|
||||
RoomPersistence.updateGames(1, [game]);
|
||||
expect(NormalizeService.normalizeGameObject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not normalize when room is not found', () => {
|
||||
const game = { gameType: null } as any;
|
||||
(RoomsSelectors.getRoom as jest.Mock).mockReturnValue(null);
|
||||
RoomPersistence.updateGames(1, [game]);
|
||||
expect(NormalizeService.normalizeGameObject).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('addMessage normalizes message and dispatches', () => {
|
||||
const msg = { name: 'alice', message: 'hi' } as any;
|
||||
RoomPersistence.addMessage(1, msg);
|
||||
expect(NormalizeService.normalizeUserMessage).toHaveBeenCalledWith(msg);
|
||||
expect(RoomsDispatch.addMessage).toHaveBeenCalledWith(1, msg);
|
||||
});
|
||||
|
||||
it('userJoined -> RoomsDispatch.userJoined', () => {
|
||||
const user = { name: 'bob' } as any;
|
||||
RoomPersistence.userJoined(1, user);
|
||||
expect(RoomsDispatch.userJoined).toHaveBeenCalledWith(1, user);
|
||||
});
|
||||
|
||||
it('userLeft -> RoomsDispatch.userLeft', () => {
|
||||
RoomPersistence.userLeft(1, 'bob');
|
||||
expect(RoomsDispatch.userLeft).toHaveBeenCalledWith(1, 'bob');
|
||||
});
|
||||
|
||||
it('removeMessages -> RoomsDispatch.removeMessages', () => {
|
||||
RoomPersistence.removeMessages(1, 'bob', 5);
|
||||
expect(RoomsDispatch.removeMessages).toHaveBeenCalledWith(1, 'bob', 5);
|
||||
});
|
||||
|
||||
it('gameCreated -> RoomsDispatch.gameCreated', () => {
|
||||
RoomPersistence.gameCreated(1);
|
||||
expect(RoomsDispatch.gameCreated).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('joinedGame -> RoomsDispatch.joinedGame', () => {
|
||||
RoomPersistence.joinedGame(1, 99);
|
||||
expect(RoomsDispatch.joinedGame).toHaveBeenCalledWith(1, 99);
|
||||
});
|
||||
});
|
||||
395
webclient/src/websocket/persistence/SessionPersistence.spec.ts
Normal file
395
webclient/src/websocket/persistence/SessionPersistence.spec.ts
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
jest.mock('store', () => ({
|
||||
ServerDispatch: {
|
||||
initialized: jest.fn(),
|
||||
clearStore: jest.fn(),
|
||||
loginSuccessful: jest.fn(),
|
||||
loginFailed: jest.fn(),
|
||||
connectionClosed: jest.fn(),
|
||||
connectionFailed: jest.fn(),
|
||||
testConnectionSuccessful: jest.fn(),
|
||||
testConnectionFailed: jest.fn(),
|
||||
updateBuddyList: jest.fn(),
|
||||
addToBuddyList: jest.fn(),
|
||||
removeFromBuddyList: jest.fn(),
|
||||
updateIgnoreList: jest.fn(),
|
||||
addToIgnoreList: jest.fn(),
|
||||
removeFromIgnoreList: jest.fn(),
|
||||
updateInfo: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
updateUsers: jest.fn(),
|
||||
userJoined: jest.fn(),
|
||||
userLeft: jest.fn(),
|
||||
serverMessage: jest.fn(),
|
||||
accountAwaitingActivation: jest.fn(),
|
||||
accountActivationSuccess: jest.fn(),
|
||||
accountActivationFailed: jest.fn(),
|
||||
registrationRequiresEmail: jest.fn(),
|
||||
registrationSuccess: jest.fn(),
|
||||
registrationFailed: jest.fn(),
|
||||
registrationEmailError: jest.fn(),
|
||||
registrationPasswordError: jest.fn(),
|
||||
registrationUserNameError: jest.fn(),
|
||||
resetPasswordChallenge: jest.fn(),
|
||||
resetPassword: jest.fn(),
|
||||
resetPasswordSuccess: jest.fn(),
|
||||
resetPasswordFailed: jest.fn(),
|
||||
accountPasswordChange: jest.fn(),
|
||||
accountEditChanged: jest.fn(),
|
||||
accountImageChanged: jest.fn(),
|
||||
directMessageSent: jest.fn(),
|
||||
getUserInfo: jest.fn(),
|
||||
notifyUser: jest.fn(),
|
||||
serverShutdown: jest.fn(),
|
||||
userMessage: jest.fn(),
|
||||
addToList: jest.fn(),
|
||||
removeFromList: jest.fn(),
|
||||
deckDelete: jest.fn(),
|
||||
backendDecks: jest.fn(),
|
||||
deckUpload: jest.fn(),
|
||||
deckNewDir: jest.fn(),
|
||||
deckDelDir: jest.fn(),
|
||||
replayList: jest.fn(),
|
||||
replayAdded: jest.fn(),
|
||||
replayModifyMatch: jest.fn(),
|
||||
replayDeleteMatch: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('websocket/utils', () => ({
|
||||
sanitizeHtml: jest.fn((msg: string) => `sanitized:${msg}`),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/NormalizeService', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
normalizeBannedUserError: jest.fn((r: string, t: number) => `banned:${r}:${t}`),
|
||||
},
|
||||
}));
|
||||
|
||||
import { SessionPersistence } from './SessionPersistence';
|
||||
import { ServerDispatch } from 'store';
|
||||
import { sanitizeHtml } from 'websocket/utils';
|
||||
import NormalizeService from '../utils/NormalizeService';
|
||||
import { StatusEnum } from 'types';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(sanitizeHtml as jest.Mock).mockImplementation((msg: string) => `sanitized:${msg}`);
|
||||
(NormalizeService.normalizeBannedUserError as jest.Mock).mockImplementation(
|
||||
(r: string, t: number) => `banned:${r}:${t}`
|
||||
);
|
||||
});
|
||||
|
||||
describe('SessionPersistence', () => {
|
||||
it('initialized -> ServerDispatch.initialized', () => {
|
||||
SessionPersistence.initialized();
|
||||
expect(ServerDispatch.initialized).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clearStore -> ServerDispatch.clearStore', () => {
|
||||
SessionPersistence.clearStore();
|
||||
expect(ServerDispatch.clearStore).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loginSuccessful passes options', () => {
|
||||
const opts = { userName: 'alice' } as any;
|
||||
SessionPersistence.loginSuccessful(opts);
|
||||
expect(ServerDispatch.loginSuccessful).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
|
||||
it('loginFailed -> ServerDispatch.loginFailed', () => {
|
||||
SessionPersistence.loginFailed();
|
||||
expect(ServerDispatch.loginFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('connectionClosed passes reason', () => {
|
||||
SessionPersistence.connectionClosed(3);
|
||||
expect(ServerDispatch.connectionClosed).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it('connectionFailed -> ServerDispatch.connectionFailed', () => {
|
||||
SessionPersistence.connectionFailed();
|
||||
expect(ServerDispatch.connectionFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('testConnectionSuccessful -> ServerDispatch.testConnectionSuccessful', () => {
|
||||
SessionPersistence.testConnectionSuccessful();
|
||||
expect(ServerDispatch.testConnectionSuccessful).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('testConnectionFailed -> ServerDispatch.testConnectionFailed', () => {
|
||||
SessionPersistence.testConnectionFailed();
|
||||
expect(ServerDispatch.testConnectionFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateBuddyList passes list', () => {
|
||||
SessionPersistence.updateBuddyList(['user']);
|
||||
expect(ServerDispatch.updateBuddyList).toHaveBeenCalledWith(['user']);
|
||||
});
|
||||
|
||||
it('addToBuddyList passes user', () => {
|
||||
const user = { name: 'bob' } as any;
|
||||
SessionPersistence.addToBuddyList(user);
|
||||
expect(ServerDispatch.addToBuddyList).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('removeFromBuddyList passes userName', () => {
|
||||
SessionPersistence.removeFromBuddyList('bob');
|
||||
expect(ServerDispatch.removeFromBuddyList).toHaveBeenCalledWith('bob');
|
||||
});
|
||||
|
||||
it('updateIgnoreList passes list', () => {
|
||||
SessionPersistence.updateIgnoreList(['user']);
|
||||
expect(ServerDispatch.updateIgnoreList).toHaveBeenCalledWith(['user']);
|
||||
});
|
||||
|
||||
it('addToIgnoreList passes user', () => {
|
||||
const user = { name: 'bob' } as any;
|
||||
SessionPersistence.addToIgnoreList(user);
|
||||
expect(ServerDispatch.addToIgnoreList).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList passes userName', () => {
|
||||
SessionPersistence.removeFromIgnoreList('bob');
|
||||
expect(ServerDispatch.removeFromIgnoreList).toHaveBeenCalledWith('bob');
|
||||
});
|
||||
|
||||
it('updateInfo passes name and version', () => {
|
||||
SessionPersistence.updateInfo('Server', '1.0');
|
||||
expect(ServerDispatch.updateInfo).toHaveBeenCalledWith('Server', '1.0');
|
||||
});
|
||||
|
||||
it('updateStatus dispatches status and calls connectionClosed when DISCONNECTED', () => {
|
||||
SessionPersistence.updateStatus(StatusEnum.DISCONNECTED, 'bye');
|
||||
expect(ServerDispatch.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'bye');
|
||||
expect(ServerDispatch.connectionClosed).toHaveBeenCalledWith(StatusEnum.DISCONNECTED);
|
||||
});
|
||||
|
||||
it('updateStatus does not call connectionClosed when not DISCONNECTED', () => {
|
||||
SessionPersistence.updateStatus(StatusEnum.CONNECTED, 'hi');
|
||||
expect(ServerDispatch.connectionClosed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updateUser passes user', () => {
|
||||
const user = { name: 'alice' } as any;
|
||||
SessionPersistence.updateUser(user);
|
||||
expect(ServerDispatch.updateUser).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('updateUsers passes users array', () => {
|
||||
SessionPersistence.updateUsers([]);
|
||||
expect(ServerDispatch.updateUsers).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('userJoined passes user', () => {
|
||||
const user = { name: 'carol' } as any;
|
||||
SessionPersistence.userJoined(user);
|
||||
expect(ServerDispatch.userJoined).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('userLeft passes userName', () => {
|
||||
SessionPersistence.userLeft('carol');
|
||||
expect(ServerDispatch.userLeft).toHaveBeenCalledWith('carol');
|
||||
});
|
||||
|
||||
it('serverMessage sanitizes message', () => {
|
||||
SessionPersistence.serverMessage('<b>hello</b>');
|
||||
expect(sanitizeHtml).toHaveBeenCalledWith('<b>hello</b>');
|
||||
expect(ServerDispatch.serverMessage).toHaveBeenCalledWith('sanitized:<b>hello</b>');
|
||||
});
|
||||
|
||||
it('accountAwaitingActivation passes options', () => {
|
||||
const opts = { userName: 'u' } as any;
|
||||
SessionPersistence.accountAwaitingActivation(opts);
|
||||
expect(ServerDispatch.accountAwaitingActivation).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
|
||||
it('accountActivationSuccess -> ServerDispatch.accountActivationSuccess', () => {
|
||||
SessionPersistence.accountActivationSuccess();
|
||||
expect(ServerDispatch.accountActivationSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accountActivationFailed -> ServerDispatch.accountActivationFailed', () => {
|
||||
SessionPersistence.accountActivationFailed();
|
||||
expect(ServerDispatch.accountActivationFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registrationRequiresEmail -> ServerDispatch.registrationRequiresEmail', () => {
|
||||
SessionPersistence.registrationRequiresEmail();
|
||||
expect(ServerDispatch.registrationRequiresEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registrationSuccess -> ServerDispatch.registrationSuccess', () => {
|
||||
SessionPersistence.registrationSuccess();
|
||||
expect(ServerDispatch.registrationSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registrationFailed normalizes ban error when endTime is given', () => {
|
||||
SessionPersistence.registrationFailed('reason', 999);
|
||||
expect(NormalizeService.normalizeBannedUserError).toHaveBeenCalledWith('reason', 999);
|
||||
expect(ServerDispatch.registrationFailed).toHaveBeenCalledWith('banned:reason:999');
|
||||
});
|
||||
|
||||
it('registrationFailed uses reason directly when no endTime', () => {
|
||||
SessionPersistence.registrationFailed('plain reason');
|
||||
expect(ServerDispatch.registrationFailed).toHaveBeenCalledWith('plain reason');
|
||||
});
|
||||
|
||||
it('registrationEmailError passes error', () => {
|
||||
SessionPersistence.registrationEmailError('bad email');
|
||||
expect(ServerDispatch.registrationEmailError).toHaveBeenCalledWith('bad email');
|
||||
});
|
||||
|
||||
it('registrationPasswordError passes error', () => {
|
||||
SessionPersistence.registrationPasswordError('short password');
|
||||
expect(ServerDispatch.registrationPasswordError).toHaveBeenCalledWith('short password');
|
||||
});
|
||||
|
||||
it('registrationUserNameError passes error', () => {
|
||||
SessionPersistence.registrationUserNameError('taken');
|
||||
expect(ServerDispatch.registrationUserNameError).toHaveBeenCalledWith('taken');
|
||||
});
|
||||
|
||||
it('resetPasswordChallenge -> ServerDispatch.resetPasswordChallenge', () => {
|
||||
SessionPersistence.resetPasswordChallenge();
|
||||
expect(ServerDispatch.resetPasswordChallenge).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resetPassword -> ServerDispatch.resetPassword', () => {
|
||||
SessionPersistence.resetPassword();
|
||||
expect(ServerDispatch.resetPassword).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resetPasswordSuccess -> ServerDispatch.resetPasswordSuccess', () => {
|
||||
SessionPersistence.resetPasswordSuccess();
|
||||
expect(ServerDispatch.resetPasswordSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resetPasswordFailed -> ServerDispatch.resetPasswordFailed', () => {
|
||||
SessionPersistence.resetPasswordFailed();
|
||||
expect(ServerDispatch.resetPasswordFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accountPasswordChange -> ServerDispatch.accountPasswordChange', () => {
|
||||
SessionPersistence.accountPasswordChange();
|
||||
expect(ServerDispatch.accountPasswordChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accountEditChanged passes fields', () => {
|
||||
SessionPersistence.accountEditChanged('Alice', 'a@b.com', 'US');
|
||||
expect(ServerDispatch.accountEditChanged).toHaveBeenCalledWith({ realName: 'Alice', email: 'a@b.com', country: 'US' });
|
||||
});
|
||||
|
||||
it('accountImageChanged passes avatarBmp', () => {
|
||||
const buf = new Uint8Array([1, 2, 3]);
|
||||
SessionPersistence.accountImageChanged(buf);
|
||||
expect(ServerDispatch.accountImageChanged).toHaveBeenCalledWith({ avatarBmp: buf });
|
||||
});
|
||||
|
||||
it('directMessageSent passes userName and message', () => {
|
||||
SessionPersistence.directMessageSent('bob', 'hi');
|
||||
expect(ServerDispatch.directMessageSent).toHaveBeenCalledWith('bob', 'hi');
|
||||
});
|
||||
|
||||
it('getUserInfo passes userInfo', () => {
|
||||
const user = { name: 'u' } as any;
|
||||
SessionPersistence.getUserInfo(user);
|
||||
expect(ServerDispatch.getUserInfo).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('getGamesOfUser logs to console', () => {
|
||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
SessionPersistence.getGamesOfUser('user1', {});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('gameJoined logs to console', () => {
|
||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
SessionPersistence.gameJoined({ gameInfo: {} } as any);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('notifyUser passes notification', () => {
|
||||
const notif = { type: 1 } as any;
|
||||
SessionPersistence.notifyUser(notif);
|
||||
expect(ServerDispatch.notifyUser).toHaveBeenCalledWith(notif);
|
||||
});
|
||||
|
||||
it('playerPropertiesChanged logs to console', () => {
|
||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
SessionPersistence.playerPropertiesChanged({} as any);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it('serverShutdown passes data', () => {
|
||||
const data = { gracePeriod: 5 } as any;
|
||||
SessionPersistence.serverShutdown(data);
|
||||
expect(ServerDispatch.serverShutdown).toHaveBeenCalledWith(data);
|
||||
});
|
||||
|
||||
it('userMessage passes messageData', () => {
|
||||
const msg = { message: 'hello' } as any;
|
||||
SessionPersistence.userMessage(msg);
|
||||
expect(ServerDispatch.userMessage).toHaveBeenCalledWith(msg);
|
||||
});
|
||||
|
||||
it('addToList passes list and userName', () => {
|
||||
SessionPersistence.addToList('buddy', 'alice');
|
||||
expect(ServerDispatch.addToList).toHaveBeenCalledWith('buddy', 'alice');
|
||||
});
|
||||
|
||||
it('removeFromList passes list and userName', () => {
|
||||
SessionPersistence.removeFromList('ignore', 'bob');
|
||||
expect(ServerDispatch.removeFromList).toHaveBeenCalledWith('ignore', 'bob');
|
||||
});
|
||||
|
||||
it('deleteServerDeck passes deckId', () => {
|
||||
SessionPersistence.deleteServerDeck(42);
|
||||
expect(ServerDispatch.deckDelete).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it('updateServerDecks passes deckList', () => {
|
||||
SessionPersistence.updateServerDecks({ folders: [] } as any);
|
||||
expect(ServerDispatch.backendDecks).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uploadServerDeck passes path and treeItem', () => {
|
||||
SessionPersistence.uploadServerDeck('/path', { id: 1 } as any);
|
||||
expect(ServerDispatch.deckUpload).toHaveBeenCalledWith('/path', { id: 1 });
|
||||
});
|
||||
|
||||
it('createServerDeckDir passes path and dirName', () => {
|
||||
SessionPersistence.createServerDeckDir('/path', 'newdir');
|
||||
expect(ServerDispatch.deckNewDir).toHaveBeenCalledWith('/path', 'newdir');
|
||||
});
|
||||
|
||||
it('deleteServerDeckDir passes path', () => {
|
||||
SessionPersistence.deleteServerDeckDir('/path');
|
||||
expect(ServerDispatch.deckDelDir).toHaveBeenCalledWith('/path');
|
||||
});
|
||||
|
||||
it('replayList passes matchList', () => {
|
||||
SessionPersistence.replayList([]);
|
||||
expect(ServerDispatch.replayList).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('replayAdded passes matchInfo', () => {
|
||||
const match = { gameId: 1 } as any;
|
||||
SessionPersistence.replayAdded(match);
|
||||
expect(ServerDispatch.replayAdded).toHaveBeenCalledWith(match);
|
||||
});
|
||||
|
||||
it('replayModifyMatch passes gameId and doNotHide', () => {
|
||||
SessionPersistence.replayModifyMatch(7, true);
|
||||
expect(ServerDispatch.replayModifyMatch).toHaveBeenCalledWith(7, true);
|
||||
});
|
||||
|
||||
it('replayDeleteMatch passes gameId', () => {
|
||||
SessionPersistence.replayDeleteMatch(7);
|
||||
expect(ServerDispatch.replayDeleteMatch).toHaveBeenCalledWith(7);
|
||||
});
|
||||
});
|
||||
119
webclient/src/websocket/services/BackendService.spec.ts
Normal file
119
webclient/src/websocket/services/BackendService.spec.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { makeMockProtoRoot } from '../__mocks__/helpers';
|
||||
|
||||
jest.mock('./ProtoController', () => ({
|
||||
ProtoController: { root: null },
|
||||
}));
|
||||
|
||||
jest.mock('../WebClient', () => {
|
||||
const mockProtobuf = {
|
||||
sendSessionCommand: jest.fn(),
|
||||
sendRoomCommand: jest.fn(),
|
||||
sendModeratorCommand: jest.fn(),
|
||||
sendAdminCommand: jest.fn(),
|
||||
};
|
||||
return { __esModule: true, default: { protobuf: mockProtobuf } };
|
||||
});
|
||||
|
||||
import { BackendService } from './BackendService';
|
||||
import { ProtoController } from './ProtoController';
|
||||
import webClient from '../WebClient';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
ProtoController.root = makeMockProtoRoot();
|
||||
ProtoController.root['Command_Test'] = { create: jest.fn(p => ({ ...p })) };
|
||||
ProtoController.root['Command_Room'] = { create: jest.fn(p => ({ ...p })) };
|
||||
ProtoController.root['Command_Mod'] = { create: jest.fn(p => ({ ...p })) };
|
||||
ProtoController.root['Command_Admin'] = { create: jest.fn(p => ({ ...p })) };
|
||||
ProtoController.root['Response_Test'] = {};
|
||||
});
|
||||
|
||||
function captureCallback(sendFn: jest.Mock) {
|
||||
return sendFn.mock.calls[0][sendFn === (webClient.protobuf as any).sendRoomCommand ? 2 : 1];
|
||||
}
|
||||
|
||||
describe('BackendService', () => {
|
||||
describe('send commands', () => {
|
||||
it.each<[string, () => void]>([
|
||||
['sendSessionCommand', () => BackendService.sendSessionCommand('Command_Test', { x: 1 }, {})],
|
||||
['sendRoomCommand', () => BackendService.sendRoomCommand(5, 'Command_Room', { y: 2 }, {})],
|
||||
['sendModeratorCommand', () => BackendService.sendModeratorCommand('Command_Mod', { z: 3 }, {})],
|
||||
['sendAdminCommand', () => BackendService.sendAdminCommand('Command_Admin', {}, {})],
|
||||
])('%s creates the command and delegates to protobuf', (methodName, invoke) => {
|
||||
invoke();
|
||||
expect((webClient.protobuf as any)[methodName]).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleResponse via non-session command callbacks', () => {
|
||||
it('sendRoomCommand callback invokes handleResponse', () => {
|
||||
const onSuccess = jest.fn();
|
||||
BackendService.sendRoomCommand(5, 'Command_Room', {}, { onSuccess });
|
||||
captureCallback((webClient.protobuf as any).sendRoomCommand)({ responseCode: 0 });
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sendModeratorCommand callback invokes handleResponse', () => {
|
||||
const onSuccess = jest.fn();
|
||||
BackendService.sendModeratorCommand('Command_Mod', {}, { onSuccess });
|
||||
captureCallback((webClient.protobuf as any).sendModeratorCommand)({ responseCode: 0 });
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sendAdminCommand callback invokes handleResponse', () => {
|
||||
const onSuccess = jest.fn();
|
||||
BackendService.sendAdminCommand('Command_Admin', {}, { onSuccess });
|
||||
captureCallback((webClient.protobuf as any).sendAdminCommand)({ responseCode: 0 });
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleResponse (via sendSessionCommand callback)', () => {
|
||||
function invokeCallback(options: any, raw: any) {
|
||||
BackendService.sendSessionCommand('Command_Test', {}, options);
|
||||
const cb = (webClient.protobuf as any).sendSessionCommand.mock.calls[0][1];
|
||||
cb(raw);
|
||||
}
|
||||
|
||||
it('calls onResponse and returns early when provided', () => {
|
||||
const onResponse = jest.fn();
|
||||
const onSuccess = jest.fn();
|
||||
invokeCallback({ onResponse, onSuccess }, { responseCode: 99 });
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
expect(onSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onSuccess with raw when responseCode is RespOk and no responseName', () => {
|
||||
const onSuccess = jest.fn();
|
||||
const raw = { responseCode: 0 };
|
||||
invokeCallback({ onSuccess }, raw);
|
||||
expect(onSuccess).toHaveBeenCalledWith(raw, raw);
|
||||
});
|
||||
|
||||
it('calls onSuccess with nested response when responseName is set', () => {
|
||||
const onSuccess = jest.fn();
|
||||
const raw = { responseCode: 0, '.Response_Test.ext': { nested: true } };
|
||||
invokeCallback({ onSuccess, responseName: 'Response_Test' }, raw);
|
||||
expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw);
|
||||
});
|
||||
|
||||
it('calls onResponseCode handler when code matches', () => {
|
||||
const specificHandler = jest.fn();
|
||||
invokeCallback({ onResponseCode: { 5: specificHandler } }, { responseCode: 5 });
|
||||
expect(specificHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onError when responseCode is not RespOk and no specific handler', () => {
|
||||
const onError = jest.fn();
|
||||
invokeCallback({ onError }, { responseCode: 99 });
|
||||
expect(onError).toHaveBeenCalledWith(99, { responseCode: 99 });
|
||||
});
|
||||
|
||||
it('logs error to console when no callbacks for non-RespOk response', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
invokeCallback({}, { responseCode: 42 });
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
41
webclient/src/websocket/services/ProtoController.spec.ts
Normal file
41
webclient/src/websocket/services/ProtoController.spec.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
jest.mock('../persistence', () => ({
|
||||
SessionPersistence: { initialized: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('../../proto-files.json', () => ['test.proto'], { virtual: true });
|
||||
|
||||
import { ProtoController } from './ProtoController';
|
||||
import { SessionPersistence } from '../persistence';
|
||||
import protobuf from 'protobufjs';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
ProtoController.root = null;
|
||||
(process.env as any).PUBLIC_URL = '';
|
||||
});
|
||||
|
||||
describe('ProtoController', () => {
|
||||
describe('load', () => {
|
||||
it('creates a new protobuf.Root', () => {
|
||||
ProtoController.load();
|
||||
expect(ProtoController.root).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls initialized when callback succeeds', () => {
|
||||
const loadSpy = jest.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
|
||||
(_files: any, _opts: any, cb: any) => cb(null)
|
||||
);
|
||||
ProtoController.load();
|
||||
expect(SessionPersistence.initialized).toHaveBeenCalled();
|
||||
loadSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('throws when callback receives an error', () => {
|
||||
const loadSpy = jest.spyOn(protobuf.Root.prototype, 'load').mockImplementation(
|
||||
(_files: any, _opts: any, cb: any) => cb(new Error('load failed'))
|
||||
);
|
||||
expect(() => ProtoController.load()).toThrow('load failed');
|
||||
loadSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
321
webclient/src/websocket/services/ProtobufService.spec.ts
Normal file
321
webclient/src/websocket/services/ProtobufService.spec.ts
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
import { makeMockProtoRoot } from '../__mocks__/helpers';
|
||||
|
||||
jest.mock('./ProtoController', () => ({
|
||||
ProtoController: { root: null, load: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('../commands/session', () => ({
|
||||
SessionCommands: { ping: jest.fn() },
|
||||
ping: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../events', () => ({
|
||||
CommonEvents: {},
|
||||
GameEvents: { '.Event_Game.ext': jest.fn() },
|
||||
RoomEvents: { '.Event_Room.ext': jest.fn() },
|
||||
SessionEvents: { '.Event_Session.ext': jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('../WebClient');
|
||||
|
||||
import { ProtobufService } from './ProtobufService';
|
||||
import { ProtoController } from './ProtoController';
|
||||
import { ping as sessionPing } from '../commands/session';
|
||||
|
||||
let mockSocket: any;
|
||||
let mockWebClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
ProtoController.root = makeMockProtoRoot();
|
||||
const encodeResult = { finish: jest.fn().mockReturnValue(new Uint8Array([1, 2])) };
|
||||
ProtoController.root.CommandContainer.encode = jest.fn().mockReturnValue(encodeResult);
|
||||
|
||||
mockSocket = {
|
||||
checkReadyState: jest.fn().mockReturnValue(true),
|
||||
send: jest.fn(),
|
||||
};
|
||||
|
||||
mockWebClient = {
|
||||
socket: mockSocket,
|
||||
};
|
||||
});
|
||||
|
||||
describe('ProtobufService', () => {
|
||||
it('calls ProtoController.load on construction', () => {
|
||||
new ProtobufService(mockWebClient);
|
||||
expect(ProtoController.load).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('resetCommands', () => {
|
||||
it('resets cmdId and pendingCommands', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
// add a pending command
|
||||
service.sendSessionCommand({}, jest.fn());
|
||||
expect((service as any).cmdId).toBe(1);
|
||||
service.resetCommands();
|
||||
expect((service as any).cmdId).toBe(0);
|
||||
expect((service as any).pendingCommands).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendCommand', () => {
|
||||
it('increments cmdId and stores callback', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendCommand({}, cb);
|
||||
expect((service as any).cmdId).toBe(1);
|
||||
expect((service as any).pendingCommands[1]).toBe(cb);
|
||||
});
|
||||
|
||||
it('sends encoded data when socket is OPEN', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
mockSocket.checkReadyState.mockReturnValue(true);
|
||||
service.sendCommand({}, jest.fn());
|
||||
expect(mockSocket.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not send when socket is not OPEN', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
mockSocket.checkReadyState.mockReturnValue(false);
|
||||
service.sendCommand({}, jest.fn());
|
||||
expect(mockSocket.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendSessionCommand', () => {
|
||||
it('creates a CommandContainer and calls sendCommand', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendSessionCommand({ cmdType: 'test' }, cb);
|
||||
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionCommand: expect.anything() })
|
||||
);
|
||||
});
|
||||
|
||||
it('invokes callback with raw response when the pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendSessionCommand({ cmdType: 'test' }, cb);
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
storedCb({ responseData: true });
|
||||
|
||||
expect(cb).toHaveBeenCalledWith({ responseData: true });
|
||||
});
|
||||
|
||||
it('does not throw when no callback is provided and pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendSessionCommand({ cmdType: 'test' });
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
expect(() => storedCb({ responseData: true })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendRoomCommand', () => {
|
||||
it('creates a CommandContainer with roomId and calls sendCommand', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendRoomCommand(42, { roomCmdType: 'test' }, jest.fn());
|
||||
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ roomId: 42 })
|
||||
);
|
||||
});
|
||||
|
||||
it('invokes callback with raw response when the pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendRoomCommand(42, { roomCmdType: 'test' }, cb);
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
storedCb({ responseData: true });
|
||||
|
||||
expect(cb).toHaveBeenCalledWith({ responseData: true });
|
||||
});
|
||||
|
||||
it('does not throw when no callback is provided and pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendRoomCommand(42, { roomCmdType: 'test' });
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
expect(() => storedCb({ responseData: true })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendModeratorCommand', () => {
|
||||
it('creates a CommandContainer with moderatorCommand', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendModeratorCommand({ modCmdType: 'test' }, jest.fn());
|
||||
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ moderatorCommand: expect.anything() })
|
||||
);
|
||||
});
|
||||
|
||||
it('invokes callback with raw response when the pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendModeratorCommand({ modCmdType: 'test' }, cb);
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
storedCb({ responseData: true });
|
||||
|
||||
expect(cb).toHaveBeenCalledWith({ responseData: true });
|
||||
});
|
||||
|
||||
it('does not throw when no callback is provided and pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendModeratorCommand({ modCmdType: 'test' });
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
expect(() => storedCb({ responseData: true })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendAdminCommand', () => {
|
||||
it('creates a CommandContainer with adminCommand', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendAdminCommand({ adminCmdType: 'test' }, jest.fn());
|
||||
expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ adminCommand: expect.anything() })
|
||||
);
|
||||
});
|
||||
|
||||
it('invokes callback with raw response when the pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
service.sendAdminCommand({ adminCmdType: 'test' }, cb);
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
storedCb({ responseData: true });
|
||||
|
||||
expect(cb).toHaveBeenCalledWith({ responseData: true });
|
||||
});
|
||||
|
||||
it('does not throw when no callback is provided and pending command is triggered', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
service.sendAdminCommand({ adminCmdType: 'test' });
|
||||
|
||||
const storedCb = (service as any).pendingCommands[1];
|
||||
expect(() => storedCb({ responseData: true })).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendKeepAliveCommand', () => {
|
||||
it('delegates to SessionCommands.ping', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const pingReceived = jest.fn();
|
||||
service.sendKeepAliveCommand(pingReceived);
|
||||
expect(sessionPing).toHaveBeenCalledWith(pingReceived);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleMessageEvent', () => {
|
||||
it('routes RESPONSE message to processServerResponse', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const cb = jest.fn();
|
||||
// store a callback for cmdId 1
|
||||
(service as any).cmdId = 1;
|
||||
(service as any).pendingCommands[1] = cb;
|
||||
|
||||
const response = { cmdId: 1 };
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
|
||||
messageType: ProtoController.root.ServerMessage.MessageType.RESPONSE,
|
||||
response,
|
||||
});
|
||||
|
||||
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
|
||||
expect(cb).toHaveBeenCalledWith(response);
|
||||
expect((service as any).pendingCommands[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('routes ROOM_EVENT message', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const processRoomEvent = jest.spyOn(service as any, 'processRoomEvent');
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
|
||||
messageType: ProtoController.root.ServerMessage.MessageType.ROOM_EVENT,
|
||||
roomEvent: { '.Event_Room.ext': {} },
|
||||
});
|
||||
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
|
||||
expect(processRoomEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes SESSION_EVENT message', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const processSessionEvent = jest.spyOn(service as any, 'processSessionEvent');
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
|
||||
messageType: ProtoController.root.ServerMessage.MessageType.SESSION_EVENT,
|
||||
sessionEvent: { '.Event_Session.ext': {} },
|
||||
});
|
||||
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
|
||||
expect(processSessionEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes GAME_EVENT_CONTAINER message', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const processGameEvent = jest.spyOn(service as any, 'processGameEvent');
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
|
||||
messageType: ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER,
|
||||
gameEvent: { '.Event_Game.ext': {} },
|
||||
});
|
||||
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
|
||||
expect(processGameEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs unknown message types (default case)', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue({
|
||||
messageType: 'UNKNOWN_TYPE',
|
||||
});
|
||||
service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent);
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does nothing when decoded message is null', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockReturnValue(null);
|
||||
expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow();
|
||||
});
|
||||
|
||||
it('catches and logs decode errors', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
ProtoController.root.ServerMessage.decode = jest.fn().mockImplementation(() => {
|
||||
throw new Error('decode error');
|
||||
});
|
||||
expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow();
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processEvent', () => {
|
||||
it('calls matching event handler with payload and raw', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const handler = jest.fn();
|
||||
const events = { '.Event_Test.ext': handler };
|
||||
const payload = { someData: 1 };
|
||||
const response = { '.Event_Test.ext': payload };
|
||||
const raw = { extra: true };
|
||||
|
||||
(service as any).processEvent(response, events, raw);
|
||||
|
||||
expect(handler).toHaveBeenCalledWith(payload, raw);
|
||||
});
|
||||
|
||||
it('stops after first matching event', () => {
|
||||
const service = new ProtobufService(mockWebClient);
|
||||
const handler1 = jest.fn();
|
||||
const handler2 = jest.fn();
|
||||
const events = { '.Event_A.ext': handler1, '.Event_B.ext': handler2 };
|
||||
const response = { '.Event_A.ext': { x: 1 } };
|
||||
|
||||
(service as any).processEvent(response, events, {});
|
||||
|
||||
expect(handler1).toHaveBeenCalled();
|
||||
expect(handler2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
288
webclient/src/websocket/services/WebSocketService.spec.ts
Normal file
288
webclient/src/websocket/services/WebSocketService.spec.ts
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import { installMockWebSocket } from '../__mocks__/helpers';
|
||||
|
||||
jest.mock('../commands/session', () => ({
|
||||
updateStatus: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../persistence', () => ({
|
||||
SessionPersistence: {
|
||||
connectionFailed: jest.fn(),
|
||||
testConnectionSuccessful: jest.fn(),
|
||||
testConnectionFailed: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { WebSocketService } from './WebSocketService';
|
||||
import { SessionPersistence } from '../persistence';
|
||||
import { updateStatus } from '../commands/session';
|
||||
import { StatusEnum } from 'types';
|
||||
|
||||
let MockWS: jest.Mock;
|
||||
let mockInstance: ReturnType<typeof installMockWebSocket>['mockInstance'];
|
||||
let mockWebClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
|
||||
const installed = installMockWebSocket();
|
||||
MockWS = installed.MockWS;
|
||||
mockInstance = installed.mockInstance;
|
||||
|
||||
mockWebClient = {
|
||||
status: StatusEnum.CONNECTED,
|
||||
clientOptions: { keepalive: 1000 },
|
||||
keepAlive: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('WebSocketService', () => {
|
||||
function createConnectedService() {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
service.connect({ host: 'h', port: 1 } as any, 'ws');
|
||||
return service;
|
||||
}
|
||||
|
||||
function createTestConnectedService() {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
service.testConnect({ host: 'h', port: 1 } as any, 'ws');
|
||||
return service;
|
||||
}
|
||||
|
||||
describe('constructor', () => {
|
||||
it('subscribes disconnected$ from KeepAliveService', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls disconnect and updateStatus when keepAlive disconnected$ fires', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
service.connect({ host: 'localhost', port: 8080 } as any, 'ws');
|
||||
// trigger keepAliveService.disconnected$
|
||||
(service as any).keepAliveService.disconnected$.next();
|
||||
expect(mockInstance.close).toHaveBeenCalled();
|
||||
expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection timeout');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connect', () => {
|
||||
it('creates a WebSocket with wss protocol by default', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: 'example.com' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
service.connect({ host: 'example.com', port: 8080 } as any);
|
||||
expect(MockWS).toHaveBeenCalledWith('wss://example.com:8080');
|
||||
});
|
||||
|
||||
it('switches to ws protocol when hostname is localhost', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: 'localhost' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
service.connect({ host: 'somehost', port: 1234 } as any);
|
||||
expect(MockWS).toHaveBeenCalledWith('ws://somehost:1234');
|
||||
});
|
||||
|
||||
it('sets binaryType to arraybuffer', () => {
|
||||
createConnectedService();
|
||||
expect(mockInstance.binaryType).toBe('arraybuffer');
|
||||
});
|
||||
|
||||
it('fires socket.close after keepalive timeout', () => {
|
||||
createConnectedService();
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(mockInstance.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('socket event handlers (onopen)', () => {
|
||||
it('clears the connection timeout when socket opens', () => {
|
||||
const clearSpy = jest.spyOn(global, 'clearTimeout');
|
||||
createConnectedService();
|
||||
mockInstance.onopen();
|
||||
expect(clearSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls updateStatus CONNECTED on open', () => {
|
||||
createConnectedService();
|
||||
mockInstance.onopen();
|
||||
expect(updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected');
|
||||
});
|
||||
|
||||
it('starts the ping loop with the keepalive interval', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
const startSpy = jest.spyOn((service as any).keepAliveService, 'startPingLoop');
|
||||
service.connect({ host: 'h', port: 1 } as any, 'ws');
|
||||
mockInstance.onopen();
|
||||
expect(startSpy).toHaveBeenCalledWith(1000, expect.any(Function));
|
||||
});
|
||||
|
||||
it('ping loop callback calls webClient.keepAlive', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
const startSpy = jest.spyOn((service as any).keepAliveService, 'startPingLoop');
|
||||
service.connect({ host: 'h', port: 1 } as any, 'ws');
|
||||
mockInstance.onopen();
|
||||
const pingCb = startSpy.mock.calls[0][1];
|
||||
const done = jest.fn();
|
||||
pingCb(done);
|
||||
expect(mockWebClient.keepAlive).toHaveBeenCalledWith(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('socket event handlers (onclose)', () => {
|
||||
it('calls updateStatus DISCONNECTED on close when not already DISCONNECTED', () => {
|
||||
createConnectedService();
|
||||
mockInstance.onclose();
|
||||
expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed');
|
||||
});
|
||||
|
||||
it('does not overwrite status if already DISCONNECTED', () => {
|
||||
createConnectedService();
|
||||
mockWebClient.status = StatusEnum.DISCONNECTED;
|
||||
mockInstance.onclose();
|
||||
expect(updateStatus).not.toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed');
|
||||
});
|
||||
|
||||
it('ends the ping loop on close', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
const endSpy = jest.spyOn((service as any).keepAliveService, 'endPingLoop');
|
||||
service.connect({ host: 'h', port: 1 } as any, 'ws');
|
||||
mockInstance.onclose();
|
||||
expect(endSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('socket event handlers (onerror)', () => {
|
||||
it('calls updateStatus DISCONNECTED on error', () => {
|
||||
createConnectedService();
|
||||
mockInstance.onerror();
|
||||
expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Failed');
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.connectionFailed on error', () => {
|
||||
createConnectedService();
|
||||
mockInstance.onerror();
|
||||
expect(SessionPersistence.connectionFailed).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('socket event handlers (onmessage)', () => {
|
||||
it('emits on message$ subject', () => {
|
||||
const service = createConnectedService();
|
||||
const handler = jest.fn();
|
||||
service.message$.subscribe(handler);
|
||||
const event = { data: new ArrayBuffer(4) } as MessageEvent;
|
||||
mockInstance.onmessage(event);
|
||||
expect(handler).toHaveBeenCalledWith(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('closes the socket', () => {
|
||||
const service = createConnectedService();
|
||||
service.disconnect();
|
||||
expect(mockInstance.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('send', () => {
|
||||
it('delegates to socket.send', () => {
|
||||
const service = createConnectedService();
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
service.send(data);
|
||||
expect(mockInstance.send).toHaveBeenCalledWith(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkReadyState', () => {
|
||||
it('returns true when readyState matches', () => {
|
||||
const service = createConnectedService();
|
||||
mockInstance.readyState = WebSocket.OPEN;
|
||||
expect(service.checkReadyState(WebSocket.OPEN)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when readyState does not match', () => {
|
||||
const service = createConnectedService();
|
||||
mockInstance.readyState = 3; // CLOSED
|
||||
expect(service.checkReadyState(WebSocket.OPEN)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when socket is null', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
// no connect called, socket is undefined
|
||||
expect(service.checkReadyState(WebSocket.OPEN)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testConnect', () => {
|
||||
it('creates a test WebSocket with correct URL', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: 'example.com' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
service.testConnect({ host: 'example.com', port: 9000 } as any);
|
||||
expect(MockWS).toHaveBeenCalledWith('wss://example.com:9000');
|
||||
});
|
||||
|
||||
it('uses ws protocol on localhost', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { hostname: 'localhost' },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
service.testConnect({ host: 'h', port: 1 } as any);
|
||||
expect(MockWS).toHaveBeenCalledWith('ws://h:1');
|
||||
});
|
||||
|
||||
it('closes previous testSocket when connecting again', () => {
|
||||
const service = new WebSocketService(mockWebClient);
|
||||
service.testConnect({ host: 'h', port: 1 } as any, 'ws');
|
||||
const firstInstance = mockInstance;
|
||||
// install second mock instance and restore after test
|
||||
const installed2 = installMockWebSocket();
|
||||
service.testConnect({ host: 'h', port: 2 } as any, 'ws');
|
||||
expect(firstInstance.close).toHaveBeenCalled();
|
||||
// restore original mock so subsequent tests see a clean global
|
||||
mockInstance = installed2.mockInstance;
|
||||
MockWS = installed2.MockWS;
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.testConnectionSuccessful on open', () => {
|
||||
createTestConnectedService();
|
||||
const timer = jest.spyOn(global, 'clearTimeout');
|
||||
mockInstance.onopen();
|
||||
expect(SessionPersistence.testConnectionSuccessful).toHaveBeenCalled();
|
||||
expect(mockInstance.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires socket.close after keepalive timeout for testConnect', () => {
|
||||
createTestConnectedService();
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(mockInstance.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls SessionPersistence.testConnectionFailed on error', () => {
|
||||
createTestConnectedService();
|
||||
mockInstance.onerror();
|
||||
expect(SessionPersistence.testConnectionFailed).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('nulls out testSocket on close', () => {
|
||||
const service = createTestConnectedService();
|
||||
mockInstance.onclose();
|
||||
expect((service as any).testSocket).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
110
webclient/src/websocket/utils/NormalizeService.spec.ts
Normal file
110
webclient/src/websocket/utils/NormalizeService.spec.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import NormalizeService from './NormalizeService';
|
||||
|
||||
describe('NormalizeService', () => {
|
||||
describe('normalizeRoomInfo', () => {
|
||||
it('builds gametypeMap from gametypeList', () => {
|
||||
const roomInfo: any = {
|
||||
gametypeList: [
|
||||
{ gameTypeId: 1, description: 'Standard' },
|
||||
{ gameTypeId: 2, description: 'Draft' },
|
||||
],
|
||||
gametypeMap: {},
|
||||
gameList: [],
|
||||
};
|
||||
NormalizeService.normalizeRoomInfo(roomInfo);
|
||||
expect(roomInfo.gametypeMap).toEqual({ 1: 'Standard', 2: 'Draft' });
|
||||
});
|
||||
|
||||
it('normalizes each game in gameList', () => {
|
||||
const roomInfo: any = {
|
||||
gametypeList: [{ gameTypeId: 5, description: 'Modern' }],
|
||||
gametypeMap: {},
|
||||
gameList: [{ gameTypes: [5], description: 'My Game' }],
|
||||
};
|
||||
NormalizeService.normalizeRoomInfo(roomInfo);
|
||||
expect(roomInfo.gameList[0].gameType).toBe('Modern');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeGameObject', () => {
|
||||
it('sets gameType from first element of gameTypes', () => {
|
||||
const game: any = { gameTypes: [3], description: 'Test' };
|
||||
const map: any = { 3: 'Legacy' };
|
||||
NormalizeService.normalizeGameObject(game, map);
|
||||
expect(game.gameType).toBe('Legacy');
|
||||
});
|
||||
|
||||
it('sets gameType to empty string when gameTypes is empty', () => {
|
||||
const game: any = { gameTypes: [], description: 'Test' };
|
||||
NormalizeService.normalizeGameObject(game, {});
|
||||
expect(game.gameType).toBe('');
|
||||
});
|
||||
|
||||
it('sets gameType to empty string when gameTypes is null', () => {
|
||||
const game: any = { gameTypes: null, description: 'Test' };
|
||||
NormalizeService.normalizeGameObject(game, {});
|
||||
expect(game.gameType).toBe('');
|
||||
});
|
||||
|
||||
it('sets description to empty string when description is falsy', () => {
|
||||
const game: any = { gameTypes: [], description: null };
|
||||
NormalizeService.normalizeGameObject(game, {});
|
||||
expect(game.description).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeLogs', () => {
|
||||
it('groups logs by targetType', () => {
|
||||
const logs: any[] = [
|
||||
{ targetType: 'room', msg: 'a' },
|
||||
{ targetType: 'chat', msg: 'b' },
|
||||
{ targetType: 'room', msg: 'c' },
|
||||
];
|
||||
const result = NormalizeService.normalizeLogs(logs);
|
||||
expect(result['room']).toHaveLength(2);
|
||||
expect(result['chat']).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns empty object for empty array', () => {
|
||||
expect(NormalizeService.normalizeLogs([])).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeUserMessage', () => {
|
||||
it('prepends username when name is present', () => {
|
||||
const message: any = { name: 'Alice', message: 'hello' };
|
||||
NormalizeService.normalizeUserMessage(message);
|
||||
expect(message.message).toBe('Alice: hello');
|
||||
});
|
||||
|
||||
it('does not modify message when name is absent', () => {
|
||||
const message: any = { name: '', message: 'hello' };
|
||||
NormalizeService.normalizeUserMessage(message);
|
||||
expect(message.message).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeBannedUserError', () => {
|
||||
it('returns permanently banned message when endTime is 0', () => {
|
||||
const result = NormalizeService.normalizeBannedUserError('', 0);
|
||||
expect(result).toBe('You are permanently banned');
|
||||
});
|
||||
|
||||
it('returns banned until date when endTime is given', () => {
|
||||
const endTime = new Date('2030-01-01').getTime();
|
||||
const result = NormalizeService.normalizeBannedUserError('', endTime);
|
||||
expect(result).toContain('You are banned until');
|
||||
expect(result).toContain(new Date(endTime).toString());
|
||||
});
|
||||
|
||||
it('appends reasonStr when provided', () => {
|
||||
const result = NormalizeService.normalizeBannedUserError('bad behavior', 0);
|
||||
expect(result).toContain('\n\nbad behavior');
|
||||
});
|
||||
|
||||
it('does not append when reasonStr is empty', () => {
|
||||
const result = NormalizeService.normalizeBannedUserError('', 0);
|
||||
expect(result).not.toContain('\n\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
19
webclient/src/websocket/utils/guid.util.spec.ts
Normal file
19
webclient/src/websocket/utils/guid.util.spec.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { guid } from './guid.util';
|
||||
|
||||
describe('guid', () => {
|
||||
it('returns a string', () => {
|
||||
expect(typeof guid()).toBe('string');
|
||||
});
|
||||
|
||||
it('matches UUID v4 pattern', () => {
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
expect(guid()).toMatch(uuidPattern);
|
||||
});
|
||||
|
||||
it('returns deterministic value when Math.random is mocked', () => {
|
||||
const spy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
const result = guid();
|
||||
expect(result).toBe(guid());
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
58
webclient/src/websocket/utils/passwordHasher.spec.ts
Normal file
58
webclient/src/websocket/utils/passwordHasher.spec.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { makeMockProtoRoot } from '../__mocks__/helpers';
|
||||
|
||||
jest.mock('../services/ProtoController', () => ({
|
||||
ProtoController: { root: null },
|
||||
}));
|
||||
|
||||
import { ProtoController } from '../services/ProtoController';
|
||||
import { hashPassword, generateSalt, passwordSaltSupported } from './passwordHasher';
|
||||
|
||||
beforeEach(() => {
|
||||
ProtoController.root = makeMockProtoRoot();
|
||||
});
|
||||
|
||||
describe('hashPassword', () => {
|
||||
it('returns a string starting with the salt', () => {
|
||||
const result = hashPassword('mysalt', 'mypassword');
|
||||
expect(result.startsWith('mysalt')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns the same value for the same inputs (deterministic)', () => {
|
||||
expect(hashPassword('salt', 'pass')).toBe(hashPassword('salt', 'pass'));
|
||||
});
|
||||
|
||||
it('returns different values for different salts', () => {
|
||||
expect(hashPassword('salt1', 'pass')).not.toBe(hashPassword('salt2', 'pass'));
|
||||
});
|
||||
|
||||
it('returns different values for different passwords', () => {
|
||||
expect(hashPassword('salt', 'pass1')).not.toBe(hashPassword('salt', 'pass2'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSalt', () => {
|
||||
it('returns a string of 16 characters', () => {
|
||||
expect(generateSalt()).toHaveLength(16);
|
||||
});
|
||||
|
||||
it('only contains alphanumeric characters', () => {
|
||||
expect(generateSalt()).toMatch(/^[A-Za-z0-9]{16}$/);
|
||||
});
|
||||
|
||||
it('returns different values on successive calls (not constant)', () => {
|
||||
const salts = new Set(Array.from({ length: 10 }, () => generateSalt()));
|
||||
expect(salts.size).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passwordSaltSupported', () => {
|
||||
it('returns non-zero when SupportsPasswordHash bit is set', () => {
|
||||
// SupportsPasswordHash = 2 from mock; 2 & 2 = 2
|
||||
expect(passwordSaltSupported(2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns zero when SupportsPasswordHash bit is not set', () => {
|
||||
// 1 & 2 = 0
|
||||
expect(passwordSaltSupported(1)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
55
webclient/src/websocket/utils/sanitizeHtml.util.spec.ts
Normal file
55
webclient/src/websocket/utils/sanitizeHtml.util.spec.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { sanitizeHtml } from './sanitizeHtml.util';
|
||||
|
||||
describe('sanitizeHtml', () => {
|
||||
it('passes through plain text unchanged', () => {
|
||||
expect(sanitizeHtml('hello world')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('allows <br> tag', () => {
|
||||
expect(sanitizeHtml('line1<br>line2')).toBe('line1<br />line2');
|
||||
});
|
||||
|
||||
it('allows <b> tag', () => {
|
||||
expect(sanitizeHtml('<b>bold</b>')).toBe('<b>bold</b>');
|
||||
});
|
||||
|
||||
it('allows <img> tag', () => {
|
||||
expect(sanitizeHtml('<img>')).toBe('<img />');
|
||||
});
|
||||
|
||||
it('allows <center> tag', () => {
|
||||
expect(sanitizeHtml('<center>x</center>')).toBe('<center>x</center>');
|
||||
});
|
||||
|
||||
it('allows <font> tag with color attribute', () => {
|
||||
expect(sanitizeHtml('<font color="red">text</font>')).toBe('<font color="red">text</font>');
|
||||
});
|
||||
|
||||
it('strips disallowed tag <script>', () => {
|
||||
expect(sanitizeHtml('<script>alert(1)</script>')).toBe('');
|
||||
});
|
||||
|
||||
it('strips disallowed tag <div>', () => {
|
||||
expect(sanitizeHtml('<div>content</div>')).toBe('content');
|
||||
});
|
||||
|
||||
it('strips disallowed attribute onclick from <b>', () => {
|
||||
expect(sanitizeHtml('<b onclick="evil()">hi</b>')).toBe('<b>hi</b>');
|
||||
});
|
||||
|
||||
it('adds target=_blank and rel=noopener noreferrer to <a> tags', () => {
|
||||
const result = sanitizeHtml('<a href="https://example.com">link</a>');
|
||||
expect(result).toContain('target="_blank"');
|
||||
expect(result).toContain('rel="noopener noreferrer"');
|
||||
});
|
||||
|
||||
it('allows href attribute on <a>', () => {
|
||||
const result = sanitizeHtml('<a href="https://example.com">link</a>');
|
||||
expect(result).toContain('href="https://example.com"');
|
||||
});
|
||||
|
||||
it('strips disallowed schemes like javascript:', () => {
|
||||
const result = sanitizeHtml('<a href="javascript:alert(1)">xss</a>');
|
||||
expect(result).not.toContain('javascript:');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue