mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
fix unit tests and refactor types
This commit is contained in:
parent
decebc25c7
commit
fea21b5057
75 changed files with 908 additions and 501 deletions
|
|
@ -1 +0,0 @@
|
|||
export const PROTOCOL_VERSION = 14;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import type { Enriched } from '@app/types';
|
||||
|
||||
let pendingOptions: Enriched.WebSocketConnectOptions | null = null;
|
||||
|
||||
export function setPendingOptions(options: Enriched.WebSocketConnectOptions) {
|
||||
pendingOptions = options;
|
||||
}
|
||||
|
||||
export function consumePendingOptions(): Enriched.WebSocketConnectOptions | null {
|
||||
const opts = pendingOptions;
|
||||
pendingOptions = null;
|
||||
return opts;
|
||||
}
|
||||
|
|
@ -1,109 +1,19 @@
|
|||
import { App } from '@app/types';
|
||||
import {
|
||||
WebClient,
|
||||
StatusEnum,
|
||||
SessionEvents,
|
||||
RoomEvents,
|
||||
GameEvents,
|
||||
SessionCommands,
|
||||
generateSalt,
|
||||
passwordSaltSupported,
|
||||
} from '@app/websocket';
|
||||
import type { WebClientConfig } from '@app/websocket';
|
||||
|
||||
import { createWebClientResponse } from './response';
|
||||
import { consumePendingOptions } from './connectionState';
|
||||
import { PROTOCOL_VERSION } from './config';
|
||||
|
||||
export function initWebClient(): void {
|
||||
const response = createWebClientResponse();
|
||||
|
||||
const config: WebClientConfig = {
|
||||
response,
|
||||
|
||||
onServerIdentified: (info) => {
|
||||
const { serverName, serverVersion, protocolVersion, serverOptions } = info;
|
||||
if (protocolVersion !== PROTOCOL_VERSION) {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
|
||||
SessionCommands.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const getPasswordSalt = passwordSaltSupported(serverOptions);
|
||||
const options = consumePendingOptions();
|
||||
|
||||
if (!options) {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Missing connection options');
|
||||
SessionCommands.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (options.reason) {
|
||||
case App.WebSocketConnectReason.LOGIN: {
|
||||
const { password, ...rest } = options;
|
||||
SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.login(rest, password, salt),
|
||||
() => {
|
||||
response.session.loginFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.login(rest, password);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case App.WebSocketConnectReason.REGISTER: {
|
||||
const { password, ...rest } = options;
|
||||
const passwordSalt = getPasswordSalt ? generateSalt() : null;
|
||||
SessionCommands.register(rest, password, passwordSalt);
|
||||
break;
|
||||
}
|
||||
case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: {
|
||||
const { password, ...rest } = options;
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.activate(rest, password, salt),
|
||||
() => {
|
||||
response.session.accountActivationFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.activate(rest, password);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST:
|
||||
SessionCommands.forgotPasswordRequest(options);
|
||||
break;
|
||||
case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE:
|
||||
SessionCommands.forgotPasswordChallenge(options);
|
||||
break;
|
||||
case App.WebSocketConnectReason.PASSWORD_RESET: {
|
||||
const { newPassword, ...rest } = options;
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.forgotPasswordReset(rest, newPassword, salt),
|
||||
() => {
|
||||
response.session.resetPasswordFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.forgotPasswordReset(rest, newPassword);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${options.reason}`);
|
||||
SessionCommands.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.session.updateInfo(serverName, serverVersion);
|
||||
},
|
||||
|
||||
sessionEvents: SessionEvents,
|
||||
roomEvents: RoomEvents,
|
||||
gameEvents: GameEvents,
|
||||
|
|
|
|||
|
|
@ -1,56 +1,69 @@
|
|||
import { App, Enriched } from '@app/types';
|
||||
import { WebClient, StatusEnum, SessionCommands } from '@app/websocket';
|
||||
import type { IAuthenticationRequest, AuthRequestMap } from '@app/websocket';
|
||||
|
||||
import { setPendingOptions } from '../connectionState';
|
||||
import {
|
||||
WebClient,
|
||||
StatusEnum,
|
||||
SessionCommands,
|
||||
WebSocketConnectReason,
|
||||
setPendingOptions,
|
||||
} from '@app/websocket';
|
||||
import type {
|
||||
IAuthenticationRequest,
|
||||
AuthRequestMap,
|
||||
LoginConnectOptions,
|
||||
TestConnectionOptions,
|
||||
RegisterConnectOptions,
|
||||
ActivateConnectOptions,
|
||||
PasswordResetRequestConnectOptions,
|
||||
PasswordResetChallengeConnectOptions,
|
||||
PasswordResetConnectOptions,
|
||||
} from '@app/websocket';
|
||||
|
||||
interface AppAuthRequestOverrides extends AuthRequestMap {
|
||||
LoginParams: Omit<Enriched.LoginConnectOptions, 'reason'>;
|
||||
ConnectTarget: Omit<Enriched.TestConnectionOptions, 'reason'>;
|
||||
RegisterParams: Omit<Enriched.RegisterConnectOptions, 'reason'>;
|
||||
ActivateParams: Omit<Enriched.ActivateConnectOptions, 'reason'>;
|
||||
ForgotPasswordRequestParams: Omit<Enriched.PasswordResetRequestConnectOptions, 'reason'>;
|
||||
ForgotPasswordChallengeParams: Omit<Enriched.PasswordResetChallengeConnectOptions, 'reason'>;
|
||||
ForgotPasswordResetParams: Omit<Enriched.PasswordResetConnectOptions, 'reason'>;
|
||||
LoginParams: Omit<LoginConnectOptions, 'reason'>;
|
||||
ConnectTarget: Omit<TestConnectionOptions, 'reason'>;
|
||||
RegisterParams: Omit<RegisterConnectOptions, 'reason'>;
|
||||
ActivateParams: Omit<ActivateConnectOptions, 'reason'>;
|
||||
ForgotPasswordRequestParams: Omit<PasswordResetRequestConnectOptions, 'reason'>;
|
||||
ForgotPasswordChallengeParams: Omit<PasswordResetChallengeConnectOptions, 'reason'>;
|
||||
ForgotPasswordResetParams: Omit<PasswordResetConnectOptions, 'reason'>;
|
||||
}
|
||||
|
||||
export class AuthenticationRequestImpl implements IAuthenticationRequest<AppAuthRequestOverrides> {
|
||||
login(options: Omit<Enriched.LoginConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.LOGIN });
|
||||
login(options: Omit<LoginConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.LOGIN });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
testConnection(options: Omit<Enriched.TestConnectionOptions, 'reason'>): void {
|
||||
testConnection(options: Omit<TestConnectionOptions, 'reason'>): void {
|
||||
WebClient.instance.testConnect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
register(options: Omit<Enriched.RegisterConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.REGISTER });
|
||||
register(options: Omit<RegisterConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.REGISTER });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
activateAccount(options: Omit<Enriched.ActivateConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT });
|
||||
activateAccount(options: Omit<ActivateConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
resetPasswordRequest(options: Omit<Enriched.PasswordResetRequestConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST });
|
||||
resetPasswordRequest(options: Omit<PasswordResetRequestConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
resetPasswordChallenge(options: Omit<Enriched.PasswordResetChallengeConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE });
|
||||
resetPasswordChallenge(options: Omit<PasswordResetChallengeConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
||||
resetPassword(options: Omit<Enriched.PasswordResetConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET });
|
||||
resetPassword(options: Omit<PasswordResetConnectOptions, 'reason'>): void {
|
||||
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET });
|
||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Selectors } from './rooms.selectors';
|
||||
import { RoomsState } from './rooms.interfaces';
|
||||
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
||||
import { App } from '@app/types';
|
||||
|
||||
function rootState(rooms: RoomsState) {
|
||||
return { rooms };
|
||||
|
|
@ -111,13 +112,23 @@ describe('Selectors', () => {
|
|||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.users);
|
||||
});
|
||||
|
||||
it('getSortedRoomGames → returns sorted array view of games map', () => {
|
||||
const game1 = makeGame({ gameId: 1, description: 'beta' });
|
||||
const game2 = makeGame({ gameId: 2, description: 'alpha' });
|
||||
it('getSortedRoomGames → returns games sorted by the active sort config', () => {
|
||||
const game1 = makeGame({ gameId: 1, description: 'Beta' });
|
||||
const game2 = makeGame({ gameId: 2, description: 'Alpha' });
|
||||
const room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const state = makeRoomsState({
|
||||
rooms: { 1: room },
|
||||
sortGamesBy: { field: 'info.description' as App.GameSortField, order: App.SortDirection.ASC },
|
||||
});
|
||||
const result = Selectors.getSortedRoomGames(rootState(state), 1);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].info.description).toBe('Alpha');
|
||||
expect(result[1].info.description).toBe('Beta');
|
||||
});
|
||||
|
||||
it('getSortedRoomGames → returns EMPTY_GAMES for unknown roomId', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
expect(Selectors.getSortedRoomGames(rootState(state), 999)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('getSortedRoomUsers → returns sorted user array sorted by name', () => {
|
||||
|
|
@ -129,4 +140,40 @@ describe('Selectors', () => {
|
|||
expect(result[0].name).toBe('Alice');
|
||||
expect(result[1].name).toBe('Zane');
|
||||
});
|
||||
|
||||
it('getSortedRoomUsers → returns EMPTY_USERS for unknown roomId', () => {
|
||||
const state = makeRoomsState({ rooms: {} });
|
||||
expect(Selectors.getSortedRoomUsers(rootState(state), 999)).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── createSelector reference stability ──────────────────────────────
|
||||
|
||||
it('getSortedRoomGames → returns same array reference for identical state', () => {
|
||||
const game = makeGame({ gameId: 1 });
|
||||
const room = makeRoom({ roomId: 1, games: { 1: game } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getSortedRoomGames(root, 1);
|
||||
const b = Selectors.getSortedRoomGames(root, 1);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getSortedRoomUsers → returns same array reference for identical state', () => {
|
||||
const user = makeUser({ name: 'Alice' });
|
||||
const room = makeRoom({ roomId: 1, users: { Alice: user } });
|
||||
const state = makeRoomsState({ rooms: { 1: room } });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getSortedRoomUsers(root, 1);
|
||||
const b = Selectors.getSortedRoomUsers(root, 1);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getJoinedRooms → returns same array reference for identical state', () => {
|
||||
const room = makeRoom({ roomId: 1 });
|
||||
const state = makeRoomsState({ rooms: { 1: room }, joinedRoomIds: { 1: true } });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getJoinedRooms(root);
|
||||
const b = Selectors.getJoinedRooms(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
makeServerState,
|
||||
makeUser,
|
||||
} from './__mocks__/server-fixtures';
|
||||
import { App } from '@app/types';
|
||||
import { App, Data } from '@app/types';
|
||||
|
||||
function rootState(server: ServerState) {
|
||||
return { server };
|
||||
|
|
@ -149,4 +149,86 @@ describe('Selectors', () => {
|
|||
const state = makeServerState({ registrationError: 'bad input' });
|
||||
expect(Selectors.getRegistrationError(rootState(state))).toBe('bad input');
|
||||
});
|
||||
|
||||
// ── derived selectors (createSelector) ──────────────────────────────
|
||||
|
||||
it('getIsConnected → true when state is LOGGED_IN', () => {
|
||||
const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.LOGGED_IN, description: null } });
|
||||
expect(Selectors.getIsConnected(rootState(state))).toBe(true);
|
||||
});
|
||||
|
||||
it('getIsConnected → false when state is CONNECTED', () => {
|
||||
const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.CONNECTED, description: null } });
|
||||
expect(Selectors.getIsConnected(rootState(state))).toBe(false);
|
||||
});
|
||||
|
||||
it('getIsConnected → false when state is DISCONNECTED', () => {
|
||||
const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.DISCONNECTED, description: null } });
|
||||
expect(Selectors.getIsConnected(rootState(state))).toBe(false);
|
||||
});
|
||||
|
||||
it('getIsUserModerator → true when user has IsModerator flag', () => {
|
||||
const Flag = Data.ServerInfo_User_UserLevelFlag;
|
||||
const user = makeUser({ userLevel: Flag.IsUser | Flag.IsModerator });
|
||||
const state = makeServerState({ user });
|
||||
expect(Selectors.getIsUserModerator(rootState(state))).toBe(true);
|
||||
});
|
||||
|
||||
it('getIsUserModerator → false when user lacks IsModerator flag', () => {
|
||||
const Flag = Data.ServerInfo_User_UserLevelFlag;
|
||||
const user = makeUser({ userLevel: Flag.IsUser | Flag.IsRegistered });
|
||||
const state = makeServerState({ user });
|
||||
expect(Selectors.getIsUserModerator(rootState(state))).toBe(false);
|
||||
});
|
||||
|
||||
it('getIsUserModerator → false when user is null', () => {
|
||||
const state = makeServerState({ user: null });
|
||||
expect(Selectors.getIsUserModerator(rootState(state))).toBe(false);
|
||||
});
|
||||
|
||||
// ── createSelector reference stability ──────────────────────────────
|
||||
|
||||
it('getIsConnected → returns same value reference for identical state', () => {
|
||||
const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.LOGGED_IN, description: null } });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getIsConnected(root);
|
||||
const b = Selectors.getIsConnected(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getSortedUsers → returns same array reference for identical state', () => {
|
||||
const users = { Alice: makeUser({ name: 'Alice' }), Bob: makeUser({ name: 'Bob' }) };
|
||||
const state = makeServerState({ users });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getSortedUsers(root);
|
||||
const b = Selectors.getSortedUsers(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getSortedBuddyList → returns same array reference for identical state', () => {
|
||||
const buddyList = { Alice: makeUser({ name: 'Alice' }) };
|
||||
const state = makeServerState({ buddyList });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getSortedBuddyList(root);
|
||||
const b = Selectors.getSortedBuddyList(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getSortedIgnoreList → returns same array reference for identical state', () => {
|
||||
const ignoreList = { Troll: makeUser({ name: 'Troll' }) };
|
||||
const state = makeServerState({ ignoreList });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getSortedIgnoreList(root);
|
||||
const b = Selectors.getSortedIgnoreList(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('getReplaysList → returns same array reference for identical state', () => {
|
||||
const replays = { 1: makeReplayMatch({ gameId: 1 }) };
|
||||
const state = makeServerState({ replays });
|
||||
const root = rootState(state);
|
||||
const a = Selectors.getReplaysList(root);
|
||||
const b = Selectors.getReplaysList(root);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import type {
|
|||
ServerInfo_User,
|
||||
} from '@app/generated';
|
||||
|
||||
import { WebSocketConnectReason } from './server';
|
||||
|
||||
// ── Domain model types (composition: raw proto + client-side fields) ──────────
|
||||
//
|
||||
// `info` holds the proto snapshot verbatim. Normalized/client-only fields
|
||||
|
|
@ -133,84 +131,20 @@ export interface LogGroups {
|
|||
chat: ServerInfo_ChatMessage[];
|
||||
}
|
||||
|
||||
// ── Connect options ───────────────────────────────────────────────────────────
|
||||
// Each variant is the enriched input for one session flow: the network
|
||||
// transport fields (host/port) + the subset of proto Command_* fields the UI
|
||||
// actually produces (user-entered credentials, tokens, email, etc.) + a
|
||||
// `reason` discriminator so the websocket layer can route.
|
||||
//
|
||||
// Hand-written instead of `MessageInitShape<typeof Command_XSchema> & ...`
|
||||
// because MessageInitShape is a `Message<T> | { initShape }` union which
|
||||
// collapses to the Message branding when intersected, requiring `$typeName`
|
||||
// on literals. Keep these in sync with the corresponding proto command by
|
||||
// convention; fields here map 1:1 to Command_* members.
|
||||
// ── Connect options (re-exported from @app/websocket) ────────────────────────
|
||||
// Source of truth lives in src/websocket/connectOptions.ts. Re-exported here
|
||||
// so UI code can use the Enriched.* namespace without importing @app/websocket.
|
||||
|
||||
interface ConnectTransport {
|
||||
host: string;
|
||||
port: string;
|
||||
keepalive?: number;
|
||||
autojoinrooms?: boolean;
|
||||
clientid?: string;
|
||||
}
|
||||
|
||||
export interface LoginConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.LOGIN;
|
||||
userName: string;
|
||||
password?: string;
|
||||
hashedPassword?: string;
|
||||
}
|
||||
|
||||
export interface RegisterConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.REGISTER;
|
||||
userName: string;
|
||||
password: string;
|
||||
email: string;
|
||||
country: string;
|
||||
realName: string;
|
||||
}
|
||||
|
||||
export interface ActivateConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.ACTIVATE_ACCOUNT;
|
||||
userName: string;
|
||||
token: string;
|
||||
/** Plaintext password carried through so post-activation auto-login can hash it. */
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetRequestConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetChallengeConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE;
|
||||
userName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET;
|
||||
userName: string;
|
||||
token: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection has no proto command — it just opens and closes a socket to
|
||||
* verify reachability.
|
||||
*/
|
||||
export interface TestConnectionOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.TEST_CONNECTION;
|
||||
}
|
||||
|
||||
export type WebSocketConnectOptions =
|
||||
| LoginConnectOptions
|
||||
| RegisterConnectOptions
|
||||
| ActivateConnectOptions
|
||||
| PasswordResetRequestConnectOptions
|
||||
| PasswordResetChallengeConnectOptions
|
||||
| PasswordResetConnectOptions
|
||||
| TestConnectionOptions;
|
||||
export type {
|
||||
LoginConnectOptions,
|
||||
RegisterConnectOptions,
|
||||
ActivateConnectOptions,
|
||||
PasswordResetRequestConnectOptions,
|
||||
PasswordResetChallengeConnectOptions,
|
||||
PasswordResetConnectOptions,
|
||||
TestConnectionOptions,
|
||||
WebSocketConnectOptions,
|
||||
} from '@app/websocket';
|
||||
|
||||
/**
|
||||
* Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { StatusEnum } from '@app/websocket';
|
||||
export { StatusEnum, WebSocketConnectReason } from '@app/websocket';
|
||||
import type { StatusEnum } from '@app/websocket';
|
||||
|
||||
export interface ServerStatus {
|
||||
|
|
@ -6,16 +6,6 @@ export interface ServerStatus {
|
|||
description: string;
|
||||
}
|
||||
|
||||
export enum WebSocketConnectReason {
|
||||
LOGIN,
|
||||
REGISTER,
|
||||
ACTIVATE_ACCOUNT,
|
||||
PASSWORD_RESET_REQUEST,
|
||||
PASSWORD_RESET_CHALLENGE,
|
||||
PASSWORD_RESET,
|
||||
TEST_CONNECTION,
|
||||
}
|
||||
|
||||
export class Host {
|
||||
id?: number;
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ vi.mock('./services/ProtobufService', () => ({
|
|||
import { WebClient } from './WebClient';
|
||||
import { WebSocketService } from './services/WebSocketService';
|
||||
import { ProtobufService } from './services/ProtobufService';
|
||||
import { StatusEnum } from './StatusEnum';
|
||||
import { StatusEnum } from './interfaces/StatusEnum';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Mock } from 'vitest';
|
||||
import { SocketTransport, EventRegistries } from './services/ProtobufService';
|
||||
import { WebSocketServiceConfig } from './services/WebSocketService';
|
||||
import type { IWebClientResponse } from './interfaces';
|
||||
import type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
||||
import { installMockWebSocket, useWebClientCleanup } from './__mocks__/helpers';
|
||||
import type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||
import { installMockWebSocket } from './__mocks__/helpers';
|
||||
|
||||
function makeMockResponse(): IWebClientResponse {
|
||||
return {
|
||||
|
|
@ -61,7 +61,6 @@ function makeMockResponse(): IWebClientResponse {
|
|||
function makeMockConfig(response: IWebClientResponse): WebClientConfig {
|
||||
return {
|
||||
response,
|
||||
onServerIdentified: vi.fn(),
|
||||
sessionEvents: [],
|
||||
roomEvents: [],
|
||||
gameEvents: [],
|
||||
|
|
@ -69,8 +68,6 @@ function makeMockConfig(response: IWebClientResponse): WebClientConfig {
|
|||
};
|
||||
}
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
describe('WebClient', () => {
|
||||
let client: WebClient;
|
||||
let mockResponse: IWebClientResponse;
|
||||
|
|
@ -78,10 +75,6 @@ describe('WebClient', () => {
|
|||
let messageSubject: Subject<MessageEvent>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the singleton so each test starts fresh.
|
||||
// This direct reset is needed in addition to useWebClientCleanup() because
|
||||
// this file imports the real WebClient (not a mock), and with isolate:false
|
||||
// the helper's import may resolve to a different (mocked) module reference.
|
||||
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
|
||||
|
||||
(ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport, events: EventRegistries) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { StatusEnum } from './StatusEnum';
|
||||
import { StatusEnum } from './interfaces/StatusEnum';
|
||||
import { ProtobufService } from './services/ProtobufService';
|
||||
import { WebSocketService } from './services/WebSocketService';
|
||||
import { CLIENT_OPTIONS } from './config';
|
||||
import type { IWebClientResponse } from './interfaces';
|
||||
import type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
||||
import type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||
|
||||
export class WebClient {
|
||||
private static _instance: WebClient | null = null;
|
||||
|
|
|
|||
185
webclient/src/websocket/__mocks__/WebClient.ts
Normal file
185
webclient/src/websocket/__mocks__/WebClient.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* Shared WebClient mock — the single source of truth for all websocket
|
||||
* layer unit tests.
|
||||
*
|
||||
* Vitest resolves this file whenever a spec calls `vi.mock('...WebClient')`
|
||||
* without providing a factory. Each spec file gets its own module graph
|
||||
* (isolate: true), so there are no factory-conflict issues.
|
||||
*
|
||||
* Usage in spec files:
|
||||
*
|
||||
* vi.mock('../../WebClient');
|
||||
* import { WebClient } from '../../WebClient';
|
||||
* // WebClient.instance.response.game.cardMoved ← vi.fn()
|
||||
* // WebClient.instance.protobuf.sendGameCommand ← vi.fn()
|
||||
*
|
||||
* `useWebClientCleanup()` is NOT required — `instance` is a plain
|
||||
* property, not a getter that throws.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response.session (ISessionResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
const session = {
|
||||
initialized: vi.fn(),
|
||||
connectionAttempted: vi.fn(),
|
||||
clearStore: vi.fn(),
|
||||
loginSuccessful: vi.fn(),
|
||||
loginFailed: vi.fn(),
|
||||
connectionFailed: vi.fn(),
|
||||
testConnectionSuccessful: vi.fn(),
|
||||
testConnectionFailed: vi.fn(),
|
||||
updateBuddyList: vi.fn(),
|
||||
addToBuddyList: vi.fn(),
|
||||
removeFromBuddyList: vi.fn(),
|
||||
updateIgnoreList: vi.fn(),
|
||||
addToIgnoreList: vi.fn(),
|
||||
removeFromIgnoreList: vi.fn(),
|
||||
updateInfo: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
updateUser: vi.fn(),
|
||||
updateUsers: vi.fn(),
|
||||
userJoined: vi.fn(),
|
||||
userLeft: vi.fn(),
|
||||
serverMessage: vi.fn(),
|
||||
accountAwaitingActivation: vi.fn(),
|
||||
accountActivationSuccess: vi.fn(),
|
||||
accountActivationFailed: vi.fn(),
|
||||
registrationRequiresEmail: vi.fn(),
|
||||
registrationSuccess: vi.fn(),
|
||||
registrationFailed: vi.fn(),
|
||||
registrationEmailError: vi.fn(),
|
||||
registrationPasswordError: vi.fn(),
|
||||
registrationUserNameError: vi.fn(),
|
||||
resetPasswordChallenge: vi.fn(),
|
||||
resetPassword: vi.fn(),
|
||||
resetPasswordSuccess: vi.fn(),
|
||||
resetPasswordFailed: vi.fn(),
|
||||
accountPasswordChange: vi.fn(),
|
||||
accountEditChanged: vi.fn(),
|
||||
accountImageChanged: vi.fn(),
|
||||
getUserInfo: vi.fn(),
|
||||
getGamesOfUser: vi.fn(),
|
||||
gameJoined: vi.fn(),
|
||||
notifyUser: vi.fn(),
|
||||
playerPropertiesChanged: vi.fn(),
|
||||
serverShutdown: vi.fn(),
|
||||
userMessage: vi.fn(),
|
||||
addToList: vi.fn(),
|
||||
removeFromList: vi.fn(),
|
||||
deleteServerDeck: vi.fn(),
|
||||
updateServerDecks: vi.fn(),
|
||||
uploadServerDeck: vi.fn(),
|
||||
downloadServerDeck: vi.fn(),
|
||||
createServerDeckDir: vi.fn(),
|
||||
deleteServerDeckDir: vi.fn(),
|
||||
replayList: vi.fn(),
|
||||
replayAdded: vi.fn(),
|
||||
replayModifyMatch: vi.fn(),
|
||||
replayDeleteMatch: vi.fn(),
|
||||
replayDownloaded: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response.room (IRoomResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
const room = {
|
||||
clearStore: vi.fn(),
|
||||
joinRoom: vi.fn(),
|
||||
leaveRoom: vi.fn(),
|
||||
updateRooms: vi.fn(),
|
||||
updateGames: vi.fn(),
|
||||
addMessage: vi.fn(),
|
||||
userJoined: vi.fn(),
|
||||
userLeft: vi.fn(),
|
||||
removeMessages: vi.fn(),
|
||||
gameCreated: vi.fn(),
|
||||
joinedGame: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response.game (IGameResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
const game = {
|
||||
clearStore: vi.fn(),
|
||||
gameStateChanged: vi.fn(),
|
||||
playerJoined: vi.fn(),
|
||||
playerLeft: vi.fn(),
|
||||
playerPropertiesChanged: vi.fn(),
|
||||
gameClosed: vi.fn(),
|
||||
gameHostChanged: vi.fn(),
|
||||
kicked: vi.fn(),
|
||||
gameSay: vi.fn(),
|
||||
cardMoved: vi.fn(),
|
||||
cardFlipped: vi.fn(),
|
||||
cardDestroyed: vi.fn(),
|
||||
cardAttached: vi.fn(),
|
||||
tokenCreated: vi.fn(),
|
||||
cardAttrChanged: vi.fn(),
|
||||
cardCounterChanged: vi.fn(),
|
||||
arrowCreated: vi.fn(),
|
||||
arrowDeleted: vi.fn(),
|
||||
counterCreated: vi.fn(),
|
||||
counterSet: vi.fn(),
|
||||
counterDeleted: vi.fn(),
|
||||
cardsDrawn: vi.fn(),
|
||||
cardsRevealed: vi.fn(),
|
||||
zoneShuffled: vi.fn(),
|
||||
dieRolled: vi.fn(),
|
||||
activePlayerSet: vi.fn(),
|
||||
activePhaseSet: vi.fn(),
|
||||
turnReversed: vi.fn(),
|
||||
zoneDumped: vi.fn(),
|
||||
zonePropertiesChanged: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response.admin (IAdminResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
const admin = {
|
||||
adjustMod: vi.fn(),
|
||||
reloadConfig: vi.fn(),
|
||||
shutdownServer: vi.fn(),
|
||||
updateServerMessage: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// response.moderator (IModeratorResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
const moderator = {
|
||||
banFromServer: vi.fn(),
|
||||
banHistory: vi.fn(),
|
||||
viewLogs: vi.fn(),
|
||||
warnHistory: vi.fn(),
|
||||
warnListOptions: vi.fn(),
|
||||
warnUser: vi.fn(),
|
||||
grantReplayAccess: vi.fn(),
|
||||
forceActivateUser: vi.fn(),
|
||||
getAdminNotes: vi.fn(),
|
||||
updateAdminNotes: vi.fn(),
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported mock — replaces the real WebClient module for all consumers.
|
||||
// ---------------------------------------------------------------------------
|
||||
export const WebClient = {
|
||||
_instance: null as any,
|
||||
instance: {
|
||||
connect: vi.fn(),
|
||||
testConnect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
status: 0 as number,
|
||||
config: {},
|
||||
protobuf: {
|
||||
sendSessionCommand: vi.fn(),
|
||||
sendRoomCommand: vi.fn(),
|
||||
sendGameCommand: vi.fn(),
|
||||
sendAdminCommand: vi.fn(),
|
||||
sendModeratorCommand: vi.fn(),
|
||||
resetCommands: vi.fn(),
|
||||
},
|
||||
response: { session, room, game, admin, moderator },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ export function makeWebClientMock() {
|
|||
testConnect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
config: { onServerIdentified: vi.fn() },
|
||||
config: {},
|
||||
status: 0,
|
||||
protobuf: {
|
||||
sendSessionCommand: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -1,31 +1,20 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
protobuf: { sendAdminCommand: vi.fn() },
|
||||
response: {
|
||||
admin: {
|
||||
adjustMod: vi.fn(),
|
||||
reloadConfig: vi.fn(),
|
||||
shutdownServer: vi.fn(),
|
||||
updateServerMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import { adjustMod } from './adjustMod';
|
||||
import { reloadConfig } from './reloadConfig';
|
||||
import { shutdownServer } from './shutdownServer';
|
||||
import { updateServerMessage } from './updateServerMessage';
|
||||
import {
|
||||
Command_AdjustMod_ext,
|
||||
Command_ReloadConfig_ext,
|
||||
Command_ShutdownServer_ext,
|
||||
Command_UpdateServerMessage_ext,
|
||||
} from '@app/generated';
|
||||
|
||||
import { Mock } from 'vitest';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||
WebClient.instance.protobuf.sendAdminCommand as Mock,
|
||||
2
|
||||
|
|
@ -36,9 +25,13 @@ const { invokeOnSuccess } = makeCallbackHelpers(
|
|||
// ----------------------------------------------------------------
|
||||
describe('adjustMod', () => {
|
||||
|
||||
it('calls sendAdminCommand with Command_AdjustMod', () => {
|
||||
it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => {
|
||||
adjustMod('alice', true, false);
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object));
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
||||
Command_AdjustMod_ext,
|
||||
expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls response.admin.adjustMod', () => {
|
||||
|
|
@ -53,9 +46,13 @@ describe('adjustMod', () => {
|
|||
// ----------------------------------------------------------------
|
||||
describe('reloadConfig', () => {
|
||||
|
||||
it('calls sendAdminCommand with Command_ReloadConfig', () => {
|
||||
it('calls sendAdminCommand with Command_ReloadConfig extension', () => {
|
||||
reloadConfig();
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object));
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
||||
Command_ReloadConfig_ext,
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls response.admin.reloadConfig', () => {
|
||||
|
|
@ -70,9 +67,13 @@ describe('reloadConfig', () => {
|
|||
// ----------------------------------------------------------------
|
||||
describe('shutdownServer', () => {
|
||||
|
||||
it('calls sendAdminCommand with Command_ShutdownServer', () => {
|
||||
it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => {
|
||||
shutdownServer('maintenance', 10);
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object));
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
||||
Command_ShutdownServer_ext,
|
||||
expect.objectContaining({ reason: 'maintenance', minutes: 10 }),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls response.admin.shutdownServer', () => {
|
||||
|
|
@ -87,9 +88,13 @@ describe('shutdownServer', () => {
|
|||
// ----------------------------------------------------------------
|
||||
describe('updateServerMessage', () => {
|
||||
|
||||
it('calls sendAdminCommand with Command_UpdateServerMessage', () => {
|
||||
it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => {
|
||||
updateServerMessage();
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object));
|
||||
expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(
|
||||
Command_UpdateServerMessage_ext,
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('onSuccess calls response.admin.updateServerMessage', () => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
protobuf: { sendGameCommand: vi.fn() },
|
||||
response: { game: {} },
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
import { WebClient } from '../../WebClient';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
|
||||
useWebClientCleanup();
|
||||
import { create, setExtension } from '@bufbuild/protobuf';
|
||||
import {
|
||||
Command_AttachCard_ext,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,6 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
protobuf: { sendModeratorCommand: vi.fn() },
|
||||
response: {
|
||||
moderator: {
|
||||
banFromServer: vi.fn(),
|
||||
forceActivateUser: vi.fn(),
|
||||
getAdminNotes: vi.fn(),
|
||||
banHistory: vi.fn(),
|
||||
warnHistory: vi.fn(),
|
||||
warnListOptions: vi.fn(),
|
||||
grantReplayAccess: vi.fn(),
|
||||
updateAdminNotes: vi.fn(),
|
||||
viewLogs: vi.fn(),
|
||||
warnUser: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import {
|
||||
Command_BanFromServer_ext,
|
||||
|
|
@ -55,8 +34,6 @@ import { warnUser } from './warnUser';
|
|||
import { create } from '@bufbuild/protobuf';
|
||||
import { Mock } from 'vitest';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||
WebClient.instance.protobuf.sendModeratorCommand as Mock,
|
||||
2
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
protobuf: { sendRoomCommand: vi.fn() },
|
||||
response: {
|
||||
room: {
|
||||
gameCreated: vi.fn(),
|
||||
joinedGame: vi.fn(),
|
||||
leaveRoom: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import {
|
||||
Command_CreateGame_ext,
|
||||
|
|
@ -32,8 +18,6 @@ import { roomSay } from './roomSay';
|
|||
import { create } from '@bufbuild/protobuf';
|
||||
import { Mock } from 'vitest';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||
WebClient.instance.protobuf.sendRoomCommand as Mock,
|
||||
// sendRoomCommand(roomId, ext, value, options) — options at index 3
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import {
|
|||
type ActivateParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { disconnect, login, updateStatus } from './';
|
||||
|
||||
export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
|
||||
export function connect(target: ConnectTarget): void {
|
||||
WebClient.instance.connect(target);
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import {
|
|||
type ForgotPasswordChallengeParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { disconnect, updateStatus } from './';
|
||||
|
||||
export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import {
|
|||
type ForgotPasswordRequestParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { disconnect, updateStatus } from './';
|
||||
|
||||
export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import {
|
|||
type ForgotPasswordResetParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { hashPassword } from '../../utils';
|
||||
import { disconnect, updateStatus } from '.';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import {
|
|||
type LoginParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { hashPassword } from '../../utils';
|
||||
import {
|
||||
disconnect,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import {
|
|||
type RegisterParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { hashPassword } from '../../utils';
|
||||
import { login, disconnect, updateStatus } from './';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import {
|
|||
type RequestPasswordSaltParams,
|
||||
} from '@app/generated';
|
||||
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { CLIENT_CONFIG } from '../../config';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import type { ConnectTarget } from '../../WebClientConfig';
|
||||
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||
import { updateStatus } from './';
|
||||
|
||||
export function requestPasswordSalt(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
// Tests for complex session commands that call WebClient directly
|
||||
// or have multiple branching callbacks.
|
||||
|
||||
vi.mock('../../WebClient', async () => {
|
||||
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||
return { WebClient: { instance: makeWebClientMock() } };
|
||||
});
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
vi.mock('../../utils', async () => {
|
||||
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||
|
|
@ -19,11 +16,10 @@ vi.mock('./', async () => {
|
|||
|
||||
import { Mock } from 'vitest';
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import * as SessionIndexMocks from './';
|
||||
import { App, Enriched } from '@app/types';
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import {
|
||||
Command_Activate_ext,
|
||||
Command_ForgotPasswordChallenge_ext,
|
||||
|
|
@ -54,8 +50,6 @@ import { forgotPasswordRequest } from './forgotPasswordRequest';
|
|||
import { forgotPasswordReset } from './forgotPasswordReset';
|
||||
import { requestPasswordSalt } from './requestPasswordSalt';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
|
||||
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
||||
2
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
// Shared mock setup for session command tests
|
||||
|
||||
vi.mock('../../WebClient', async () => {
|
||||
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||
return { WebClient: { instance: makeWebClientMock() } };
|
||||
});
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
vi.mock('../../utils', async () => {
|
||||
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||
|
|
@ -19,7 +16,6 @@ vi.mock('./', async () => {
|
|||
|
||||
import { Mock } from 'vitest';
|
||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
|
||||
|
||||
|
|
@ -85,8 +81,6 @@ import {
|
|||
Response_ReplayList_ext,
|
||||
} from '@app/generated';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers(
|
||||
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
||||
2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function updateStatus(status: StatusEnum, description: string): void {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const PROTOCOL_VERSION = 14;
|
||||
|
||||
export const CLIENT_CONFIG = {
|
||||
clientid: 'webatrice',
|
||||
clientver: 'webclient-1.0 (2019-10-31)',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_AttachCard } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_ChangeZoneProperties } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_CreateArrow } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_CreateCounter } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_CreateToken } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function createToken(data: Event_CreateToken, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_DelCounter } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_DeleteArrow } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_DestroyCard } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_DrawCards } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_DumpZone } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_FlipCard } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function flipCard(data: Event_FlipCard, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function gameClosed(_data: {}, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,4 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
response: {
|
||||
game: {
|
||||
gameStateChanged: vi.fn(),
|
||||
playerJoined: vi.fn(),
|
||||
playerLeft: vi.fn(),
|
||||
playerPropertiesChanged: vi.fn(),
|
||||
gameClosed: vi.fn(),
|
||||
gameHostChanged: vi.fn(),
|
||||
kicked: vi.fn(),
|
||||
gameSay: vi.fn(),
|
||||
cardMoved: vi.fn(),
|
||||
cardFlipped: vi.fn(),
|
||||
cardDestroyed: vi.fn(),
|
||||
cardAttached: vi.fn(),
|
||||
tokenCreated: vi.fn(),
|
||||
cardAttrChanged: vi.fn(),
|
||||
cardCounterChanged: vi.fn(),
|
||||
arrowCreated: vi.fn(),
|
||||
arrowDeleted: vi.fn(),
|
||||
counterCreated: vi.fn(),
|
||||
counterSet: vi.fn(),
|
||||
counterDeleted: vi.fn(),
|
||||
cardsDrawn: vi.fn(),
|
||||
cardsRevealed: vi.fn(),
|
||||
zoneShuffled: vi.fn(),
|
||||
dieRolled: vi.fn(),
|
||||
activePlayerSet: vi.fn(),
|
||||
activePhaseSet: vi.fn(),
|
||||
turnReversed: vi.fn(),
|
||||
zoneDumped: vi.fn(),
|
||||
zonePropertiesChanged: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
vi.mock('../../WebClient');
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import {
|
||||
Event_AttachCardSchema,
|
||||
|
|
@ -67,7 +27,6 @@ import {
|
|||
ServerInfo_PlayerPropertiesSchema,
|
||||
} from '@app/generated';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
import { attachCard } from './attachCard';
|
||||
import { changeZoneProperties } from './changeZoneProperties';
|
||||
import { createArrow } from './createArrow';
|
||||
|
|
@ -98,8 +57,6 @@ import { setCardCounter } from './setCardCounter';
|
|||
import { setCounter } from './setCounter';
|
||||
import { shuffle } from './shuffle';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 };
|
||||
|
||||
describe('joinGame event', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_GameSay } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function gameSay(data: Event_GameSay, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_GameStateChanged } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import {
|
|||
Event_ReverseTurn_ext,
|
||||
} from '@app/generated';
|
||||
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
|
||||
import { attachCard } from './attachCard';
|
||||
import { changeZoneProperties } from './changeZoneProperties';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_Join } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function kicked(_data: {}, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function leaveGame(data: { reason: number }, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_MoveCard } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_PlayerPropertiesChanged } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_RevealCards } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_ReverseTurn } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_RollDie } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function rollDie(data: Event_RollDie, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_SetActivePhase } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_SetActivePlayer } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_SetCardAttr } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_SetCardCounter } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_SetCounter } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Event_Shuffle } from '@app/generated';
|
||||
import type { GameEventMeta } from '../../types';
|
||||
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||
import { WebClient } from '../../WebClient';
|
||||
|
||||
export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,5 @@
|
|||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
response: {
|
||||
room: {
|
||||
userJoined: vi.fn(),
|
||||
userLeft: vi.fn(),
|
||||
updateGames: vi.fn(),
|
||||
removeMessages: vi.fn(),
|
||||
addMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import {
|
||||
Event_JoinRoomSchema,
|
||||
|
|
@ -31,8 +16,6 @@ import { listGames } from './listGames';
|
|||
import { removeMessages } from './removeMessages';
|
||||
import { roomSay } from './roomSay';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId });
|
||||
|
||||
describe('joinRoom room event', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated';
|
||||
import { StatusEnum } from '../../StatusEnum';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { updateStatus } from '../../commands/session';
|
||||
|
||||
export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,93 @@
|
|||
import type { Event_ServerIdentification } from '@app/generated';
|
||||
import { WebClient } from '../../WebClient';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { PROTOCOL_VERSION } from '../../config';
|
||||
import { consumePendingOptions } from '../../utils/connectionState';
|
||||
import { WebSocketConnectReason } from '../../interfaces/ConnectOptions';
|
||||
import { generateSalt, passwordSaltSupported } from '../../utils';
|
||||
import * as SessionCommands from '../../commands/session';
|
||||
|
||||
export function serverIdentification(info: Event_ServerIdentification): void {
|
||||
WebClient.instance.config.onServerIdentified(info);
|
||||
const { serverName, serverVersion, protocolVersion, serverOptions } = info;
|
||||
const response = WebClient.instance.response;
|
||||
|
||||
if (protocolVersion !== PROTOCOL_VERSION) {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
|
||||
SessionCommands.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const getPasswordSalt = passwordSaltSupported(serverOptions);
|
||||
const options = consumePendingOptions();
|
||||
|
||||
if (!options) {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Missing connection options');
|
||||
SessionCommands.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (options.reason) {
|
||||
case WebSocketConnectReason.LOGIN: {
|
||||
const { password, ...rest } = options;
|
||||
SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.login(rest, password, salt),
|
||||
() => {
|
||||
response.session.loginFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.login(rest, password);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WebSocketConnectReason.REGISTER: {
|
||||
const { password, ...rest } = options;
|
||||
const passwordSalt = getPasswordSalt ? generateSalt() : null;
|
||||
SessionCommands.register(rest, password, passwordSalt);
|
||||
break;
|
||||
}
|
||||
case WebSocketConnectReason.ACTIVATE_ACCOUNT: {
|
||||
const { password, ...rest } = options;
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.activate(rest, password, salt),
|
||||
() => {
|
||||
response.session.accountActivationFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.activate(rest, password);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WebSocketConnectReason.PASSWORD_RESET_REQUEST:
|
||||
SessionCommands.forgotPasswordRequest(options);
|
||||
break;
|
||||
case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE:
|
||||
SessionCommands.forgotPasswordChallenge(options);
|
||||
break;
|
||||
case WebSocketConnectReason.PASSWORD_RESET: {
|
||||
const { newPassword, ...rest } = options;
|
||||
if (getPasswordSalt) {
|
||||
SessionCommands.requestPasswordSalt(rest,
|
||||
(salt) => SessionCommands.forgotPasswordReset(rest, newPassword, salt),
|
||||
() => {
|
||||
response.session.resetPasswordFailed(); SessionCommands.disconnect();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
SessionCommands.forgotPasswordReset(rest, newPassword);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${(options as { reason: number }).reason}`);
|
||||
SessionCommands.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.session.updateInfo(serverName, serverVersion);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,36 @@
|
|||
// Tests for simple session events that delegate 1:1 to SessionPersistence
|
||||
// or RoomPersistence with minimal logic.
|
||||
|
||||
vi.mock('../../WebClient', () => ({
|
||||
WebClient: {
|
||||
instance: {
|
||||
config: { onServerIdentified: vi.fn() },
|
||||
response: {
|
||||
session: {
|
||||
gameJoined: vi.fn(),
|
||||
notifyUser: vi.fn(),
|
||||
replayAdded: vi.fn(),
|
||||
serverMessage: vi.fn(),
|
||||
serverShutdown: vi.fn(),
|
||||
updateUsers: vi.fn(),
|
||||
updateInfo: vi.fn(),
|
||||
userJoined: vi.fn(),
|
||||
userLeft: vi.fn(),
|
||||
userMessage: vi.fn(),
|
||||
addToBuddyList: vi.fn(),
|
||||
addToIgnoreList: vi.fn(),
|
||||
removeFromBuddyList: vi.fn(),
|
||||
removeFromIgnoreList: vi.fn(),
|
||||
playerPropertiesChanged: vi.fn(),
|
||||
},
|
||||
room: {
|
||||
updateRooms: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
vi.mock('../../WebClient');
|
||||
|
||||
vi.mock('../../config', () => ({
|
||||
CLIENT_OPTIONS: { autojoinrooms: false },
|
||||
PROTOCOL_VERSION: 14,
|
||||
}));
|
||||
|
||||
vi.mock('../../commands/session', () => ({
|
||||
joinRoom: vi.fn(),
|
||||
updateStatus: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
login: vi.fn(),
|
||||
register: vi.fn(),
|
||||
activate: vi.fn(),
|
||||
forgotPasswordRequest: vi.fn(),
|
||||
forgotPasswordChallenge: vi.fn(),
|
||||
forgotPasswordReset: vi.fn(),
|
||||
requestPasswordSalt: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils', () => ({
|
||||
sanitizeHtml: vi.fn((msg: string) => msg),
|
||||
generateSalt: vi.fn().mockReturnValue('randSalt'),
|
||||
passwordSaltSupported: vi.fn().mockReturnValue(0),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/connectionState', () => ({
|
||||
consumePendingOptions: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
||||
import {
|
||||
Event_AddToListSchema,
|
||||
Event_ConnectionClosedSchema,
|
||||
|
|
@ -71,6 +56,11 @@ import { create } from '@bufbuild/protobuf';
|
|||
import { WebClient } from '../../WebClient';
|
||||
import * as Config from '../../config';
|
||||
import * as SessionCmds from '../../commands/session';
|
||||
import { consumePendingOptions } from '../../utils/connectionState';
|
||||
import { passwordSaltSupported } from '../../utils';
|
||||
import { WebSocketConnectReason } from '../../interfaces/ConnectOptions';
|
||||
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||
import { Mock } from 'vitest';
|
||||
import { gameJoined } from './gameJoined';
|
||||
import { notifyUser } from './notifyUser';
|
||||
import { replayAdded } from './replayAdded';
|
||||
|
|
@ -86,8 +76,6 @@ import { listRooms } from './listRooms';
|
|||
import { connectionClosed } from './connectionClosed';
|
||||
import { serverIdentification } from './serverIdentification';
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] };
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
|
@ -387,11 +375,149 @@ describe('connectionClosed', () => {
|
|||
// serverIdentification
|
||||
// ----------------------------------------------------------------
|
||||
describe('serverIdentification', () => {
|
||||
const makeInfo = (overrides: Record<string, unknown> = {}) =>
|
||||
create(Event_ServerIdentificationSchema, {
|
||||
serverName: 'TestServer',
|
||||
serverVersion: '1.0',
|
||||
protocolVersion: 14,
|
||||
serverOptions: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('calls config.onServerIdentified with the event info', () => {
|
||||
const info = create(Event_ServerIdentificationSchema,
|
||||
{ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 });
|
||||
serverIdentification(info);
|
||||
expect(WebClient.instance.config.onServerIdentified).toHaveBeenCalledWith(info);
|
||||
const makeLoginOptions = () => ({
|
||||
host: 'h', port: '1', userName: 'alice', password: 'pw',
|
||||
reason: WebSocketConnectReason.LOGIN as const,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(consumePendingOptions as Mock).mockReturnValue(null);
|
||||
(passwordSaltSupported as Mock).mockReturnValue(0);
|
||||
});
|
||||
|
||||
it('disconnects on protocol version mismatch', () => {
|
||||
serverIdentification(makeInfo({ protocolVersion: 99 }));
|
||||
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.stringContaining('Protocol version mismatch'));
|
||||
expect(SessionCmds.disconnect).toHaveBeenCalled();
|
||||
expect(WebClient.instance.response.session.updateInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disconnects when pending options are missing', () => {
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Missing connection options');
|
||||
expect(SessionCmds.disconnect).toHaveBeenCalled();
|
||||
expect(WebClient.instance.response.session.updateInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('LOGIN → calls login without salt when server does not support it', () => {
|
||||
const opts = makeLoginOptions();
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGING_IN, 'Logging In...');
|
||||
expect(SessionCmds.login).toHaveBeenCalledWith(expect.objectContaining({ userName: 'alice' }), 'pw');
|
||||
expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('TestServer', '1.0');
|
||||
});
|
||||
|
||||
it('LOGIN → calls requestPasswordSalt when server supports it', () => {
|
||||
const opts = makeLoginOptions();
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
(passwordSaltSupported as Mock).mockReturnValue(1);
|
||||
serverIdentification(makeInfo({ serverOptions: 1 }));
|
||||
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice' }),
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('REGISTER → calls register', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', password: 'pw',
|
||||
email: 'a@b.com', country: 'US', realName: 'Al',
|
||||
reason: WebSocketConnectReason.REGISTER as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice' }), 'pw', null,
|
||||
);
|
||||
});
|
||||
|
||||
it('REGISTER with password salt → passes generated salt', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', password: 'pw',
|
||||
email: 'a@b.com', country: 'US', realName: 'Al',
|
||||
reason: WebSocketConnectReason.REGISTER as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
(passwordSaltSupported as Mock).mockReturnValue(1);
|
||||
serverIdentification(makeInfo({ serverOptions: 1 }));
|
||||
expect(SessionCmds.register).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice' }), 'pw', 'randSalt',
|
||||
);
|
||||
});
|
||||
|
||||
it('ACTIVATE_ACCOUNT → calls activate', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', token: 'tok', password: 'pw',
|
||||
reason: WebSocketConnectReason.ACTIVATE_ACCOUNT as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.activate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice', token: 'tok' }), 'pw',
|
||||
);
|
||||
});
|
||||
|
||||
it('PASSWORD_RESET_REQUEST → calls forgotPasswordRequest', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice',
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.forgotPasswordRequest).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
|
||||
it('PASSWORD_RESET_CHALLENGE → calls forgotPasswordChallenge', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', email: 'a@b.com',
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalledWith(opts);
|
||||
});
|
||||
|
||||
it('PASSWORD_RESET → calls forgotPasswordReset without salt', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', token: 'tok', newPassword: 'newpw',
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
serverIdentification(makeInfo());
|
||||
expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice', token: 'tok' }), 'newpw',
|
||||
);
|
||||
});
|
||||
|
||||
it('PASSWORD_RESET with salt → calls requestPasswordSalt', () => {
|
||||
const opts = {
|
||||
host: 'h', port: '1', userName: 'alice', token: 'tok', newPassword: 'newpw',
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET as const,
|
||||
};
|
||||
(consumePendingOptions as Mock).mockReturnValue(opts);
|
||||
(passwordSaltSupported as Mock).mockReturnValue(1);
|
||||
serverIdentification(makeInfo({ serverOptions: 1 }));
|
||||
expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userName: 'alice' }),
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('always calls updateInfo after successful routing', () => {
|
||||
(consumePendingOptions as Mock).mockReturnValue(makeLoginOptions());
|
||||
serverIdentification(makeInfo());
|
||||
expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('TestServer', '1.0');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,17 +2,31 @@ export * from './commands';
|
|||
export * from './interfaces';
|
||||
|
||||
export { WebClient } from './WebClient';
|
||||
export { StatusEnum } from './StatusEnum';
|
||||
export type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
||||
export { StatusEnum } from './interfaces/StatusEnum';
|
||||
export type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||
export type {
|
||||
KeyOf,
|
||||
GameEventMeta,
|
||||
WebSocketSessionResponseOverrides,
|
||||
WebSocketRoomResponseOverrides,
|
||||
} from './types';
|
||||
} from './interfaces/WebSocketConfig';
|
||||
|
||||
export { SessionEvents } from './events/session';
|
||||
export { RoomEvents } from './events/room';
|
||||
export { GameEvents } from './events/game';
|
||||
|
||||
export { generateSalt, passwordSaltSupported, hashPassword } from './utils';
|
||||
|
||||
export { WebSocketConnectReason } from './interfaces/ConnectOptions';
|
||||
export type {
|
||||
LoginConnectOptions,
|
||||
RegisterConnectOptions,
|
||||
ActivateConnectOptions,
|
||||
PasswordResetRequestConnectOptions,
|
||||
PasswordResetChallengeConnectOptions,
|
||||
PasswordResetConnectOptions,
|
||||
TestConnectionOptions,
|
||||
WebSocketConnectOptions,
|
||||
} from './interfaces/ConnectOptions';
|
||||
|
||||
export { setPendingOptions, consumePendingOptions } from './utils/connectionState';
|
||||
|
|
|
|||
82
webclient/src/websocket/interfaces/ConnectOptions.ts
Normal file
82
webclient/src/websocket/interfaces/ConnectOptions.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import type { ConnectTarget } from './WebClientConfig';
|
||||
|
||||
export enum WebSocketConnectReason {
|
||||
LOGIN,
|
||||
REGISTER,
|
||||
ACTIVATE_ACCOUNT,
|
||||
PASSWORD_RESET_REQUEST,
|
||||
PASSWORD_RESET_CHALLENGE,
|
||||
PASSWORD_RESET,
|
||||
TEST_CONNECTION,
|
||||
}
|
||||
|
||||
// ── Connect options ───────────────────────────────────────────────────────────
|
||||
// Each variant is the enriched input for one session flow: the network
|
||||
// transport fields (host/port) + the subset of proto Command_* fields the UI
|
||||
// actually produces (user-entered credentials, tokens, email, etc.) + a
|
||||
// `reason` discriminator so the websocket layer can route.
|
||||
|
||||
interface ConnectTransport extends ConnectTarget {
|
||||
keepalive?: number;
|
||||
autojoinrooms?: boolean;
|
||||
clientid?: string;
|
||||
}
|
||||
|
||||
export interface LoginConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.LOGIN;
|
||||
userName: string;
|
||||
password?: string;
|
||||
hashedPassword?: string;
|
||||
}
|
||||
|
||||
export interface RegisterConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.REGISTER;
|
||||
userName: string;
|
||||
password: string;
|
||||
email: string;
|
||||
country: string;
|
||||
realName: string;
|
||||
}
|
||||
|
||||
export interface ActivateConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.ACTIVATE_ACCOUNT;
|
||||
userName: string;
|
||||
token: string;
|
||||
/** Plaintext password carried through so post-activation auto-login can hash it. */
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetRequestConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetChallengeConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE;
|
||||
userName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface PasswordResetConnectOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.PASSWORD_RESET;
|
||||
userName: string;
|
||||
token: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection has no proto command -- it just opens and closes a socket to
|
||||
* verify reachability.
|
||||
*/
|
||||
export interface TestConnectionOptions extends ConnectTransport {
|
||||
reason: WebSocketConnectReason.TEST_CONNECTION;
|
||||
}
|
||||
|
||||
export type WebSocketConnectOptions =
|
||||
| LoginConnectOptions
|
||||
| RegisterConnectOptions
|
||||
| ActivateConnectOptions
|
||||
| PasswordResetRequestConnectOptions
|
||||
| PasswordResetChallengeConnectOptions
|
||||
| PasswordResetConnectOptions
|
||||
| TestConnectionOptions;
|
||||
|
|
@ -3,11 +3,10 @@ import type {
|
|||
SessionEvent,
|
||||
RoomEvent,
|
||||
GameEvent,
|
||||
Event_ServerIdentification,
|
||||
} from '@app/generated';
|
||||
|
||||
import type { GameEventMeta } from './types';
|
||||
import type { IWebClientResponse } from './interfaces';
|
||||
import type { GameEventMeta } from './WebSocketConfig';
|
||||
import type { IWebClientResponse } from '.';
|
||||
|
||||
export interface ConnectTarget {
|
||||
host: string;
|
||||
|
|
@ -17,8 +16,6 @@ export interface ConnectTarget {
|
|||
export interface WebClientConfig {
|
||||
response: IWebClientResponse;
|
||||
|
||||
onServerIdentified(info: Event_ServerIdentification): void;
|
||||
|
||||
sessionEvents: RegistryEntry<unknown, SessionEvent>[];
|
||||
roomEvents: RegistryEntry<unknown, RoomEvent, RoomEvent>[];
|
||||
gameEvents: RegistryEntry<unknown, GameEvent, GameEventMeta>[];
|
||||
|
|
@ -36,8 +36,8 @@ import type {
|
|||
GameCommand,
|
||||
} from '@app/generated';
|
||||
|
||||
import type { ConnectTarget } from '../WebClientConfig';
|
||||
import type { KeyOf } from '../types';
|
||||
import type { ConnectTarget } from './WebClientConfig';
|
||||
import type { KeyOf } from './WebSocketConfig';
|
||||
|
||||
// ── Auth request type map ────────────────────────────────────────────────────
|
||||
// Keys = generated *Params type names composed with ConnectTarget.
|
||||
|
|
|
|||
|
|
@ -44,12 +44,12 @@ import type {
|
|||
ServerInfo_ReplayMatch,
|
||||
} from '@app/generated';
|
||||
|
||||
import type { StatusEnum } from '../StatusEnum';
|
||||
import type { StatusEnum } from './StatusEnum';
|
||||
import type {
|
||||
KeyOf,
|
||||
WebSocketSessionResponseOverrides,
|
||||
WebSocketRoomResponseOverrides,
|
||||
} from '../types';
|
||||
} from './WebSocketConfig';
|
||||
|
||||
export interface ISessionResponse<T extends ResponseMap = WebSocketSessionResponseOverrides> {
|
||||
initialized(): void;
|
||||
|
|
|
|||
|
|
@ -7,13 +7,8 @@ vi.mock('@bufbuild/protobuf', async (importOriginal) => ({
|
|||
setExtension: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../WebClient', () => ({
|
||||
__esModule: true,
|
||||
default: {},
|
||||
WebClient: { _instance: null },
|
||||
}));
|
||||
vi.mock('../WebClient');
|
||||
|
||||
import { useWebClientCleanup } from '../__mocks__/helpers';
|
||||
import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf';
|
||||
import type { GenExtension } from '@bufbuild/protobuf/codegenv2';
|
||||
|
||||
|
|
@ -47,8 +42,6 @@ type ProtobufInternal = ProtobufService & {
|
|||
processServerResponse(response: unknown): void;
|
||||
};
|
||||
|
||||
useWebClientCleanup();
|
||||
|
||||
let mockSocket: { isOpen: ReturnType<typeof vi.fn>; send: ReturnType<typeof vi.fn> };
|
||||
let mockEvents: EventRegistries;
|
||||
|
||||
|
|
@ -444,3 +437,72 @@ describe('ProtobufService', () => {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
// ── Real protobuf round-trip test ─────────────────────────────────────────────
|
||||
// This describe block does NOT mock @bufbuild/protobuf so it exercises real
|
||||
// binary serialization. It proves that the schemas ProtobufService uses
|
||||
// survive a toBinary → fromBinary cycle without data loss.
|
||||
describe('ProtobufService protobuf round-trip (real @bufbuild/protobuf)', () => {
|
||||
it('CommandContainer round-trips cmdId through toBinary → fromBinary', async () => {
|
||||
const { create, toBinary, fromBinary: realFromBinary } =
|
||||
await vi.importActual<typeof import('@bufbuild/protobuf')>('@bufbuild/protobuf');
|
||||
const { CommandContainerSchema } =
|
||||
await vi.importActual<typeof import('@app/generated')>('@app/generated');
|
||||
|
||||
const original = create(CommandContainerSchema, { cmdId: BigInt(42) });
|
||||
const bytes = toBinary(CommandContainerSchema, original);
|
||||
const decoded = realFromBinary(CommandContainerSchema, bytes);
|
||||
|
||||
expect(decoded.cmdId).toBe(BigInt(42));
|
||||
});
|
||||
|
||||
it('ServerMessage RESPONSE round-trips with cmdId and responseCode', async () => {
|
||||
const { create, toBinary, fromBinary: realFromBinary } =
|
||||
await vi.importActual<typeof import('@bufbuild/protobuf')>('@bufbuild/protobuf');
|
||||
const { ServerMessageSchema, ServerMessage_MessageType, ResponseSchema, Response_ResponseCode } =
|
||||
await vi.importActual<typeof import('@app/generated')>('@app/generated');
|
||||
|
||||
const response = create(ResponseSchema, {
|
||||
cmdId: BigInt(7),
|
||||
responseCode: Response_ResponseCode.RespOk,
|
||||
});
|
||||
const msg = create(ServerMessageSchema, {
|
||||
messageType: ServerMessage_MessageType.RESPONSE,
|
||||
response,
|
||||
});
|
||||
|
||||
const bytes = toBinary(ServerMessageSchema, msg);
|
||||
const decoded = realFromBinary(ServerMessageSchema, bytes);
|
||||
|
||||
expect(decoded.messageType).toBe(ServerMessage_MessageType.RESPONSE);
|
||||
expect(decoded.response?.cmdId).toBe(BigInt(7));
|
||||
expect(decoded.response?.responseCode).toBe(Response_ResponseCode.RespOk);
|
||||
});
|
||||
|
||||
it('SessionCommand with extension round-trips through CommandContainer', async () => {
|
||||
const { create, toBinary, fromBinary: realFromBinary, setExtension, getExtension: realGetExtension } =
|
||||
await vi.importActual<typeof import('@bufbuild/protobuf')>('@bufbuild/protobuf');
|
||||
const {
|
||||
CommandContainerSchema, SessionCommandSchema,
|
||||
Command_Ping_ext, Command_PingSchema,
|
||||
} = await vi.importActual<typeof import('@app/generated')>('@app/generated');
|
||||
|
||||
const pingCmd = create(Command_PingSchema, {});
|
||||
const sesCmd = create(SessionCommandSchema, {});
|
||||
setExtension(sesCmd, Command_Ping_ext, pingCmd);
|
||||
|
||||
const container = create(CommandContainerSchema, {
|
||||
cmdId: BigInt(1),
|
||||
sessionCommand: [sesCmd],
|
||||
});
|
||||
|
||||
const bytes = toBinary(CommandContainerSchema, container);
|
||||
const decoded = realFromBinary(CommandContainerSchema, bytes);
|
||||
|
||||
expect(decoded.cmdId).toBe(BigInt(1));
|
||||
expect(decoded.sessionCommand).toHaveLength(1);
|
||||
|
||||
const decodedPing = realGetExtension(decoded.sessionCommand[0], Command_Ping_ext);
|
||||
expect(decodedPing).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
type GameEvent,
|
||||
} from '@app/generated';
|
||||
|
||||
import type { GameEventMeta } from '../types';
|
||||
import type { GameEventMeta } from '../interfaces/WebSocketConfig';
|
||||
import { type CommandOptions, handleResponse } from './command-options';
|
||||
|
||||
export interface SocketTransport {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ vi.mock('../config', () => ({
|
|||
import { WebSocketService } from './WebSocketService';
|
||||
import type { WebSocketServiceConfig } from './WebSocketService';
|
||||
import { KeepAliveService } from './KeepAliveService';
|
||||
import { StatusEnum } from '../StatusEnum';
|
||||
import { StatusEnum } from '../interfaces/StatusEnum';
|
||||
|
||||
type WebSocketInternal = WebSocketService & {
|
||||
keepAliveService: KeepAliveService;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Subject } from 'rxjs';
|
||||
|
||||
import { StatusEnum } from '../StatusEnum';
|
||||
import { StatusEnum } from '../interfaces/StatusEnum';
|
||||
import { KeepAliveService } from './KeepAliveService';
|
||||
import { CLIENT_OPTIONS } from '../config';
|
||||
import type { ConnectTarget } from '../WebClientConfig';
|
||||
import type { ConnectTarget } from '../interfaces/WebClientConfig';
|
||||
|
||||
export interface WebSocketServiceConfig {
|
||||
keepAliveFn: (pingReceived: () => void) => void;
|
||||
|
|
|
|||
13
webclient/src/websocket/utils/connectionState.ts
Normal file
13
webclient/src/websocket/utils/connectionState.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { WebSocketConnectOptions } from '../interfaces/ConnectOptions';
|
||||
|
||||
let pendingOptions: WebSocketConnectOptions | null = null;
|
||||
|
||||
export function setPendingOptions(options: WebSocketConnectOptions) {
|
||||
pendingOptions = options;
|
||||
}
|
||||
|
||||
export function consumePendingOptions(): WebSocketConnectOptions | null {
|
||||
const opts = pendingOptions;
|
||||
pendingOptions = null;
|
||||
return opts;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue