mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
harden
This commit is contained in:
parent
d04aa83258
commit
dcd6dc00f4
83 changed files with 1797 additions and 390 deletions
|
|
@ -18,7 +18,7 @@ import { Mock } from 'vitest';
|
|||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import * as SessionIndexMocks from './';
|
||||
import { App, Enriched } from '@app/types';
|
||||
import { Enriched } from '@app/types';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import {
|
||||
Command_Activate_ext,
|
||||
|
|
@ -59,7 +59,7 @@ const baseTransport = { host: 'h', port: '1' };
|
|||
const makeLoginOpts = (overrides: Partial<Enriched.LoginConnectOptions> = {}): Enriched.LoginConnectOptions => ({
|
||||
...baseTransport,
|
||||
userName: 'alice',
|
||||
reason: App.WebSocketConnectReason.LOGIN,
|
||||
reason: Enriched.WebSocketConnectReason.LOGIN,
|
||||
...overrides,
|
||||
});
|
||||
const makeRegisterOpts = (
|
||||
|
|
@ -71,7 +71,7 @@ const makeRegisterOpts = (
|
|||
email: 'a@b.com',
|
||||
country: 'US',
|
||||
realName: 'Al',
|
||||
reason: App.WebSocketConnectReason.REGISTER,
|
||||
reason: Enriched.WebSocketConnectReason.REGISTER,
|
||||
...overrides,
|
||||
});
|
||||
const makeActivateOpts = (
|
||||
|
|
@ -80,26 +80,26 @@ const makeActivateOpts = (
|
|||
...baseTransport,
|
||||
userName: 'alice',
|
||||
token: 'tok',
|
||||
reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT,
|
||||
reason: Enriched.WebSocketConnectReason.ACTIVATE_ACCOUNT,
|
||||
...overrides,
|
||||
});
|
||||
const makeForgotRequestOpts = (): Enriched.PasswordResetRequestConnectOptions => ({
|
||||
...baseTransport,
|
||||
userName: 'alice',
|
||||
reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST,
|
||||
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET_REQUEST,
|
||||
});
|
||||
const makeForgotChallengeOpts = (): Enriched.PasswordResetChallengeConnectOptions => ({
|
||||
...baseTransport,
|
||||
userName: 'alice',
|
||||
email: 'a@b.com',
|
||||
reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE,
|
||||
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE,
|
||||
});
|
||||
const makeForgotResetOpts = (): Enriched.PasswordResetConnectOptions => ({
|
||||
...baseTransport,
|
||||
userName: 'alice',
|
||||
token: 'tok',
|
||||
newPassword: 'newpw',
|
||||
reason: App.WebSocketConnectReason.PASSWORD_RESET,
|
||||
reason: Enriched.WebSocketConnectReason.PASSWORD_RESET,
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ describe('login', () => {
|
|||
});
|
||||
|
||||
it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => {
|
||||
login({ host: 'h', port: '1', userName: 'alice', reason: App.WebSocketConnectReason.LOGIN }, 'pw', 'salt');
|
||||
login({ host: 'h', port: '1', userName: 'alice', reason: Enriched.WebSocketConnectReason.LOGIN }, 'pw', 'salt');
|
||||
const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } };
|
||||
invokeOnSuccess(loginResp, { responseCode: 0 });
|
||||
const calledWith = (WebClient.instance.response.session.loginSuccessful as Mock).mock.calls[0][0];
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@ describe('playerPropertiesChanged event', () => {
|
|||
});
|
||||
|
||||
describe('gameSay event', () => {
|
||||
it('delegates to WebClient.instance.response.game.gameSay with gameId, playerId, message', () => {
|
||||
it('delegates to WebClient.instance.response.game.gameSay with gameId, playerId, message, timeReceived', () => {
|
||||
const data = create(Event_GameSaySchema, { message: 'gg' });
|
||||
gameSay(data, meta);
|
||||
expect(WebClient.instance.response.game.gameSay).toHaveBeenCalledWith(5, 2, 'gg');
|
||||
expect(WebClient.instance.response.game.gameSay).toHaveBeenCalledWith(5, 2, 'gg', expect.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
|||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function gameSay(data: Event_GameSay, meta: GameEventMeta): void {
|
||||
WebClient.instance.response.game.gameSay(meta.gameId, meta.playerId, data.message);
|
||||
WebClient.instance.response.game.gameSay(meta.gameId, meta.playerId, data.message, Date.now());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export interface IGameResponse {
|
|||
gameClosed(gameId: number): void;
|
||||
gameHostChanged(gameId: number, hostId: number): void;
|
||||
kicked(gameId: number): void;
|
||||
gameSay(gameId: number, playerId: number, message: string): void;
|
||||
gameSay(gameId: number, playerId: number, message: string, timeReceived: number): void;
|
||||
cardMoved(gameId: number, playerId: number, data: Event_MoveCard): void;
|
||||
cardFlipped(gameId: number, playerId: number, data: Event_FlipCard): void;
|
||||
cardDestroyed(gameId: number, playerId: number, data: Event_DestroyCard): void;
|
||||
|
|
|
|||
|
|
@ -64,5 +64,14 @@ describe('KeepAliveService', () => {
|
|||
|
||||
expect(service.endPingLoop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should clear previous interval when startPingLoop is called again', () => {
|
||||
const clearSpy = vi.spyOn(globalThis, 'clearInterval');
|
||||
const previousCb = (service as KeepAliveInternal).keepalivecb;
|
||||
|
||||
service.startPingLoop(interval, ping);
|
||||
|
||||
expect(clearSpy).toHaveBeenCalledWith(previousCb);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export class KeepAliveService {
|
|||
}
|
||||
|
||||
public startPingLoop(interval: number, ping: (onPong: () => void) => void): void {
|
||||
this.endPingLoop();
|
||||
this.keepalivecb = setInterval(() => {
|
||||
// check if the previous ping got no reply
|
||||
if (this.lastPingPending) {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,67 @@ describe('ProtobufService', () => {
|
|||
expect((service as ProtobufInternal).cmdId).toBe(0);
|
||||
expect((service as ProtobufInternal).pendingCommands.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns true when command is sent', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
const result = service.sendCommand(create(CommandContainerSchema), vi.fn());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when transport is closed', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const result = service.sendCommand(create(CommandContainerSchema), vi.fn());
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('send*Command when transport is closed', () => {
|
||||
it('calls onError when sendSessionCommand is dropped', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const onError = vi.fn();
|
||||
service.sendSessionCommand(sessionExt, {}, { onError });
|
||||
expect(onError).toHaveBeenCalledWith(-1, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls onError when sendRoomCommand is dropped', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const onError = vi.fn();
|
||||
service.sendRoomCommand(42, roomExt, {}, { onError });
|
||||
expect(onError).toHaveBeenCalledWith(-1, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls onError when sendGameCommand is dropped', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const onError = vi.fn();
|
||||
service.sendGameCommand(7, gameExt, {}, { onError });
|
||||
expect(onError).toHaveBeenCalledWith(-1, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls onError when sendModeratorCommand is dropped', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const onError = vi.fn();
|
||||
service.sendModeratorCommand(moderatorExt, {}, { onError });
|
||||
expect(onError).toHaveBeenCalledWith(-1, expect.any(Object));
|
||||
});
|
||||
|
||||
it('calls onError when sendAdminCommand is dropped', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
const onError = vi.fn();
|
||||
service.sendAdminCommand(adminExt, {}, { onError });
|
||||
expect(onError).toHaveBeenCalledWith(-1, expect.any(Object));
|
||||
});
|
||||
|
||||
it('does not throw when command is dropped with no options', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
mockSocket.isOpen.mockReturnValue(false);
|
||||
expect(() => service.sendSessionCommand(sessionExt, {})).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendSessionCommand', () => {
|
||||
|
|
@ -311,9 +372,9 @@ describe('ProtobufService', () => {
|
|||
expect(processGameEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs unknown message types (default case)', () => {
|
||||
it('warns on unknown message types (default case)', () => {
|
||||
const service = new ProtobufService(mockSocket);
|
||||
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
vi.mocked(fromBinary).mockReturnValue(
|
||||
create(ServerMessageSchema, {
|
||||
|
|
|
|||
|
|
@ -54,11 +54,7 @@ export class ProtobufService {
|
|||
const gameCmd = create(GameCommandSchema);
|
||||
setExtension(gameCmd, ext, value);
|
||||
const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd] });
|
||||
this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(ext.typeName, raw, options);
|
||||
}
|
||||
});
|
||||
this.dispatchCommand(ext.typeName, cmd, options);
|
||||
}
|
||||
|
||||
public sendRoomCommand<V, R = unknown>(
|
||||
|
|
@ -70,11 +66,7 @@ export class ProtobufService {
|
|||
const roomCmd = create(RoomCommandSchema);
|
||||
setExtension(roomCmd, ext, value);
|
||||
const cmd = create(CommandContainerSchema, { roomId, roomCommand: [roomCmd] });
|
||||
this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(ext.typeName, raw, options);
|
||||
}
|
||||
});
|
||||
this.dispatchCommand(ext.typeName, cmd, options);
|
||||
}
|
||||
|
||||
public sendSessionCommand<V, R = unknown>(
|
||||
|
|
@ -85,11 +77,7 @@ export class ProtobufService {
|
|||
const sesCmd = create(SessionCommandSchema);
|
||||
setExtension(sesCmd, ext, value);
|
||||
const cmd = create(CommandContainerSchema, { sessionCommand: [sesCmd] });
|
||||
this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(ext.typeName, raw, options);
|
||||
}
|
||||
});
|
||||
this.dispatchCommand(ext.typeName, cmd, options);
|
||||
}
|
||||
|
||||
public sendModeratorCommand<V, R = unknown>(
|
||||
|
|
@ -100,11 +88,7 @@ export class ProtobufService {
|
|||
const modCmd = create(ModeratorCommandSchema);
|
||||
setExtension(modCmd, ext, value);
|
||||
const cmd = create(CommandContainerSchema, { moderatorCommand: [modCmd] });
|
||||
this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(ext.typeName, raw, options);
|
||||
}
|
||||
});
|
||||
this.dispatchCommand(ext.typeName, cmd, options);
|
||||
}
|
||||
|
||||
public sendAdminCommand<V, R = unknown>(
|
||||
|
|
@ -115,22 +99,31 @@ export class ProtobufService {
|
|||
const adminCmd = create(AdminCommandSchema);
|
||||
setExtension(adminCmd, ext, value);
|
||||
const cmd = create(CommandContainerSchema, { adminCommand: [adminCmd] });
|
||||
this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(ext.typeName, raw, options);
|
||||
}
|
||||
});
|
||||
this.dispatchCommand(ext.typeName, cmd, options);
|
||||
}
|
||||
|
||||
public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void) {
|
||||
private dispatchCommand<R>(typeName: string, cmd: CommandContainer, options?: CommandOptions<R>): void {
|
||||
const sent = this.sendCommand(cmd, raw => {
|
||||
if (options) {
|
||||
handleResponse(typeName, raw, options);
|
||||
}
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
options?.onError?.(-1, {} as Response);
|
||||
}
|
||||
}
|
||||
|
||||
public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void): boolean {
|
||||
if (!this.transport.isOpen()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cmdId++;
|
||||
cmd.cmdId = BigInt(this.cmdId);
|
||||
this.pendingCommands.set(this.cmdId, callback);
|
||||
this.transport.send(toBinary(CommandContainerSchema, cmd));
|
||||
return true;
|
||||
}
|
||||
|
||||
public handleMessageEvent({ data }: MessageEvent): void {
|
||||
|
|
@ -153,7 +146,7 @@ export class ProtobufService {
|
|||
this.processGameEvent(msg.gameEventContainer);
|
||||
break;
|
||||
default:
|
||||
console.log(msg);
|
||||
console.warn('Unknown message type:', msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,12 @@ describe('WebSocketService', () => {
|
|||
service.send(data);
|
||||
expect(mockInstance.send).toHaveBeenCalledWith(data);
|
||||
});
|
||||
|
||||
it('does not throw when socket is undefined (before connect)', () => {
|
||||
const service = new WebSocketService(mockConfig);
|
||||
const data = new Uint8Array([1, 2, 3]);
|
||||
expect(() => service.send(data)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkReadyState', () => {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export class WebSocketService {
|
|||
}
|
||||
|
||||
public send(message: Uint8Array): void {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
this.socket.send(message as unknown as ArrayBufferView);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue