mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-14 19:18:55 -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 {
|
import {
|
||||||
WebClient,
|
WebClient,
|
||||||
StatusEnum,
|
|
||||||
SessionEvents,
|
SessionEvents,
|
||||||
RoomEvents,
|
RoomEvents,
|
||||||
GameEvents,
|
GameEvents,
|
||||||
SessionCommands,
|
SessionCommands,
|
||||||
generateSalt,
|
|
||||||
passwordSaltSupported,
|
|
||||||
} from '@app/websocket';
|
} from '@app/websocket';
|
||||||
import type { WebClientConfig } from '@app/websocket';
|
import type { WebClientConfig } from '@app/websocket';
|
||||||
|
|
||||||
import { createWebClientResponse } from './response';
|
import { createWebClientResponse } from './response';
|
||||||
import { consumePendingOptions } from './connectionState';
|
|
||||||
import { PROTOCOL_VERSION } from './config';
|
|
||||||
|
|
||||||
export function initWebClient(): void {
|
export function initWebClient(): void {
|
||||||
const response = createWebClientResponse();
|
const response = createWebClientResponse();
|
||||||
|
|
||||||
const config: WebClientConfig = {
|
const config: WebClientConfig = {
|
||||||
response,
|
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,
|
sessionEvents: SessionEvents,
|
||||||
roomEvents: RoomEvents,
|
roomEvents: RoomEvents,
|
||||||
gameEvents: GameEvents,
|
gameEvents: GameEvents,
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,69 @@
|
||||||
import { App, Enriched } from '@app/types';
|
import {
|
||||||
import { WebClient, StatusEnum, SessionCommands } from '@app/websocket';
|
WebClient,
|
||||||
import type { IAuthenticationRequest, AuthRequestMap } from '@app/websocket';
|
StatusEnum,
|
||||||
|
SessionCommands,
|
||||||
import { setPendingOptions } from '../connectionState';
|
WebSocketConnectReason,
|
||||||
|
setPendingOptions,
|
||||||
|
} from '@app/websocket';
|
||||||
|
import type {
|
||||||
|
IAuthenticationRequest,
|
||||||
|
AuthRequestMap,
|
||||||
|
LoginConnectOptions,
|
||||||
|
TestConnectionOptions,
|
||||||
|
RegisterConnectOptions,
|
||||||
|
ActivateConnectOptions,
|
||||||
|
PasswordResetRequestConnectOptions,
|
||||||
|
PasswordResetChallengeConnectOptions,
|
||||||
|
PasswordResetConnectOptions,
|
||||||
|
} from '@app/websocket';
|
||||||
|
|
||||||
interface AppAuthRequestOverrides extends AuthRequestMap {
|
interface AppAuthRequestOverrides extends AuthRequestMap {
|
||||||
LoginParams: Omit<Enriched.LoginConnectOptions, 'reason'>;
|
LoginParams: Omit<LoginConnectOptions, 'reason'>;
|
||||||
ConnectTarget: Omit<Enriched.TestConnectionOptions, 'reason'>;
|
ConnectTarget: Omit<TestConnectionOptions, 'reason'>;
|
||||||
RegisterParams: Omit<Enriched.RegisterConnectOptions, 'reason'>;
|
RegisterParams: Omit<RegisterConnectOptions, 'reason'>;
|
||||||
ActivateParams: Omit<Enriched.ActivateConnectOptions, 'reason'>;
|
ActivateParams: Omit<ActivateConnectOptions, 'reason'>;
|
||||||
ForgotPasswordRequestParams: Omit<Enriched.PasswordResetRequestConnectOptions, 'reason'>;
|
ForgotPasswordRequestParams: Omit<PasswordResetRequestConnectOptions, 'reason'>;
|
||||||
ForgotPasswordChallengeParams: Omit<Enriched.PasswordResetChallengeConnectOptions, 'reason'>;
|
ForgotPasswordChallengeParams: Omit<PasswordResetChallengeConnectOptions, 'reason'>;
|
||||||
ForgotPasswordResetParams: Omit<Enriched.PasswordResetConnectOptions, 'reason'>;
|
ForgotPasswordResetParams: Omit<PasswordResetConnectOptions, 'reason'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthenticationRequestImpl implements IAuthenticationRequest<AppAuthRequestOverrides> {
|
export class AuthenticationRequestImpl implements IAuthenticationRequest<AppAuthRequestOverrides> {
|
||||||
login(options: Omit<Enriched.LoginConnectOptions, 'reason'>): void {
|
login(options: Omit<LoginConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.LOGIN });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.LOGIN });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
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 });
|
WebClient.instance.testConnect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
||||||
register(options: Omit<Enriched.RegisterConnectOptions, 'reason'>): void {
|
register(options: Omit<RegisterConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.REGISTER });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.REGISTER });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
||||||
activateAccount(options: Omit<Enriched.ActivateConnectOptions, 'reason'>): void {
|
activateAccount(options: Omit<ActivateConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPasswordRequest(options: Omit<Enriched.PasswordResetRequestConnectOptions, 'reason'>): void {
|
resetPasswordRequest(options: Omit<PasswordResetRequestConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPasswordChallenge(options: Omit<Enriched.PasswordResetChallengeConnectOptions, 'reason'>): void {
|
resetPasswordChallenge(options: Omit<PasswordResetChallengeConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPassword(options: Omit<Enriched.PasswordResetConnectOptions, 'reason'>): void {
|
resetPassword(options: Omit<PasswordResetConnectOptions, 'reason'>): void {
|
||||||
setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET });
|
setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET });
|
||||||
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...');
|
||||||
WebClient.instance.connect({ host: options.host, port: options.port });
|
WebClient.instance.connect({ host: options.host, port: options.port });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Selectors } from './rooms.selectors';
|
import { Selectors } from './rooms.selectors';
|
||||||
import { RoomsState } from './rooms.interfaces';
|
import { RoomsState } from './rooms.interfaces';
|
||||||
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures';
|
||||||
|
import { App } from '@app/types';
|
||||||
|
|
||||||
function rootState(rooms: RoomsState) {
|
function rootState(rooms: RoomsState) {
|
||||||
return { rooms };
|
return { rooms };
|
||||||
|
|
@ -111,13 +112,23 @@ describe('Selectors', () => {
|
||||||
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.users);
|
expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.users);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getSortedRoomGames → returns sorted array view of games map', () => {
|
it('getSortedRoomGames → returns games sorted by the active sort config', () => {
|
||||||
const game1 = makeGame({ gameId: 1, description: 'beta' });
|
const game1 = makeGame({ gameId: 1, description: 'Beta' });
|
||||||
const game2 = makeGame({ gameId: 2, description: 'alpha' });
|
const game2 = makeGame({ gameId: 2, description: 'Alpha' });
|
||||||
const room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } });
|
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);
|
const result = Selectors.getSortedRoomGames(rootState(state), 1);
|
||||||
expect(result).toHaveLength(2);
|
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', () => {
|
it('getSortedRoomUsers → returns sorted user array sorted by name', () => {
|
||||||
|
|
@ -129,4 +140,40 @@ describe('Selectors', () => {
|
||||||
expect(result[0].name).toBe('Alice');
|
expect(result[0].name).toBe('Alice');
|
||||||
expect(result[1].name).toBe('Zane');
|
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,
|
makeServerState,
|
||||||
makeUser,
|
makeUser,
|
||||||
} from './__mocks__/server-fixtures';
|
} from './__mocks__/server-fixtures';
|
||||||
import { App } from '@app/types';
|
import { App, Data } from '@app/types';
|
||||||
|
|
||||||
function rootState(server: ServerState) {
|
function rootState(server: ServerState) {
|
||||||
return { server };
|
return { server };
|
||||||
|
|
@ -149,4 +149,86 @@ describe('Selectors', () => {
|
||||||
const state = makeServerState({ registrationError: 'bad input' });
|
const state = makeServerState({ registrationError: 'bad input' });
|
||||||
expect(Selectors.getRegistrationError(rootState(state))).toBe('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,
|
ServerInfo_User,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { WebSocketConnectReason } from './server';
|
|
||||||
|
|
||||||
// ── Domain model types (composition: raw proto + client-side fields) ──────────
|
// ── Domain model types (composition: raw proto + client-side fields) ──────────
|
||||||
//
|
//
|
||||||
// `info` holds the proto snapshot verbatim. Normalized/client-only fields
|
// `info` holds the proto snapshot verbatim. Normalized/client-only fields
|
||||||
|
|
@ -133,84 +131,20 @@ export interface LogGroups {
|
||||||
chat: ServerInfo_ChatMessage[];
|
chat: ServerInfo_ChatMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Connect options ───────────────────────────────────────────────────────────
|
// ── Connect options (re-exported from @app/websocket) ────────────────────────
|
||||||
// Each variant is the enriched input for one session flow: the network
|
// Source of truth lives in src/websocket/connectOptions.ts. Re-exported here
|
||||||
// transport fields (host/port) + the subset of proto Command_* fields the UI
|
// so UI code can use the Enriched.* namespace without importing @app/websocket.
|
||||||
// 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.
|
|
||||||
|
|
||||||
interface ConnectTransport {
|
export type {
|
||||||
host: string;
|
LoginConnectOptions,
|
||||||
port: string;
|
RegisterConnectOptions,
|
||||||
keepalive?: number;
|
ActivateConnectOptions,
|
||||||
autojoinrooms?: boolean;
|
PasswordResetRequestConnectOptions,
|
||||||
clientid?: string;
|
PasswordResetChallengeConnectOptions,
|
||||||
}
|
PasswordResetConnectOptions,
|
||||||
|
TestConnectionOptions,
|
||||||
export interface LoginConnectOptions extends ConnectTransport {
|
WebSocketConnectOptions,
|
||||||
reason: WebSocketConnectReason.LOGIN;
|
} from '@app/websocket';
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the
|
* 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';
|
import type { StatusEnum } from '@app/websocket';
|
||||||
|
|
||||||
export interface ServerStatus {
|
export interface ServerStatus {
|
||||||
|
|
@ -6,16 +6,6 @@ export interface ServerStatus {
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WebSocketConnectReason {
|
|
||||||
LOGIN,
|
|
||||||
REGISTER,
|
|
||||||
ACTIVATE_ACCOUNT,
|
|
||||||
PASSWORD_RESET_REQUEST,
|
|
||||||
PASSWORD_RESET_CHALLENGE,
|
|
||||||
PASSWORD_RESET,
|
|
||||||
TEST_CONNECTION,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Host {
|
export class Host {
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,14 @@ vi.mock('./services/ProtobufService', () => ({
|
||||||
import { WebClient } from './WebClient';
|
import { WebClient } from './WebClient';
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
import { WebSocketService } from './services/WebSocketService';
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
import { ProtobufService } from './services/ProtobufService';
|
||||||
import { StatusEnum } from './StatusEnum';
|
import { StatusEnum } from './interfaces/StatusEnum';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
import { SocketTransport, EventRegistries } from './services/ProtobufService';
|
import { SocketTransport, EventRegistries } from './services/ProtobufService';
|
||||||
import { WebSocketServiceConfig } from './services/WebSocketService';
|
import { WebSocketServiceConfig } from './services/WebSocketService';
|
||||||
import type { IWebClientResponse } from './interfaces';
|
import type { IWebClientResponse } from './interfaces';
|
||||||
import type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
import type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||||
import { installMockWebSocket, useWebClientCleanup } from './__mocks__/helpers';
|
import { installMockWebSocket } from './__mocks__/helpers';
|
||||||
|
|
||||||
function makeMockResponse(): IWebClientResponse {
|
function makeMockResponse(): IWebClientResponse {
|
||||||
return {
|
return {
|
||||||
|
|
@ -61,7 +61,6 @@ function makeMockResponse(): IWebClientResponse {
|
||||||
function makeMockConfig(response: IWebClientResponse): WebClientConfig {
|
function makeMockConfig(response: IWebClientResponse): WebClientConfig {
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
onServerIdentified: vi.fn(),
|
|
||||||
sessionEvents: [],
|
sessionEvents: [],
|
||||||
roomEvents: [],
|
roomEvents: [],
|
||||||
gameEvents: [],
|
gameEvents: [],
|
||||||
|
|
@ -69,8 +68,6 @@ function makeMockConfig(response: IWebClientResponse): WebClientConfig {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
describe('WebClient', () => {
|
describe('WebClient', () => {
|
||||||
let client: WebClient;
|
let client: WebClient;
|
||||||
let mockResponse: IWebClientResponse;
|
let mockResponse: IWebClientResponse;
|
||||||
|
|
@ -78,10 +75,6 @@ describe('WebClient', () => {
|
||||||
let messageSubject: Subject<MessageEvent>;
|
let messageSubject: Subject<MessageEvent>;
|
||||||
|
|
||||||
beforeEach(() => {
|
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;
|
(WebClient as unknown as { _instance: WebClient | null })._instance = null;
|
||||||
|
|
||||||
(ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport, events: EventRegistries) {
|
(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 { ProtobufService } from './services/ProtobufService';
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
import { WebSocketService } from './services/WebSocketService';
|
||||||
import { CLIENT_OPTIONS } from './config';
|
import { CLIENT_OPTIONS } from './config';
|
||||||
import type { IWebClientResponse } from './interfaces';
|
import type { IWebClientResponse } from './interfaces';
|
||||||
import type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
import type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||||
|
|
||||||
export class WebClient {
|
export class WebClient {
|
||||||
private static _instance: WebClient | null = null;
|
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(),
|
testConnect: vi.fn(),
|
||||||
disconnect: vi.fn(),
|
disconnect: vi.fn(),
|
||||||
updateStatus: vi.fn(),
|
updateStatus: vi.fn(),
|
||||||
config: { onServerIdentified: vi.fn() },
|
config: {},
|
||||||
status: 0,
|
status: 0,
|
||||||
protobuf: {
|
protobuf: {
|
||||||
sendSessionCommand: vi.fn(),
|
sendSessionCommand: vi.fn(),
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,20 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
vi.mock('../../WebClient');
|
||||||
WebClient: {
|
|
||||||
instance: {
|
|
||||||
protobuf: { sendAdminCommand: vi.fn() },
|
|
||||||
response: {
|
|
||||||
admin: {
|
|
||||||
adjustMod: vi.fn(),
|
|
||||||
reloadConfig: vi.fn(),
|
|
||||||
shutdownServer: vi.fn(),
|
|
||||||
updateServerMessage: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import { adjustMod } from './adjustMod';
|
import { adjustMod } from './adjustMod';
|
||||||
import { reloadConfig } from './reloadConfig';
|
import { reloadConfig } from './reloadConfig';
|
||||||
import { shutdownServer } from './shutdownServer';
|
import { shutdownServer } from './shutdownServer';
|
||||||
import { updateServerMessage } from './updateServerMessage';
|
import { updateServerMessage } from './updateServerMessage';
|
||||||
|
import {
|
||||||
|
Command_AdjustMod_ext,
|
||||||
|
Command_ReloadConfig_ext,
|
||||||
|
Command_ShutdownServer_ext,
|
||||||
|
Command_UpdateServerMessage_ext,
|
||||||
|
} from '@app/generated';
|
||||||
|
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||||
WebClient.instance.protobuf.sendAdminCommand as Mock,
|
WebClient.instance.protobuf.sendAdminCommand as Mock,
|
||||||
2
|
2
|
||||||
|
|
@ -36,9 +25,13 @@ const { invokeOnSuccess } = makeCallbackHelpers(
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('adjustMod', () => {
|
describe('adjustMod', () => {
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_AdjustMod', () => {
|
it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => {
|
||||||
adjustMod('alice', true, false);
|
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', () => {
|
it('onSuccess calls response.admin.adjustMod', () => {
|
||||||
|
|
@ -53,9 +46,13 @@ describe('adjustMod', () => {
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('reloadConfig', () => {
|
describe('reloadConfig', () => {
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_ReloadConfig', () => {
|
it('calls sendAdminCommand with Command_ReloadConfig extension', () => {
|
||||||
reloadConfig();
|
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', () => {
|
it('onSuccess calls response.admin.reloadConfig', () => {
|
||||||
|
|
@ -70,9 +67,13 @@ describe('reloadConfig', () => {
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('shutdownServer', () => {
|
describe('shutdownServer', () => {
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_ShutdownServer', () => {
|
it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => {
|
||||||
shutdownServer('maintenance', 10);
|
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', () => {
|
it('onSuccess calls response.admin.shutdownServer', () => {
|
||||||
|
|
@ -87,9 +88,13 @@ describe('shutdownServer', () => {
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('updateServerMessage', () => {
|
describe('updateServerMessage', () => {
|
||||||
|
|
||||||
it('calls sendAdminCommand with Command_UpdateServerMessage', () => {
|
it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => {
|
||||||
updateServerMessage();
|
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', () => {
|
it('onSuccess calls response.admin.updateServerMessage', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,6 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
vi.mock('../../WebClient');
|
||||||
WebClient: {
|
|
||||||
instance: {
|
|
||||||
protobuf: { sendGameCommand: vi.fn() },
|
|
||||||
response: { game: {} },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
import { create, setExtension } from '@bufbuild/protobuf';
|
import { create, setExtension } from '@bufbuild/protobuf';
|
||||||
import {
|
import {
|
||||||
Command_AttachCard_ext,
|
Command_AttachCard_ext,
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,6 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
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(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import {
|
import {
|
||||||
Command_BanFromServer_ext,
|
Command_BanFromServer_ext,
|
||||||
|
|
@ -55,8 +34,6 @@ import { warnUser } from './warnUser';
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||||
WebClient.instance.protobuf.sendModeratorCommand as Mock,
|
WebClient.instance.protobuf.sendModeratorCommand as Mock,
|
||||||
2
|
2
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,6 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
vi.mock('../../WebClient');
|
||||||
WebClient: {
|
|
||||||
instance: {
|
|
||||||
protobuf: { sendRoomCommand: vi.fn() },
|
|
||||||
response: {
|
|
||||||
room: {
|
|
||||||
gameCreated: vi.fn(),
|
|
||||||
joinedGame: vi.fn(),
|
|
||||||
leaveRoom: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import {
|
import {
|
||||||
Command_CreateGame_ext,
|
Command_CreateGame_ext,
|
||||||
|
|
@ -32,8 +18,6 @@ import { roomSay } from './roomSay';
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { create } from '@bufbuild/protobuf';
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const { invokeOnSuccess } = makeCallbackHelpers(
|
const { invokeOnSuccess } = makeCallbackHelpers(
|
||||||
WebClient.instance.protobuf.sendRoomCommand as Mock,
|
WebClient.instance.protobuf.sendRoomCommand as Mock,
|
||||||
// sendRoomCommand(roomId, ext, value, options) — options at index 3
|
// sendRoomCommand(roomId, ext, value, options) — options at index 3
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import {
|
||||||
type ActivateParams,
|
type ActivateParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { disconnect, login, updateStatus } from './';
|
import { disconnect, login, updateStatus } from './';
|
||||||
|
|
||||||
export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void {
|
export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
|
|
||||||
export function connect(target: ConnectTarget): void {
|
export function connect(target: ConnectTarget): void {
|
||||||
WebClient.instance.connect(target);
|
WebClient.instance.connect(target);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import {
|
||||||
type ForgotPasswordChallengeParams,
|
type ForgotPasswordChallengeParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { disconnect, updateStatus } from './';
|
import { disconnect, updateStatus } from './';
|
||||||
|
|
||||||
export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void {
|
export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import {
|
||||||
type ForgotPasswordRequestParams,
|
type ForgotPasswordRequestParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { disconnect, updateStatus } from './';
|
import { disconnect, updateStatus } from './';
|
||||||
|
|
||||||
export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void {
|
export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import {
|
||||||
type ForgotPasswordResetParams,
|
type ForgotPasswordResetParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
import { disconnect, updateStatus } from '.';
|
import { disconnect, updateStatus } from '.';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
type LoginParams,
|
type LoginParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
import {
|
import {
|
||||||
disconnect,
|
disconnect,
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
type RegisterParams,
|
type RegisterParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
import { login, disconnect, updateStatus } from './';
|
import { login, disconnect, updateStatus } from './';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import {
|
||||||
type RequestPasswordSaltParams,
|
type RequestPasswordSaltParams,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { CLIENT_CONFIG } from '../../config';
|
import { CLIENT_CONFIG } from '../../config';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import type { ConnectTarget } from '../../WebClientConfig';
|
import type { ConnectTarget } from '../../interfaces/WebClientConfig';
|
||||||
import { updateStatus } from './';
|
import { updateStatus } from './';
|
||||||
|
|
||||||
export function requestPasswordSalt(
|
export function requestPasswordSalt(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
// Tests for complex session commands that call WebClient directly
|
// Tests for complex session commands that call WebClient directly
|
||||||
// or have multiple branching callbacks.
|
// or have multiple branching callbacks.
|
||||||
|
|
||||||
vi.mock('../../WebClient', async () => {
|
vi.mock('../../WebClient');
|
||||||
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
|
|
||||||
return { WebClient: { instance: makeWebClientMock() } };
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('../../utils', async () => {
|
vi.mock('../../utils', async () => {
|
||||||
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||||
|
|
@ -19,11 +16,10 @@ vi.mock('./', async () => {
|
||||||
|
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import * as SessionIndexMocks from './';
|
import * as SessionIndexMocks from './';
|
||||||
import { App, Enriched } from '@app/types';
|
import { App, Enriched } from '@app/types';
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import {
|
import {
|
||||||
Command_Activate_ext,
|
Command_Activate_ext,
|
||||||
Command_ForgotPasswordChallenge_ext,
|
Command_ForgotPasswordChallenge_ext,
|
||||||
|
|
@ -54,8 +50,6 @@ import { forgotPasswordRequest } from './forgotPasswordRequest';
|
||||||
import { forgotPasswordReset } from './forgotPasswordReset';
|
import { forgotPasswordReset } from './forgotPasswordReset';
|
||||||
import { requestPasswordSalt } from './requestPasswordSalt';
|
import { requestPasswordSalt } from './requestPasswordSalt';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
|
const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers(
|
||||||
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
||||||
2
|
2
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
// Shared mock setup for session command tests
|
// Shared mock setup for session command tests
|
||||||
|
|
||||||
vi.mock('../../WebClient', async () => {
|
vi.mock('../../WebClient');
|
||||||
const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks');
|
|
||||||
return { WebClient: { instance: makeWebClientMock() } };
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mock('../../utils', async () => {
|
vi.mock('../../utils', async () => {
|
||||||
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks');
|
||||||
|
|
@ -19,7 +16,6 @@ vi.mock('./', async () => {
|
||||||
|
|
||||||
import { Mock } from 'vitest';
|
import { Mock } from 'vitest';
|
||||||
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers';
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
|
import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils';
|
||||||
|
|
||||||
|
|
@ -85,8 +81,6 @@ import {
|
||||||
Response_ReplayList_ext,
|
Response_ReplayList_ext,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers(
|
const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers(
|
||||||
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
WebClient.instance.protobuf.sendSessionCommand as Mock,
|
||||||
2
|
2
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function updateStatus(status: StatusEnum, description: string): void {
|
export function updateStatus(status: StatusEnum, description: string): void {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export const PROTOCOL_VERSION = 14;
|
||||||
|
|
||||||
export const CLIENT_CONFIG = {
|
export const CLIENT_CONFIG = {
|
||||||
clientid: 'webatrice',
|
clientid: 'webatrice',
|
||||||
clientver: 'webclient-1.0 (2019-10-31)',
|
clientver: 'webclient-1.0 (2019-10-31)',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_AttachCard } from '@app/generated';
|
import type { Event_AttachCard } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void {
|
export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_ChangeZoneProperties } from '@app/generated';
|
import type { Event_ChangeZoneProperties } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void {
|
export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_CreateArrow } from '@app/generated';
|
import type { Event_CreateArrow } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void {
|
export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_CreateCounter } from '@app/generated';
|
import type { Event_CreateCounter } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void {
|
export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_CreateToken } from '@app/generated';
|
import type { Event_CreateToken } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function createToken(data: Event_CreateToken, meta: GameEventMeta): void {
|
export function createToken(data: Event_CreateToken, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_DelCounter } from '@app/generated';
|
import type { Event_DelCounter } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void {
|
export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_DeleteArrow } from '@app/generated';
|
import type { Event_DeleteArrow } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void {
|
export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_DestroyCard } from '@app/generated';
|
import type { Event_DestroyCard } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void {
|
export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_DrawCards } from '@app/generated';
|
import type { Event_DrawCards } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void {
|
export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_DumpZone } from '@app/generated';
|
import type { Event_DumpZone } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void {
|
export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_FlipCard } from '@app/generated';
|
import type { Event_FlipCard } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function flipCard(data: Event_FlipCard, meta: GameEventMeta): void {
|
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';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function gameClosed(_data: {}, meta: GameEventMeta): void {
|
export function gameClosed(_data: {}, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,4 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
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';
|
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { create } from '@bufbuild/protobuf';
|
||||||
import {
|
import {
|
||||||
Event_AttachCardSchema,
|
Event_AttachCardSchema,
|
||||||
|
|
@ -67,7 +27,6 @@ import {
|
||||||
ServerInfo_PlayerPropertiesSchema,
|
ServerInfo_PlayerPropertiesSchema,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
import { attachCard } from './attachCard';
|
import { attachCard } from './attachCard';
|
||||||
import { changeZoneProperties } from './changeZoneProperties';
|
import { changeZoneProperties } from './changeZoneProperties';
|
||||||
import { createArrow } from './createArrow';
|
import { createArrow } from './createArrow';
|
||||||
|
|
@ -98,8 +57,6 @@ import { setCardCounter } from './setCardCounter';
|
||||||
import { setCounter } from './setCounter';
|
import { setCounter } from './setCounter';
|
||||||
import { shuffle } from './shuffle';
|
import { shuffle } from './shuffle';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 };
|
const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 };
|
||||||
|
|
||||||
describe('joinGame event', () => {
|
describe('joinGame event', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_GameSay } from '@app/generated';
|
import type { Event_GameSay } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function gameSay(data: Event_GameSay, meta: GameEventMeta): void {
|
export function gameSay(data: Event_GameSay, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_GameStateChanged } from '@app/generated';
|
import type { Event_GameStateChanged } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void {
|
export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import {
|
||||||
Event_ReverseTurn_ext,
|
Event_ReverseTurn_ext,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
|
|
||||||
import { attachCard } from './attachCard';
|
import { attachCard } from './attachCard';
|
||||||
import { changeZoneProperties } from './changeZoneProperties';
|
import { changeZoneProperties } from './changeZoneProperties';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_Join } from '@app/generated';
|
import type { Event_Join } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function kicked(_data: {}, meta: GameEventMeta): void {
|
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';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function leaveGame(data: { reason: number }, meta: GameEventMeta): void {
|
export function leaveGame(data: { reason: number }, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_MoveCard } from '@app/generated';
|
import type { Event_MoveCard } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void {
|
export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_PlayerPropertiesChanged } from '@app/generated';
|
import type { Event_PlayerPropertiesChanged } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: GameEventMeta): void {
|
export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_RevealCards } from '@app/generated';
|
import type { Event_RevealCards } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void {
|
export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_ReverseTurn } from '@app/generated';
|
import type { Event_ReverseTurn } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void {
|
export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_RollDie } from '@app/generated';
|
import type { Event_RollDie } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function rollDie(data: Event_RollDie, meta: GameEventMeta): void {
|
export function rollDie(data: Event_RollDie, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_SetActivePhase } from '@app/generated';
|
import type { Event_SetActivePhase } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void {
|
export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_SetActivePlayer } from '@app/generated';
|
import type { Event_SetActivePlayer } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void {
|
export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_SetCardAttr } from '@app/generated';
|
import type { Event_SetCardAttr } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void {
|
export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_SetCardCounter } from '@app/generated';
|
import type { Event_SetCardCounter } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void {
|
export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_SetCounter } from '@app/generated';
|
import type { Event_SetCounter } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void {
|
export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Event_Shuffle } from '@app/generated';
|
import type { Event_Shuffle } from '@app/generated';
|
||||||
import type { GameEventMeta } from '../../types';
|
import type { GameEventMeta } from '../../interfaces/WebSocketConfig';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
|
|
||||||
export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void {
|
export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,5 @@
|
||||||
vi.mock('../../WebClient', () => ({
|
vi.mock('../../WebClient');
|
||||||
WebClient: {
|
|
||||||
instance: {
|
|
||||||
response: {
|
|
||||||
room: {
|
|
||||||
userJoined: vi.fn(),
|
|
||||||
userLeft: vi.fn(),
|
|
||||||
updateGames: vi.fn(),
|
|
||||||
removeMessages: vi.fn(),
|
|
||||||
addMessage: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { useWebClientCleanup } from '../../__mocks__/helpers';
|
|
||||||
import { create } from '@bufbuild/protobuf';
|
import { create } from '@bufbuild/protobuf';
|
||||||
import {
|
import {
|
||||||
Event_JoinRoomSchema,
|
Event_JoinRoomSchema,
|
||||||
|
|
@ -31,8 +16,6 @@ import { listGames } from './listGames';
|
||||||
import { removeMessages } from './removeMessages';
|
import { removeMessages } from './removeMessages';
|
||||||
import { roomSay } from './roomSay';
|
import { roomSay } from './roomSay';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId });
|
const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId });
|
||||||
|
|
||||||
describe('joinRoom room event', () => {
|
describe('joinRoom room event', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated';
|
import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated';
|
||||||
import { StatusEnum } from '../../StatusEnum';
|
import { StatusEnum } from '../../interfaces/StatusEnum';
|
||||||
import { updateStatus } from '../../commands/session';
|
import { updateStatus } from '../../commands/session';
|
||||||
|
|
||||||
export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void {
|
export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,93 @@
|
||||||
import type { Event_ServerIdentification } from '@app/generated';
|
import type { Event_ServerIdentification } from '@app/generated';
|
||||||
import { WebClient } from '../../WebClient';
|
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 {
|
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
|
// Tests for simple session events that delegate 1:1 to SessionPersistence
|
||||||
// or RoomPersistence with minimal logic.
|
// or RoomPersistence with minimal logic.
|
||||||
|
|
||||||
vi.mock('../../WebClient', () => ({
|
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('../../config', () => ({
|
vi.mock('../../config', () => ({
|
||||||
CLIENT_OPTIONS: { autojoinrooms: false },
|
CLIENT_OPTIONS: { autojoinrooms: false },
|
||||||
|
PROTOCOL_VERSION: 14,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../commands/session', () => ({
|
vi.mock('../../commands/session', () => ({
|
||||||
joinRoom: vi.fn(),
|
joinRoom: vi.fn(),
|
||||||
updateStatus: vi.fn(),
|
updateStatus: vi.fn(),
|
||||||
disconnect: 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', () => ({
|
vi.mock('../../utils', () => ({
|
||||||
sanitizeHtml: vi.fn((msg: string) => msg),
|
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 {
|
import {
|
||||||
Event_AddToListSchema,
|
Event_AddToListSchema,
|
||||||
Event_ConnectionClosedSchema,
|
Event_ConnectionClosedSchema,
|
||||||
|
|
@ -71,6 +56,11 @@ import { create } from '@bufbuild/protobuf';
|
||||||
import { WebClient } from '../../WebClient';
|
import { WebClient } from '../../WebClient';
|
||||||
import * as Config from '../../config';
|
import * as Config from '../../config';
|
||||||
import * as SessionCmds from '../../commands/session';
|
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 { gameJoined } from './gameJoined';
|
||||||
import { notifyUser } from './notifyUser';
|
import { notifyUser } from './notifyUser';
|
||||||
import { replayAdded } from './replayAdded';
|
import { replayAdded } from './replayAdded';
|
||||||
|
|
@ -86,8 +76,6 @@ import { listRooms } from './listRooms';
|
||||||
import { connectionClosed } from './connectionClosed';
|
import { connectionClosed } from './connectionClosed';
|
||||||
import { serverIdentification } from './serverIdentification';
|
import { serverIdentification } from './serverIdentification';
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] };
|
const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] };
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
@ -387,11 +375,149 @@ describe('connectionClosed', () => {
|
||||||
// serverIdentification
|
// serverIdentification
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
describe('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 makeLoginOptions = () => ({
|
||||||
const info = create(Event_ServerIdentificationSchema,
|
host: 'h', port: '1', userName: 'alice', password: 'pw',
|
||||||
{ serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 });
|
reason: WebSocketConnectReason.LOGIN as const,
|
||||||
serverIdentification(info);
|
});
|
||||||
expect(WebClient.instance.config.onServerIdentified).toHaveBeenCalledWith(info);
|
|
||||||
|
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 * from './interfaces';
|
||||||
|
|
||||||
export { WebClient } from './WebClient';
|
export { WebClient } from './WebClient';
|
||||||
export { StatusEnum } from './StatusEnum';
|
export { StatusEnum } from './interfaces/StatusEnum';
|
||||||
export type { WebClientConfig, ConnectTarget } from './WebClientConfig';
|
export type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig';
|
||||||
export type {
|
export type {
|
||||||
KeyOf,
|
KeyOf,
|
||||||
GameEventMeta,
|
GameEventMeta,
|
||||||
WebSocketSessionResponseOverrides,
|
WebSocketSessionResponseOverrides,
|
||||||
WebSocketRoomResponseOverrides,
|
WebSocketRoomResponseOverrides,
|
||||||
} from './types';
|
} from './interfaces/WebSocketConfig';
|
||||||
|
|
||||||
export { SessionEvents } from './events/session';
|
export { SessionEvents } from './events/session';
|
||||||
export { RoomEvents } from './events/room';
|
export { RoomEvents } from './events/room';
|
||||||
export { GameEvents } from './events/game';
|
export { GameEvents } from './events/game';
|
||||||
|
|
||||||
export { generateSalt, passwordSaltSupported, hashPassword } from './utils';
|
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,
|
SessionEvent,
|
||||||
RoomEvent,
|
RoomEvent,
|
||||||
GameEvent,
|
GameEvent,
|
||||||
Event_ServerIdentification,
|
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import type { GameEventMeta } from './types';
|
import type { GameEventMeta } from './WebSocketConfig';
|
||||||
import type { IWebClientResponse } from './interfaces';
|
import type { IWebClientResponse } from '.';
|
||||||
|
|
||||||
export interface ConnectTarget {
|
export interface ConnectTarget {
|
||||||
host: string;
|
host: string;
|
||||||
|
|
@ -17,8 +16,6 @@ export interface ConnectTarget {
|
||||||
export interface WebClientConfig {
|
export interface WebClientConfig {
|
||||||
response: IWebClientResponse;
|
response: IWebClientResponse;
|
||||||
|
|
||||||
onServerIdentified(info: Event_ServerIdentification): void;
|
|
||||||
|
|
||||||
sessionEvents: RegistryEntry<unknown, SessionEvent>[];
|
sessionEvents: RegistryEntry<unknown, SessionEvent>[];
|
||||||
roomEvents: RegistryEntry<unknown, RoomEvent, RoomEvent>[];
|
roomEvents: RegistryEntry<unknown, RoomEvent, RoomEvent>[];
|
||||||
gameEvents: RegistryEntry<unknown, GameEvent, GameEventMeta>[];
|
gameEvents: RegistryEntry<unknown, GameEvent, GameEventMeta>[];
|
||||||
|
|
@ -36,8 +36,8 @@ import type {
|
||||||
GameCommand,
|
GameCommand,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import type { ConnectTarget } from '../WebClientConfig';
|
import type { ConnectTarget } from './WebClientConfig';
|
||||||
import type { KeyOf } from '../types';
|
import type { KeyOf } from './WebSocketConfig';
|
||||||
|
|
||||||
// ── Auth request type map ────────────────────────────────────────────────────
|
// ── Auth request type map ────────────────────────────────────────────────────
|
||||||
// Keys = generated *Params type names composed with ConnectTarget.
|
// Keys = generated *Params type names composed with ConnectTarget.
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,12 @@ import type {
|
||||||
ServerInfo_ReplayMatch,
|
ServerInfo_ReplayMatch,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import type { StatusEnum } from '../StatusEnum';
|
import type { StatusEnum } from './StatusEnum';
|
||||||
import type {
|
import type {
|
||||||
KeyOf,
|
KeyOf,
|
||||||
WebSocketSessionResponseOverrides,
|
WebSocketSessionResponseOverrides,
|
||||||
WebSocketRoomResponseOverrides,
|
WebSocketRoomResponseOverrides,
|
||||||
} from '../types';
|
} from './WebSocketConfig';
|
||||||
|
|
||||||
export interface ISessionResponse<T extends ResponseMap = WebSocketSessionResponseOverrides> {
|
export interface ISessionResponse<T extends ResponseMap = WebSocketSessionResponseOverrides> {
|
||||||
initialized(): void;
|
initialized(): void;
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,8 @@ vi.mock('@bufbuild/protobuf', async (importOriginal) => ({
|
||||||
setExtension: vi.fn(),
|
setExtension: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../WebClient', () => ({
|
vi.mock('../WebClient');
|
||||||
__esModule: true,
|
|
||||||
default: {},
|
|
||||||
WebClient: { _instance: null },
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { useWebClientCleanup } from '../__mocks__/helpers';
|
|
||||||
import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf';
|
import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf';
|
||||||
import type { GenExtension } from '@bufbuild/protobuf/codegenv2';
|
import type { GenExtension } from '@bufbuild/protobuf/codegenv2';
|
||||||
|
|
||||||
|
|
@ -47,8 +42,6 @@ type ProtobufInternal = ProtobufService & {
|
||||||
processServerResponse(response: unknown): void;
|
processServerResponse(response: unknown): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
useWebClientCleanup();
|
|
||||||
|
|
||||||
let mockSocket: { isOpen: ReturnType<typeof vi.fn>; send: ReturnType<typeof vi.fn> };
|
let mockSocket: { isOpen: ReturnType<typeof vi.fn>; send: ReturnType<typeof vi.fn> };
|
||||||
let mockEvents: EventRegistries;
|
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,
|
type GameEvent,
|
||||||
} from '@app/generated';
|
} from '@app/generated';
|
||||||
|
|
||||||
import type { GameEventMeta } from '../types';
|
import type { GameEventMeta } from '../interfaces/WebSocketConfig';
|
||||||
import { type CommandOptions, handleResponse } from './command-options';
|
import { type CommandOptions, handleResponse } from './command-options';
|
||||||
|
|
||||||
export interface SocketTransport {
|
export interface SocketTransport {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ vi.mock('../config', () => ({
|
||||||
import { WebSocketService } from './WebSocketService';
|
import { WebSocketService } from './WebSocketService';
|
||||||
import type { WebSocketServiceConfig } from './WebSocketService';
|
import type { WebSocketServiceConfig } from './WebSocketService';
|
||||||
import { KeepAliveService } from './KeepAliveService';
|
import { KeepAliveService } from './KeepAliveService';
|
||||||
import { StatusEnum } from '../StatusEnum';
|
import { StatusEnum } from '../interfaces/StatusEnum';
|
||||||
|
|
||||||
type WebSocketInternal = WebSocketService & {
|
type WebSocketInternal = WebSocketService & {
|
||||||
keepAliveService: KeepAliveService;
|
keepAliveService: KeepAliveService;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
import { StatusEnum } from '../StatusEnum';
|
import { StatusEnum } from '../interfaces/StatusEnum';
|
||||||
import { KeepAliveService } from './KeepAliveService';
|
import { KeepAliveService } from './KeepAliveService';
|
||||||
import { CLIENT_OPTIONS } from '../config';
|
import { CLIENT_OPTIONS } from '../config';
|
||||||
import type { ConnectTarget } from '../WebClientConfig';
|
import type { ConnectTarget } from '../interfaces/WebClientConfig';
|
||||||
|
|
||||||
export interface WebSocketServiceConfig {
|
export interface WebSocketServiceConfig {
|
||||||
keepAliveFn: (pingReceived: () => void) => void;
|
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