diff --git a/webclient/buf.gen.plugin.mjs b/webclient/buf.gen.plugin.mjs index 35354ce4c..b618761db 100644 --- a/webclient/buf.gen.plugin.mjs +++ b/webclient/buf.gen.plugin.mjs @@ -11,8 +11,7 @@ import { createEcmaScriptPlugin, runNodeJs } from '@bufbuild/protoplugin'; const HEADER = [ '// @generated by protoc-gen-data. DO NOT EDIT.', - '// Rollup of all proto modules + MessageInitShape param aliases for every Command_*,', - '// plus type maps for Response/Event extensions grouped by scope.', + '// Rollup of all proto modules + MessageInitShape param aliases for every Command_*.', '/* eslint-disable */', '', '', @@ -56,71 +55,6 @@ const inner = createEcmaScriptPlugin({ } f.print(); - // ── Type maps for Response/Event extensions, grouped by extendee ──────── - // - // Scans all messages for nested `extend` declarations and groups them by - // which message they extend (Response, SessionEvent, RoomEvent, GameEvent). - // Emits one `interface *Map { TypeName: TypeName; ... }` per scope. - - /** @type {Map} */ - const extendeeGroups = new Map(); - - for (const file of sortedFiles) { - for (const msg of file.messages) { - for (const ext of msg.nestedExtensions) { - const target = ext.extendee.name; - const group = extendeeGroups.get(target); - if (group) { - group.push(msg); - } else { - extendeeGroups.set(target, [msg]); - } - } - } - } - - /** @type {[string, string, import('@bufbuild/protobuf').DescMessage | null][]} */ - const maps = [ - ['ResponseMap', 'Response', null], - ['SessionEventMap', 'SessionEvent', null], - ['RoomEventMap', 'RoomEvent', null], - ['GameEventMap', 'GameEvent', null], - ]; - - // Resolve the base extendee message for maps that need the base type included - for (const file of sortedFiles) { - for (const msg of file.messages) { - for (const entry of maps) { - if (msg.name === entry[1]) { - entry[2] = msg; - } - } - } - } - - for (const [mapName, extendeeName, baseMsg] of maps) { - const msgs = (extendeeGroups.get(extendeeName) || []).slice(); - msgs.sort((a, b) => a.name.localeCompare(b.name)); - - if (msgs.length === 0 && !baseMsg) continue; - - f.print('export interface ', mapName, ' {'); - - // Include the base extendee type itself (e.g. Response in ResponseMap) - if (baseMsg) { - const sym = f.import(baseMsg.name, `./proto/${baseMsg.file.name}_pb`, true); - f.print(' ', baseMsg.name, ': ', sym, ';'); - } - - for (const msg of msgs) { - const sym = f.import(msg.name, `./proto/${msg.file.name}_pb`, true); - f.print(' ', msg.name, ': ', sym, ';'); - } - - f.print('}'); - f.print(); - } - // Generic extension registry infrastructure. Consolidates the three // near-duplicate registry types and helpers that used to live in // src/websocket/services/protobuf-types.ts into one generic pair. diff --git a/webclient/eslint.boundaries.mjs b/webclient/eslint.boundaries.mjs index 05bb6cf83..0d67c6ae9 100644 --- a/webclient/eslint.boundaries.mjs +++ b/webclient/eslint.boundaries.mjs @@ -19,49 +19,37 @@ const types = (...types) => types.map((type) => ({ to: { type } })); const rules = [ { from: { type: 'generated' }, allow: [] }, - { from: { type: 'types' }, allow: types('generated', 'websocket') }, + { from: { type: 'types' }, allow: types('generated') }, - { from: { type: 'websocket' }, allow: types('generated') }, + { from: { type: 'websocket' }, allow: types('types') }, { from: { type: 'store' }, allow: types('types') }, - { from: { type: 'api' }, allow: types('store', 'types', 'websocket') }, + { from: { type: 'api' }, allow: types('types', 'store', 'websocket') }, - { from: { type: 'hooks' }, allow: types('api', 'services', 'types', 'websocket') }, + { from: { type: 'hooks' }, allow: types('services', 'types') }, { from: { type: 'images' }, allow: types('types') }, { from: { type: 'services' }, allow: types('api', 'store', 'types') }, - { - from: { type: 'components' }, - allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types') - }, - { - from: { type: 'containers' }, - allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types') - }, - { from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'store', 'types') }, - { from: { type: 'forms' }, allow: types('components', 'hooks', 'services', 'store', 'types') }, + { from: { type: 'components' }, allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'types', 'store') }, + { from: { type: 'containers' }, allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'types', 'store') }, + { from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'types', 'store') }, + { from: { type: 'forms' }, allow: types('components', 'hooks', 'types', 'services', 'store') }, ]; -export const boundariesConfig = [ - { - plugins: { boundaries }, - settings: { - 'boundaries/elements': elements, - 'import/resolver': { +export const boundariesConfig = { + plugins: { boundaries }, + settings: { + 'boundaries/elements': elements, + 'import/resolver': { typescript: { - alwaysTryTypes: true, - project: './tsconfig.json', + alwaysTryTypes: true, + project: './tsconfig.json', }, - }, - }, - rules: { - 'boundaries/dependencies': ['error', { - default: 'disallow', - rules, - }], }, }, - { - files: ['**/*.spec.*'], - rules: { 'boundaries/dependencies': 'off' }, + rules: { + 'boundaries/dependencies': ['error', { + default: 'disallow', + rules, + }], }, -]; +}; diff --git a/webclient/eslint.config.mjs b/webclient/eslint.config.mjs index f8885d58e..28a00a0ae 100644 --- a/webclient/eslint.config.mjs +++ b/webclient/eslint.config.mjs @@ -14,7 +14,7 @@ export default tseslint.config( ...tseslint.configs.recommended, // Enforce module boundaries - ...boundariesConfig, + boundariesConfig, // Project-specific config { diff --git a/webclient/integration/src/admin.spec.ts b/webclient/integration/src/admin.spec.ts deleted file mode 100644 index f67bcfc6f..000000000 --- a/webclient/integration/src/admin.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Admin command pipeline smoke test — validates that sendAdminCommand -// encodes, correlates, and persists correctly end-to-end. - -import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; - -import { Data } from '@app/types'; -import { store } from '@app/store'; -import { AdminCommands } from '@app/websocket'; - -import { connectAndLogin } from './helpers/setup'; -import { - buildResponse, - buildResponseMessage, - buildSessionEventMessage, - deliverMessage, -} from './helpers/protobuf-builders'; -import { findLastAdminCommand } from './helpers/command-capture'; - -describe('admin commands', () => { - it('adjustMod modifies the user level bitflags on success', () => { - connectAndLogin(); - - // Add bob to the user list so the reducer has a target - deliverMessage(buildSessionEventMessage( - Data.Event_UserJoined_ext, - create(Data.Event_UserJoinedSchema, { - userInfo: create(Data.ServerInfo_UserSchema, { - name: 'bob', - userLevel: Data.ServerInfo_User_UserLevelFlag.IsRegistered, - }), - }) - )); - expect(store.getState().server.users.bob).toBeDefined(); - - AdminCommands.adjustMod('bob', true, false); - - const { cmdId, value } = findLastAdminCommand(Data.Command_AdjustMod_ext); - expect(value.userName).toBe('bob'); - expect(value.shouldBeMod).toBe(true); - expect(value.shouldBeJudge).toBe(false); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - const bobLevel = store.getState().server.users.bob.userLevel; - expect(bobLevel & Data.ServerInfo_User_UserLevelFlag.IsModerator).toBeTruthy(); - }); - - it('shutdownServer sends command and dispatches on success', () => { - connectAndLogin(); - - AdminCommands.shutdownServer('Scheduled maintenance', 10); - - const { cmdId, value } = findLastAdminCommand(Data.Command_ShutdownServer_ext); - expect(value.reason).toBe('Scheduled maintenance'); - expect(value.minutes).toBe(10); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - }); -}); \ No newline at end of file diff --git a/webclient/integration/src/authentication.spec.ts b/webclient/integration/src/authentication.spec.ts index c5149c809..fb852b565 100644 --- a/webclient/integration/src/authentication.spec.ts +++ b/webclient/integration/src/authentication.spec.ts @@ -1,14 +1,12 @@ -// Authentication scenarios — login success/failure, register, activate, -// and the hashed-password (salt) login path. +// Authentication scenarios — login success/failure, register, and activate. import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; -import { Data } from '@app/types'; +import { App, Data } from '@app/types'; import { store } from '@app/store'; -import { StatusEnum, WebSocketConnectReason } from '@app/websocket'; -import { connectAndHandshake, connectAndHandshakeWithSalt } from './helpers/setup'; +import { connectAndHandshake } from './helpers/setup'; import { buildResponse, buildResponseMessage, @@ -44,7 +42,7 @@ describe('authentication', () => { }))); const state = store.getState().server; - expect(state.status.state).toBe(StatusEnum.LOGGED_IN); + expect(state.status.state).toBe(App.StatusEnum.LOGGED_IN); expect(state.status.description).toBe('Logged in.'); expect(state.user?.name).toBe('alice'); expect(Object.keys(state.buddyList)).toEqual(['bob']); @@ -64,7 +62,7 @@ describe('authentication', () => { }))); const state = store.getState().server; - expect(state.status.state).toBe(StatusEnum.DISCONNECTED); + expect(state.status.state).toBe(App.StatusEnum.DISCONNECTED); expect(state.user).toBeNull(); expect(state.buddyList).toEqual({}); }); @@ -72,7 +70,7 @@ describe('authentication', () => { describe('register', () => { const registerOptions = { - reason: WebSocketConnectReason.REGISTER as const, + reason: App.WebSocketConnectReason.REGISTER, host: 'localhost', port: '4748', userName: 'newbie', @@ -80,10 +78,10 @@ describe('authentication', () => { email: 'newbie@example.com', country: 'US', realName: 'New Bie', - }; + } as const; it('auto-logs-in on RespRegistrationAccepted', () => { - connectAndHandshake(registerOptions); + connectAndHandshake(registerOptions as any); const register = findLastSessionCommand(Data.Command_Register_ext); expect(register.value.userName).toBe('newbie'); @@ -99,7 +97,7 @@ describe('authentication', () => { }); it('parks registration in awaiting-activation on RespRegistrationAcceptedNeedsActivation', () => { - connectAndHandshake(registerOptions); + connectAndHandshake(registerOptions as any); const register = findLastSessionCommand(Data.Command_Register_ext); deliverMessage(buildResponseMessage(buildResponse({ @@ -107,7 +105,7 @@ describe('authentication', () => { responseCode: Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation, }))); - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow(); }); }); @@ -115,13 +113,13 @@ describe('authentication', () => { describe('activate', () => { it('auto-logs-in on RespActivationAccepted', () => { connectAndHandshake({ - reason: WebSocketConnectReason.ACTIVATE_ACCOUNT as const, + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, host: 'localhost', port: '4748', userName: 'alice', token: 'abc-123', password: 'secret', - }); + } as any); const activate = findLastSessionCommand(Data.Command_Activate_ext); expect(activate.value.userName).toBe('alice'); @@ -135,43 +133,4 @@ describe('authentication', () => { expect(login.value.userName).toBe('alice'); }); }); - - describe('hashed-password login (salt path)', () => { - it('requests salt then sends login with hashedPassword instead of plaintext', () => { - connectAndHandshakeWithSalt({ userName: 'alice', password: 'secret' }); - - // First command should be RequestPasswordSalt, not Login - const salt = findLastSessionCommand(Data.Command_RequestPasswordSalt_ext); - expect(salt.value.userName).toBe('alice'); - expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow(); - - // Deliver salt response - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: salt.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_PasswordSalt_ext, - value: create(Data.Response_PasswordSaltSchema, { passwordSalt: 'test-salt-value' }), - }))); - - // Now login should have been sent with hashedPassword - const login = findLastSessionCommand(Data.Command_Login_ext); - expect(login.value.userName).toBe('alice'); - expect(login.value.hashedPassword).toBeTruthy(); - expect(login.value.password).toBeFalsy(); - - // Complete login - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: login.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_Login_ext, - value: create(Data.Response_LoginSchema, { - userInfo: makeUser('alice'), - buddyList: [], - ignoreList: [], - }), - }))); - - expect(store.getState().server.status.state).toBe(StatusEnum.LOGGED_IN); - }); - }); }); diff --git a/webclient/integration/src/connection.spec.ts b/webclient/integration/src/connection.spec.ts index 31cd41b83..d81c35f49 100644 --- a/webclient/integration/src/connection.spec.ts +++ b/webclient/integration/src/connection.spec.ts @@ -1,44 +1,30 @@ // Connection-lifecycle scenarios. Exercises the full transport handshake -// from webClient.connect() through onopen, ServerIdentification, and +// from `webClient.connect()` through `onopen`, ServerIdentification, and // disconnect — with only the browser WebSocket constructor mocked. import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; -import { Data } from '@app/types'; +import { App, Data } from '@app/types'; import { store } from '@app/store'; -import { StatusEnum } from '@app/websocket'; import { PROTOCOL_VERSION } from '../../src/websocket/config'; -import { - getMockWebSocket, - getWebClient, - openMockWebSocket, - setPendingOptions, - connectAndHandshake, -} from './helpers/setup'; -import type { WebSocketConnectOptions } from '@app/websocket'; -import { WebSocketConnectReason } from '@app/websocket'; +import { getMockWebSocket, getWebClient, openMockWebSocket } from './helpers/setup'; import { buildSessionEventMessage, deliverMessage, } from './helpers/protobuf-builders'; import { findLastSessionCommand } from './helpers/command-capture'; -function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}): WebSocketConnectOptions { +function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}) { return { - reason: WebSocketConnectReason.LOGIN, + reason: App.WebSocketConnectReason.LOGIN, host: 'localhost', port: '4748', userName: overrides.userName ?? 'alice', password: overrides.password ?? 'secret', - }; -} - -function connectWithOptions(opts: WebSocketConnectOptions): void { - setPendingOptions(opts); - getWebClient().connect({ host: opts.host, port: opts.port }); + } as const; } function serverIdentification( @@ -57,45 +43,47 @@ function serverIdentification( describe('connection lifecycle', () => { it('flips status through CONNECTING → CONNECTED on socket open', () => { - connectWithOptions(loginOptions()); + getWebClient().connect(loginOptions()); expect(store.getState().server.status.connectionAttemptMade).toBe(true); openMockWebSocket(); - expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); expect(store.getState().server.status.description).toBe('Connected'); }); it('routes a matching ServerIdentification into LOGGING_IN and sends Command_Login', () => { - connectWithOptions(loginOptions({ userName: 'alice' })); + getWebClient().connect(loginOptions({ userName: 'alice' })); openMockWebSocket(); deliverMessage(serverIdentification()); - expect(store.getState().server.status.state).toBe(StatusEnum.LOGGING_IN); + expect(store.getState().server.status.state).toBe(App.StatusEnum.LOGGING_IN); expect(store.getState().server.info.name).toBe('TestServer'); expect(store.getState().server.info.version).toBe('2.8.0'); const { value, cmdId } = findLastSessionCommand(Data.Command_Login_ext); expect(value.userName).toBe('alice'); expect(cmdId).toBeGreaterThan(0); + + expect(getWebClient().options).toBeNull(); }); it('disconnects on protocol version mismatch without sending a login command', () => { - connectWithOptions(loginOptions()); + getWebClient().connect(loginOptions()); openMockWebSocket(); deliverMessage(serverIdentification(PROTOCOL_VERSION + 1)); const mock = getMockWebSocket(); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow(); }); it('times out when onopen never fires within the keepalive window', () => { - connectWithOptions(loginOptions()); + getWebClient().connect(loginOptions()); const mock = getMockWebSocket(); expect(mock.close).not.toHaveBeenCalled(); @@ -103,11 +91,11 @@ describe('connection lifecycle', () => { vi.advanceTimersByTime(5000); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); }); it('releases keep-alive ping loop on explicit disconnect', () => { - connectWithOptions(loginOptions()); + getWebClient().connect(loginOptions()); openMockWebSocket(); deliverMessage(serverIdentification()); @@ -115,20 +103,6 @@ describe('connection lifecycle', () => { getWebClient().disconnect(); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); }); - - it('drops pending commands and clears state on unexpected socket close', () => { - connectAndHandshake(); - - // A login command is now pending (sent during handshake) - expect(() => findLastSessionCommand(Data.Command_Login_ext)).not.toThrow(); - - // Simulate unexpected socket close - const mock = getMockWebSocket(); - mock.readyState = 3; - mock.onclose?.({ code: 1006, reason: '', wasClean: false } as CloseEvent); - - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); - }); -}); \ No newline at end of file +}); diff --git a/webclient/integration/src/deck.spec.ts b/webclient/integration/src/deck.spec.ts deleted file mode 100644 index 76ed032be..000000000 --- a/webclient/integration/src/deck.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Deck and replay command round-trips — validates the session command pipeline -// for deck CRUD and replay operations end-to-end. - -import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; - -import { Data } from '@app/types'; -import { store } from '@app/store'; -import { SessionCommands } from '@app/websocket'; - -import { connectAndLogin } from './helpers/setup'; -import { - buildResponse, - buildResponseMessage, - deliverMessage, -} from './helpers/protobuf-builders'; -import { findLastSessionCommand } from './helpers/command-capture'; - -describe('deck operations', () => { - it('populates backendDecks from deckList response', () => { - connectAndLogin(); - - SessionCommands.deckList(); - - const { cmdId } = findLastSessionCommand(Data.Command_DeckList_ext); - - const deckFile = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { - id: 1, - name: 'MyDeck.cod', - file: create(Data.ServerInfo_DeckStorage_FileSchema, { creationTime: 1000 }), - }); - const root = create(Data.ServerInfo_DeckStorage_FolderSchema, { - items: [deckFile], - }); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_DeckList_ext, - value: create(Data.Response_DeckListSchema, { root }), - }))); - - const backendDecks = store.getState().server.backendDecks; - expect(backendDecks).not.toBeNull(); - expect(backendDecks?.root?.items).toHaveLength(1); - expect(backendDecks?.root?.items[0]?.name).toBe('MyDeck.cod'); - }); - - it('populates downloadedDeck from deckDownload response', () => { - connectAndLogin(); - - SessionCommands.deckDownload(42); - - const { cmdId } = findLastSessionCommand(Data.Command_DeckDownload_ext); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_DeckDownload_ext, - value: create(Data.Response_DeckDownloadSchema, { deck: '4 Lightning Bolt\n20 Mountain' }), - }))); - - const downloaded = store.getState().server.downloadedDeck; - expect(downloaded).not.toBeNull(); - expect(downloaded?.deckId).toBe(42); - expect(downloaded?.deck).toContain('Lightning Bolt'); - }); -}); - -describe('replay operations', () => { - it('populates replays from replayList response', () => { - connectAndLogin(); - - SessionCommands.replayList(); - - const { cmdId } = findLastSessionCommand(Data.Command_ReplayList_ext); - - const match = create(Data.ServerInfo_ReplayMatchSchema, { - gameId: 99, - gameName: 'Casual Game', - }); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_ReplayList_ext, - value: create(Data.Response_ReplayListSchema, { matchList: [match] }), - }))); - - const replays = store.getState().server.replays; - expect(replays[99]).toBeDefined(); - expect(replays[99].gameName).toBe('Casual Game'); - }); - - it('removes replay from state on replayDeleteMatch round-trip', () => { - connectAndLogin(); - - // First populate a replay - SessionCommands.replayList(); - const list = findLastSessionCommand(Data.Command_ReplayList_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: list.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_ReplayList_ext, - value: create(Data.Response_ReplayListSchema, { - matchList: [create(Data.ServerInfo_ReplayMatchSchema, { gameId: 99, gameName: 'Old Game' })], - }), - }))); - expect(store.getState().server.replays[99]).toBeDefined(); - - // Now delete it - SessionCommands.replayDeleteMatch(99); - const del = findLastSessionCommand(Data.Command_ReplayDeleteMatch_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: del.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().server.replays[99]).toBeUndefined(); - }); -}); \ No newline at end of file diff --git a/webclient/integration/src/game.spec.ts b/webclient/integration/src/game.spec.ts deleted file mode 100644 index eb88e6b2b..000000000 --- a/webclient/integration/src/game.spec.ts +++ /dev/null @@ -1,416 +0,0 @@ -// Game scenarios — game join, state initialization, card operations, -// player counters, game chat, game close, and outbound game commands. - -import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; - -import { Data } from '@app/types'; -import { store } from '@app/store'; -import { GameCommands, RoomCommands } from '@app/websocket'; - -import { connectAndHandshake, connectAndLogin } from './helpers/setup'; -import { - buildResponse, - buildResponseMessage, - buildSessionEventMessage, - buildRoomEventMessage, - buildGameEventMessage, - deliverMessage, -} from './helpers/protobuf-builders'; -import { findLastGameCommand, findLastRoomCommand, findLastSessionCommand } from './helpers/command-capture'; - -function joinGame(gameId: number): void { - deliverMessage(buildSessionEventMessage( - Data.Event_GameJoined_ext, - create(Data.Event_GameJoinedSchema, { - gameInfo: create(Data.ServerInfo_GameSchema, { - gameId, - description: 'Test Game', - maxPlayers: 2, - playerCount: 1, - }), - playerId: 1, - hostId: 1, - spectator: false, - judge: false, - resuming: false, - }) - )); -} - -function setupGameState(gameId: number): void { - const deckCard = create(Data.ServerInfo_CardSchema, { id: 100, name: 'Forest' }); - const handCard = create(Data.ServerInfo_CardSchema, { id: 101, name: 'Lightning Bolt' }); - - const deckZone = create(Data.ServerInfo_ZoneSchema, { - name: 'deck', - type: Data.ServerInfo_Zone_ZoneType.HiddenZone, - cardList: [deckCard], - }); - const handZone = create(Data.ServerInfo_ZoneSchema, { - name: 'hand', - type: Data.ServerInfo_Zone_ZoneType.HiddenZone, - cardList: [handCard], - }); - const tableZone = create(Data.ServerInfo_ZoneSchema, { - name: 'table', - type: Data.ServerInfo_Zone_ZoneType.PublicZone, - withCoords: true, - cardList: [], - }); - - const player = create(Data.ServerInfo_PlayerSchema, { - properties: create(Data.ServerInfo_PlayerPropertiesSchema, { - playerId: 1, - userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), - }), - zoneList: [deckZone, handZone, tableZone], - counterList: [], - arrowList: [], - }); - - deliverMessage(buildGameEventMessage({ - gameId, - playerId: -1, - ext: Data.Event_GameStateChanged_ext, - value: create(Data.Event_GameStateChangedSchema, { - playerList: [player], - gameStarted: true, - activePlayerId: 1, - activePhase: 0, - }), - })); -} - -describe('game', () => { - it('initializes game state from Event_GameJoined + Event_GameStateChanged', () => { - connectAndLogin(); - joinGame(42); - - const game = store.getState().games.games[42]; - expect(game).toBeDefined(); - expect(game.info.description).toBe('Test Game'); - expect(game.localPlayerId).toBe(1); - - setupGameState(42); - - const updated = store.getState().games.games[42]; - expect(updated.started).toBe(true); - expect(updated.activePlayerId).toBe(1); - expect(updated.players[1]).toBeDefined(); - expect(updated.players[1].zones.hand).toBeDefined(); - expect(updated.players[1].zones.deck).toBeDefined(); - expect(updated.players[1].zones.hand.order).toContain(101); - expect(updated.players[1].zones.deck.order).toContain(100); - }); - - it('draws cards from deck to hand on Event_DrawCards', () => { - connectAndLogin(); - joinGame(42); - setupGameState(42); - - const drawnCard = create(Data.ServerInfo_CardSchema, { id: 200, name: 'Mountain' }); - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: 1, - ext: Data.Event_DrawCards_ext, - value: create(Data.Event_DrawCardsSchema, { - number: 1, - cards: [drawnCard], - }), - })); - - const player = store.getState().games.games[42].players[1]; - expect(player.zones.hand.order).toContain(200); - expect(player.zones.hand.byId[200]?.name).toBe('Mountain'); - }); - - it('appends chat messages on Event_GameSay', () => { - connectAndLogin(); - joinGame(42); - - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: 1, - ext: Data.Event_GameSay_ext, - value: create(Data.Event_GameSaySchema, { message: 'good game' }), - })); - - const messages = store.getState().games.games[42].messages; - expect(messages).toHaveLength(1); - expect(messages[0].message).toBe('good game'); - expect(messages[0].playerId).toBe(1); - }); - - it('removes game from store on Event_GameClosed', () => { - connectAndLogin(); - joinGame(42); - - expect(store.getState().games.games[42]).toBeDefined(); - - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: -1, - ext: Data.Event_GameClosed_ext, - value: create(Data.Event_GameClosedSchema), - })); - - expect(store.getState().games.games[42]).toBeUndefined(); - }); - - it('sends outbound Command_GameSay with correct gameId and message', () => { - connectAndLogin(); - joinGame(42); - - GameCommands.gameSay(42, { message: 'hello opponent' }); - - const { value, cmdId } = findLastGameCommand(Data.Command_GameSay_ext); - expect(value.message).toBe('hello opponent'); - expect(cmdId).toBeGreaterThan(0); - }); - - it('moves a card from hand to table on Event_MoveCard', () => { - connectAndLogin(); - joinGame(42); - setupGameState(42); - - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: 1, - ext: Data.Event_MoveCard_ext, - value: create(Data.Event_MoveCardSchema, { - cardId: 101, - cardName: 'Lightning Bolt', - startPlayerId: 1, - startZone: 'hand', - targetPlayerId: 1, - targetZone: 'table', - x: 100, - y: 200, - faceDown: false, - newCardId: 101, - }), - })); - - const player = store.getState().games.games[42].players[1]; - expect(player.zones.hand.order).not.toContain(101); - expect(player.zones.table.order).toContain(101); - expect(player.zones.table.byId[101]?.name).toBe('Lightning Bolt'); - expect(player.zones.table.byId[101]?.x).toBe(100); - }); - - it('creates and updates player counters', () => { - connectAndLogin(); - joinGame(42); - setupGameState(42); - - const counterInfo = create(Data.ServerInfo_CounterSchema, { - id: 1, - name: 'Life', - count: 20, - radius: 1, - }); - - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: 1, - ext: Data.Event_CreateCounter_ext, - value: create(Data.Event_CreateCounterSchema, { counterInfo }), - })); - - const player = store.getState().games.games[42].players[1]; - expect(player.counters[1]).toBeDefined(); - expect(player.counters[1].name).toBe('Life'); - expect(player.counters[1].count).toBe(20); - - deliverMessage(buildGameEventMessage({ - gameId: 42, - playerId: 1, - ext: Data.Event_SetCounter_ext, - value: create(Data.Event_SetCounterSchema, { counterId: 1, value: 17 }), - })); - - expect(store.getState().games.games[42].players[1].counters[1].count).toBe(17); - }); - - it('full lifecycle: create → join → deck select → draw → chat → discard → concede → leave', () => { - connectAndHandshake(); - - // ── Setup: join a room so we can create a game in it ────────────────── - deliverMessage(buildSessionEventMessage( - Data.Event_ListRooms_ext, - create(Data.Event_ListRoomsSchema, { - roomList: [create(Data.ServerInfo_RoomSchema, { roomId: 1, autoJoin: true, gameList: [], userList: [], gametypeList: [] })], - }) - )); - const roomJoin = findLastSessionCommand(Data.Command_JoinRoom_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: roomJoin.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_JoinRoom_ext, - value: create(Data.Response_JoinRoomSchema, { - roomInfo: create(Data.ServerInfo_RoomSchema, { roomId: 1, gameList: [], userList: [], gametypeList: [] }), - }), - }))); - - // ── 1. Create game ─────────────────────────────────────────────────── - RoomCommands.createGame(1, { description: 'Ranked Match', maxPlayers: 2 }); - const createCmd = findLastRoomCommand(Data.Command_CreateGame_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: createCmd.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - // ── 2. Join game ───────────────────────────────────────────────────── - RoomCommands.joinGame(1, { gameId: 99 }); - const joinCmd = findLastRoomCommand(Data.Command_JoinGame_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: joinCmd.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - expect(store.getState().rooms.joinedGameIds[1]?.[99]).toBe(true); - - // Server sends Event_GameJoined (session event) - deliverMessage(buildSessionEventMessage( - Data.Event_GameJoined_ext, - create(Data.Event_GameJoinedSchema, { - gameInfo: create(Data.ServerInfo_GameSchema, { gameId: 99, description: 'Ranked Match', maxPlayers: 2 }), - playerId: 1, - hostId: 1, - spectator: false, - judge: false, - resuming: false, - }) - )); - expect(store.getState().games.games[99]).toBeDefined(); - - // ── 3. Select deck ─────────────────────────────────────────────────── - GameCommands.deckSelect(99, { deck: '4 Lightning Bolt\n20 Mountain\n4 Goblin Guide' }); - const deckCmd = findLastGameCommand(Data.Command_DeckSelect_ext); - expect(deckCmd.value.deck).toContain('Lightning Bolt'); - - // Server responds with full game state (deck in zones) - const deckCards = [ - create(Data.ServerInfo_CardSchema, { id: 1, name: 'Lightning Bolt' }), - create(Data.ServerInfo_CardSchema, { id: 2, name: 'Mountain' }), - create(Data.ServerInfo_CardSchema, { id: 3, name: 'Goblin Guide' }), - ]; - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: -1, - ext: Data.Event_GameStateChanged_ext, - value: create(Data.Event_GameStateChangedSchema, { - playerList: [create(Data.ServerInfo_PlayerSchema, { - properties: create(Data.ServerInfo_PlayerPropertiesSchema, { - playerId: 1, - userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), - }), - zoneList: [ - create(Data.ServerInfo_ZoneSchema, { name: 'deck', type: Data.ServerInfo_Zone_ZoneType.HiddenZone, cardList: deckCards, cardCount: 3 }), - create(Data.ServerInfo_ZoneSchema, { name: 'hand', type: Data.ServerInfo_Zone_ZoneType.HiddenZone, cardList: [], cardCount: 0 }), - create(Data.ServerInfo_ZoneSchema, { name: 'table', type: Data.ServerInfo_Zone_ZoneType.PublicZone, withCoords: true, cardList: [], cardCount: 0 }), - create(Data.ServerInfo_ZoneSchema, { name: 'grave', type: Data.ServerInfo_Zone_ZoneType.PublicZone, cardList: [], cardCount: 0 }), - ], - counterList: [], - arrowList: [], - })], - gameStarted: true, - activePlayerId: 1, - activePhase: 0, - }), - })); - - const gameAfterDeck = store.getState().games.games[99]; - expect(gameAfterDeck.players[1].zones.deck.order).toHaveLength(3); - expect(gameAfterDeck.players[1].zones.hand.order).toHaveLength(0); - - // ── 4. Draw cards ──────────────────────────────────────────────────── - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: 1, - ext: Data.Event_DrawCards_ext, - value: create(Data.Event_DrawCardsSchema, { - number: 2, - cards: [ - create(Data.ServerInfo_CardSchema, { id: 1, name: 'Lightning Bolt' }), - create(Data.ServerInfo_CardSchema, { id: 2, name: 'Mountain' }), - ], - }), - })); - - const afterDraw = store.getState().games.games[99].players[1]; - expect(afterDraw.zones.hand.order).toHaveLength(2); - expect(afterDraw.zones.hand.order).toContain(1); - expect(afterDraw.zones.hand.order).toContain(2); - expect(afterDraw.zones.deck.cardCount).toBe(1); - - // ── 5. Send game message ───────────────────────────────────────────── - GameCommands.gameSay(99, { message: 'good luck!' }); - const sayCmd = findLastGameCommand(Data.Command_GameSay_ext); - expect(sayCmd.value.message).toBe('good luck!'); - - // Server echoes the message back - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: 1, - ext: Data.Event_GameSay_ext, - value: create(Data.Event_GameSaySchema, { message: 'good luck!' }), - })); - expect(store.getState().games.games[99].messages).toHaveLength(1); - - // ── 6. Discard (move card from hand to graveyard) ──────────────────── - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: 1, - ext: Data.Event_MoveCard_ext, - value: create(Data.Event_MoveCardSchema, { - cardId: 1, - cardName: 'Lightning Bolt', - startPlayerId: 1, - startZone: 'hand', - targetPlayerId: 1, - targetZone: 'grave', - faceDown: false, - newCardId: 1, - }), - })); - - const afterDiscard = store.getState().games.games[99].players[1]; - expect(afterDiscard.zones.hand.order).not.toContain(1); - expect(afterDiscard.zones.grave.order).toContain(1); - expect(afterDiscard.zones.grave.byId[1]?.name).toBe('Lightning Bolt'); - - // ── 7. Concede ─────────────────────────────────────────────────────── - GameCommands.concede(99); - expect(() => findLastGameCommand(Data.Command_Concede_ext)).not.toThrow(); - - // Server confirms concession - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: 1, - ext: Data.Event_PlayerPropertiesChanged_ext, - value: create(Data.Event_PlayerPropertiesChangedSchema, { - playerProperties: create(Data.ServerInfo_PlayerPropertiesSchema, { - playerId: 1, - conceded: true, - userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), - }), - }), - })); - expect(store.getState().games.games[99].players[1].properties.conceded).toBe(true); - - // ── 8. Leave game ──────────────────────────────────────────────────── - GameCommands.leaveGame(99); - expect(() => findLastGameCommand(Data.Command_LeaveGame_ext)).not.toThrow(); - - // Server confirms player left - deliverMessage(buildGameEventMessage({ - gameId: 99, - playerId: 1, - ext: Data.Event_Leave_ext, - value: create(Data.Event_LeaveSchema, { reason: Data.Event_Leave_LeaveReason.USER_LEFT }), - })); - - expect(store.getState().games.games[99].players[1]).toBeUndefined(); - }); -}); \ No newline at end of file diff --git a/webclient/integration/src/helpers/command-capture.ts b/webclient/integration/src/helpers/command-capture.ts index b0d93cd2f..0b48f4df6 100644 --- a/webclient/integration/src/helpers/command-capture.ts +++ b/webclient/integration/src/helpers/command-capture.ts @@ -11,12 +11,10 @@ import { Data } from '@app/types'; import { getMockWebSocket } from './setup'; -/** The command scopes a CommandContainer can carry in practice. */ +/** The three command scopes a CommandContainer can carry in practice. */ type SessionCmd = Data.SessionCommand; type RoomCmd = Data.RoomCommand; type GameCmd = Data.GameCommand; -type AdminCmd = Data.AdminCommand; -type ModeratorCmd = Data.ModeratorCommand; /** Decode every CommandContainer sent through the mock socket so far. */ export function captureAllOutbound(): Data.CommandContainer[] { @@ -112,47 +110,3 @@ export function findLastGameCommand( `No outbound game command with extension ${ext.typeName} has been sent.` ); } - -/** Admin-scoped equivalent of {@link findLastSessionCommand}. */ -export function findLastAdminCommand( - ext: GenExtension -): { container: Data.CommandContainer; value: V; cmdId: number } { - const containers = captureAllOutbound(); - for (let i = containers.length - 1; i >= 0; i--) { - const container = containers[i]; - for (const adminCmd of container.adminCommand ?? []) { - if (hasExtension(adminCmd, ext)) { - return { - container, - value: getExtension(adminCmd, ext), - cmdId: Number(container.cmdId), - }; - } - } - } - throw new Error( - `No outbound admin command with extension ${ext.typeName} has been sent.` - ); -} - -/** Moderator-scoped equivalent of {@link findLastSessionCommand}. */ -export function findLastModeratorCommand( - ext: GenExtension -): { container: Data.CommandContainer; value: V; cmdId: number } { - const containers = captureAllOutbound(); - for (let i = containers.length - 1; i >= 0; i--) { - const container = containers[i]; - for (const modCmd of container.moderatorCommand ?? []) { - if (hasExtension(modCmd, ext)) { - return { - container, - value: getExtension(modCmd, ext), - cmdId: Number(container.cmdId), - }; - } - } - } - throw new Error( - `No outbound moderator command with extension ${ext.typeName} has been sent.` - ); -} diff --git a/webclient/integration/src/helpers/setup.ts b/webclient/integration/src/helpers/setup.ts index 8e4413d98..99d351311 100644 --- a/webclient/integration/src/helpers/setup.ts +++ b/webclient/integration/src/helpers/setup.ts @@ -14,16 +14,10 @@ import { create } from '@bufbuild/protobuf'; import { afterEach, beforeEach, vi } from 'vitest'; import { ServerDispatch, RoomsDispatch, GameDispatch } from '@app/store'; -import { Data } from '@app/types'; -import { - WebClient, - StatusEnum, - WebSocketConnectReason, - setPendingOptions, -} from '@app/websocket'; -import type { WebSocketConnectOptions } from '@app/websocket'; +import { App, Data, Enriched } from '@app/types'; +import { WebClient } from '@app/websocket'; import { PROTOCOL_VERSION } from '../../../src/websocket/config'; -import { createWebClientRequest, createWebClientResponse } from '@app/api'; +import { createWebClientResponse, createWebClientRequest } from '@app/api'; import { buildResponse, @@ -33,8 +27,6 @@ import { } from './protobuf-builders'; import { findLastSessionCommand } from './command-capture'; -export { setPendingOptions }; - export interface MockWebSocketInstance { send: ReturnType; close: ReturnType; @@ -105,7 +97,8 @@ function resetAll(): void { } client.protobuf.resetCommands(); - client.status = StatusEnum.DISCONNECTED; + client.options = null; + client.status = App.StatusEnum.DISCONNECTED; ServerDispatch.clearStore(); RoomsDispatch.clearStore(); @@ -124,8 +117,8 @@ function resetAll(): void { // ── Shared connect helpers ────────────────────────────────────────────────── -const DEFAULT_LOGIN_OPTIONS: WebSocketConnectOptions = { - reason: WebSocketConnectReason.LOGIN, +const DEFAULT_LOGIN_OPTIONS: Enriched.LoginConnectOptions = { + reason: App.WebSocketConnectReason.LOGIN, host: 'localhost', port: '4748', userName: 'alice', @@ -133,16 +126,14 @@ const DEFAULT_LOGIN_OPTIONS: WebSocketConnectOptions = { }; export function connectRaw( - overrides: Partial = {} + overrides: Partial = {} ): void { - const opts = { ...DEFAULT_LOGIN_OPTIONS, ...overrides }; - setPendingOptions(opts as WebSocketConnectOptions); - getWebClient().connect({ host: opts.host, port: opts.port }); + getWebClient().connect({ ...DEFAULT_LOGIN_OPTIONS, ...overrides }); openMockWebSocket(); } export function connectAndHandshake( - overrides: Partial = {} + overrides: Partial = {} ): void { connectRaw(overrides); deliverMessage(buildSessionEventMessage( @@ -155,21 +146,6 @@ export function connectAndHandshake( )); } -export function connectAndHandshakeWithSalt( - overrides: Partial = {} -): void { - connectRaw(overrides); - deliverMessage(buildSessionEventMessage( - Data.Event_ServerIdentification_ext, - create(Data.Event_ServerIdentificationSchema, { - serverName: 'TestServer', - serverVersion: '2.8.0', - protocolVersion: PROTOCOL_VERSION, - serverOptions: Data.Event_ServerIdentification_ServerOptions.SupportsPasswordHash, - }) - )); -} - export function connectAndLogin(userName: string = 'alice'): void { connectAndHandshake({ userName }); @@ -196,7 +172,7 @@ installMockWebSocket(); beforeEach(() => { vi.useFakeTimers(); - new WebClient(createWebClientRequest(), createWebClientResponse()); + new WebClient(createWebClientResponse(), createWebClientRequest()); }); afterEach(() => { diff --git a/webclient/integration/src/keep-alive.spec.ts b/webclient/integration/src/keep-alive.spec.ts index c4889e0fc..521d4dce9 100644 --- a/webclient/integration/src/keep-alive.spec.ts +++ b/webclient/integration/src/keep-alive.spec.ts @@ -2,9 +2,8 @@ import { describe, expect, it } from 'vitest'; -import { Data } from '@app/types'; +import { App, Data } from '@app/types'; import { store } from '@app/store'; -import { StatusEnum } from '@app/websocket'; import { connectRaw, getMockWebSocket } from './helpers/setup'; import { @@ -32,7 +31,7 @@ describe('keep-alive', () => { vi.advanceTimersByTime(5000); const second = findLastSessionCommand(Data.Command_Ping_ext); expect(second.cmdId).toBeGreaterThan(first.cmdId); - expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); }); it('stays CONNECTED while pongs arrive before the next tick', () => { @@ -47,7 +46,7 @@ describe('keep-alive', () => { }))); } - expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); expect(getMockWebSocket().close).not.toHaveBeenCalled(); }); @@ -56,11 +55,11 @@ describe('keep-alive', () => { vi.advanceTimersByTime(5000); expect(() => findLastSessionCommand(Data.Command_Ping_ext)).not.toThrow(); - expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); vi.advanceTimersByTime(5000); expect(getMockWebSocket().close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); }); }); diff --git a/webclient/integration/src/moderator.spec.ts b/webclient/integration/src/moderator.spec.ts deleted file mode 100644 index c90c3b026..000000000 --- a/webclient/integration/src/moderator.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Moderator command pipeline smoke tests — validates that sendModeratorCommand -// encodes, correlates, and persists correctly end-to-end. One test per -// distinct response pattern (simple vs. extension-payload). - -import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; - -import { Data } from '@app/types'; -import { store } from '@app/store'; -import { ModeratorCommands } from '@app/websocket'; - -import { connectAndLogin } from './helpers/setup'; -import { - buildResponse, - buildResponseMessage, - deliverMessage, -} from './helpers/protobuf-builders'; -import { findLastModeratorCommand } from './helpers/command-capture'; - -describe('moderator commands', () => { - it('getBanHistory populates server.banHistory on success', () => { - connectAndLogin(); - - ModeratorCommands.getBanHistory('baduser'); - - const { cmdId, value } = findLastModeratorCommand(Data.Command_GetBanHistory_ext); - expect(value.userName).toBe('baduser'); - - const banEntry = create(Data.ServerInfo_BanSchema, { - adminId: 'admin1', - adminName: 'Admin', - banTime: '2026-01-01', - banLength: '60', - visibleReason: 'spamming', - }); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_BanHistory_ext, - value: create(Data.Response_BanHistorySchema, { banList: [banEntry] }), - }))); - - const history = store.getState().server.banHistory.baduser; - expect(history).toHaveLength(1); - expect(history[0].visibleReason).toBe('spamming'); - }); - - it('viewLogHistory populates server.logs on success', () => { - connectAndLogin(); - - ModeratorCommands.viewLogHistory({ dateRange: 30 }); - - const { cmdId } = findLastModeratorCommand(Data.Command_ViewLogHistory_ext); - - const logMsg = create(Data.ServerInfo_ChatMessageSchema, { - senderName: 'alice', - message: 'test message', - }); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_ViewLogHistory_ext, - value: create(Data.Response_ViewLogHistorySchema, { logMessage: [logMsg] }), - }))); - - const logs = store.getState().server.logs; - expect(Object.keys(logs).length).toBeGreaterThan(0); - }); - - it('warnUser sends command and updates state on success', () => { - connectAndLogin(); - - ModeratorCommands.warnUser('troublemaker', 'spamming chat'); - - const { cmdId, value } = findLastModeratorCommand(Data.Command_WarnUser_ext); - expect(value.userName).toBe('troublemaker'); - expect(value.reason).toBe('spamming chat'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().server.warnUser).toBe('troublemaker'); - }); - - it('banFromServer sends command and updates state on success', () => { - connectAndLogin(); - - ModeratorCommands.banFromServer(60, 'baduser', undefined, 'repeated offenses', 'rule violation'); - - const { cmdId, value } = findLastModeratorCommand(Data.Command_BanFromServer_ext); - expect(value.userName).toBe('baduser'); - expect(value.minutes).toBe(60); - expect(value.visibleReason).toBe('rule violation'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().server.banUser).toBe('baduser'); - }); -}); \ No newline at end of file diff --git a/webclient/integration/src/password-reset.spec.ts b/webclient/integration/src/password-reset.spec.ts deleted file mode 100644 index ec842c3ec..000000000 --- a/webclient/integration/src/password-reset.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Password-reset scenarios — the 3-step forgot-password flow. Each step -// is a separate connect → handshake → command → disconnect cycle. - -import { create } from '@bufbuild/protobuf'; -import { describe, expect, it } from 'vitest'; - -import { Data } from '@app/types'; -import { store } from '@app/store'; -import { StatusEnum, WebSocketConnectReason } from '@app/websocket'; - -import { connectAndHandshake } from './helpers/setup'; -import { - buildResponse, - buildResponseMessage, - deliverMessage, -} from './helpers/protobuf-builders'; -import { findLastSessionCommand } from './helpers/command-capture'; - -describe('password reset', () => { - it('forgotPasswordRequest sends command and disconnects on success', () => { - connectAndHandshake({ - reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST as const, - host: 'localhost', - port: '4748', - userName: 'alice', - }); - - const req = findLastSessionCommand(Data.Command_ForgotPasswordRequest_ext); - expect(req.value.userName).toBe('alice'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: req.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_ForgotPasswordRequest_ext, - value: create(Data.Response_ForgotPasswordRequestSchema, { - challengeEmail: 'a@example.com', - }), - }))); - - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); - }); - - it('forgotPasswordChallenge sends command with userName and email', () => { - connectAndHandshake({ - reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE as const, - host: 'localhost', - port: '4748', - userName: 'alice', - email: 'alice@example.com', - }); - - const challenge = findLastSessionCommand(Data.Command_ForgotPasswordChallenge_ext); - expect(challenge.value.userName).toBe('alice'); - expect(challenge.value.email).toBe('alice@example.com'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: challenge.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); - }); - - it('forgotPasswordReset sends command with userName, token, and newPassword', () => { - connectAndHandshake({ - reason: WebSocketConnectReason.PASSWORD_RESET as const, - host: 'localhost', - port: '4748', - userName: 'alice', - token: 'reset-token-123', - newPassword: 'new-secret', - }); - - const reset = findLastSessionCommand(Data.Command_ForgotPasswordReset_ext); - expect(reset.value.userName).toBe('alice'); - expect(reset.value.token).toBe('reset-token-123'); - expect(reset.value.newPassword).toBe('new-secret'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: reset.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); - }); -}); \ No newline at end of file diff --git a/webclient/integration/src/rooms.spec.ts b/webclient/integration/src/rooms.spec.ts index 5bd776d08..7e9ed519f 100644 --- a/webclient/integration/src/rooms.spec.ts +++ b/webclient/integration/src/rooms.spec.ts @@ -1,12 +1,11 @@ // Room scenarios — Event_ListRooms handling, auto-join, Response_JoinRoom, -// room chat (inbound + outbound), game list updates, and leaveRoom. +// room chat, and in-room game list updates. import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; import { Data } from '@app/types'; import { store } from '@app/store'; -import { RoomCommands } from '@app/websocket'; import { connectAndHandshake } from './helpers/setup'; import { @@ -16,8 +15,7 @@ import { buildSessionEventMessage, deliverMessage, } from './helpers/protobuf-builders'; -import { findLastSessionCommand, findLastRoomCommand, captureAllOutbound } from './helpers/command-capture'; -import { fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; +import { findLastSessionCommand } from './helpers/command-capture'; function makeRoom(overrides: Partial<{ roomId: number; @@ -37,21 +35,6 @@ function makeRoom(overrides: Partial<{ }); } -/** Deliver Event_ListRooms then join a single auto-join room, returning the roomId. */ -function setupJoinedRoom(roomId = 1): void { - deliverMessage(buildSessionEventMessage( - Data.Event_ListRooms_ext, - create(Data.Event_ListRoomsSchema, { roomList: [makeRoom({ roomId, autoJoin: true })] }) - )); - const join = findLastSessionCommand(Data.Command_JoinRoom_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: join.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - ext: Data.Response_JoinRoom_ext, - value: create(Data.Response_JoinRoomSchema, { roomInfo: makeRoom({ roomId }) }), - }))); -} - describe('rooms', () => { it('populates rooms state from Event_ListRooms', () => { connectAndHandshake(); @@ -98,7 +81,18 @@ describe('rooms', () => { it('appends a room chat message on Event_RoomSay', () => { connectAndHandshake(); - setupJoinedRoom(1); + + deliverMessage(buildSessionEventMessage( + Data.Event_ListRooms_ext, + create(Data.Event_ListRoomsSchema, { roomList: [makeRoom({ roomId: 1, autoJoin: true })] }) + )); + const join = findLastSessionCommand(Data.Command_JoinRoom_ext); + deliverMessage(buildResponseMessage(buildResponse({ + cmdId: join.cmdId, + responseCode: Data.Response_ResponseCode.RespOk, + ext: Data.Response_JoinRoom_ext, + value: create(Data.Response_JoinRoomSchema, { roomInfo: makeRoom({ roomId: 1 }) }), + }))); const say = create(Data.Event_RoomSaySchema, { name: 'bob', @@ -115,7 +109,18 @@ describe('rooms', () => { it('updates the game list on Event_ListGames', () => { connectAndHandshake(); - setupJoinedRoom(1); + + deliverMessage(buildSessionEventMessage( + Data.Event_ListRooms_ext, + create(Data.Event_ListRoomsSchema, { roomList: [makeRoom({ roomId: 1, autoJoin: true })] }) + )); + const join = findLastSessionCommand(Data.Command_JoinRoom_ext); + deliverMessage(buildResponseMessage(buildResponse({ + cmdId: join.cmdId, + responseCode: Data.Response_ResponseCode.RespOk, + ext: Data.Response_JoinRoom_ext, + value: create(Data.Response_JoinRoomSchema, { roomInfo: makeRoom({ roomId: 1 }) }), + }))); const game = create(Data.ServerInfo_GameSchema, { gameId: 42, @@ -132,102 +137,4 @@ describe('rooms', () => { expect(roomGames?.[42]?.info?.description).toBe('Test Game'); expect(roomGames?.[42]?.info?.gameId).toBe(42); }); - - it('auto-join filters correctly across multiple rooms', () => { - connectAndHandshake(); - - deliverMessage(buildSessionEventMessage( - Data.Event_ListRooms_ext, - create(Data.Event_ListRoomsSchema, { - roomList: [ - makeRoom({ roomId: 1, name: 'Lobby', autoJoin: true }), - makeRoom({ roomId: 2, name: 'Legacy', autoJoin: false }), - makeRoom({ roomId: 3, name: 'Modern', autoJoin: true }), - ], - }) - )); - - // Count outbound JoinRoom commands - const containers = captureAllOutbound(); - const joinCommands: number[] = []; - for (const container of containers) { - for (const cmd of container.sessionCommand ?? []) { - if (hasExtension(cmd, Data.Command_JoinRoom_ext)) { - joinCommands.push(getExtension(cmd, Data.Command_JoinRoom_ext).roomId); - } - } - } - expect(joinCommands).toHaveLength(2); - expect(joinCommands).toContain(1); - expect(joinCommands).toContain(3); - expect(joinCommands).not.toContain(2); - }); - - it('sends outbound Command_RoomSay with trimmed message', () => { - connectAndHandshake(); - setupJoinedRoom(1); - - RoomCommands.roomSay(1, ' hello '); - - const { value } = findLastRoomCommand(Data.Command_RoomSay_ext); - expect(value.message).toBe('hello'); - }); - - it('removes room from joinedRoomIds on leaveRoom round-trip', () => { - connectAndHandshake(); - setupJoinedRoom(1); - expect(store.getState().rooms.joinedRoomIds[1]).toBe(true); - - RoomCommands.leaveRoom(1); - - const leave = findLastRoomCommand(Data.Command_LeaveRoom_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: leave.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().rooms.joinedRoomIds[1]).toBeUndefined(); - }); - - it('tracks user join and leave within a room', () => { - connectAndHandshake(); - setupJoinedRoom(1); - - deliverMessage(buildRoomEventMessage(1, Data.Event_JoinRoom_ext, create(Data.Event_JoinRoomSchema, { - userInfo: create(Data.ServerInfo_UserSchema, { name: 'bob' }), - }))); - - expect(store.getState().rooms.rooms[1]?.users?.bob).toBeDefined(); - - deliverMessage(buildRoomEventMessage(1, Data.Event_LeaveRoom_ext, create(Data.Event_LeaveRoomSchema, { - name: 'bob', - }))); - - expect(store.getState().rooms.rooms[1]?.users?.bob).toBeUndefined(); - }); - - it('tracks game creation and join within a room', () => { - connectAndHandshake(); - setupJoinedRoom(1); - - RoomCommands.createGame(1, { description: 'Casual', maxPlayers: 2 }); - - const create_ = findLastRoomCommand(Data.Command_CreateGame_ext); - expect(create_.value.description).toBe('Casual'); - - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: create_.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - RoomCommands.joinGame(1, { gameId: 99 }); - - const join = findLastRoomCommand(Data.Command_JoinGame_ext); - deliverMessage(buildResponseMessage(buildResponse({ - cmdId: join.cmdId, - responseCode: Data.Response_ResponseCode.RespOk, - }))); - - expect(store.getState().rooms.joinedGameIds[1]?.[99]).toBe(true); - }); -}); \ No newline at end of file +}); diff --git a/webclient/integration/src/server-events.spec.ts b/webclient/integration/src/server-events.spec.ts index 16ac3a6bf..faa42f11a 100644 --- a/webclient/integration/src/server-events.spec.ts +++ b/webclient/integration/src/server-events.spec.ts @@ -4,9 +4,8 @@ import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; -import { Data } from '@app/types'; +import { App, Data } from '@app/types'; import { store } from '@app/store'; -import { StatusEnum } from '@app/websocket'; import { connectAndHandshake } from './helpers/setup'; import { @@ -73,7 +72,7 @@ describe('server events', () => { )); const status = store.getState().server.status; - expect(status.state).toBe(StatusEnum.DISCONNECTED); + expect(status.state).toBe(App.StatusEnum.DISCONNECTED); expect(status.description).toBe('kicked by admin'); }); diff --git a/webclient/integration/src/users.spec.ts b/webclient/integration/src/users.spec.ts index 36062963d..03005abba 100644 --- a/webclient/integration/src/users.spec.ts +++ b/webclient/integration/src/users.spec.ts @@ -117,4 +117,4 @@ describe('users', () => { expect(messages.bob).toHaveLength(1); expect(messages.bob[0].message).toBe('hey bob'); }); -}); \ No newline at end of file +}); diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts index fb201a02a..0acaf2d93 100644 --- a/webclient/src/api/index.ts +++ b/webclient/src/api/index.ts @@ -1,2 +1,35 @@ +import { WebClient } from '@app/websocket'; +import type { IWebClientRequest } from '@app/websocket'; + export { createWebClientResponse } from './response'; export { createWebClientRequest } from './request'; + +/** + * UI-facing request surface. Each property is a lazy getter that resolves + * `WebClient.instance` at call time, so consumers can import this before the + * singleton is bootstrapped — it only needs to exist by the first actual call. + * + * Prefer this over importing `WebClient` directly: it keeps UI code free of + * transport-layer names and makes `@app/websocket` an internal detail of the + * `api` layer. + */ +export const request: IWebClientRequest = { + get authentication() { + return WebClient.instance.request.authentication; + }, + get session() { + return WebClient.instance.request.session; + }, + get rooms() { + return WebClient.instance.request.rooms; + }, + get admin() { + return WebClient.instance.request.admin; + }, + get moderator() { + return WebClient.instance.request.moderator; + }, + get game() { + return WebClient.instance.request.game; + }, +}; diff --git a/webclient/src/api/request/AuthenticationRequestImpl.ts b/webclient/src/api/request/AuthenticationRequestImpl.ts index 91788d862..9157d736f 100644 --- a/webclient/src/api/request/AuthenticationRequestImpl.ts +++ b/webclient/src/api/request/AuthenticationRequestImpl.ts @@ -1,71 +1,34 @@ -import { - WebClient, - StatusEnum, - SessionCommands, - WebSocketConnectReason, - setPendingOptions, -} from '@app/websocket'; -import type { - IAuthenticationRequest, - AuthRequestMap, - LoginConnectOptions, - TestConnectionOptions, - RegisterConnectOptions, - ActivateConnectOptions, - PasswordResetRequestConnectOptions, - PasswordResetChallengeConnectOptions, - PasswordResetConnectOptions, -} from '@app/websocket'; +import { App, Enriched } from '@app/types'; +import type { IAuthenticationRequest } from '@app/websocket'; +import { SessionCommands } from '@app/websocket'; -interface AppAuthRequestOverrides extends AuthRequestMap { - LoginParams: Omit; - ConnectTarget: Omit; - RegisterParams: Omit; - ActivateParams: Omit; - ForgotPasswordRequestParams: Omit; - ForgotPasswordChallengeParams: Omit; - ForgotPasswordResetParams: Omit; -} - -export class AuthenticationRequestImpl implements IAuthenticationRequest { - login(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.LOGIN }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); +export class AuthenticationRequestImpl implements IAuthenticationRequest { + login(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.LOGIN }); } - testConnection(options: Omit): void { - WebClient.instance.testConnect({ host: options.host, port: options.port }); + testConnection(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.TEST_CONNECTION }); } - register(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.REGISTER }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); + register(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.REGISTER }); } - activateAccount(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.ACTIVATE_ACCOUNT }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); + activateAccount(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); } - resetPasswordRequest(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); + resetPasswordRequest(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); } - resetPasswordChallenge(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); + resetPasswordChallenge(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); } - resetPassword(options: Omit): void { - setPendingOptions({ ...options, reason: WebSocketConnectReason.PASSWORD_RESET }); - SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - WebClient.instance.connect({ host: options.host, port: options.port }); + resetPassword(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET }); } disconnect(): void { diff --git a/webclient/src/api/response/RoomResponseImpl.ts b/webclient/src/api/response/RoomResponseImpl.ts index d450158c0..1a11bc4c5 100644 --- a/webclient/src/api/response/RoomResponseImpl.ts +++ b/webclient/src/api/response/RoomResponseImpl.ts @@ -1,10 +1,8 @@ -import { Data } from '@app/types'; -import type { IRoomResponse, WebSocketRoomResponseOverrides } from '@app/websocket'; +import { Data, Enriched } from '@app/types'; +import type { IRoomResponse } from '@app/websocket'; import { RoomsDispatch } from '@app/store'; -type Message = WebSocketRoomResponseOverrides['Event_RoomSay']; - -export class RoomResponseImpl implements IRoomResponse { +export class RoomResponseImpl implements IRoomResponse { clearStore(): void { RoomsDispatch.clearStore(); } @@ -25,7 +23,7 @@ export class RoomResponseImpl implements IRoomResponse { +export class SessionResponseImpl implements ISessionResponse { initialized(): void { ServerDispatch.initialized(); } @@ -19,7 +15,7 @@ export class SessionResponseImpl implements ISessionResponse { const { touched, error, warning } = meta; const { t } = useTranslation(); - const webClient = useWebClient(); const [hostsState, setHostsState] = useState({ hosts: [], @@ -198,7 +197,7 @@ const KnownHosts = (props) => { setTestingConnection(TestConnection.TESTING); const options = { ...App.getHostPort(hostsState.selectedHost) }; - webClient.request.authentication.testConnection(options); + request.authentication.testConnection(options); } return ( diff --git a/webclient/src/components/Message/Message.tsx b/webclient/src/components/Message/Message.tsx index bb5617242..bc9d640ec 100644 --- a/webclient/src/components/Message/Message.tsx +++ b/webclient/src/components/Message/Message.tsx @@ -1,5 +1,5 @@ - -import React, { useEffect, useState } from 'react'; +// eslint-disable-next-line +import React, { useEffect, useMemo, useState } from 'react'; import { NavLink, generatePath } from 'react-router-dom'; @@ -20,7 +20,7 @@ const ParsedMessage = ({ message }) => { const [messageChunks, setMessageChunks] = useState(null); const [name, setName] = useState(null); - useEffect(() => { + useMemo(() => { const name = message.match(App.MESSAGE_SENDER_REGEX); if (name) { diff --git a/webclient/src/components/UserDisplay/UserDisplay.tsx b/webclient/src/components/UserDisplay/UserDisplay.tsx index c3d46446e..6e38fab5c 100644 --- a/webclient/src/components/UserDisplay/UserDisplay.tsx +++ b/webclient/src/components/UserDisplay/UserDisplay.tsx @@ -6,7 +6,7 @@ import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { Images } from '@app/images'; -import { useWebClient } from '@app/hooks'; +import { request } from '@app/api'; import { ServerSelectors } from '@app/store'; import { App, Data } from '@app/types'; import { useAppSelector } from '@app/store'; @@ -18,7 +18,6 @@ const UserDisplay = ({ user }: UserDisplayProps) => { const buddyList = useAppSelector(state => ServerSelectors.getBuddyList(state)); const ignoreList = useAppSelector(state => ServerSelectors.getIgnoreList(state)); const [position, setPosition] = useState<{ x: number; y: number } | null>(null); - const webClient = useWebClient(); const { name, country } = user; @@ -33,19 +32,19 @@ const UserDisplay = ({ user }: UserDisplayProps) => { const isIgnored = Boolean(ignoreList[user.name]); const onAddBuddy = () => { - webClient.request.session.addToBuddyList(user.name); + request.session.addToBuddyList(user.name); handleClose(); }; const onRemoveBuddy = () => { - webClient.request.session.removeFromBuddyList(user.name); + request.session.removeFromBuddyList(user.name); handleClose(); }; const onAddIgnore = () => { - webClient.request.session.addToIgnoreList(user.name); + request.session.addToIgnoreList(user.name); handleClose(); }; const onRemoveIgnore = () => { - webClient.request.session.removeFromIgnoreList(user.name); + request.session.removeFromIgnoreList(user.name); handleClose(); }; diff --git a/webclient/src/containers/Account/Account.tsx b/webclient/src/containers/Account/Account.tsx index ee06f590a..05d0b1161 100644 --- a/webclient/src/containers/Account/Account.tsx +++ b/webclient/src/containers/Account/Account.tsx @@ -7,7 +7,7 @@ import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from '@app/components'; -import { useWebClient } from '@app/hooks'; +import { request } from '@app/api'; import { ServerSelectors } from '@app/store'; import Layout from '../Layout/Layout'; import { useAppSelector } from '@app/store'; @@ -23,18 +23,17 @@ const Account = () => { const serverName = useAppSelector(state => ServerSelectors.getName(state)); const serverVersion = useAppSelector(state => ServerSelectors.getVersion(state)); const user = useAppSelector(state => ServerSelectors.getUser(state)); - const webClient = useWebClient(); const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user || {}; let url = URL.createObjectURL(new Blob([avatarBmp as BlobPart], { 'type': 'image/png' })); const { t } = useTranslation(); const handleAddToBuddies = ({ userName }) => { - webClient.request.session.addToBuddyList(userName); + request.session.addToBuddyList(userName); }; const handleAddToIgnore = ({ userName }) => { - webClient.request.session.addToIgnoreList(userName); + request.session.addToIgnoreList(userName); }; return ( diff --git a/webclient/src/containers/Layout/LeftNav.tsx b/webclient/src/containers/Layout/LeftNav.tsx index 00d89455b..b9b2d377c 100644 --- a/webclient/src/containers/Layout/LeftNav.tsx +++ b/webclient/src/containers/Layout/LeftNav.tsx @@ -8,8 +8,8 @@ import CloseIcon from '@mui/icons-material/Close'; import MailOutlineRoundedIcon from '@mui/icons-material/MailOutlineRounded'; import MenuRoundedIcon from '@mui/icons-material/MenuRounded'; +import { request } from '@app/api'; import { CardImportDialog } from '@app/dialogs'; -import { useWebClient } from '@app/hooks'; import { Images } from '@app/images'; import { RoomsSelectors, ServerSelectors } from '@app/store'; import { App } from '@app/types'; @@ -28,7 +28,6 @@ const LeftNav = () => { const isConnected = useAppSelector(ServerSelectors.getIsConnected); const isModerator = useAppSelector(ServerSelectors.getIsUserModerator); const navigate = useNavigate(); - const webClient = useWebClient(); const [state, setState] = useState({ anchorEl: null, showCardImportDialog: false, @@ -67,7 +66,7 @@ const LeftNav = () => { const leaveRoom = (event, roomId) => { event.preventDefault(); - webClient.request.rooms.leaveRoom(roomId); + request.rooms.leaveRoom(roomId); }; const openImportCardWizard = () => { diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx index 70896ce77..6727bf89d 100644 --- a/webclient/src/containers/Login/Login.tsx +++ b/webclient/src/containers/Login/Login.tsx @@ -6,10 +6,11 @@ import Button from '@mui/material/Button'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; +import { request } from '@app/api'; import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from '@app/dialogs'; import { LanguageDropdown } from '@app/components'; import { LoginForm } from '@app/forms'; -import { useReduxEffect, useFireOnce, useWebClient } from '@app/hooks'; +import { useReduxEffect, useFireOnce } from '@app/hooks'; import { Images } from '@app/images'; import { HostDTO, serverProps } from '@app/services'; import { App, Enriched } from '@app/types'; @@ -66,7 +67,6 @@ const Root = styled('div')(({ theme }) => ({ const Login = () => { const description = useAppSelector(s => ServerSelectors.getDescription(s)); const isConnected = useAppSelector(ServerSelectors.getIsConnected); - const webClient = useWebClient(); const { t } = useTranslation(); const [pendingActivationOptions, setPendingActivationOptions] = useState(null); @@ -134,7 +134,7 @@ const Login = () => { options.hashedPassword = selectedHost.hashedPassword; } - webClient.request.authentication.login(options); + request.authentication.login(options); }, []); const [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin); @@ -153,7 +153,7 @@ const Login = () => { setRememberLogin(registerForm); const { userName, password, email, country, realName, selectedHost } = registerForm; - webClient.request.authentication.register({ + request.authentication.register({ ...App.getHostPort(selectedHost), userName, password, @@ -167,7 +167,7 @@ const Login = () => { if (!pendingActivationOptions) { return; } - webClient.request.authentication.activateAccount({ + request.authentication.activateAccount({ host: pendingActivationOptions.host, port: pendingActivationOptions.port, userName: pendingActivationOptions.userName, @@ -180,17 +180,17 @@ const Login = () => { const { host, port } = App.getHostPort(selectedHost); if (email) { - webClient.request.authentication.resetPasswordChallenge({ userName, email, host, port }); + request.authentication.resetPasswordChallenge({ userName, email, host, port }); } else { setUserToResetPassword(userName); - webClient.request.authentication.resetPasswordRequest({ userName, host, port }); + request.authentication.resetPasswordRequest({ userName, host, port }); } }; const handleResetPasswordDialogSubmit = ({ userName, token, newPassword, selectedHost }) => { const { host, port } = App.getHostPort(selectedHost); - webClient.request.authentication.resetPassword({ userName, token, newPassword, host, port }); + request.authentication.resetPassword({ userName, token, newPassword, host, port }); }; const skipTokenRequest = (userName) => { diff --git a/webclient/src/containers/Logs/Logs.tsx b/webclient/src/containers/Logs/Logs.tsx index 8ab9a181b..7d592b82c 100644 --- a/webclient/src/containers/Logs/Logs.tsx +++ b/webclient/src/containers/Logs/Logs.tsx @@ -1,9 +1,9 @@ // eslint-disable-next-line import React, { useEffect } from "react"; +import { request } from '@app/api'; import { AuthGuard, ModGuard } from '@app/components'; import { SearchForm } from '@app/forms'; -import { useWebClient } from '@app/hooks'; import { ServerDispatch, ServerSelectors } from '@app/store'; import { Data } from '@app/types'; import { useAppSelector } from '@app/store'; @@ -13,7 +13,6 @@ import './Logs.css'; const Logs = () => { const logs = useAppSelector(state => ServerSelectors.getLogs(state)); - const webClient = useWebClient(); const MAXIMUM_RESULTS = 1000; useEffect(() => { @@ -52,7 +51,7 @@ const Logs = () => { trimmedFields.maximumResults = MAXIMUM_RESULTS; if (required.length) { - webClient.request.moderator.viewLogHistory(trimmedFields); + request.moderator.viewLogHistory(trimmedFields); } else { // @TODO use yet-to-be-implemented banner/alert } diff --git a/webclient/src/containers/Room/Room.tsx b/webclient/src/containers/Room/Room.tsx index 5877c57b5..a0312ae94 100644 --- a/webclient/src/containers/Room/Room.tsx +++ b/webclient/src/containers/Room/Room.tsx @@ -4,8 +4,8 @@ import { useNavigate, useParams, generatePath } from 'react-router-dom'; import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; +import { request } from '@app/api'; import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from '@app/components'; -import { useWebClient } from '@app/hooks'; import { RoomsSelectors } from '@app/store'; import { useAppSelector } from '@app/store'; import { App } from '@app/types'; @@ -29,7 +29,6 @@ const Room = () => { const room = rooms[roomId]; const roomMessages = messages[roomId]; const users = useAppSelector(state => RoomsSelectors.getSortedRoomUsers(state, roomId)); - const webClient = useWebClient(); useEffect(() => { if (!joined.find(r => r.info.roomId === roomId)) { @@ -39,7 +38,7 @@ const Room = () => { const handleRoomSay = ({ message }) => { if (message) { - webClient.request.rooms.roomSay(roomId, message); + request.rooms.roomSay(roomId, message); } } diff --git a/webclient/src/containers/Server/Rooms.tsx b/webclient/src/containers/Server/Rooms.tsx index 1eecce729..c4f16652c 100644 --- a/webclient/src/containers/Server/Rooms.tsx +++ b/webclient/src/containers/Server/Rooms.tsx @@ -9,20 +9,20 @@ import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import { useWebClient } from '@app/hooks'; + +import { request } from '@app/api'; import { App } from '@app/types'; import './Rooms.css'; const Rooms = ({ rooms, joinedRooms }) => { const navigate = useNavigate(); - const webClient = useWebClient(); function onClick(roomId) { if (joinedRooms.find(room => room.info.roomId === roomId)) { navigate(generatePath(App.RouteEnum.ROOM, { roomId })); } else { - webClient.request.rooms.joinRoom(roomId); + request.rooms.joinRoom(roomId); } } diff --git a/webclient/src/hooks/index.ts b/webclient/src/hooks/index.ts index a9385d50b..b5e9cca55 100644 --- a/webclient/src/hooks/index.ts +++ b/webclient/src/hooks/index.ts @@ -3,4 +3,3 @@ export * from './useFireOnce'; export * from './useDebounce'; export * from './useLocaleSort'; export * from './useReduxEffect'; -export * from './useWebClient'; diff --git a/webclient/src/hooks/useWebClient.tsx b/webclient/src/hooks/useWebClient.tsx deleted file mode 100644 index 469f03dc9..000000000 --- a/webclient/src/hooks/useWebClient.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createContext, useContext, useState, ReactNode } from 'react'; -import { WebClient } from '@app/websocket'; -import { createWebClientRequest, createWebClientResponse } from '@app/api'; - -const WebClientContext = createContext(null); - -export function WebClientProvider({ children }: { children: ReactNode }) { - const [client] = useState(() => new WebClient(createWebClientRequest(), createWebClientResponse())); - - return {children}; -} - -export function useWebClient(): WebClient { - const client = useContext(WebClientContext); - if (!client) { - throw new Error('useWebClient must be used within a WebClientProvider'); - } - return client; -} diff --git a/webclient/src/index.tsx b/webclient/src/index.tsx index be51341a4..57165f173 100644 --- a/webclient/src/index.tsx +++ b/webclient/src/index.tsx @@ -2,29 +2,41 @@ // creates the Redux store or connects to Redux DevTools. import './polyfills'; -import { StrictMode } from 'react'; +import { StrictMode, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material'; import { ThemeProvider } from '@mui/material/styles'; -import { WebClientProvider } from '@app/hooks'; +import { WebClient } from '@app/websocket'; +import { createWebClientResponse, createWebClientRequest } from '@app/api'; import { AppShell } from '@app/containers'; import { materialTheme } from './material-theme'; import './i18n'; import './index.css'; +function initWebClient() { + const initialized = useRef(false); + + if (!initialized.current) { + initialized.current = true; + new WebClient(createWebClientResponse(), createWebClientRequest()); + } +} + const AppWithMaterialTheme = () => { + // Instantiate the WebClient singleton before any container renders or any + // hook touches WebClient.instance. + initWebClient(); + return ( - - - - - - - - - + + + + + + + ); } diff --git a/webclient/src/store/game/game.reducer.spec.ts b/webclient/src/store/game/game.reducer.spec.ts index 3d4b8abb8..ccf5c1fd7 100644 --- a/webclient/src/store/game/game.reducer.spec.ts +++ b/webclient/src/store/game/game.reducer.spec.ts @@ -121,12 +121,12 @@ describe('2B: Game state & player management', () => { const state = makeState(); const result = gamesReducer(state, Actions.gameStateChanged({ gameId: 1, - data: create(Data.Event_GameStateChangedSchema, { + data: { gameStarted: true, activePlayerId: 3, activePhase: 2, secondsElapsed: 60, - }), + }, })); expect(result.games[1].started).toBe(true); @@ -394,7 +394,7 @@ describe('2C: CARD_MOVED', () => { expect(moved.providerId).toBe('new-prov'); }); - it('CARD_MOVED → no-ops when targetZone does not exist on player', () => { + it('CARD_MOVED → returns newState (card removed from source) when targetZone does not exist on player', () => { const { state } = stateWithCard(); const result = gamesReducer(state, Actions.cardMoved({ gameId: 1, @@ -414,7 +414,7 @@ describe('2C: CARD_MOVED', () => { newCardProviderId: '', }, })); - expect(cardsIn(result, 1, 1, 'hand')).toHaveLength(1); + expect(cardsIn(result, 1, 1, 'hand')).toHaveLength(0); expect(result.games[1].players[1].zones['nonexistent']).toBeUndefined(); }); }); @@ -850,9 +850,7 @@ describe('2I: Zone operations', () => { const result = gamesReducer(state, Actions.zonePropertiesChanged({ gameId: 1, playerId: 1, - data: create(Data.Event_ChangeZonePropertiesSchema, { - zoneName: 'hand', alwaysRevealTopCard: true, alwaysLookAtTopCard: true, - }), + data: { zoneName: 'hand', alwaysRevealTopCard: true, alwaysLookAtTopCard: true }, })); const zone = result.games[1].players[1].zones['hand']; diff --git a/webclient/src/store/game/game.reducer.ts b/webclient/src/store/game/game.reducer.ts index eff362c32..3e854b48a 100644 --- a/webclient/src/store/game/game.reducer.ts +++ b/webclient/src/store/game/game.reducer.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Data, Enriched } from '@app/types'; -import { create, isFieldSet } from '@bufbuild/protobuf'; +import { create } from '@bufbuild/protobuf'; import { GamesState } from './game.interfaces'; export const MAX_GAME_MESSAGES = 1000; @@ -129,16 +129,16 @@ export const gamesSlice = createSlice({ if (data.playerList?.length > 0) { game.players = normalizePlayers(data.playerList); } - if (isFieldSet(data, Data.Event_GameStateChangedSchema.field.gameStarted)) { + if (data.gameStarted !== undefined && data.gameStarted !== null) { game.started = data.gameStarted; } - if (isFieldSet(data, Data.Event_GameStateChangedSchema.field.activePlayerId)) { + if (data.activePlayerId !== undefined && data.activePlayerId !== null) { game.activePlayerId = data.activePlayerId; } - if (isFieldSet(data, Data.Event_GameStateChangedSchema.field.activePhase)) { + if (data.activePhase !== undefined && data.activePhase !== null) { game.activePhase = data.activePhase; } - if (isFieldSet(data, Data.Event_GameStateChangedSchema.field.secondsElapsed)) { + if (data.secondsElapsed !== undefined) { game.secondsElapsed = data.secondsElapsed; } }, @@ -201,12 +201,6 @@ export const gamesSlice = createSlice({ return; } - const targetPlayer = game.players[targetPlayerId]; - const targetZoneEntry = targetPlayer?.zones[targetZone]; - if (!targetPlayer || !targetZoneEntry) { - return; - } - let resolvedCardId = -1; if (cardId >= 0) { resolvedCardId = cardId; @@ -234,6 +228,12 @@ export const gamesSlice = createSlice({ } : buildEmptyCard(effectiveNewId, cardName, x, y, faceDown, newCardProviderId ?? ''); + const targetPlayer = game.players[targetPlayerId]; + const targetZoneEntry = targetPlayer?.zones[targetZone]; + if (!targetPlayer || !targetZoneEntry) { + return; + } + targetZoneEntry.order.push(movedCard.id); targetZoneEntry.byId[movedCard.id] = movedCard; targetZoneEntry.cardCount++; @@ -432,10 +432,10 @@ export const gamesSlice = createSlice({ if (!zone) { return; } - if (isFieldSet(data, Data.Event_ChangeZonePropertiesSchema.field.alwaysRevealTopCard)) { + if (data.alwaysRevealTopCard !== undefined && data.alwaysRevealTopCard !== null) { zone.alwaysRevealTopCard = data.alwaysRevealTopCard; } - if (isFieldSet(data, Data.Event_ChangeZonePropertiesSchema.field.alwaysLookAtTopCard)) { + if (data.alwaysLookAtTopCard !== undefined && data.alwaysLookAtTopCard !== null) { zone.alwaysLookAtTopCard = data.alwaysLookAtTopCard; } }, diff --git a/webclient/src/store/rooms/rooms.reducer.spec.ts b/webclient/src/store/rooms/rooms.reducer.spec.ts index ea09fefc7..6bf8de4ca 100644 --- a/webclient/src/store/rooms/rooms.reducer.spec.ts +++ b/webclient/src/store/rooms/rooms.reducer.spec.ts @@ -114,12 +114,12 @@ describe('LEAVE_ROOM', () => { // ── ADD_MESSAGE ─────────────────────────────────────────────────────────────── describe('ADD_MESSAGE', () => { - it('appends message preserving the timeReceived from the event handler', () => { + it('appends message with timeReceived set', () => { const state = makeRoomsState({ messages: { 1: [] } }); - const message = makeMessage({ message: 'hello', timeReceived: 1700000000000 }); + const message = makeMessage({ message: 'hello', timeReceived: 0 }); const result = roomsReducer(state, Actions.addMessage({ roomId: 1, message })); expect(result.messages[1]).toHaveLength(1); - expect(result.messages[1][0].timeReceived).toBe(1700000000000); + expect(result.messages[1][0].timeReceived).toBeGreaterThan(0); }); it('creates message list for roomId when none exists', () => { diff --git a/webclient/src/store/rooms/rooms.reducer.tsx b/webclient/src/store/rooms/rooms.reducer.tsx index efd2c916d..3335dabc9 100644 --- a/webclient/src/store/rooms/rooms.reducer.tsx +++ b/webclient/src/store/rooms/rooms.reducer.tsx @@ -69,21 +69,14 @@ export const roomsSlice = createSlice({ const { roomId } = action.payload; delete state.joinedRoomIds[roomId]; - delete state.joinedGameIds[roomId]; delete state.messages[roomId]; - - const room = state.rooms[roomId]; - if (room) { - room.games = {}; - room.users = {}; - } }, addMessage: (state, action: PayloadAction<{ roomId: number; message: Enriched.Message }>) => { const { roomId, message } = action.payload; const existing = state.messages[roomId] ?? []; - const normalized = normalizeUserMessage(message); + const normalized = normalizeUserMessage({ ...message, timeReceived: Date.now() }); const next = existing.length >= MAX_ROOM_MESSAGES ? [...existing.slice(existing.length - MAX_ROOM_MESSAGES + 1), normalized] diff --git a/webclient/src/store/rooms/rooms.selectors.spec.ts b/webclient/src/store/rooms/rooms.selectors.spec.ts index adbbcfe04..d1c2d8e02 100644 --- a/webclient/src/store/rooms/rooms.selectors.spec.ts +++ b/webclient/src/store/rooms/rooms.selectors.spec.ts @@ -1,7 +1,6 @@ import { Selectors } from './rooms.selectors'; import { RoomsState } from './rooms.interfaces'; import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures'; -import { App } from '@app/types'; function rootState(rooms: RoomsState) { return { rooms }; @@ -112,23 +111,13 @@ describe('Selectors', () => { expect(Selectors.getRoomUsers(rootState(state), 1)).toBe(room.users); }); - it('getSortedRoomGames → returns games sorted by the active sort config', () => { - const game1 = makeGame({ gameId: 1, description: 'Beta' }); - const game2 = makeGame({ gameId: 2, description: 'Alpha' }); + it('getSortedRoomGames → returns sorted array view of games map', () => { + const game1 = makeGame({ gameId: 1, description: 'beta' }); + const game2 = makeGame({ gameId: 2, description: 'alpha' }); const room = makeRoom({ roomId: 1, games: { 1: game1, 2: game2 } }); - const state = makeRoomsState({ - rooms: { 1: room }, - sortGamesBy: { field: 'info.description' as App.GameSortField, order: App.SortDirection.ASC }, - }); + const state = makeRoomsState({ rooms: { 1: room } }); const result = Selectors.getSortedRoomGames(rootState(state), 1); expect(result).toHaveLength(2); - expect(result[0].info.description).toBe('Alpha'); - expect(result[1].info.description).toBe('Beta'); - }); - - it('getSortedRoomGames → returns EMPTY_GAMES for unknown roomId', () => { - const state = makeRoomsState({ rooms: {} }); - expect(Selectors.getSortedRoomGames(rootState(state), 999)).toHaveLength(0); }); it('getSortedRoomUsers → returns sorted user array sorted by name', () => { @@ -140,40 +129,4 @@ describe('Selectors', () => { expect(result[0].name).toBe('Alice'); expect(result[1].name).toBe('Zane'); }); - - it('getSortedRoomUsers → returns EMPTY_USERS for unknown roomId', () => { - const state = makeRoomsState({ rooms: {} }); - expect(Selectors.getSortedRoomUsers(rootState(state), 999)).toHaveLength(0); - }); - - // ── createSelector reference stability ────────────────────────────── - - it('getSortedRoomGames → returns same array reference for identical state', () => { - const game = makeGame({ gameId: 1 }); - const room = makeRoom({ roomId: 1, games: { 1: game } }); - const state = makeRoomsState({ rooms: { 1: room } }); - const root = rootState(state); - const a = Selectors.getSortedRoomGames(root, 1); - const b = Selectors.getSortedRoomGames(root, 1); - expect(a).toBe(b); - }); - - it('getSortedRoomUsers → returns same array reference for identical state', () => { - const user = makeUser({ name: 'Alice' }); - const room = makeRoom({ roomId: 1, users: { Alice: user } }); - const state = makeRoomsState({ rooms: { 1: room } }); - const root = rootState(state); - const a = Selectors.getSortedRoomUsers(root, 1); - const b = Selectors.getSortedRoomUsers(root, 1); - expect(a).toBe(b); - }); - - it('getJoinedRooms → returns same array reference for identical state', () => { - const room = makeRoom({ roomId: 1 }); - const state = makeRoomsState({ rooms: { 1: room }, joinedRoomIds: { 1: true } }); - const root = rootState(state); - const a = Selectors.getJoinedRooms(root); - const b = Selectors.getJoinedRooms(root); - expect(a).toBe(b); - }); }); diff --git a/webclient/src/store/rooms/rooms.selectors.tsx b/webclient/src/store/rooms/rooms.selectors.tsx index 47e4b7d27..82bb44b85 100644 --- a/webclient/src/store/rooms/rooms.selectors.tsx +++ b/webclient/src/store/rooms/rooms.selectors.tsx @@ -31,18 +31,14 @@ export const Selectors = { * Reads from the room's normalized `games` map — fixes the pre-existing * bug where this selector read from a never-populated top-level `games` field. */ - getJoinedGames: createSelector( - [ - (state: State, roomId: number) => state.rooms.rooms[roomId]?.games, - (state: State, roomId: number) => state.rooms.joinedGameIds[roomId], - ], - (games, joined): Enriched.Game[] => { - if (!games || !joined) { - return EMPTY_GAMES; - } - return Object.values(games).filter(game => joined[game.info.gameId]); + getJoinedGames: (state: State, roomId: number): Enriched.Game[] => { + const room = state.rooms.rooms[roomId]; + const joined = state.rooms.joinedGameIds[roomId]; + if (!room || !joined) { + return EMPTY_GAMES; } - ), + return Object.values(room.games).filter(game => joined[game.info.gameId]); + }, getRoomMessages: (state: State, roomId: number) => state.rooms.messages[roomId], diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index 2a4e1ca5a..4f3e05ca9 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -36,7 +36,7 @@ export interface ServerState { backendDecks: Data.Response_DeckList | null; downloadedDeck: { deckId: number; deck: string } | null; downloadedReplay: { replayId: number; replayData: Uint8Array } | null; - gamesOfUser: { [userName: string]: { [gameId: number]: Enriched.Game } }; + gamesOfUser: { [userName: string]: Enriched.Game[] }; registrationError: string | null; } diff --git a/webclient/src/store/server/server.reducer.spec.ts b/webclient/src/store/server/server.reducer.spec.ts index 99ac79e97..33bfc42ff 100644 --- a/webclient/src/store/server/server.reducer.spec.ts +++ b/webclient/src/store/server/server.reducer.spec.ts @@ -648,29 +648,29 @@ describe('Deck Storage', () => { // ── GAMES_OF_USER ───────────────────────────────────────────────────────────── describe('GAMES_OF_USER', () => { - it('stores normalized games keyed by userName and gameId', () => { + it('stores normalized games keyed by userName', () => { const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [create(Data.ServerInfo_GameSchema, { gameId: 5, description: '' })], roomList: [], }); const state = makeServerState(); const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response })); - expect(result.gamesOfUser['alice']).toEqual({ 5: makeGame({ gameId: 5 }) }); + expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 5 })]); }); it('overwrites previous games for same user', () => { - const old = { 1: makeGame({ gameId: 1 }) }; + const old = [makeGame({ gameId: 1 })]; const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [create(Data.ServerInfo_GameSchema, { gameId: 2, description: '' })], roomList: [], }); const state = makeServerState({ gamesOfUser: { alice: old } }); const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response })); - expect(result.gamesOfUser['alice']).toEqual({ 2: makeGame({ gameId: 2 }) }); + expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 2 })]); }); it('does not affect other users\' entries', () => { - const bobGames = { 3: makeGame({ gameId: 3 }) }; + const bobGames = [makeGame({ gameId: 3 })]; const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [], roomList: [] }); const state = makeServerState({ gamesOfUser: { bob: bobGames } }); const result = serverReducer(state, Actions.gamesOfUser({ userName: 'alice', response })); diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index a7e7682fa..3fa13ce71 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { App, Data, Enriched } from '@app/types'; +import { App, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { normalizeBannedUserError, normalizeGameObject, normalizeGametypeMap, normalizeLogs } from '../common'; @@ -179,10 +179,8 @@ export const serverSlice = createSlice({ } }, - updateUser: (state, action: PayloadAction<{ user: Partial }>) => { - state.user = state.user - ? { ...state.user, ...action.payload.user } as Data.ServerInfo_User - : action.payload.user as Data.ServerInfo_User; + updateUser: (state, action: PayloadAction<{ user: Data.ServerInfo_User | Partial }>) => { + state.user = { ...state.user, ...action.payload.user } as Data.ServerInfo_User; }, updateUsers: (state, action: PayloadAction<{ users: Data.ServerInfo_User[] }>) => { @@ -358,12 +356,8 @@ export const serverSlice = createSlice({ const gametypeMap = normalizeGametypeMap( (response.roomList ?? []).flatMap(room => room.gametypeList ?? []) ); - const games: { [gameId: number]: Enriched.Game } = {}; - for (const g of response.gameList ?? []) { - const normalized = normalizeGameObject(g, gametypeMap); - games[normalized.info.gameId] = normalized; - } - state.gamesOfUser[userName] = games; + const normalizedGames = (response.gameList ?? []).map(g => normalizeGameObject(g, gametypeMap)); + state.gamesOfUser[userName] = normalizedGames; }, registrationFailed: (state, action: PayloadAction<{ reason: string; endTime?: number }>) => { diff --git a/webclient/src/store/server/server.selectors.spec.ts b/webclient/src/store/server/server.selectors.spec.ts index 8d9a75097..61917e580 100644 --- a/webclient/src/store/server/server.selectors.spec.ts +++ b/webclient/src/store/server/server.selectors.spec.ts @@ -6,7 +6,7 @@ import { makeServerState, makeUser, } from './__mocks__/server-fixtures'; -import { App, Data } from '@app/types'; +import { App } from '@app/types'; function rootState(server: ServerState) { return { server }; @@ -149,86 +149,4 @@ describe('Selectors', () => { const state = makeServerState({ registrationError: 'bad input' }); expect(Selectors.getRegistrationError(rootState(state))).toBe('bad input'); }); - - // ── derived selectors (createSelector) ────────────────────────────── - - it('getIsConnected → true when state is LOGGED_IN', () => { - const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.LOGGED_IN, description: null } }); - expect(Selectors.getIsConnected(rootState(state))).toBe(true); - }); - - it('getIsConnected → false when state is CONNECTED', () => { - const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.CONNECTED, description: null } }); - expect(Selectors.getIsConnected(rootState(state))).toBe(false); - }); - - it('getIsConnected → false when state is DISCONNECTED', () => { - const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.DISCONNECTED, description: null } }); - expect(Selectors.getIsConnected(rootState(state))).toBe(false); - }); - - it('getIsUserModerator → true when user has IsModerator flag', () => { - const Flag = Data.ServerInfo_User_UserLevelFlag; - const user = makeUser({ userLevel: Flag.IsUser | Flag.IsModerator }); - const state = makeServerState({ user }); - expect(Selectors.getIsUserModerator(rootState(state))).toBe(true); - }); - - it('getIsUserModerator → false when user lacks IsModerator flag', () => { - const Flag = Data.ServerInfo_User_UserLevelFlag; - const user = makeUser({ userLevel: Flag.IsUser | Flag.IsRegistered }); - const state = makeServerState({ user }); - expect(Selectors.getIsUserModerator(rootState(state))).toBe(false); - }); - - it('getIsUserModerator → false when user is null', () => { - const state = makeServerState({ user: null }); - expect(Selectors.getIsUserModerator(rootState(state))).toBe(false); - }); - - // ── createSelector reference stability ────────────────────────────── - - it('getIsConnected → returns same value reference for identical state', () => { - const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.LOGGED_IN, description: null } }); - const root = rootState(state); - const a = Selectors.getIsConnected(root); - const b = Selectors.getIsConnected(root); - expect(a).toBe(b); - }); - - it('getSortedUsers → returns same array reference for identical state', () => { - const users = { Alice: makeUser({ name: 'Alice' }), Bob: makeUser({ name: 'Bob' }) }; - const state = makeServerState({ users }); - const root = rootState(state); - const a = Selectors.getSortedUsers(root); - const b = Selectors.getSortedUsers(root); - expect(a).toBe(b); - }); - - it('getSortedBuddyList → returns same array reference for identical state', () => { - const buddyList = { Alice: makeUser({ name: 'Alice' }) }; - const state = makeServerState({ buddyList }); - const root = rootState(state); - const a = Selectors.getSortedBuddyList(root); - const b = Selectors.getSortedBuddyList(root); - expect(a).toBe(b); - }); - - it('getSortedIgnoreList → returns same array reference for identical state', () => { - const ignoreList = { Troll: makeUser({ name: 'Troll' }) }; - const state = makeServerState({ ignoreList }); - const root = rootState(state); - const a = Selectors.getSortedIgnoreList(root); - const b = Selectors.getSortedIgnoreList(root); - expect(a).toBe(b); - }); - - it('getReplaysList → returns same array reference for identical state', () => { - const replays = { 1: makeReplayMatch({ gameId: 1 }) }; - const state = makeServerState({ replays }); - const root = rootState(state); - const a = Selectors.getReplaysList(root); - const b = Selectors.getReplaysList(root); - expect(a).toBe(b); - }); }); diff --git a/webclient/src/types/enriched.ts b/webclient/src/types/enriched.ts index b111c0e5e..c0033d3c2 100644 --- a/webclient/src/types/enriched.ts +++ b/webclient/src/types/enriched.ts @@ -11,6 +11,8 @@ import type { ServerInfo_User, } from '@app/generated'; +import { WebSocketConnectReason } from './server'; + // ── Domain model types (composition: raw proto + client-side fields) ────────── // // `info` holds the proto snapshot verbatim. Normalized/client-only fields @@ -131,20 +133,84 @@ export interface LogGroups { chat: ServerInfo_ChatMessage[]; } -// ── Connect options (re-exported from @app/websocket) ──────────────────────── -// Source of truth lives in src/websocket/connectOptions.ts. Re-exported here -// so UI code can use the Enriched.* namespace without importing @app/websocket. +// ── Connect options ─────────────────────────────────────────────────────────── +// Each variant is the enriched input for one session flow: the network +// transport fields (host/port) + the subset of proto Command_* fields the UI +// actually produces (user-entered credentials, tokens, email, etc.) + a +// `reason` discriminator so the websocket layer can route. +// +// Hand-written instead of `MessageInitShape & ...` +// because MessageInitShape is a `Message | { 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. -export type { - LoginConnectOptions, - RegisterConnectOptions, - ActivateConnectOptions, - PasswordResetRequestConnectOptions, - PasswordResetChallengeConnectOptions, - PasswordResetConnectOptions, - TestConnectionOptions, - WebSocketConnectOptions, -} from '@app/websocket'; +interface ConnectTransport { + host: string; + port: string; + keepalive?: number; + autojoinrooms?: boolean; + clientid?: string; +} + +export interface LoginConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.LOGIN; + userName: string; + password?: string; + hashedPassword?: string; +} + +export interface RegisterConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.REGISTER; + userName: string; + password: string; + email: string; + country: string; + realName: string; +} + +export interface ActivateConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.ACTIVATE_ACCOUNT; + userName: string; + token: string; + /** Plaintext password carried through so post-activation auto-login can hash it. */ + password?: string; +} + +export interface PasswordResetRequestConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST; + userName: string; +} + +export interface PasswordResetChallengeConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE; + userName: string; + email: string; +} + +export interface PasswordResetConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET; + userName: string; + token: string; + newPassword: string; +} + +/** + * Test connection has no proto command — it just opens and closes a socket to + * verify reachability. + */ +export interface TestConnectionOptions extends ConnectTransport { + reason: WebSocketConnectReason.TEST_CONNECTION; +} + +export type WebSocketConnectOptions = + | LoginConnectOptions + | RegisterConnectOptions + | ActivateConnectOptions + | PasswordResetRequestConnectOptions + | PasswordResetChallengeConnectOptions + | PasswordResetConnectOptions + | TestConnectionOptions; /** * Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the diff --git a/webclient/src/types/server.ts b/webclient/src/types/server.ts index 19135bc8f..6c3fa8006 100644 --- a/webclient/src/types/server.ts +++ b/webclient/src/types/server.ts @@ -1,11 +1,27 @@ -export { StatusEnum, WebSocketConnectReason } from '@app/websocket'; -import type { StatusEnum } from '@app/websocket'; - export interface ServerStatus { status: StatusEnum; description: string; } +export enum StatusEnum { + DISCONNECTED, + CONNECTING, + CONNECTED, + LOGGING_IN, + LOGGED_IN, + DISCONNECTING = 99 +} + +export enum WebSocketConnectReason { + LOGIN, + REGISTER, + ACTIVATE_ACCOUNT, + PASSWORD_RESET_REQUEST, + PASSWORD_RESET_CHALLENGE, + PASSWORD_RESET, + TEST_CONNECTION, +} + export class Host { id?: number; name: string; diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts index 2fab4d695..0c5010ec1 100644 --- a/webclient/src/websocket/WebClient.spec.ts +++ b/webclient/src/websocket/WebClient.spec.ts @@ -17,8 +17,8 @@ vi.mock('./services/WebSocketService', () => ({ })); vi.mock('./services/ProtobufService', () => ({ - ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) { - captured.pbOptions = transport; + ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { + captured.pbOptions = options; return { handleMessageEvent: vi.fn(), resetCommands: vi.fn(), @@ -26,16 +26,20 @@ vi.mock('./services/ProtobufService', () => ({ }), })); +vi.mock('./commands/session', () => ({ + ping: vi.fn(), +})); + import { WebClient } from './WebClient'; import { WebSocketService } from './services/WebSocketService'; import { ProtobufService } from './services/ProtobufService'; -import { StatusEnum } from './interfaces/StatusEnum'; +import { ping } from './commands/session'; +import { App, Enriched } from '@app/types'; import { Subject } from 'rxjs'; import { Mock } from 'vitest'; import { SocketTransport } from './services/ProtobufService'; import { WebSocketServiceConfig } from './services/WebSocketService'; import type { IWebClientResponse, IWebClientRequest } from './interfaces'; -import type { ConnectTarget } from './interfaces/WebClientConfig'; import { installMockWebSocket } from './__mocks__/helpers'; function makeMockResponse(): IWebClientResponse { @@ -43,11 +47,8 @@ function makeMockResponse(): IWebClientResponse { session: { initialized: vi.fn(), connectionAttempted: vi.fn(), - connectionFailed: vi.fn(), clearStore: vi.fn(), updateStatus: vi.fn(), - testConnectionSuccessful: vi.fn(), - testConnectionFailed: vi.fn(), }, room: { clearStore: vi.fn() }, game: { clearStore: vi.fn() }, @@ -57,7 +58,13 @@ function makeMockResponse(): IWebClientResponse { } function makeMockRequest(): IWebClientRequest { - return {} as IWebClientRequest; + return { + authentication: {}, + session: {}, + rooms: {}, + admin: {}, + moderator: {}, + } as unknown as IWebClientRequest; } describe('WebClient', () => { @@ -67,10 +74,11 @@ describe('WebClient', () => { let messageSubject: Subject; beforeEach(() => { + // Reset the singleton so each test starts fresh. (WebClient as unknown as { _instance: WebClient | null })._instance = null; - (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) { - captured.pbOptions = transport; + (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { + captured.pbOptions = options; return { handleMessageEvent: vi.fn(), resetCommands: vi.fn(), @@ -91,7 +99,7 @@ describe('WebClient', () => { mockResponse = makeMockResponse(); mockRequest = makeMockRequest(); - client = new WebClient(mockRequest, mockResponse); + client = new WebClient(mockResponse, mockRequest); }); afterEach(() => { @@ -100,9 +108,9 @@ describe('WebClient', () => { }); describe('constructor', () => { - it('stores the request and response on the instance', () => { - expect(client.request).toBe(mockRequest); + it('stores the response and request on the instance', () => { expect(client.response).toBe(mockResponse); + expect(client.request).toBe(mockRequest); }); it('subscribes socket.message$ to protobuf.handleMessageEvent', () => { @@ -120,7 +128,7 @@ describe('WebClient', () => { }); it('throws when instantiated more than once', () => { - expect(() => new WebClient(makeMockRequest(), makeMockResponse())).toThrow(/singleton/); + expect(() => new WebClient(makeMockResponse(), makeMockRequest())).toThrow(/singleton/); }); }); @@ -133,15 +141,16 @@ describe('WebClient', () => { describe('connect', () => { it('calls response.session.connectionAttempted', () => { - const target: ConnectTarget = { host: 'h', port: '1' }; - client.connect(target); + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; + client.connect(opts); expect(mockResponse.session.connectionAttempted).toHaveBeenCalled(); }); - it('calls socket.connect with target', () => { - const target: ConnectTarget = { host: 'h', port: '1' }; - client.connect(target); - expect(client.socket.connect).toHaveBeenCalledWith(target); + it('stores options and calls socket.connect', () => { + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; + client.connect(opts); + expect(client.options).toBe(opts); + expect(client.socket.connect).toHaveBeenCalledWith(opts); }); }); @@ -163,28 +172,30 @@ describe('WebClient', () => { vi.useRealTimers(); }); - const target: ConnectTarget = { host: 'h', port: '1' }; + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; it('creates a WebSocket with the correct URL', () => { - client.testConnect(target); + client.testConnect(opts); expect(MockWS).toHaveBeenCalledWith(expect.stringContaining('://h:1')); }); it('calls testConnectionSuccessful and closes on open', () => { - client.testConnect(target); + (mockResponse.session as any).testConnectionSuccessful = vi.fn(); + client.testConnect(opts); wsMockInstance.onopen(); - expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalled(); + expect((mockResponse.session as any).testConnectionSuccessful).toHaveBeenCalled(); expect(wsMockInstance.close).toHaveBeenCalled(); }); it('calls testConnectionFailed on error', () => { - client.testConnect(target); + (mockResponse.session as any).testConnectionFailed = vi.fn(); + client.testConnect(opts); wsMockInstance.onerror(); - expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled(); + expect((mockResponse.session as any).testConnectionFailed).toHaveBeenCalled(); }); it('closes socket after keepalive timeout', () => { - client.testConnect(target); + client.testConnect(opts); vi.advanceTimersByTime(5000); expect(wsMockInstance.close).toHaveBeenCalled(); }); @@ -199,36 +210,32 @@ describe('WebClient', () => { describe('updateStatus', () => { it('sets the status', () => { - client.updateStatus(StatusEnum.CONNECTED); - expect(client.status).toBe(StatusEnum.CONNECTED); + client.updateStatus(App.StatusEnum.CONNECTED); + expect(client.status).toBe(App.StatusEnum.CONNECTED); }); it('calls protobuf.resetCommands on DISCONNECTED', () => { - client.updateStatus(StatusEnum.DISCONNECTED); + client.updateStatus(App.StatusEnum.DISCONNECTED); expect(client.protobuf.resetCommands).toHaveBeenCalled(); }); it('does not reset protobuf when status is not DISCONNECTED', () => { - client.updateStatus(StatusEnum.CONNECTED); + client.updateStatus(App.StatusEnum.CONNECTED); expect(client.protobuf.resetCommands).not.toHaveBeenCalled(); }); }); describe('constructor closures', () => { - it('keepAliveFn is set to ping function in WebSocketService', () => { - expect(captured.wsOptions!.keepAliveFn).toBeDefined(); - expect(typeof captured.wsOptions!.keepAliveFn).toBe('function'); + it('keepAliveFn calls ping with the callback', () => { + const cb = vi.fn(); + captured.wsOptions!.keepAliveFn(cb); + expect(ping).toHaveBeenCalledWith(cb); }); it('onStatusChange routes to response.session.updateStatus and updates own status', () => { - captured.wsOptions!.onStatusChange(StatusEnum.CONNECTED, 'Connected'); - expect(mockResponse.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); - expect(client.status).toBe(StatusEnum.CONNECTED); - }); - - it('onConnectionFailed routes to response.session.connectionFailed', () => { - captured.wsOptions!.onConnectionFailed(); - expect(mockResponse.session.connectionFailed).toHaveBeenCalled(); + captured.wsOptions!.onStatusChange(App.StatusEnum.CONNECTED, 'Connected'); + expect(mockResponse.session.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'Connected'); + expect(client.status).toBe(App.StatusEnum.CONNECTED); }); it('send closure delegates to socket.send', () => { diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index ddd197ce6..310ffae40 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -1,55 +1,52 @@ -import { ping } from './commands/session'; -import { CLIENT_OPTIONS } from './config'; -import type { - ConnectTarget, - IWebClientRequest, - IWebClientResponse, -} from './interfaces'; -import { StatusEnum } from './interfaces'; +import { App, Enriched } from '@app/types'; + import { ProtobufService } from './services/ProtobufService'; import { WebSocketService } from './services/WebSocketService'; +import { ping } from './commands/session'; +import { CLIENT_OPTIONS } from './config'; +import { IWebClientResponse, IWebClientRequest } from './interfaces'; export class WebClient { private static _instance: WebClient | null = null; - static get instance(): WebClient { + public static get instance(): WebClient { if (!WebClient._instance) { throw new Error( - 'WebClient has not been initialized. Instantiate it via `new WebClient()` before accessing `WebClient.instance`.' + 'WebClient has not been initialized. Instantiate it via `new WebClient(response, request)` before accessing `WebClient.instance`.' ); } return WebClient._instance; } - protobuf: ProtobufService; - socket: WebSocketService; - status: StatusEnum; + public socket: WebSocketService; + public protobuf: ProtobufService; + public response: IWebClientResponse; + public request: IWebClientRequest; - constructor( - public request: IWebClientRequest, - public response: IWebClientResponse - ) { + public options: Enriched.WebSocketConnectOptions | null = null; + public status: App.StatusEnum; + + constructor(response: IWebClientResponse, request: IWebClientRequest) { if (WebClient._instance) { throw new Error('WebClient is a singleton and has already been initialized.'); } + this.response = response; + this.request = request; + this.socket = new WebSocketService({ - keepAliveFn: ping, + keepAliveFn: (cb) => ping(cb), + response, onStatusChange: (status, description) => { this.response.session.updateStatus(status, description); this.updateStatus(status); }, - onConnectionFailed: () => { - this.response.session.connectionFailed(); - }, }); - this.protobuf = new ProtobufService( - { - send: (data) => this.socket.send(data), - isOpen: () => this.socket.checkReadyState(WebSocket.OPEN), - } - ); + this.protobuf = new ProtobufService({ + send: (data) => this.socket.send(data), + isOpen: () => this.socket.checkReadyState(WebSocket.OPEN), + }); this.socket.message$.subscribe((message: MessageEvent) => { this.protobuf.handleMessageEvent(message); @@ -60,14 +57,15 @@ export class WebClient { this.response.session.initialized(); } - public connect(target: ConnectTarget) { + public connect(options: Enriched.WebSocketConnectOptions) { this.response.session.connectionAttempted(); - this.socket.connect(target); + this.options = options; + this.socket.connect(options); } - public testConnect(target: ConnectTarget) { + public testConnect(options: Enriched.WebSocketConnectOptions) { const protocol = window.location.hostname === 'localhost' ? 'ws' : 'wss'; - const { host, port } = target; + const { host, port } = options; const socket = new WebSocket(`${protocol}://${host}:${port}`); socket.binaryType = 'arraybuffer'; @@ -90,10 +88,10 @@ export class WebClient { this.socket.disconnect(); } - public updateStatus(status: StatusEnum) { + public updateStatus(status: App.StatusEnum) { this.status = status; - if (status === StatusEnum.DISCONNECTED) { + if (status === App.StatusEnum.DISCONNECTED) { this.protobuf.resetCommands(); } } diff --git a/webclient/src/websocket/__mocks__/WebClient.ts b/webclient/src/websocket/__mocks__/WebClient.ts deleted file mode 100644 index 1219c3466..000000000 --- a/webclient/src/websocket/__mocks__/WebClient.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * 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 }, - }, -}; - diff --git a/webclient/src/websocket/__mocks__/helpers.ts b/webclient/src/websocket/__mocks__/helpers.ts index c76fd9f17..ec05d87d2 100644 --- a/webclient/src/websocket/__mocks__/helpers.ts +++ b/webclient/src/websocket/__mocks__/helpers.ts @@ -1,27 +1,8 @@ /** * Shared mock factories for websocket layer unit tests. * Import the helpers you need in each spec file via: - * import { makeMockWebSocket, useWebClientCleanup } from '../__mocks__/helpers'; + * import { makeMockWebSocket } from '../__mocks__/helpers'; */ -import { WebClient } from '../WebClient'; - -/** - * Resets the WebClient singleton to null. Call directly, or use - * `useWebClientCleanup()` to register automatic beforeEach/afterEach hooks. - */ -export function resetWebClientSingleton() { - (WebClient as unknown as { _instance: WebClient | null })._instance = null; -} - -/** - * Registers beforeEach/afterEach hooks that reset the WebClient singleton. - * Call at describe-level or file-level in any spec that mocks WebClient. - * Prevents isolate:false singleton leakage between spec files. - */ -export function useWebClientCleanup() { - beforeEach(() => resetWebClientSingleton()); - afterEach(() => resetWebClientSingleton()); -} /** Builds a mock WebSocket instance */ export function makeMockWebSocketInstance() { diff --git a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts index d82cbed0f..e12d89ccf 100644 --- a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts +++ b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts @@ -16,7 +16,7 @@ export function makeWebClientMock() { testConnect: vi.fn(), disconnect: vi.fn(), updateStatus: vi.fn(), - config: {}, + options: {}, status: 0, protobuf: { sendSessionCommand: vi.fn(), diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts index 7d9562506..1641f64ce 100644 --- a/webclient/src/websocket/commands/admin/adjustMod.ts +++ b/webclient/src/websocket/commands/admin/adjustMod.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; -import { Command_AdjustMod_ext, Command_AdjustModSchema } from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { WebClient.instance.protobuf.sendAdminCommand( - Command_AdjustMod_ext, - create(Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), + Data.Command_AdjustMod_ext, + create(Data.Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), { onSuccess: () => { WebClient.instance.response.admin.adjustMod(userName, shouldBeMod, shouldBeJudge); diff --git a/webclient/src/websocket/commands/admin/adminCommands.spec.ts b/webclient/src/websocket/commands/admin/adminCommands.spec.ts index 51764a627..88a055f1a 100644 --- a/webclient/src/websocket/commands/admin/adminCommands.spec.ts +++ b/webclient/src/websocket/commands/admin/adminCommands.spec.ts @@ -1,4 +1,18 @@ -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 { WebClient } from '../../WebClient'; @@ -6,12 +20,6 @@ import { adjustMod } from './adjustMod'; import { reloadConfig } from './reloadConfig'; import { shutdownServer } from './shutdownServer'; import { updateServerMessage } from './updateServerMessage'; -import { - Command_AdjustMod_ext, - Command_ReloadConfig_ext, - Command_ShutdownServer_ext, - Command_UpdateServerMessage_ext, -} from '@app/generated'; import { Mock } from 'vitest'; @@ -25,13 +33,9 @@ const { invokeOnSuccess } = makeCallbackHelpers( // ---------------------------------------------------------------- describe('adjustMod', () => { - it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => { + it('calls sendAdminCommand with Command_AdjustMod', () => { adjustMod('alice', true, false); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_AdjustMod_ext, - expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }), - expect.any(Object) - ); + expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls response.admin.adjustMod', () => { @@ -46,13 +50,9 @@ describe('adjustMod', () => { // ---------------------------------------------------------------- describe('reloadConfig', () => { - it('calls sendAdminCommand with Command_ReloadConfig extension', () => { + it('calls sendAdminCommand with Command_ReloadConfig', () => { reloadConfig(); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_ReloadConfig_ext, - expect.any(Object), - expect.any(Object) - ); + expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls response.admin.reloadConfig', () => { @@ -67,13 +67,9 @@ describe('reloadConfig', () => { // ---------------------------------------------------------------- describe('shutdownServer', () => { - it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => { + it('calls sendAdminCommand with Command_ShutdownServer', () => { shutdownServer('maintenance', 10); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_ShutdownServer_ext, - expect.objectContaining({ reason: 'maintenance', minutes: 10 }), - expect.any(Object) - ); + expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls response.admin.shutdownServer', () => { @@ -88,13 +84,9 @@ describe('shutdownServer', () => { // ---------------------------------------------------------------- describe('updateServerMessage', () => { - it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => { + it('calls sendAdminCommand with Command_UpdateServerMessage', () => { updateServerMessage(); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_UpdateServerMessage_ext, - expect.any(Object), - expect.any(Object) - ); + expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls response.admin.updateServerMessage', () => { diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts index 0856db673..25ad85b1f 100644 --- a/webclient/src/websocket/commands/admin/reloadConfig.ts +++ b/webclient/src/websocket/commands/admin/reloadConfig.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; export function reloadConfig(): void { - WebClient.instance.protobuf.sendAdminCommand(Command_ReloadConfig_ext, create(Command_ReloadConfigSchema), { + WebClient.instance.protobuf.sendAdminCommand(Data.Command_ReloadConfig_ext, create(Data.Command_ReloadConfigSchema), { onSuccess: () => { WebClient.instance.response.admin.reloadConfig(); }, diff --git a/webclient/src/websocket/commands/admin/shutdownServer.ts b/webclient/src/websocket/commands/admin/shutdownServer.ts index a341ffa81..69a2e56d7 100644 --- a/webclient/src/websocket/commands/admin/shutdownServer.ts +++ b/webclient/src/websocket/commands/admin/shutdownServer.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; -import { Command_ShutdownServer_ext, Command_ShutdownServerSchema } from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; export function shutdownServer(reason: string, minutes: number): void { WebClient.instance.protobuf.sendAdminCommand( - Command_ShutdownServer_ext, - create(Command_ShutdownServerSchema, { reason, minutes }), + Data.Command_ShutdownServer_ext, + create(Data.Command_ShutdownServerSchema, { reason, minutes }), { onSuccess: () => { WebClient.instance.response.admin.shutdownServer(); diff --git a/webclient/src/websocket/commands/admin/updateServerMessage.ts b/webclient/src/websocket/commands/admin/updateServerMessage.ts index 7c99ddd5b..b56b351b0 100644 --- a/webclient/src/websocket/commands/admin/updateServerMessage.ts +++ b/webclient/src/websocket/commands/admin/updateServerMessage.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; export function updateServerMessage(): void { - WebClient.instance.protobuf.sendAdminCommand(Command_UpdateServerMessage_ext, create(Command_UpdateServerMessageSchema), { + WebClient.instance.protobuf.sendAdminCommand(Data.Command_UpdateServerMessage_ext, create(Data.Command_UpdateServerMessageSchema), { onSuccess: () => { WebClient.instance.response.admin.updateServerMessage(); }, diff --git a/webclient/src/websocket/commands/game/attachCard.ts b/webclient/src/websocket/commands/game/attachCard.ts index 5864a331a..ac9128436 100644 --- a/webclient/src/websocket/commands/game/attachCard.ts +++ b/webclient/src/websocket/commands/game/attachCard.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_AttachCard_ext, Command_AttachCardSchema, type AttachCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function attachCard(gameId: number, params: AttachCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params)); +import { Data } from '@app/types'; + +export function attachCard(gameId: number, params: Data.AttachCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_AttachCard_ext, create(Data.Command_AttachCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/changeZoneProperties.ts b/webclient/src/websocket/commands/game/changeZoneProperties.ts index e186bba91..ad10f0978 100644 --- a/webclient/src/websocket/commands/game/changeZoneProperties.ts +++ b/webclient/src/websocket/commands/game/changeZoneProperties.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; -import { Command_ChangeZoneProperties_ext, Command_ChangeZonePropertiesSchema, type ChangeZonePropertiesParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void { +import { Data } from '@app/types'; + +export function changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Command_ChangeZoneProperties_ext, - create(Command_ChangeZonePropertiesSchema, params) + Data.Command_ChangeZoneProperties_ext, + create(Data.Command_ChangeZonePropertiesSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/concede.ts b/webclient/src/websocket/commands/game/concede.ts index 9007e1223..fb9634a34 100644 --- a/webclient/src/websocket/commands/game/concede.ts +++ b/webclient/src/websocket/commands/game/concede.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_Concede_ext, Command_ConcedeSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function concede(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Concede_ext, create(Command_ConcedeSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Concede_ext, create(Data.Command_ConcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/createArrow.ts b/webclient/src/websocket/commands/game/createArrow.ts index 87aaf094c..6b727d631 100644 --- a/webclient/src/websocket/commands/game/createArrow.ts +++ b/webclient/src/websocket/commands/game/createArrow.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_CreateArrow_ext, Command_CreateArrowSchema, type CreateArrowParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function createArrow(gameId: number, params: CreateArrowParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params)); +import { Data } from '@app/types'; + +export function createArrow(gameId: number, params: Data.CreateArrowParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_CreateArrow_ext, create(Data.Command_CreateArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createCounter.ts b/webclient/src/websocket/commands/game/createCounter.ts index 886c63b3b..28f897944 100644 --- a/webclient/src/websocket/commands/game/createCounter.ts +++ b/webclient/src/websocket/commands/game/createCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_CreateCounter_ext, Command_CreateCounterSchema, type CreateCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function createCounter(gameId: number, params: CreateCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params)); +import { Data } from '@app/types'; + +export function createCounter(gameId: number, params: Data.CreateCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_CreateCounter_ext, create(Data.Command_CreateCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createToken.ts b/webclient/src/websocket/commands/game/createToken.ts index b4c837417..2e8902981 100644 --- a/webclient/src/websocket/commands/game/createToken.ts +++ b/webclient/src/websocket/commands/game/createToken.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_CreateToken_ext, Command_CreateTokenSchema, type CreateTokenParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function createToken(gameId: number, params: CreateTokenParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params)); +import { Data } from '@app/types'; + +export function createToken(gameId: number, params: Data.CreateTokenParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_CreateToken_ext, create(Data.Command_CreateTokenSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deckSelect.ts b/webclient/src/websocket/commands/game/deckSelect.ts index d2f28a7c0..ea4c7f455 100644 --- a/webclient/src/websocket/commands/game/deckSelect.ts +++ b/webclient/src/websocket/commands/game/deckSelect.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_DeckSelect_ext, Command_DeckSelectSchema, type DeckSelectParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function deckSelect(gameId: number, params: DeckSelectParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params)); +import { Data } from '@app/types'; + +export function deckSelect(gameId: number, params: Data.DeckSelectParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_DeckSelect_ext, create(Data.Command_DeckSelectSchema, params)); } diff --git a/webclient/src/websocket/commands/game/delCounter.ts b/webclient/src/websocket/commands/game/delCounter.ts index 6b215d123..fec44bd68 100644 --- a/webclient/src/websocket/commands/game/delCounter.ts +++ b/webclient/src/websocket/commands/game/delCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_DelCounter_ext, Command_DelCounterSchema, type DelCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function delCounter(gameId: number, params: DelCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params)); +import { Data } from '@app/types'; + +export function delCounter(gameId: number, params: Data.DelCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_DelCounter_ext, create(Data.Command_DelCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deleteArrow.ts b/webclient/src/websocket/commands/game/deleteArrow.ts index 8391277f7..8e8dc9be3 100644 --- a/webclient/src/websocket/commands/game/deleteArrow.ts +++ b/webclient/src/websocket/commands/game/deleteArrow.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_DeleteArrow_ext, Command_DeleteArrowSchema, type DeleteArrowParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function deleteArrow(gameId: number, params: DeleteArrowParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params)); +import { Data } from '@app/types'; + +export function deleteArrow(gameId: number, params: Data.DeleteArrowParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_DeleteArrow_ext, create(Data.Command_DeleteArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/drawCards.ts b/webclient/src/websocket/commands/game/drawCards.ts index 5c7ed8c0a..04309bf63 100644 --- a/webclient/src/websocket/commands/game/drawCards.ts +++ b/webclient/src/websocket/commands/game/drawCards.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_DrawCards_ext, Command_DrawCardsSchema, type DrawCardsParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function drawCards(gameId: number, params: DrawCardsParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params)); +import { Data } from '@app/types'; + +export function drawCards(gameId: number, params: Data.DrawCardsParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_DrawCards_ext, create(Data.Command_DrawCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/dumpZone.ts b/webclient/src/websocket/commands/game/dumpZone.ts index 62d792d2e..c5663a732 100644 --- a/webclient/src/websocket/commands/game/dumpZone.ts +++ b/webclient/src/websocket/commands/game/dumpZone.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_DumpZone_ext, Command_DumpZoneSchema, type DumpZoneParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function dumpZone(gameId: number, params: DumpZoneParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params)); +import { Data } from '@app/types'; + +export function dumpZone(gameId: number, params: Data.DumpZoneParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_DumpZone_ext, create(Data.Command_DumpZoneSchema, params)); } diff --git a/webclient/src/websocket/commands/game/flipCard.ts b/webclient/src/websocket/commands/game/flipCard.ts index b4b894962..ee16a938a 100644 --- a/webclient/src/websocket/commands/game/flipCard.ts +++ b/webclient/src/websocket/commands/game/flipCard.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_FlipCard_ext, Command_FlipCardSchema, type FlipCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function flipCard(gameId: number, params: FlipCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params)); +import { Data } from '@app/types'; + +export function flipCard(gameId: number, params: Data.FlipCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_FlipCard_ext, create(Data.Command_FlipCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts index 8706a9e3d..dee19f57c 100644 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ b/webclient/src/websocket/commands/game/gameCommands.spec.ts @@ -1,45 +1,15 @@ -vi.mock('../../WebClient'); +vi.mock('../../WebClient', () => ({ + WebClient: { + instance: { + protobuf: { sendGameCommand: vi.fn() }, + response: { game: {} }, + }, + }, +})); import { WebClient } from '../../WebClient'; import { create, setExtension } from '@bufbuild/protobuf'; -import { - Command_AttachCard_ext, - Command_ChangeZoneProperties_ext, - Command_Concede_ext, - Command_CreateArrow_ext, - Command_CreateCounter_ext, - Command_CreateToken_ext, - Command_DeckSelect_ext, - Command_DelCounter_ext, - Command_DeleteArrow_ext, - Command_DrawCards_ext, - Command_DrawCardsSchema, - Command_DumpZone_ext, - Command_FlipCard_ext, - Command_GameSay_ext, - Command_IncCardCounter_ext, - Command_IncCounter_ext, - Command_Judge_ext, - Command_KickFromGame_ext, - Command_LeaveGame_ext, - Command_MoveCard_ext, - Command_Mulligan_ext, - Command_NextTurn_ext, - Command_ReadyStart_ext, - Command_RevealCards_ext, - Command_ReverseTurn_ext, - Command_RollDie_ext, - Command_SetActivePhase_ext, - Command_SetCardAttr_ext, - Command_SetCardCounter_ext, - Command_SetCounter_ext, - Command_SetSideboardLock_ext, - Command_SetSideboardPlan_ext, - Command_Shuffle_ext, - Command_UndoDraw_ext, - Command_Unconcede_ext, - GameCommandSchema, -} from '@app/generated'; +import { Data } from '@app/types'; import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; @@ -82,122 +52,122 @@ describe('Game commands — delegate to WebClient.instance.protobuf.sendGameComm it('attachCard sends Command_AttachCard', () => { attachCard(gameId, { cardId: 10, startZone: 'hand' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) + gameId, Data.Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) ); }); it('changeZoneProperties sends Command_ChangeZoneProperties', () => { changeZoneProperties(gameId, { zoneName: 'side' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) + gameId, Data.Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) ); }); it('concede sends Command_Concede with empty object', () => { concede(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Concede_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Concede_ext, expect.any(Object)); }); it('createArrow sends Command_CreateArrow', () => { createArrow(gameId, { startPlayerId: 1, startZone: 'hand' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) + gameId, Data.Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) ); }); it('createCounter sends Command_CreateCounter', () => { createCounter(gameId, { counterName: 'life' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) + gameId, Data.Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) ); }); it('createToken sends Command_CreateToken', () => { createToken(gameId, { cardName: 'Goblin', zone: 'play' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) + gameId, Data.Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) ); }); it('deckSelect sends Command_DeckSelect', () => { deckSelect(gameId, { deckId: 5 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) + gameId, Data.Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) ); }); it('delCounter sends Command_DelCounter', () => { delCounter(gameId, { counterId: 3 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) + gameId, Data.Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) ); }); it('deleteArrow sends Command_DeleteArrow', () => { deleteArrow(gameId, { arrowId: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) + gameId, Data.Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) ); }); it('drawCards sends Command_DrawCards', () => { drawCards(gameId, { number: 3 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DrawCards_ext, expect.objectContaining({ number: 3 }) + gameId, Data.Command_DrawCards_ext, expect.objectContaining({ number: 3 }) ); }); it('dumpZone sends Command_DumpZone', () => { dumpZone(gameId, { playerId: 2, zoneName: 'library' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) + gameId, Data.Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) ); }); it('flipCard sends Command_FlipCard', () => { flipCard(gameId, { cardId: 7, faceDown: false }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) + gameId, Data.Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) ); }); it('gameSay sends Command_GameSay', () => { gameSay(gameId, { message: 'hello' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_GameSay_ext, expect.objectContaining({ message: 'hello' }) + gameId, Data.Command_GameSay_ext, expect.objectContaining({ message: 'hello' }) ); }); it('incCardCounter sends Command_IncCardCounter', () => { incCardCounter(gameId, { cardId: 5, counterId: 1 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, Data.Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) ); }); it('incCounter sends Command_IncCounter', () => { incCounter(gameId, { counterId: 1, delta: 5 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) + gameId, Data.Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) ); }); it('kickFromGame sends Command_KickFromGame', () => { kickFromGame(gameId, { playerId: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) + gameId, Data.Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) ); }); it('leaveGame sends Command_LeaveGame with empty object', () => { leaveGame(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_LeaveGame_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_LeaveGame_ext, expect.any(Object)); }); it('moveCard sends Command_MoveCard', () => { moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_MoveCard_ext, + gameId, Data.Command_MoveCard_ext, expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' }) ); }); @@ -205,45 +175,45 @@ describe('Game commands — delegate to WebClient.instance.protobuf.sendGameComm it('mulligan sends Command_Mulligan', () => { mulligan(gameId, { number: 7 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_Mulligan_ext, expect.objectContaining({ number: 7 }) + gameId, Data.Command_Mulligan_ext, expect.objectContaining({ number: 7 }) ); }); it('nextTurn sends Command_NextTurn with empty object', () => { nextTurn(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_NextTurn_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_NextTurn_ext, expect.any(Object)); }); it('readyStart sends Command_ReadyStart', () => { readyStart(gameId, { ready: true }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_ReadyStart_ext, expect.objectContaining({ ready: true }) + gameId, Data.Command_ReadyStart_ext, expect.objectContaining({ ready: true }) ); }); it('revealCards sends Command_RevealCards', () => { revealCards(gameId, { zoneName: 'hand', cardId: [1, 2] }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) + gameId, Data.Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) ); }); it('reverseTurn sends Command_ReverseTurn with empty object', () => { reverseTurn(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReverseTurn_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_ReverseTurn_ext, expect.any(Object)); }); it('setActivePhase sends Command_SetActivePhase', () => { setActivePhase(gameId, { phase: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) + gameId, Data.Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) ); }); it('setCardAttr sends Command_SetCardAttr', () => { setCardAttr(gameId, { zone: 'play', cardId: 5, attrValue: '2' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardAttr_ext, + gameId, Data.Command_SetCardAttr_ext, expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' }) ); }); @@ -251,63 +221,63 @@ describe('Game commands — delegate to WebClient.instance.protobuf.sendGameComm it('setCardCounter sends Command_SetCardCounter', () => { setCardCounter(gameId, { cardId: 5, counterId: 1 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, Data.Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) ); }); it('setCounter sends Command_SetCounter', () => { setCounter(gameId, { counterId: 1, value: 10 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) + gameId, Data.Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) ); }); it('setSideboardLock sends Command_SetSideboardLock', () => { setSideboardLock(gameId, { locked: true }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) + gameId, Data.Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) ); }); it('setSideboardPlan sends Command_SetSideboardPlan', () => { setSideboardPlan(gameId, { moveList: [] }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) + gameId, Data.Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) ); }); it('shuffle sends Command_Shuffle', () => { shuffle(gameId, { zoneName: 'hand' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) + gameId, Data.Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) ); }); it('undoDraw sends Command_UndoDraw with empty object', () => { undoDraw(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_UndoDraw_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_UndoDraw_ext, expect.any(Object)); }); it('unconcede sends Command_Unconcede with empty object', () => { unconcede(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Unconcede_ext, expect.any(Object)); }); it('rollDie sends Command_RollDie', () => { rollDie(gameId, { sides: 6, count: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 }) + gameId, Data.Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 }) ); }); it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => { const targetId = 3; - const innerCmd = create(GameCommandSchema); - setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 })); + const innerCmd = create(Data.GameCommandSchema); + setExtension(innerCmd, Data.Command_DrawCards_ext, create(Data.Command_DrawCardsSchema, { number: 2 })); judge(gameId, targetId, innerCmd); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( gameId, - Command_Judge_ext, + Data.Command_Judge_ext, expect.objectContaining({ targetId: 3, gameCommand: expect.any(Array) }) ); }); diff --git a/webclient/src/websocket/commands/game/gameSay.ts b/webclient/src/websocket/commands/game/gameSay.ts index 7ae7263e9..371a3e19d 100644 --- a/webclient/src/websocket/commands/game/gameSay.ts +++ b/webclient/src/websocket/commands/game/gameSay.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_GameSay_ext, Command_GameSaySchema, type GameSayParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function gameSay(gameId: number, params: GameSayParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params)); +import { Data } from '@app/types'; + +export function gameSay(gameId: number, params: Data.GameSayParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_GameSay_ext, create(Data.Command_GameSaySchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCardCounter.ts b/webclient/src/websocket/commands/game/incCardCounter.ts index 54349d920..7374c8ab9 100644 --- a/webclient/src/websocket/commands/game/incCardCounter.ts +++ b/webclient/src/websocket/commands/game/incCardCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_IncCardCounter_ext, Command_IncCardCounterSchema, type IncCardCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function incCardCounter(gameId: number, params: IncCardCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params)); +import { Data } from '@app/types'; + +export function incCardCounter(gameId: number, params: Data.IncCardCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_IncCardCounter_ext, create(Data.Command_IncCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCounter.ts b/webclient/src/websocket/commands/game/incCounter.ts index 4ca0b8558..038dbfb42 100644 --- a/webclient/src/websocket/commands/game/incCounter.ts +++ b/webclient/src/websocket/commands/game/incCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_IncCounter_ext, Command_IncCounterSchema, type IncCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function incCounter(gameId: number, params: IncCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params)); +import { Data } from '@app/types'; + +export function incCounter(gameId: number, params: Data.IncCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_IncCounter_ext, create(Data.Command_IncCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/judge.ts b/webclient/src/websocket/commands/game/judge.ts index c61a1092d..274945428 100644 --- a/webclient/src/websocket/commands/game/judge.ts +++ b/webclient/src/websocket/commands/game/judge.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; -import { Command_Judge_ext, Command_JudgeSchema, type GameCommand } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; -export function judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Judge_ext, create(Command_JudgeSchema, { +export function judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Judge_ext, create(Data.Command_JudgeSchema, { targetId, gameCommand: [innerGameCommand], })); } + diff --git a/webclient/src/websocket/commands/game/kickFromGame.ts b/webclient/src/websocket/commands/game/kickFromGame.ts index c7b724b88..3f55e9078 100644 --- a/webclient/src/websocket/commands/game/kickFromGame.ts +++ b/webclient/src/websocket/commands/game/kickFromGame.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_KickFromGame_ext, Command_KickFromGameSchema, type KickFromGameParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function kickFromGame(gameId: number, params: KickFromGameParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params)); +import { Data } from '@app/types'; + +export function kickFromGame(gameId: number, params: Data.KickFromGameParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_KickFromGame_ext, create(Data.Command_KickFromGameSchema, params)); } diff --git a/webclient/src/websocket/commands/game/leaveGame.ts b/webclient/src/websocket/commands/game/leaveGame.ts index 05d160059..ad8b40634 100644 --- a/webclient/src/websocket/commands/game/leaveGame.ts +++ b/webclient/src/websocket/commands/game/leaveGame.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_LeaveGame_ext, Command_LeaveGameSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function leaveGame(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_LeaveGame_ext, create(Command_LeaveGameSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_LeaveGame_ext, create(Data.Command_LeaveGameSchema)); } diff --git a/webclient/src/websocket/commands/game/moveCard.ts b/webclient/src/websocket/commands/game/moveCard.ts index 9b5dc52b5..16390d3a5 100644 --- a/webclient/src/websocket/commands/game/moveCard.ts +++ b/webclient/src/websocket/commands/game/moveCard.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_MoveCard_ext, Command_MoveCardSchema, type MoveCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function moveCard(gameId: number, params: MoveCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params)); +import { Data } from '@app/types'; + +export function moveCard(gameId: number, params: Data.MoveCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_MoveCard_ext, create(Data.Command_MoveCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/mulligan.ts b/webclient/src/websocket/commands/game/mulligan.ts index b6ac92cb9..5d690b754 100644 --- a/webclient/src/websocket/commands/game/mulligan.ts +++ b/webclient/src/websocket/commands/game/mulligan.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_Mulligan_ext, Command_MulliganSchema, type MulliganParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function mulligan(gameId: number, params: MulliganParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params)); +import { Data } from '@app/types'; + +export function mulligan(gameId: number, params: Data.MulliganParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Mulligan_ext, create(Data.Command_MulliganSchema, params)); } diff --git a/webclient/src/websocket/commands/game/nextTurn.ts b/webclient/src/websocket/commands/game/nextTurn.ts index 4633e9233..08bf084d8 100644 --- a/webclient/src/websocket/commands/game/nextTurn.ts +++ b/webclient/src/websocket/commands/game/nextTurn.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_NextTurn_ext, Command_NextTurnSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function nextTurn(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_NextTurn_ext, create(Command_NextTurnSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_NextTurn_ext, create(Data.Command_NextTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/readyStart.ts b/webclient/src/websocket/commands/game/readyStart.ts index 29c65c2a8..f6f32c401 100644 --- a/webclient/src/websocket/commands/game/readyStart.ts +++ b/webclient/src/websocket/commands/game/readyStart.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_ReadyStart_ext, Command_ReadyStartSchema, type ReadyStartParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function readyStart(gameId: number, params: ReadyStartParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params)); +import { Data } from '@app/types'; + +export function readyStart(gameId: number, params: Data.ReadyStartParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_ReadyStart_ext, create(Data.Command_ReadyStartSchema, params)); } diff --git a/webclient/src/websocket/commands/game/revealCards.ts b/webclient/src/websocket/commands/game/revealCards.ts index e4a1cf715..774074378 100644 --- a/webclient/src/websocket/commands/game/revealCards.ts +++ b/webclient/src/websocket/commands/game/revealCards.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_RevealCards_ext, Command_RevealCardsSchema, type RevealCardsParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function revealCards(gameId: number, params: RevealCardsParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params)); +import { Data } from '@app/types'; + +export function revealCards(gameId: number, params: Data.RevealCardsParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_RevealCards_ext, create(Data.Command_RevealCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/reverseTurn.ts b/webclient/src/websocket/commands/game/reverseTurn.ts index 7ce90a669..5b03e8e53 100644 --- a/webclient/src/websocket/commands/game/reverseTurn.ts +++ b/webclient/src/websocket/commands/game/reverseTurn.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_ReverseTurn_ext, Command_ReverseTurnSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function reverseTurn(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReverseTurn_ext, create(Command_ReverseTurnSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_ReverseTurn_ext, create(Data.Command_ReverseTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/rollDie.ts b/webclient/src/websocket/commands/game/rollDie.ts index e76bc2445..c568e2de4 100644 --- a/webclient/src/websocket/commands/game/rollDie.ts +++ b/webclient/src/websocket/commands/game/rollDie.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_RollDie_ext, Command_RollDieSchema, type RollDieParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function rollDie(gameId: number, params: RollDieParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_RollDie_ext, create(Command_RollDieSchema, params)); +import { Data } from '@app/types'; + +export function rollDie(gameId: number, params: Data.RollDieParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_RollDie_ext, create(Data.Command_RollDieSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setActivePhase.ts b/webclient/src/websocket/commands/game/setActivePhase.ts index c56d42d37..8987ff9d2 100644 --- a/webclient/src/websocket/commands/game/setActivePhase.ts +++ b/webclient/src/websocket/commands/game/setActivePhase.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetActivePhase_ext, Command_SetActivePhaseSchema, type SetActivePhaseParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setActivePhase(gameId: number, params: SetActivePhaseParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params)); +import { Data } from '@app/types'; + +export function setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_SetActivePhase_ext, create(Data.Command_SetActivePhaseSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardAttr.ts b/webclient/src/websocket/commands/game/setCardAttr.ts index 3f62f82a6..4808fa4e1 100644 --- a/webclient/src/websocket/commands/game/setCardAttr.ts +++ b/webclient/src/websocket/commands/game/setCardAttr.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetCardAttr_ext, Command_SetCardAttrSchema, type SetCardAttrParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setCardAttr(gameId: number, params: SetCardAttrParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params)); +import { Data } from '@app/types'; + +export function setCardAttr(gameId: number, params: Data.SetCardAttrParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_SetCardAttr_ext, create(Data.Command_SetCardAttrSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardCounter.ts b/webclient/src/websocket/commands/game/setCardCounter.ts index e70631ed3..47ebc6f60 100644 --- a/webclient/src/websocket/commands/game/setCardCounter.ts +++ b/webclient/src/websocket/commands/game/setCardCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetCardCounter_ext, Command_SetCardCounterSchema, type SetCardCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setCardCounter(gameId: number, params: SetCardCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params)); +import { Data } from '@app/types'; + +export function setCardCounter(gameId: number, params: Data.SetCardCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_SetCardCounter_ext, create(Data.Command_SetCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCounter.ts b/webclient/src/websocket/commands/game/setCounter.ts index 290c1b515..8dc163e93 100644 --- a/webclient/src/websocket/commands/game/setCounter.ts +++ b/webclient/src/websocket/commands/game/setCounter.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetCounter_ext, Command_SetCounterSchema, type SetCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setCounter(gameId: number, params: SetCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params)); +import { Data } from '@app/types'; + +export function setCounter(gameId: number, params: Data.SetCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_SetCounter_ext, create(Data.Command_SetCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardLock.ts b/webclient/src/websocket/commands/game/setSideboardLock.ts index cc7844d36..3a5ea21ab 100644 --- a/webclient/src/websocket/commands/game/setSideboardLock.ts +++ b/webclient/src/websocket/commands/game/setSideboardLock.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetSideboardLock_ext, Command_SetSideboardLockSchema, type SetSideboardLockParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void { +import { Data } from '@app/types'; + +export function setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Command_SetSideboardLock_ext, - create(Command_SetSideboardLockSchema, params) + Data.Command_SetSideboardLock_ext, + create(Data.Command_SetSideboardLockSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/setSideboardPlan.ts b/webclient/src/websocket/commands/game/setSideboardPlan.ts index 8caa495c2..9472a0df6 100644 --- a/webclient/src/websocket/commands/game/setSideboardPlan.ts +++ b/webclient/src/websocket/commands/game/setSideboardPlan.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; -import { Command_SetSideboardPlan_ext, Command_SetSideboardPlanSchema, type SetSideboardPlanParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void { +import { Data } from '@app/types'; + +export function setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Command_SetSideboardPlan_ext, - create(Command_SetSideboardPlanSchema, params) + Data.Command_SetSideboardPlan_ext, + create(Data.Command_SetSideboardPlanSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/shuffle.ts b/webclient/src/websocket/commands/game/shuffle.ts index 9e9a6772c..688bd6bb0 100644 --- a/webclient/src/websocket/commands/game/shuffle.ts +++ b/webclient/src/websocket/commands/game/shuffle.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; -import { Command_Shuffle_ext, Command_ShuffleSchema, type ShuffleParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function shuffle(gameId: number, params: ShuffleParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params)); +import { Data } from '@app/types'; + +export function shuffle(gameId: number, params: Data.ShuffleParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Shuffle_ext, create(Data.Command_ShuffleSchema, params)); } diff --git a/webclient/src/websocket/commands/game/unconcede.ts b/webclient/src/websocket/commands/game/unconcede.ts index 505b5991c..5569e76ed 100644 --- a/webclient/src/websocket/commands/game/unconcede.ts +++ b/webclient/src/websocket/commands/game/unconcede.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_Unconcede_ext, Command_UnconcedeSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function unconcede(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Unconcede_ext, create(Command_UnconcedeSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Unconcede_ext, create(Data.Command_UnconcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/undoDraw.ts b/webclient/src/websocket/commands/game/undoDraw.ts index b11e76d29..0f0677352 100644 --- a/webclient/src/websocket/commands/game/undoDraw.ts +++ b/webclient/src/websocket/commands/game/undoDraw.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; -import { Command_UndoDraw_ext, Command_UndoDrawSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; +import { Data } from '@app/types'; export function undoDraw(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_UndoDraw_ext, create(Data.Command_UndoDrawSchema)); } diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts index d2c2bae64..7bb0f9989 100644 --- a/webclient/src/websocket/commands/moderator/banFromServer.ts +++ b/webclient/src/websocket/commands/moderator/banFromServer.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_BanFromServer_ext, Command_BanFromServerSchema } from '@app/generated'; +import { Data } from '@app/types'; export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, visibleReason?: string, clientid?: string, removeMessages?: number): void { - WebClient.instance.protobuf.sendModeratorCommand(Command_BanFromServer_ext, create(Command_BanFromServerSchema, { + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_BanFromServer_ext, create(Data.Command_BanFromServerSchema, { minutes, userName, address, reason, visibleReason, clientid, removeMessages }), { onSuccess: () => { diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts index 6b7625809..53ff315ba 100644 --- a/webclient/src/websocket/commands/moderator/forceActivateUser.ts +++ b/webclient/src/websocket/commands/moderator/forceActivateUser.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ForceActivateUser_ext, Command_ForceActivateUserSchema } from '@app/generated'; +import { Data } from '@app/types'; export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { - const cmd = create(Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); - WebClient.instance.protobuf.sendModeratorCommand(Command_ForceActivateUser_ext, cmd, { + const cmd = create(Data.Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_ForceActivateUser_ext, cmd, { onSuccess: () => { WebClient.instance.response.moderator.forceActivateUser(usernameToActivate, moderatorName); }, diff --git a/webclient/src/websocket/commands/moderator/getAdminNotes.ts b/webclient/src/websocket/commands/moderator/getAdminNotes.ts index 6339b8b77..734a9d041 100644 --- a/webclient/src/websocket/commands/moderator/getAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/getAdminNotes.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema, Response_GetAdminNotes_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getAdminNotes(userName: string): void { - WebClient.instance.protobuf.sendModeratorCommand(Command_GetAdminNotes_ext, create(Command_GetAdminNotesSchema, { userName }), { - responseExt: Response_GetAdminNotes_ext, + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_GetAdminNotes_ext, create(Data.Command_GetAdminNotesSchema, { userName }), { + responseExt: Data.Response_GetAdminNotes_ext, onSuccess: (response) => { WebClient.instance.response.moderator.getAdminNotes(userName, response.notes); }, diff --git a/webclient/src/websocket/commands/moderator/getBanHistory.ts b/webclient/src/websocket/commands/moderator/getBanHistory.ts index ecc48fb26..c22e0d624 100644 --- a/webclient/src/websocket/commands/moderator/getBanHistory.ts +++ b/webclient/src/websocket/commands/moderator/getBanHistory.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetBanHistory_ext, Command_GetBanHistorySchema, Response_BanHistory_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getBanHistory(userName: string): void { - WebClient.instance.protobuf.sendModeratorCommand(Command_GetBanHistory_ext, create(Command_GetBanHistorySchema, { userName }), { - responseExt: Response_BanHistory_ext, + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_GetBanHistory_ext, create(Data.Command_GetBanHistorySchema, { userName }), { + responseExt: Data.Response_BanHistory_ext, onSuccess: (response) => { WebClient.instance.response.moderator.banHistory(userName, response.banList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnHistory.ts b/webclient/src/websocket/commands/moderator/getWarnHistory.ts index 9f27cf2c6..0aa4f6646 100644 --- a/webclient/src/websocket/commands/moderator/getWarnHistory.ts +++ b/webclient/src/websocket/commands/moderator/getWarnHistory.ts @@ -1,14 +1,14 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema, Response_WarnHistory_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getWarnHistory(userName: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Command_GetWarnHistory_ext, - create(Command_GetWarnHistorySchema, { userName }), + Data.Command_GetWarnHistory_ext, + create(Data.Command_GetWarnHistorySchema, { userName }), { - responseExt: Response_WarnHistory_ext, + responseExt: Data.Response_WarnHistory_ext, onSuccess: (response) => { WebClient.instance.response.moderator.warnHistory(userName, response.warnList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnList.ts b/webclient/src/websocket/commands/moderator/getWarnList.ts index 262cba2df..9a734727c 100644 --- a/webclient/src/websocket/commands/moderator/getWarnList.ts +++ b/webclient/src/websocket/commands/moderator/getWarnList.ts @@ -1,14 +1,14 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetWarnList_ext, Command_GetWarnListSchema, Response_WarnList_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getWarnList(modName: string, userName: string, userClientid: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Command_GetWarnList_ext, - create(Command_GetWarnListSchema, { modName, userName, userClientid }), + Data.Command_GetWarnList_ext, + create(Data.Command_GetWarnListSchema, { modName, userName, userClientid }), { - responseExt: Response_WarnList_ext, + responseExt: Data.Response_WarnList_ext, onSuccess: (response) => { WebClient.instance.response.moderator.warnListOptions([response]); }, diff --git a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts index 0b288efc7..47def64d0 100644 --- a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts +++ b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema } from '@app/generated'; +import { Data } from '@app/types'; export function grantReplayAccess(replayId: number, moderatorName: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Command_GrantReplayAccess_ext, - create(Command_GrantReplayAccessSchema, { replayId, moderatorName }), + Data.Command_GrantReplayAccess_ext, + create(Data.Command_GrantReplayAccessSchema, { replayId, moderatorName }), { onSuccess: () => { WebClient.instance.response.moderator.grantReplayAccess(replayId, moderatorName); diff --git a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts index 69ba240fd..dea409034 100644 --- a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts +++ b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts @@ -1,25 +1,28 @@ -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 { WebClient } from '../../WebClient'; -import { - Command_BanFromServer_ext, - Command_ForceActivateUser_ext, - Command_GetAdminNotes_ext, - Command_GetBanHistory_ext, - Command_GetWarnHistory_ext, - Command_GetWarnList_ext, - Command_GrantReplayAccess_ext, - Command_UpdateAdminNotes_ext, - Command_ViewLogHistory_ext, - Command_ViewLogHistorySchema, - Command_WarnUser_ext, - Response_BanHistory_ext, - Response_GetAdminNotes_ext, - Response_ViewLogHistory_ext, - Response_WarnHistory_ext, - Response_WarnList_ext, -} from '@app/generated'; +import { Data } from '@app/types'; import { banFromServer } from './banFromServer'; import { forceActivateUser } from './forceActivateUser'; @@ -47,7 +50,7 @@ describe('banFromServer', () => { it('calls sendModeratorCommand with Command_BanFromServer', () => { banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_BanFromServer_ext, + Data.Command_BanFromServer_ext, expect.objectContaining({ minutes: 30, userName: 'alice' }), expect.any(Object) ); @@ -68,7 +71,7 @@ describe('forceActivateUser', () => { it('calls sendModeratorCommand with Command_ForceActivateUser', () => { forceActivateUser('alice', 'mod1'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) + Data.Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) ); }); @@ -87,9 +90,9 @@ describe('getAdminNotes', () => { it('calls sendModeratorCommand with Command_GetAdminNotes', () => { getAdminNotes('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetAdminNotes_ext, + Data.Command_GetAdminNotes_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetAdminNotes_ext }) + expect.objectContaining({ responseExt: Data.Response_GetAdminNotes_ext }) ); }); @@ -109,9 +112,9 @@ describe('getBanHistory', () => { it('calls sendModeratorCommand with Command_GetBanHistory', () => { getBanHistory('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetBanHistory_ext, + Data.Command_GetBanHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_BanHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_BanHistory_ext }) ); }); @@ -131,9 +134,9 @@ describe('getWarnHistory', () => { it('calls sendModeratorCommand with Command_GetWarnHistory', () => { getWarnHistory('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnHistory_ext, + Data.Command_GetWarnHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_WarnHistory_ext }) ); }); @@ -153,9 +156,9 @@ describe('getWarnList', () => { it('calls sendModeratorCommand with Command_GetWarnList', () => { getWarnList('mod1', 'alice', 'US'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnList_ext, + Data.Command_GetWarnList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnList_ext }) + expect.objectContaining({ responseExt: Data.Response_WarnList_ext }) ); }); @@ -175,7 +178,7 @@ describe('grantReplayAccess', () => { it('calls sendModeratorCommand with Command_GrantReplayAccess', () => { grantReplayAccess(10, 'mod1'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) + Data.Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) ); }); @@ -194,7 +197,7 @@ describe('updateAdminNotes', () => { it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => { updateAdminNotes('alice', 'new notes'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) + Data.Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) ); }); @@ -211,17 +214,17 @@ describe('updateAdminNotes', () => { describe('viewLogHistory', () => { it('calls sendModeratorCommand with Command_ViewLogHistory', () => { - const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); + const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); viewLogHistory(filters); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ViewLogHistory_ext, + Data.Command_ViewLogHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ViewLogHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_ViewLogHistory_ext }) ); }); it('onSuccess calls response.moderator.viewLogs with logMessage', () => { - const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); + const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); viewLogHistory(filters); const resp = { logMessage: ['log1'] }; invokeOnSuccess(resp, { responseCode: 0 }); @@ -237,7 +240,7 @@ describe('warnUser', () => { it('calls sendModeratorCommand with Command_WarnUser', () => { warnUser('alice', 'bad behavior', 'cid'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_WarnUser_ext, expect.any(Object), expect.any(Object) + Data.Command_WarnUser_ext, expect.any(Object), expect.any(Object) ); }); diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts index 7d760938d..dba02650d 100644 --- a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema } from '@app/generated'; +import { Data } from '@app/types'; export function updateAdminNotes(userName: string, notes: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Command_UpdateAdminNotes_ext, - create(Command_UpdateAdminNotesSchema, { userName, notes }), + Data.Command_UpdateAdminNotes_ext, + create(Data.Command_UpdateAdminNotesSchema, { userName, notes }), { onSuccess: () => { WebClient.instance.response.moderator.updateAdminNotes(userName, notes); diff --git a/webclient/src/websocket/commands/moderator/viewLogHistory.ts b/webclient/src/websocket/commands/moderator/viewLogHistory.ts index e222899f0..956af083e 100644 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -1,12 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema, Response_ViewLogHistory_ext } from '@app/generated'; -import type { ViewLogHistoryParams } from '@app/generated'; +import { Data } from '@app/types'; -export function viewLogHistory(filters: ViewLogHistoryParams): void { - WebClient.instance.protobuf.sendModeratorCommand(Command_ViewLogHistory_ext, create(Command_ViewLogHistorySchema, filters), { - responseExt: Response_ViewLogHistory_ext, +export function viewLogHistory(filters: Data.ViewLogHistoryParams): void { + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_ViewLogHistory_ext, create(Data.Command_ViewLogHistorySchema, filters), { + responseExt: Data.Response_ViewLogHistory_ext, onSuccess: (response) => { WebClient.instance.response.moderator.viewLogs(response.logMessage); }, diff --git a/webclient/src/websocket/commands/moderator/warnUser.ts b/webclient/src/websocket/commands/moderator/warnUser.ts index 22d25ac75..13a521e59 100644 --- a/webclient/src/websocket/commands/moderator/warnUser.ts +++ b/webclient/src/websocket/commands/moderator/warnUser.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_WarnUser_ext, Command_WarnUserSchema } from '@app/generated'; +import { Data } from '@app/types'; export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); - WebClient.instance.protobuf.sendModeratorCommand(Command_WarnUser_ext, cmd, { + const cmd = create(Data.Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); + WebClient.instance.protobuf.sendModeratorCommand(Data.Command_WarnUser_ext, cmd, { onSuccess: () => { WebClient.instance.response.moderator.warnUser(userName); }, diff --git a/webclient/src/websocket/commands/room/createGame.ts b/webclient/src/websocket/commands/room/createGame.ts index 133e8a396..045cb0175 100644 --- a/webclient/src/websocket/commands/room/createGame.ts +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -1,11 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_CreateGame_ext, Command_CreateGameSchema } from '@app/generated'; -import type { CreateGameParams } from '@app/generated'; +import { Data } from '@app/types'; -export function createGame(roomId: number, gameConfig: CreateGameParams): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Command_CreateGame_ext, create(Command_CreateGameSchema, gameConfig), { +export function createGame(roomId: number, gameConfig: Data.CreateGameParams): void { + WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_CreateGame_ext, create(Data.Command_CreateGameSchema, gameConfig), { onSuccess: () => { WebClient.instance.response.room.gameCreated(roomId); }, diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts index a19721746..dbaa6ea95 100644 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -1,11 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_JoinGame_ext, Command_JoinGameSchema } from '@app/generated'; -import type { JoinGameParams } from '@app/generated'; +import { Data } from '@app/types'; -export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Command_JoinGame_ext, create(Command_JoinGameSchema, joinGameParams), { +export function joinGame(roomId: number, joinGameParams: Data.JoinGameParams): void { + WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_JoinGame_ext, create(Data.Command_JoinGameSchema, joinGameParams), { onSuccess: () => { WebClient.instance.response.room.joinedGame(roomId, joinGameParams.gameId); }, diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts index 004e704c0..8f61ee652 100644 --- a/webclient/src/websocket/commands/room/leaveRoom.ts +++ b/webclient/src/websocket/commands/room/leaveRoom.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from '@app/generated'; +import { Data } from '@app/types'; export function leaveRoom(roomId: number): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Command_LeaveRoom_ext, create(Command_LeaveRoomSchema), { + WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_LeaveRoom_ext, create(Data.Command_LeaveRoomSchema), { onSuccess: () => { WebClient.instance.response.room.leaveRoom(roomId); }, diff --git a/webclient/src/websocket/commands/room/roomCommands.spec.ts b/webclient/src/websocket/commands/room/roomCommands.spec.ts index 57dac994a..5c432f9d6 100644 --- a/webclient/src/websocket/commands/room/roomCommands.spec.ts +++ b/webclient/src/websocket/commands/room/roomCommands.spec.ts @@ -1,15 +1,21 @@ -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 { WebClient } from '../../WebClient'; -import { - Command_CreateGame_ext, - Command_CreateGameSchema, - Command_JoinGame_ext, - Command_JoinGameSchema, - Command_LeaveRoom_ext, - Command_RoomSay_ext, -} from '@app/generated'; +import { Data } from '@app/types'; import { createGame } from './createGame'; import { joinGame } from './joinGame'; @@ -30,14 +36,14 @@ const { invokeOnSuccess } = makeCallbackHelpers( describe('createGame', () => { it('calls sendRoomCommand with Command_CreateGame', () => { - createGame(5, create(Command_CreateGameSchema, { maxPlayers: 4 })); + createGame(5, create(Data.Command_CreateGameSchema, { maxPlayers: 4 })); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) + 5, Data.Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) ); }); it('onSuccess calls response.room.gameCreated with roomId', () => { - createGame(5, create(Command_CreateGameSchema, {})); + createGame(5, create(Data.Command_CreateGameSchema, {})); invokeOnSuccess(); expect(WebClient.instance.response.room.gameCreated).toHaveBeenCalledWith(5); }); @@ -49,14 +55,14 @@ describe('createGame', () => { describe('joinGame', () => { it('calls sendRoomCommand with Command_JoinGame', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42, password: '' })); + joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42, password: '' })); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) + 7, Data.Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) ); }); it('onSuccess calls response.room.joinedGame with roomId and gameId', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); + joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42 })); invokeOnSuccess(); expect(WebClient.instance.response.room.joinedGame).toHaveBeenCalledWith(7, 42); }); @@ -70,7 +76,7 @@ describe('leaveRoom', () => { it('calls sendRoomCommand with Command_LeaveRoom', () => { leaveRoom(3); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object) + 3, Data.Command_LeaveRoom_ext, expect.any(Object), expect.any(Object) ); }); @@ -90,7 +96,7 @@ describe('roomSay', () => { roomSay(2, ' hello '); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( 2, - Command_RoomSay_ext, + Data.Command_RoomSay_ext, expect.objectContaining({ message: 'hello' }) ); }); diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts index 2b3618420..ac5f964d2 100644 --- a/webclient/src/websocket/commands/room/roomSay.ts +++ b/webclient/src/websocket/commands/room/roomSay.ts @@ -1,6 +1,6 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_RoomSay_ext, Command_RoomSaySchema } from '@app/generated'; +import { Data } from '@app/types'; export function roomSay(roomId: number, message: string): void { const trimmed = message.trim(); @@ -9,5 +9,5 @@ export function roomSay(roomId: number, message: string): void { return; } - WebClient.instance.protobuf.sendRoomCommand(roomId, Command_RoomSay_ext, create(Command_RoomSaySchema, { message: trimmed })); + WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_RoomSay_ext, create(Data.Command_RoomSaySchema, { message: trimmed })); } diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts index be44b19e8..60b361c57 100644 --- a/webclient/src/websocket/commands/session/accountEdit.ts +++ b/webclient/src/websocket/commands/session/accountEdit.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_AccountEdit_ext, Command_AccountEditSchema } from '@app/generated'; +import { Data } from '@app/types'; export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country }); - WebClient.instance.protobuf.sendSessionCommand(Command_AccountEdit_ext, cmd, { + const cmd = create(Data.Command_AccountEditSchema, { passwordCheck, realName, email, country }); + WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountEdit_ext, cmd, { onSuccess: () => { WebClient.instance.response.session.accountEditChanged(realName, email, country); }, diff --git a/webclient/src/websocket/commands/session/accountImage.ts b/webclient/src/websocket/commands/session/accountImage.ts index 47df13c92..b14181ff2 100644 --- a/webclient/src/websocket/commands/session/accountImage.ts +++ b/webclient/src/websocket/commands/session/accountImage.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_AccountImage_ext, Command_AccountImageSchema } from '@app/generated'; +import { Data } from '@app/types'; export function accountImage(image: Uint8Array): void { - WebClient.instance.protobuf.sendSessionCommand(Command_AccountImage_ext, create(Command_AccountImageSchema, { image }), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountImage_ext, create(Data.Command_AccountImageSchema, { image }), { onSuccess: () => { WebClient.instance.response.session.accountImageChanged(image); }, diff --git a/webclient/src/websocket/commands/session/accountPassword.ts b/webclient/src/websocket/commands/session/accountPassword.ts index 236c2e162..2f5476461 100644 --- a/webclient/src/websocket/commands/session/accountPassword.ts +++ b/webclient/src/websocket/commands/session/accountPassword.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from '@app/generated'; +import { Data } from '@app/types'; export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); - WebClient.instance.protobuf.sendSessionCommand(Command_AccountPassword_ext, cmd, { + const cmd = create(Data.Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); + WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountPassword_ext, cmd, { onSuccess: () => { WebClient.instance.response.session.accountPasswordChange(); }, diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts index 422ed937f..b9dffea40 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -1,37 +1,32 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_Activate_ext, - Command_ActivateSchema, - Response_ResponseCode, - type ActivateParams, -} from '@app/generated'; +import { App, Enriched, Data } from '@app/types'; -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { disconnect, login, updateStatus } from './'; -export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void { +export function activate(options: Omit, password?: string, passwordSalt?: string): void { const { userName, token } = options; - WebClient.instance.protobuf.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_Activate_ext, create(Data.Command_ActivateSchema, { ...CLIENT_CONFIG, userName, token, }), { onResponseCode: { - [Response_ResponseCode.RespActivationAccepted]: () => { + [Data.Response_ResponseCode.RespActivationAccepted]: () => { WebClient.instance.response.session.accountActivationSuccess(); login({ host: options.host, port: options.port, userName: options.userName, + reason: App.WebSocketConnectReason.LOGIN, }, password, passwordSalt); }, }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Account Activation Failed'); disconnect(); WebClient.instance.response.session.accountActivationFailed(); }, diff --git a/webclient/src/websocket/commands/session/addToList.ts b/webclient/src/websocket/commands/session/addToList.ts index 76836cb38..d7d36db57 100644 --- a/webclient/src/websocket/commands/session/addToList.ts +++ b/webclient/src/websocket/commands/session/addToList.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_AddToList_ext, Command_AddToListSchema } from '@app/generated'; +import { Data } from '@app/types'; export function addToBuddyList(userName: string): void { addToList('buddy', userName); @@ -12,7 +12,7 @@ export function addToIgnoreList(userName: string): void { } export function addToList(list: string, userName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_AddToList_ext, create(Command_AddToListSchema, { list, userName }), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_AddToList_ext, create(Data.Command_AddToListSchema, { list, userName }), { onSuccess: () => { WebClient.instance.response.session.addToList(list, userName); }, diff --git a/webclient/src/websocket/commands/session/connect.ts b/webclient/src/websocket/commands/session/connect.ts index 035f5a60a..df494cbbb 100644 --- a/webclient/src/websocket/commands/session/connect.ts +++ b/webclient/src/websocket/commands/session/connect.ts @@ -1,10 +1,25 @@ +import { App, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; +import { updateStatus } from './'; -export function connect(target: ConnectTarget): void { - WebClient.instance.connect(target); -} - -export function testConnect(target: ConnectTarget): void { - WebClient.instance.testConnect(target); +export function connect(options: Enriched.WebSocketConnectOptions): void { + switch (options.reason) { + case App.WebSocketConnectReason.LOGIN: + case App.WebSocketConnectReason.REGISTER: + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: + case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: + case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + case App.WebSocketConnectReason.PASSWORD_RESET: + updateStatus(App.StatusEnum.CONNECTING, 'Connecting...'); + WebClient.instance.connect(options); + return; + case App.WebSocketConnectReason.TEST_CONNECTION: + WebClient.instance.testConnect(options); + return; + default: { + const { reason } = options as Enriched.WebSocketConnectOptions; + updateStatus(App.StatusEnum.DISCONNECTED, `Unknown Connection Attempt: ${reason}`); + return; + } + } } diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts index e5cc88483..bff1bf859 100644 --- a/webclient/src/websocket/commands/session/deckDel.ts +++ b/webclient/src/websocket/commands/session/deckDel.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckDel_ext, Command_DeckDelSchema } from '@app/generated'; +import { Data } from '@app/types'; export function deckDel(deckId: number): void { - WebClient.instance.protobuf.sendSessionCommand(Command_DeckDel_ext, create(Command_DeckDelSchema, { deckId }), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckDel_ext, create(Data.Command_DeckDelSchema, { deckId }), { onSuccess: () => { WebClient.instance.response.session.deleteServerDeck(deckId); }, diff --git a/webclient/src/websocket/commands/session/deckDelDir.ts b/webclient/src/websocket/commands/session/deckDelDir.ts index 740bbd02b..310f18907 100644 --- a/webclient/src/websocket/commands/session/deckDelDir.ts +++ b/webclient/src/websocket/commands/session/deckDelDir.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckDelDir_ext, Command_DeckDelDirSchema } from '@app/generated'; +import { Data } from '@app/types'; export function deckDelDir(path: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_DeckDelDir_ext, create(Command_DeckDelDirSchema, { path }), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckDelDir_ext, create(Data.Command_DeckDelDirSchema, { path }), { onSuccess: () => { WebClient.instance.response.session.deleteServerDeckDir(path); }, diff --git a/webclient/src/websocket/commands/session/deckDownload.ts b/webclient/src/websocket/commands/session/deckDownload.ts index 566e56272..baffbf262 100644 --- a/webclient/src/websocket/commands/session/deckDownload.ts +++ b/webclient/src/websocket/commands/session/deckDownload.ts @@ -1,14 +1,14 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckDownload_ext, Command_DeckDownloadSchema, Response_DeckDownload_ext } from '@app/generated'; +import { Data } from '@app/types'; export function deckDownload(deckId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Command_DeckDownload_ext, - create(Command_DeckDownloadSchema, { deckId }), + Data.Command_DeckDownload_ext, + create(Data.Command_DeckDownloadSchema, { deckId }), { - responseExt: Response_DeckDownload_ext, + responseExt: Data.Response_DeckDownload_ext, onSuccess: (response) => { WebClient.instance.response.session.downloadServerDeck(deckId, response); }, diff --git a/webclient/src/websocket/commands/session/deckList.ts b/webclient/src/websocket/commands/session/deckList.ts index e0940010b..b9afc6440 100644 --- a/webclient/src/websocket/commands/session/deckList.ts +++ b/webclient/src/websocket/commands/session/deckList.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckList_ext, Command_DeckListSchema, Response_DeckList_ext } from '@app/generated'; +import { Data } from '@app/types'; export function deckList(): void { - WebClient.instance.protobuf.sendSessionCommand(Command_DeckList_ext, create(Command_DeckListSchema), { - responseExt: Response_DeckList_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckList_ext, create(Data.Command_DeckListSchema), { + responseExt: Data.Response_DeckList_ext, onSuccess: (response) => { if (response.root) { WebClient.instance.response.session.updateServerDecks(response); diff --git a/webclient/src/websocket/commands/session/deckNewDir.ts b/webclient/src/websocket/commands/session/deckNewDir.ts index dc27fb0ca..0786936da 100644 --- a/webclient/src/websocket/commands/session/deckNewDir.ts +++ b/webclient/src/websocket/commands/session/deckNewDir.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckNewDir_ext, Command_DeckNewDirSchema } from '@app/generated'; +import { Data } from '@app/types'; export function deckNewDir(path: string, dirName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_DeckNewDir_ext, create(Command_DeckNewDirSchema, { path, dirName }), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckNewDir_ext, create(Data.Command_DeckNewDirSchema, { path, dirName }), { onSuccess: () => { WebClient.instance.response.session.createServerDeckDir(path, dirName); }, diff --git a/webclient/src/websocket/commands/session/deckUpload.ts b/webclient/src/websocket/commands/session/deckUpload.ts index 8b4c647ba..7f0c0de67 100644 --- a/webclient/src/websocket/commands/session/deckUpload.ts +++ b/webclient/src/websocket/commands/session/deckUpload.ts @@ -1,14 +1,14 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_DeckUpload_ext, Command_DeckUploadSchema, Response_DeckUpload_ext } from '@app/generated'; +import { Data } from '@app/types'; export function deckUpload(path: string, deckId: number, deckList: string): void { WebClient.instance.protobuf.sendSessionCommand( - Command_DeckUpload_ext, - create(Command_DeckUploadSchema, { path, deckId, deckList }), + Data.Command_DeckUpload_ext, + create(Data.Command_DeckUploadSchema, { path, deckId, deckList }), { - responseExt: Response_DeckUpload_ext, + responseExt: Data.Response_DeckUpload_ext, onSuccess: (response) => { if (response.newFile) { WebClient.instance.response.session.uploadServerDeck(path, response.newFile); diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts index 7246580af..fb4e7d468 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -1,34 +1,29 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_ForgotPasswordChallenge_ext, - Command_ForgotPasswordChallengeSchema, - type ForgotPasswordChallengeParams, -} from '@app/generated'; +import { App, Enriched, Data } from '@app/types'; -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { disconnect, updateStatus } from './'; -export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void { +export function forgotPasswordChallenge(options: Enriched.PasswordResetChallengeConnectOptions): void { const { userName, email } = options; WebClient.instance.protobuf.sendSessionCommand( - Command_ForgotPasswordChallenge_ext, - create(Command_ForgotPasswordChallengeSchema, { + Data.Command_ForgotPasswordChallenge_ext, + create(Data.Command_ForgotPasswordChallengeSchema, { ...CLIENT_CONFIG, userName, email, }), { onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPassword(); disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts index cf8246b30..ddc268c5a 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -1,37 +1,31 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_ForgotPasswordRequest_ext, - Command_ForgotPasswordRequestSchema, - Response_ForgotPasswordRequest_ext, - type ForgotPasswordRequestParams, -} from '@app/generated'; +import { App, Enriched, Data } from '@app/types'; -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { disconnect, updateStatus } from './'; -export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void { +export function forgotPasswordRequest(options: Enriched.PasswordResetRequestConnectOptions): void { const { userName } = options; - WebClient.instance.protobuf.sendSessionCommand(Command_ForgotPasswordRequest_ext, create(Command_ForgotPasswordRequestSchema, { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_ForgotPasswordRequest_ext, create(Data.Command_ForgotPasswordRequestSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Response_ForgotPasswordRequest_ext, + responseExt: Data.Response_ForgotPasswordRequest_ext, onSuccess: (resp) => { if (resp?.challengeEmail) { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordChallenge(); } else { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPassword(); } disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts index 3acd946b7..62b3ba84f 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -1,26 +1,22 @@ +import { App, Enriched, Data } from '@app/types'; + import { create } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; -import { - Command_ForgotPasswordReset_ext, - Command_ForgotPasswordResetSchema, - type ForgotPasswordResetParams, -} from '@app/generated'; - -import { StatusEnum } from '../../interfaces/StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { hashPassword } from '../../utils'; + import { disconnect, updateStatus } from '.'; export function forgotPasswordReset( - options: ConnectTarget & ForgotPasswordResetParams, + options: Omit, newPassword?: string, passwordSalt?: string ): void { const { userName, token } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, token, @@ -30,16 +26,16 @@ export function forgotPasswordReset( }; WebClient.instance.protobuf.sendSessionCommand( - Command_ForgotPasswordReset_ext, - create(Command_ForgotPasswordResetSchema, params), + Data.Command_ForgotPasswordReset_ext, + create(Data.Command_ForgotPasswordResetSchema, params), { onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordSuccess(); disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/getGamesOfUser.ts b/webclient/src/websocket/commands/session/getGamesOfUser.ts index 91cb2c3ec..23ec2f784 100644 --- a/webclient/src/websocket/commands/session/getGamesOfUser.ts +++ b/webclient/src/websocket/commands/session/getGamesOfUser.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetGamesOfUser_ext, Command_GetGamesOfUserSchema, Response_GetGamesOfUser_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getGamesOfUser(userName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_GetGamesOfUser_ext, create(Command_GetGamesOfUserSchema, { userName }), { - responseExt: Response_GetGamesOfUser_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_GetGamesOfUser_ext, create(Data.Command_GetGamesOfUserSchema, { userName }), { + responseExt: Data.Response_GetGamesOfUser_ext, onSuccess: (response) => { WebClient.instance.response.session.getGamesOfUser(userName, response); }, diff --git a/webclient/src/websocket/commands/session/getUserInfo.ts b/webclient/src/websocket/commands/session/getUserInfo.ts index ff266670f..f7033d451 100644 --- a/webclient/src/websocket/commands/session/getUserInfo.ts +++ b/webclient/src/websocket/commands/session/getUserInfo.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_GetUserInfo_ext, Command_GetUserInfoSchema, Response_GetUserInfo_ext } from '@app/generated'; +import { Data } from '@app/types'; export function getUserInfo(userName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_GetUserInfo_ext, create(Command_GetUserInfoSchema, { userName }), { - responseExt: Response_GetUserInfo_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_GetUserInfo_ext, create(Data.Command_GetUserInfoSchema, { userName }), { + responseExt: Data.Response_GetUserInfo_ext, onSuccess: (response) => { WebClient.instance.response.session.getUserInfo(response.userInfo); }, diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts index 494819208..9c67d818d 100644 --- a/webclient/src/websocket/commands/session/joinRoom.ts +++ b/webclient/src/websocket/commands/session/joinRoom.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_JoinRoom_ext, Command_JoinRoomSchema, Response_JoinRoom_ext } from '@app/generated'; +import { Data } from '@app/types'; export function joinRoom(roomId: number): void { - WebClient.instance.protobuf.sendSessionCommand(Command_JoinRoom_ext, create(Command_JoinRoomSchema, { roomId }), { - responseExt: Response_JoinRoom_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_JoinRoom_ext, create(Data.Command_JoinRoomSchema, { roomId }), { + responseExt: Data.Response_JoinRoom_ext, onSuccess: (response) => { if (response.roomInfo) { WebClient.instance.response.room.joinRoom(response.roomInfo); diff --git a/webclient/src/websocket/commands/session/listRooms.ts b/webclient/src/websocket/commands/session/listRooms.ts index 25b0837f3..a54d12ad1 100644 --- a/webclient/src/websocket/commands/session/listRooms.ts +++ b/webclient/src/websocket/commands/session/listRooms.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ListRooms_ext, Command_ListRoomsSchema } from '@app/generated'; +import { Data } from '@app/types'; export function listRooms(): void { - WebClient.instance.protobuf.sendSessionCommand(Command_ListRooms_ext, create(Command_ListRoomsSchema)); + WebClient.instance.protobuf.sendSessionCommand(Data.Command_ListRooms_ext, create(Data.Command_ListRoomsSchema)); } diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts index 9978cca48..7e867754c 100644 --- a/webclient/src/websocket/commands/session/listUsers.ts +++ b/webclient/src/websocket/commands/session/listUsers.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ListUsers_ext, Command_ListUsersSchema, Response_ListUsers_ext } from '@app/generated'; +import { Data } from '@app/types'; export function listUsers(): void { - WebClient.instance.protobuf.sendSessionCommand(Command_ListUsers_ext, create(Command_ListUsersSchema), { - responseExt: Response_ListUsers_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_ListUsers_ext, create(Data.Command_ListUsersSchema), { + responseExt: Data.Response_ListUsers_ext, onSuccess: (response) => { WebClient.instance.response.session.updateUsers(response.userList); }, diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts index 80fddfcc7..e2e759612 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -1,17 +1,9 @@ +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; -import { - Command_Login_ext, - Command_LoginSchema, - Response_Login_ext, - Response_ResponseCode, - type LoginParams, -} from '@app/generated'; - -import { StatusEnum } from '../../interfaces/StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { hashPassword } from '../../utils'; import { disconnect, @@ -20,7 +12,7 @@ import { updateStatus, } from './'; -export function login(options: ConnectTarget & LoginParams, password?: string, passwordSalt?: string): void { +export function login(options: Omit, password?: string, passwordSalt?: string): void { const { userName, hashedPassword } = options; const loginConfig = { @@ -30,52 +22,51 @@ export function login(options: ConnectTarget & LoginParams, password?: string, p ...(passwordSalt ? { hashedPassword: hashedPassword || hashPassword(passwordSalt, password) } : { password }), - } satisfies MessageInitShape; + } satisfies MessageInitShape; const onLoginError = (message: string, extra?: () => void) => { - updateStatus(StatusEnum.DISCONNECTED, message); + updateStatus(App.StatusEnum.DISCONNECTED, message); extra?.(); WebClient.instance.response.session.loginFailed(); disconnect(); }; - WebClient.instance.protobuf.sendSessionCommand(Command_Login_ext, create(Command_LoginSchema, loginConfig), { - responseExt: Response_Login_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_Login_ext, create(Data.Command_LoginSchema, loginConfig), { + responseExt: Data.Response_Login_ext, onSuccess: (resp) => { const { buddyList, ignoreList, userInfo } = resp; WebClient.instance.response.session.updateBuddyList(buddyList); WebClient.instance.response.session.updateIgnoreList(ignoreList); WebClient.instance.response.session.updateUser(userInfo); - WebClient.instance.response.session.loginSuccessful({ ...resp, hashedPassword: loginConfig.hashedPassword }); + WebClient.instance.response.session.loginSuccessful({ hashedPassword: loginConfig.hashedPassword }); listUsers(); listRooms(); - updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); + updateStatus(App.StatusEnum.LOGGED_IN, 'Logged in.'); }, onResponseCode: { - [Response_ResponseCode.RespClientUpdateRequired]: () => + [Data.Response_ResponseCode.RespClientUpdateRequired]: () => onLoginError('Login failed: missing features'), - [Response_ResponseCode.RespWrongPassword]: () => + [Data.Response_ResponseCode.RespWrongPassword]: () => onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespUsernameInvalid]: () => + [Data.Response_ResponseCode.RespUsernameInvalid]: () => onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespWouldOverwriteOldSession]: () => + [Data.Response_ResponseCode.RespWouldOverwriteOldSession]: () => onLoginError('Login failed: duplicated user session'), - [Response_ResponseCode.RespUserIsBanned]: () => + [Data.Response_ResponseCode.RespUserIsBanned]: () => onLoginError('Login failed: banned user'), - [Response_ResponseCode.RespRegistrationRequired]: () => + [Data.Response_ResponseCode.RespRegistrationRequired]: () => onLoginError('Login failed: registration required'), - [Response_ResponseCode.RespClientIdRequired]: () => + [Data.Response_ResponseCode.RespClientIdRequired]: () => onLoginError('Login failed: missing client ID'), - [Response_ResponseCode.RespContextError]: () => + [Data.Response_ResponseCode.RespContextError]: () => onLoginError('Login failed: server error'), - [Response_ResponseCode.RespAccountNotActivated]: (raw) => + [Data.Response_ResponseCode.RespAccountNotActivated]: () => onLoginError('Login failed: account not activated', () => { WebClient.instance.response.session.accountAwaitingActivation({ - ...raw, host: options.host, port: options.port, userName: options.userName, diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts index 94afefc46..3396a3947 100644 --- a/webclient/src/websocket/commands/session/message.ts +++ b/webclient/src/websocket/commands/session/message.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_Message_ext, Command_MessageSchema } from '@app/generated'; +import { Data } from '@app/types'; export function message(userName: string, message: string): void { - WebClient.instance.protobuf.sendSessionCommand(Command_Message_ext, create(Command_MessageSchema, { userName, message })); + WebClient.instance.protobuf.sendSessionCommand(Data.Command_Message_ext, create(Data.Command_MessageSchema, { userName, message })); } diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts index fab3c9272..f60687225 100644 --- a/webclient/src/websocket/commands/session/ping.ts +++ b/webclient/src/websocket/commands/session/ping.ts @@ -1,9 +1,9 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_Ping_ext, Command_PingSchema } from '@app/generated'; +import { Data } from '@app/types'; export function ping(pingReceived: () => void): void { - WebClient.instance.protobuf.sendSessionCommand(Command_Ping_ext, create(Command_PingSchema), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_Ping_ext, create(Data.Command_PingSchema), { onResponse: () => pingReceived(), }); } diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index ec0b5ecfa..5b35a1207 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -1,24 +1,18 @@ +import { App, Enriched, Data } from '@app/types'; + import { create, getExtension } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; -import { - Command_Register_ext, - Command_RegisterSchema, - Response_Register_ext, - Response_ResponseCode, - type RegisterParams, -} from '@app/generated'; - -import { StatusEnum } from '../../interfaces/StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; + import { hashPassword } from '../../utils'; + import { login, disconnect, updateStatus } from './'; -export function register(options: ConnectTarget & RegisterParams, password?: string, passwordSalt?: string): void { +export function register(options: Omit, password?: string, passwordSalt?: string): void { const { userName, email, country, realName } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, email, @@ -31,53 +25,53 @@ export function register(options: ConnectTarget & RegisterParams, password?: str const onRegistrationError = (action: () => void) => { action(); - updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Registration failed'); disconnect(); }; - WebClient.instance.protobuf.sendSessionCommand(Command_Register_ext, create(Command_RegisterSchema, params), { + WebClient.instance.protobuf.sendSessionCommand(Data.Command_Register_ext, create(Data.Command_RegisterSchema, params), { onResponseCode: { - [Response_ResponseCode.RespRegistrationAccepted]: () => { + [Data.Response_ResponseCode.RespRegistrationAccepted]: () => { login({ host: options.host, port: options.port, userName: options.userName, + reason: App.WebSocketConnectReason.LOGIN, }, password, passwordSalt); WebClient.instance.response.session.registrationSuccess(); }, - [Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: (raw) => { - updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); + [Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { + updateStatus(App.StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); WebClient.instance.response.session.accountAwaitingActivation({ - ...raw, host: options.host, port: options.port, userName: options.userName, }); disconnect(); }, - [Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + [Data.Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( () => WebClient.instance.response.session.registrationUserNameError('Username is taken') ), - [Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + [Data.Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( () => WebClient.instance.response.session.registrationUserNameError('Invalid username') ), - [Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + [Data.Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( () => WebClient.instance.response.session.registrationPasswordError('Your password was too short') ), - [Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + [Data.Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( () => WebClient.instance.response.session.registrationRequiresEmail() ), - [Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + [Data.Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( () => WebClient.instance.response.session.registrationEmailError('This email provider has been blocked') ), - [Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( + [Data.Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( () => WebClient.instance.response.session.registrationEmailError('Max accounts reached for this email') ), - [Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + [Data.Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( () => WebClient.instance.response.session.registrationFailed('Registration is currently disabled') ), - [Response_ResponseCode.RespUserIsBanned]: (raw) => { - const register = getExtension(raw, Response_Register_ext); + [Data.Response_ResponseCode.RespUserIsBanned]: (raw) => { + const register = getExtension(raw, Data.Response_Register_ext); onRegistrationError( () => WebClient.instance.response.session.registrationFailed(register.deniedReasonStr, Number(register.deniedEndTime)) ); diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts index 82d971b78..16e56adf8 100644 --- a/webclient/src/websocket/commands/session/removeFromList.ts +++ b/webclient/src/websocket/commands/session/removeFromList.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_RemoveFromList_ext, Command_RemoveFromListSchema } from '@app/generated'; +import { Data } from '@app/types'; export function removeFromBuddyList(userName: string): void { removeFromList('buddy', userName); @@ -13,8 +13,8 @@ export function removeFromIgnoreList(userName: string): void { export function removeFromList(list: string, userName: string): void { WebClient.instance.protobuf.sendSessionCommand( - Command_RemoveFromList_ext, - create(Command_RemoveFromListSchema, { list, userName }), + Data.Command_RemoveFromList_ext, + create(Data.Command_RemoveFromListSchema, { list, userName }), { onSuccess: () => { WebClient.instance.response.session.removeFromList(list, userName); diff --git a/webclient/src/websocket/commands/session/replayDeleteMatch.ts b/webclient/src/websocket/commands/session/replayDeleteMatch.ts index 1da4fe7d1..059efabda 100644 --- a/webclient/src/websocket/commands/session/replayDeleteMatch.ts +++ b/webclient/src/websocket/commands/session/replayDeleteMatch.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplayDeleteMatch_ext, Command_ReplayDeleteMatchSchema } from '@app/generated'; +import { Data } from '@app/types'; export function replayDeleteMatch(gameId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Command_ReplayDeleteMatch_ext, - create(Command_ReplayDeleteMatchSchema, { gameId }), + Data.Command_ReplayDeleteMatch_ext, + create(Data.Command_ReplayDeleteMatchSchema, { gameId }), { onSuccess: () => { WebClient.instance.response.session.replayDeleteMatch(gameId); diff --git a/webclient/src/websocket/commands/session/replayDownload.ts b/webclient/src/websocket/commands/session/replayDownload.ts index 928abfd15..950bf5891 100644 --- a/webclient/src/websocket/commands/session/replayDownload.ts +++ b/webclient/src/websocket/commands/session/replayDownload.ts @@ -1,14 +1,14 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplayDownload_ext, Command_ReplayDownloadSchema, Response_ReplayDownload_ext } from '@app/generated'; +import { Data } from '@app/types'; export function replayDownload(replayId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Command_ReplayDownload_ext, - create(Command_ReplayDownloadSchema, { replayId }), + Data.Command_ReplayDownload_ext, + create(Data.Command_ReplayDownloadSchema, { replayId }), { - responseExt: Response_ReplayDownload_ext, + responseExt: Data.Response_ReplayDownload_ext, onSuccess: (response) => { WebClient.instance.response.session.replayDownloaded(replayId, response); }, diff --git a/webclient/src/websocket/commands/session/replayGetCode.ts b/webclient/src/websocket/commands/session/replayGetCode.ts index 272307845..f53751f57 100644 --- a/webclient/src/websocket/commands/session/replayGetCode.ts +++ b/webclient/src/websocket/commands/session/replayGetCode.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplayGetCode_ext, Command_ReplayGetCodeSchema, Response_ReplayGetCode_ext } from '@app/generated'; +import { Data } from '@app/types'; export function replayGetCode(gameId: number, onCodeReceived: (code: string) => void): void { - WebClient.instance.protobuf.sendSessionCommand(Command_ReplayGetCode_ext, create(Command_ReplayGetCodeSchema, { gameId }), { - responseExt: Response_ReplayGetCode_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_ReplayGetCode_ext, create(Data.Command_ReplayGetCodeSchema, { gameId }), { + responseExt: Data.Response_ReplayGetCode_ext, onSuccess: (response) => { onCodeReceived(response.replayCode); }, diff --git a/webclient/src/websocket/commands/session/replayList.ts b/webclient/src/websocket/commands/session/replayList.ts index 60919f534..6618f41f0 100644 --- a/webclient/src/websocket/commands/session/replayList.ts +++ b/webclient/src/websocket/commands/session/replayList.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplayList_ext, Command_ReplayListSchema, Response_ReplayList_ext } from '@app/generated'; +import { Data } from '@app/types'; export function replayList(): void { - WebClient.instance.protobuf.sendSessionCommand(Command_ReplayList_ext, create(Command_ReplayListSchema), { - responseExt: Response_ReplayList_ext, + WebClient.instance.protobuf.sendSessionCommand(Data.Command_ReplayList_ext, create(Data.Command_ReplayListSchema), { + responseExt: Data.Response_ReplayList_ext, onSuccess: (response) => { WebClient.instance.response.session.replayList(response.matchList); }, diff --git a/webclient/src/websocket/commands/session/replayModifyMatch.ts b/webclient/src/websocket/commands/session/replayModifyMatch.ts index 3d4e4af53..3803d04bd 100644 --- a/webclient/src/websocket/commands/session/replayModifyMatch.ts +++ b/webclient/src/websocket/commands/session/replayModifyMatch.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplayModifyMatch_ext, Command_ReplayModifyMatchSchema } from '@app/generated'; +import { Data } from '@app/types'; export function replayModifyMatch(gameId: number, doNotHide: boolean): void { WebClient.instance.protobuf.sendSessionCommand( - Command_ReplayModifyMatch_ext, - create(Command_ReplayModifyMatchSchema, { gameId, doNotHide }), + Data.Command_ReplayModifyMatch_ext, + create(Data.Command_ReplayModifyMatchSchema, { gameId, doNotHide }), { onSuccess: () => { WebClient.instance.response.session.replayModifyMatch(gameId, doNotHide); diff --git a/webclient/src/websocket/commands/session/replaySubmitCode.ts b/webclient/src/websocket/commands/session/replaySubmitCode.ts index aa0e403e0..dec72113f 100644 --- a/webclient/src/websocket/commands/session/replaySubmitCode.ts +++ b/webclient/src/websocket/commands/session/replaySubmitCode.ts @@ -1,6 +1,6 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Command_ReplaySubmitCode_ext, Command_ReplaySubmitCodeSchema } from '@app/generated'; +import { Data } from '@app/types'; export function replaySubmitCode( replayCode: string, @@ -8,8 +8,8 @@ export function replaySubmitCode( onError?: (responseCode: number) => void, ): void { WebClient.instance.protobuf.sendSessionCommand( - Command_ReplaySubmitCode_ext, - create(Command_ReplaySubmitCodeSchema, { replayCode }), + Data.Command_ReplaySubmitCode_ext, + create(Data.Command_ReplaySubmitCodeSchema, { replayCode }), { onSuccess, onError, diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index 73fed0aec..8c7843c82 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -1,41 +1,66 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_RequestPasswordSalt_ext, - Command_RequestPasswordSaltSchema, - Response_PasswordSalt_ext, - Response_ResponseCode, - type RequestPasswordSaltParams, -} from '@app/generated'; +import { App, Enriched, Data } from '@app/types'; -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../interfaces/WebClientConfig'; -import { updateStatus } from './'; -export function requestPasswordSalt( - options: ConnectTarget & RequestPasswordSaltParams, - onSaltReceived: (passwordSalt: string) => void, - onFailure: () => void, -): void { +import { + activate, + disconnect, + login, + forgotPasswordReset, + updateStatus +} from './'; + +type PasswordSaltOptions = + | Omit + | Omit + | Omit; + +export function requestPasswordSalt(options: PasswordSaltOptions, password?: string, newPassword?: string): void { const { userName } = options; - WebClient.instance.protobuf.sendSessionCommand(Command_RequestPasswordSalt_ext, create(Command_RequestPasswordSaltSchema, { + const onFailure = () => { + switch (options.reason) { + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: + WebClient.instance.response.session.accountActivationFailed(); + break; + case App.WebSocketConnectReason.PASSWORD_RESET: + WebClient.instance.response.session.resetPasswordFailed(); + break; + default: + WebClient.instance.response.session.loginFailed(); + } + disconnect(); + }; + + WebClient.instance.protobuf.sendSessionCommand(Data.Command_RequestPasswordSalt_ext, create(Data.Command_RequestPasswordSaltSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Response_PasswordSalt_ext, + responseExt: Data.Response_PasswordSalt_ext, onSuccess: (resp) => { - onSaltReceived(resp?.passwordSalt); + const passwordSalt = resp?.passwordSalt; + + switch (options.reason) { + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: + activate(options, password, passwordSalt); + break; + case App.WebSocketConnectReason.PASSWORD_RESET: + forgotPasswordReset(options, newPassword, passwordSalt); + break; + default: + login(options, password, passwordSalt); + } }, onResponseCode: { - [Response_ResponseCode.RespRegistrationRequired]: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); + [Data.Response_ResponseCode.RespRegistrationRequired]: () => { + updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: registration required'); onFailure(); }, }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); onFailure(); }, }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts index 7f0b9e2c6..09f4876c9 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts @@ -1,7 +1,10 @@ // Tests for complex session commands that call WebClient directly // or have multiple branching callbacks. -vi.mock('../../WebClient'); +vi.mock('../../WebClient', async () => { + const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks'); + return { WebClient: { instance: makeWebClientMock() } }; +}); vi.mock('../../utils', async () => { const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); @@ -18,29 +21,12 @@ import { Mock } from 'vitest'; import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import { WebClient } from '../../WebClient'; import * as SessionIndexMocks from './'; -import { App, Enriched } from '@app/types'; -import { StatusEnum } from '../../interfaces/StatusEnum'; -import { - Command_Activate_ext, - Command_ForgotPasswordChallenge_ext, - Command_ForgotPasswordRequest_ext, - Command_ForgotPasswordReset_ext, - Command_Login_ext, - Command_Register_ext, - Command_RequestPasswordSalt_ext, - Response_ForgotPasswordRequest_ext, - Response_Login_ext, - Response_PasswordSalt_ext, - Response_Register_ext, - Response_RegisterSchema, - Response_ResponseCode, - ResponseSchema, -} from '@app/generated'; +import { App, Enriched, Data } from '@app/types'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; import { create, setExtension } from '@bufbuild/protobuf'; -import { connect, testConnect } from './connect'; +import { connect } from './connect'; import { updateStatus } from './updateStatus'; import { login } from './login'; import { register } from './register'; @@ -50,6 +36,7 @@ import { forgotPasswordRequest } from './forgotPasswordRequest'; import { forgotPasswordReset } from './forgotPasswordReset'; import { requestPasswordSalt } from './requestPasswordSalt'; + const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers( WebClient.instance.protobuf.sendSessionCommand as Mock, 2 @@ -101,7 +88,13 @@ const makeForgotResetOpts = (): Enriched.PasswordResetConnectOptions => ({ newPassword: 'newpw', reason: App.WebSocketConnectReason.PASSWORD_RESET, }); - +const makeSaltOpts = ( + reason: App.WebSocketConnectReason, + extras: Record = {} +) => ({ ...baseTransport, userName: 'alice', reason, ...extras } as + | Enriched.LoginConnectOptions + | Enriched.ActivateConnectOptions + | Enriched.PasswordResetConnectOptions); beforeEach(() => { (hashPassword as Mock).mockReturnValue('hashed_pw'); @@ -114,17 +107,47 @@ beforeEach(() => { // ---------------------------------------------------------------- describe('connect', () => { - it('calls WebClient.instance.connect with the target', () => { - connect({ host: 'h', port: '1' }); - expect(WebClient.instance.connect).toHaveBeenCalledWith({ host: 'h', port: '1' }); + it('calls updateStatus CONNECTING for LOGIN reason', () => { + connect({ host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + expect(WebClient.instance.connect).toHaveBeenCalled(); }); -}); -describe('testConnect', () => { + it('calls updateStatus CONNECTING for REGISTER reason', () => { + connect(makeRegisterOpts({ userName: 'u', realName: 'U' })); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + }); - it('calls WebClient.instance.testConnect with the target', () => { - testConnect({ host: 'h', port: '1' }); - expect(WebClient.instance.testConnect).toHaveBeenCalledWith({ host: 'h', port: '1' }); + it('calls updateStatus CONNECTING for ACTIVATE_ACCOUNT reason', () => { + connect({ host: 'h', port: '1', userName: 'u', token: 'tok', reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + }); + + it('calls updateStatus CONNECTING for PASSWORD_RESET_REQUEST reason', () => { + connect({ host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + }); + + it('calls updateStatus CONNECTING for PASSWORD_RESET_CHALLENGE reason', () => { + connect({ host: 'h', port: '1', userName: 'u', email: 'a@b.com', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + }); + + it('calls updateStatus CONNECTING for PASSWORD_RESET reason', () => { + connect({ host: 'h', port: '1', userName: 'u', token: 'tok', newPassword: 'newpw', reason: App.WebSocketConnectReason.PASSWORD_RESET }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); + }); + + it('calls testConnect for TEST_CONNECTION reason', () => { + connect({ host: 'h', port: '1', reason: App.WebSocketConnectReason.TEST_CONNECTION }); + expect(WebClient.instance.testConnect).toHaveBeenCalled(); + expect(WebClient.instance.connect).not.toHaveBeenCalled(); + }); + + it('calls updateStatus DISCONNECTED for unknown reason', () => { + const bogus = { host: 'h', port: '1', reason: 999 as App.WebSocketConnectReason }; + connect(bogus as Enriched.WebSocketConnectOptions); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, expect.stringContaining('Unknown')); }); }); @@ -134,9 +157,9 @@ describe('testConnect', () => { describe('updateStatus', () => { it('calls WebClient.instance.response.session.updateStatus and WebClient.instance.updateStatus', () => { - updateStatus(StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.response.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED); + updateStatus(App.StatusEnum.CONNECTED, 'OK'); + expect(WebClient.instance.response.session.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'OK'); + expect(WebClient.instance.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED); }); }); @@ -148,27 +171,27 @@ describe('login', () => { it('sends Command_Login with plain password when no salt', () => { login(makeLoginOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ password: 'pw' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); it('sends Command_Login with hashedPassword when salt is given', () => { login(makeLoginOpts(), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); it('uses options.hashedPassword if provided', () => { login(makeLoginOpts({ hashedPassword: 'pre_hashed' }), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ hashedPassword: 'pre_hashed' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); @@ -182,7 +205,7 @@ describe('login', () => { expect(WebClient.instance.response.session.loginSuccessful).toHaveBeenCalled(); expect(SessionIndexMocks.listUsers).toHaveBeenCalled(); expect(SessionIndexMocks.listRooms).toHaveBeenCalled(); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.'); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.LOGGED_IN, 'Logged in.'); }); it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { @@ -203,56 +226,56 @@ describe('login', () => { it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientUpdateRequired); + invokeResponseCode(Data.Response_ResponseCode.RespClientUpdateRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespWrongPassword', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespWrongPassword); + invokeResponseCode(Data.Response_ResponseCode.RespWrongPassword); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUsernameInvalid', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); + invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespWouldOverwriteOldSession', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespWouldOverwriteOldSession); + invokeResponseCode(Data.Response_ResponseCode.RespWouldOverwriteOldSession); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUserIsBanned', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned); + invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespClientIdRequired', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientIdRequired); + invokeResponseCode(Data.Response_ResponseCode.RespClientIdRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespContextError', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespContextError); + invokeResponseCode(Data.Response_ResponseCode.RespContextError); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation without password in options', () => { login(makeLoginOpts({ password: 'leaked' }), 'pw'); - invokeResponseCode(Response_ResponseCode.RespAccountNotActivated); + invokeResponseCode(Data.Response_ResponseCode.RespAccountNotActivated); expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -274,7 +297,7 @@ describe('register', () => { it('sends Command_Register with plain password when no salt', () => { register(makeRegisterOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, + Data.Command_Register_ext, expect.objectContaining({ password: 'pw' }), expect.any(Object) ); @@ -283,7 +306,7 @@ describe('register', () => { it('uses hashedPassword when salt is provided', () => { register(makeRegisterOpts(), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, + Data.Command_Register_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.any(Object) ); @@ -291,21 +314,21 @@ describe('register', () => { it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', undefined); expect(WebClient.instance.response.session.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAccepted forwards salt to login', () => { register(makeRegisterOpts(), 'pw', 'mySalt'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt'); expect(WebClient.instance.response.session.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => { register(makeRegisterOpts({ password: 'leaked' }), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -314,53 +337,53 @@ describe('register', () => { it('RespUserAlreadyExists calls registrationUserNameError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserAlreadyExists); + invokeResponseCode(Data.Response_ResponseCode.RespUserAlreadyExists); expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); }); it('RespUsernameInvalid calls registrationUserNameError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); + invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); }); it('RespPasswordTooShort calls registrationPasswordError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespPasswordTooShort); + invokeResponseCode(Data.Response_ResponseCode.RespPasswordTooShort); expect(WebClient.instance.response.session.registrationPasswordError).toHaveBeenCalled(); }); it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailRequiredToRegister); + invokeResponseCode(Data.Response_ResponseCode.RespEmailRequiredToRegister); expect(WebClient.instance.response.session.registrationRequiresEmail).toHaveBeenCalled(); }); it('RespEmailBlackListed calls registrationEmailError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailBlackListed); + invokeResponseCode(Data.Response_ResponseCode.RespEmailBlackListed); expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); }); it('RespTooManyRequests calls registrationEmailError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespTooManyRequests); + invokeResponseCode(Data.Response_ResponseCode.RespTooManyRequests); expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); }); it('RespRegistrationDisabled calls registrationFailed', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationDisabled); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationDisabled); expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalled(); }); it('RespUserIsBanned calls registrationFailed with deniedReasonStr and deniedEndTime', () => { register(makeRegisterOpts(), 'pw'); - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespUserIsBanned }); - setExtension(raw, Response_Register_ext, create(Response_RegisterSchema, { + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespUserIsBanned }); + setExtension(raw, Data.Response_Register_ext, create(Data.Response_RegisterSchema, { deniedReasonStr: 'bad user', deniedEndTime: 9999n, })); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned, raw); + invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned, raw); expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalledWith('bad user', 9999); }); @@ -379,12 +402,12 @@ describe('activate', () => { it('sends Command_Activate with userName and token, not password', () => { activate(makeActivateOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, + Data.Command_Activate_ext, expect.objectContaining({ userName: 'alice', token: 'tok' }), expect.any(Object) ); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, + Data.Command_Activate_ext, expect.not.objectContaining({ password: expect.anything() }), expect.any(Object) ); @@ -392,7 +415,7 @@ describe('activate', () => { it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { activate(makeActivateOpts(), 'pw', 'salt'); - invokeResponseCode(Response_ResponseCode.RespActivationAccepted); + invokeResponseCode(Data.Response_ResponseCode.RespActivationAccepted); expect(WebClient.instance.response.session.accountActivationSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt'); }); @@ -413,7 +436,7 @@ describe('forgotPasswordChallenge', () => { it('sends Command_ForgotPasswordChallenge', () => { forgotPasswordChallenge(makeForgotChallengeOpts()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) + Data.Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) ); }); @@ -440,9 +463,9 @@ describe('forgotPasswordRequest', () => { it('sends Command_ForgotPasswordRequest', () => { forgotPasswordRequest(makeForgotRequestOpts()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordRequest_ext, + Data.Command_ForgotPasswordRequest_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ForgotPasswordRequest_ext }) + expect.objectContaining({ responseExt: Data.Response_ForgotPasswordRequest_ext }) ); }); @@ -478,7 +501,7 @@ describe('forgotPasswordReset', () => { it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { forgotPasswordReset(makeForgotResetOpts(), 'newpw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, + Data.Command_ForgotPasswordReset_ext, expect.objectContaining({ newPassword: 'newpw' }), expect.any(Object) ); @@ -487,7 +510,7 @@ describe('forgotPasswordReset', () => { it('sends hashed new password when salt provided', () => { forgotPasswordReset(makeForgotResetOpts(), 'newpw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, + Data.Command_ForgotPasswordReset_ext, expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), expect.any(Object) ); @@ -514,40 +537,66 @@ describe('forgotPasswordReset', () => { describe('requestPasswordSalt', () => { it('sends Command_RequestPasswordSalt', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RequestPasswordSalt_ext, + Data.Command_RequestPasswordSalt_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_PasswordSalt_ext }) + expect.objectContaining({ responseExt: Data.Response_PasswordSalt_ext }) ); }); - it('onSuccess calls onSaltReceived with the salt', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); + it('onSuccess with LOGIN reason forwards password+salt to login', () => { + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); const resp = { passwordSalt: 'salt123' }; invokeOnSuccess(resp, { responseCode: 0 }); - expect(onSaltReceived).toHaveBeenCalledWith('salt123'); + expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); - it('onResponseCode RespRegistrationRequired calls updateStatus and onFailure', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); - expect(onFailure).toHaveBeenCalled(); + it('onSuccess with ACTIVATE_ACCOUNT reason forwards password+salt to activate', () => { + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.ACTIVATE_ACCOUNT, { token: 'tok' }), 'pw'); + const resp = { passwordSalt: 'salt123' }; + invokeOnSuccess(resp, { responseCode: 0 }); + expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); - it('onError calls updateStatus DISCONNECTED and onFailure', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); + it('onSuccess with PASSWORD_RESET reason forwards newPassword+salt to forgotPasswordReset', () => { + requestPasswordSalt( + makeSaltOpts(App.WebSocketConnectReason.PASSWORD_RESET, { token: 'tok', newPassword: 'newpw' }), + undefined, + 'newpw' + ); + const resp = { passwordSalt: 'salt123' }; + invokeOnSuccess(resp, { responseCode: 0 }); + expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalledWith(expect.any(Object), 'newpw', 'salt123'); + }); + + it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => { + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, expect.any(String)); + expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); + }); + + it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => { + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.ACTIVATE_ACCOUNT, { token: 'tok' }), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); + expect(WebClient.instance.response.session.accountActivationFailed).toHaveBeenCalled(); + }); + + it('onError calls updateStatus DISCONNECTED and disconnect', () => { + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); invokeOnError(); expect(SessionIndexMocks.updateStatus).toHaveBeenCalled(); - expect(onFailure).toHaveBeenCalled(); + expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); + }); + + it('onError with PASSWORD_RESET reason calls resetPasswordFailed', () => { + requestPasswordSalt( + makeSaltOpts(App.WebSocketConnectReason.PASSWORD_RESET, { token: 'tok', newPassword: 'newpw' }), + undefined, + 'newpw' + ); + invokeOnError(); + expect(WebClient.instance.response.session.resetPasswordFailed).toHaveBeenCalled(); }); }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index 18c8be728..97b2acdab 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -1,6 +1,9 @@ // Shared mock setup for session command tests -vi.mock('../../WebClient'); +vi.mock('../../WebClient', async () => { + const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks'); + return { WebClient: { instance: makeWebClientMock() } }; +}); vi.mock('../../utils', async () => { const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); @@ -44,42 +47,8 @@ import { addToList, addToBuddyList, addToIgnoreList } from './addToList'; import { removeFromList, removeFromBuddyList, removeFromIgnoreList } from './removeFromList'; import { replayGetCode } from './replayGetCode'; import { replaySubmitCode } from './replaySubmitCode'; -import { - Command_AccountEdit_ext, - Command_AccountImage_ext, - Command_AccountPassword_ext, - Command_AddToList_ext, - Command_DeckDel_ext, - Command_DeckDelDir_ext, - Command_DeckDownload_ext, - Command_DeckList_ext, - Command_DeckNewDir_ext, - Command_DeckUpload_ext, - Command_GetGamesOfUser_ext, - Command_GetUserInfo_ext, - Command_JoinRoom_ext, - Command_ListRooms_ext, - Command_ListUsers_ext, - Command_Message_ext, - Command_Ping_ext, - Command_RemoveFromList_ext, - Command_ReplayDeleteMatch_ext, - Command_ReplayDownload_ext, - Command_ReplayGetCode_ext, - Command_ReplayList_ext, - Command_ReplayModifyMatch_ext, - Command_ReplaySubmitCode_ext, - Response_DeckDownload_ext, - Response_DeckList_ext, - Response_DeckUpload_ext, - Response_GetGamesOfUser_ext, - Response_GetUserInfo_ext, - Response_JoinRoom_ext, - Response_ListUsers_ext, - Response_ReplayDownload_ext, - Response_ReplayGetCode_ext, - Response_ReplayList_ext, -} from '@app/generated'; +import { Data } from '@app/types'; + const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( WebClient.instance.protobuf.sendSessionCommand as Mock, @@ -98,7 +67,7 @@ describe('accountEdit', () => { it('sends Command_AccountEdit with correct params', () => { accountEdit('pw', 'Alice', 'a@b.com', 'US'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountEdit_ext, + Data.Command_AccountEdit_ext, expect.objectContaining({ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }), expect.any(Object) ); @@ -116,7 +85,7 @@ describe('accountImage', () => { const img = new Uint8Array([1, 2]); accountImage(img); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) + Data.Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) ); }); @@ -132,7 +101,7 @@ describe('accountPassword', () => { it('sends Command_AccountPassword', () => { accountPassword('old', 'new', 'hashed'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountPassword_ext, + Data.Command_AccountPassword_ext, expect.objectContaining({ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }), expect.any(Object) ); @@ -149,7 +118,7 @@ describe('deckDel', () => { it('sends Command_DeckDel', () => { deckDel(42); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDel_ext, + Data.Command_DeckDel_ext, expect.objectContaining({ deckId: 42 }), expect.any(Object) ); @@ -166,7 +135,7 @@ describe('deckDelDir', () => { it('sends Command_DeckDelDir', () => { deckDelDir('/path'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) + Data.Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) ); }); @@ -181,9 +150,9 @@ describe('deckList', () => { it('sends Command_DeckList', () => { deckList(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckList_ext, + Data.Command_DeckList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_DeckList_ext }) + expect.objectContaining({ responseExt: Data.Response_DeckList_ext }) ); }); @@ -199,7 +168,7 @@ describe('deckNewDir', () => { it('sends Command_DeckNewDir', () => { deckNewDir('/path', 'dir'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) + Data.Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) ); }); @@ -214,9 +183,9 @@ describe('deckUpload', () => { it('sends Command_DeckUpload', () => { deckUpload('/path', 1, 'content'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckUpload_ext, + Data.Command_DeckUpload_ext, expect.objectContaining({ path: '/path', deckId: 1, deckList: 'content' }), - expect.objectContaining({ responseExt: Response_DeckUpload_ext }) + expect.objectContaining({ responseExt: Data.Response_DeckUpload_ext }) ); }); @@ -239,9 +208,9 @@ describe('getGamesOfUser', () => { it('sends Command_GetGamesOfUser', () => { getGamesOfUser('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetGamesOfUser_ext, + Data.Command_GetGamesOfUser_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetGamesOfUser_ext }) + expect.objectContaining({ responseExt: Data.Response_GetGamesOfUser_ext }) ); }); @@ -257,9 +226,9 @@ describe('getUserInfo', () => { it('sends Command_GetUserInfo', () => { getUserInfo('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetUserInfo_ext, + Data.Command_GetUserInfo_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetUserInfo_ext }) + expect.objectContaining({ responseExt: Data.Response_GetUserInfo_ext }) ); }); @@ -275,9 +244,9 @@ describe('joinRoom', () => { it('sends Command_JoinRoom', () => { joinRoom(5); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_JoinRoom_ext, + Data.Command_JoinRoom_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_JoinRoom_ext }) + expect.objectContaining({ responseExt: Data.Response_JoinRoom_ext }) ); }); @@ -292,7 +261,7 @@ describe('joinRoom', () => { describe('listRooms (command)', () => { it('sends Command_ListRooms', () => { listRooms(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith(Command_ListRooms_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith(Data.Command_ListRooms_ext, expect.any(Object)); }); }); @@ -300,9 +269,9 @@ describe('listUsers', () => { it('sends Command_ListUsers', () => { listUsers(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ListUsers_ext, + Data.Command_ListUsers_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ListUsers_ext }) + expect.objectContaining({ responseExt: Data.Response_ListUsers_ext }) ); }); @@ -318,7 +287,7 @@ describe('message', () => { it('sends Command_Message', () => { message('bob', 'hi'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) + Data.Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) ); }); @@ -329,7 +298,7 @@ describe('ping', () => { const pingReceived = vi.fn(); ping(pingReceived); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Ping_ext, expect.any(Object), expect.any(Object) + Data.Command_Ping_ext, expect.any(Object), expect.any(Object) ); }); @@ -345,7 +314,7 @@ describe('replayDeleteMatch', () => { it('sends Command_ReplayDeleteMatch', () => { replayDeleteMatch(7); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayDeleteMatch_ext, + Data.Command_ReplayDeleteMatch_ext, expect.objectContaining({ gameId: 7 }), expect.any(Object) ); @@ -362,9 +331,9 @@ describe('replayList', () => { it('sends Command_ReplayList', () => { replayList(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayList_ext, + Data.Command_ReplayList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayList_ext }) + expect.objectContaining({ responseExt: Data.Response_ReplayList_ext }) ); }); @@ -380,7 +349,7 @@ describe('replayModifyMatch', () => { it('sends Command_ReplayModifyMatch', () => { replayModifyMatch(7, true); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) + Data.Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) ); }); @@ -395,7 +364,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToBuddyList sends Command_AddToList with list=buddy', () => { addToBuddyList('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, + Data.Command_AddToList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -404,7 +373,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToIgnoreList sends Command_AddToList with list=ignore', () => { addToIgnoreList('bob'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, + Data.Command_AddToList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -421,7 +390,7 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => { removeFromBuddyList('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, + Data.Command_RemoveFromList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -430,7 +399,7 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => { removeFromIgnoreList('bob'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, + Data.Command_RemoveFromList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -447,9 +416,9 @@ describe('replayGetCode', () => { it('sends Command_ReplayGetCode with gameId and responseExt', () => { replayGetCode(42, vi.fn()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayGetCode_ext, + Data.Command_ReplayGetCode_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayGetCode_ext }) + expect.objectContaining({ responseExt: Data.Response_ReplayGetCode_ext }) ); }); @@ -465,7 +434,7 @@ describe('replaySubmitCode', () => { it('sends Command_ReplaySubmitCode with replayCode', () => { replaySubmitCode('42-abc123'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) + Data.Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) ); }); @@ -488,9 +457,9 @@ describe('deckDownload', () => { it('sends Command_DeckDownload', () => { deckDownload(42); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDownload_ext, + Data.Command_DeckDownload_ext, expect.objectContaining({ deckId: 42 }), - expect.objectContaining({ responseExt: Response_DeckDownload_ext }) + expect.objectContaining({ responseExt: Data.Response_DeckDownload_ext }) ); }); @@ -506,9 +475,9 @@ describe('replayDownload', () => { it('sends Command_ReplayDownload', () => { replayDownload(99); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayDownload_ext, + Data.Command_ReplayDownload_ext, expect.objectContaining({ replayId: 99 }), - expect.objectContaining({ responseExt: Response_ReplayDownload_ext }) + expect.objectContaining({ responseExt: Data.Response_ReplayDownload_ext }) ); }); diff --git a/webclient/src/websocket/commands/session/updateStatus.ts b/webclient/src/websocket/commands/session/updateStatus.ts index 52cb9ccbc..1289c5ed8 100644 --- a/webclient/src/websocket/commands/session/updateStatus.ts +++ b/webclient/src/websocket/commands/session/updateStatus.ts @@ -1,7 +1,7 @@ -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { App } from '@app/types'; import { WebClient } from '../../WebClient'; - -export function updateStatus(status: StatusEnum, description: string): void { +export function updateStatus(status: App.StatusEnum, description: string): void { WebClient.instance.response.session.updateStatus(status, description); + WebClient.instance.updateStatus(status); } diff --git a/webclient/src/websocket/config.ts b/webclient/src/websocket/config.ts index f27988561..9f3d12643 100644 --- a/webclient/src/websocket/config.ts +++ b/webclient/src/websocket/config.ts @@ -1,5 +1,3 @@ -export const PROTOCOL_VERSION = 14; - export const CLIENT_CONFIG = { clientid: 'webatrice', clientver: 'webclient-1.0 (2019-10-31)', @@ -21,6 +19,8 @@ export const CLIENT_CONFIG = { ] } as const; +export const PROTOCOL_VERSION = 14; + export const CLIENT_OPTIONS = { autojoinrooms: true, keepalive: 5000 diff --git a/webclient/src/websocket/events/game/attachCard.ts b/webclient/src/websocket/events/game/attachCard.ts index a3671cb05..c88082378 100644 --- a/webclient/src/websocket/events/game/attachCard.ts +++ b/webclient/src/websocket/events/game/attachCard.ts @@ -1,7 +1,6 @@ -import type { Event_AttachCard } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void { +export function attachCard(data: Data.Event_AttachCard, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardAttached(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/changeZoneProperties.ts b/webclient/src/websocket/events/game/changeZoneProperties.ts index 88d7f95f1..65b062274 100644 --- a/webclient/src/websocket/events/game/changeZoneProperties.ts +++ b/webclient/src/websocket/events/game/changeZoneProperties.ts @@ -1,7 +1,6 @@ -import type { Event_ChangeZoneProperties } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void { +export function changeZoneProperties(data: Data.Event_ChangeZoneProperties, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.zonePropertiesChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createArrow.ts b/webclient/src/websocket/events/game/createArrow.ts index 3f39fa064..6ae8946aa 100644 --- a/webclient/src/websocket/events/game/createArrow.ts +++ b/webclient/src/websocket/events/game/createArrow.ts @@ -1,7 +1,6 @@ -import type { Event_CreateArrow } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void { +export function createArrow(data: Data.Event_CreateArrow, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.arrowCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createCounter.ts b/webclient/src/websocket/events/game/createCounter.ts index db9ca6086..8c1873339 100644 --- a/webclient/src/websocket/events/game/createCounter.ts +++ b/webclient/src/websocket/events/game/createCounter.ts @@ -1,7 +1,6 @@ -import type { Event_CreateCounter } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void { +export function createCounter(data: Data.Event_CreateCounter, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.counterCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createToken.ts b/webclient/src/websocket/events/game/createToken.ts index 1542d4418..6396f5b82 100644 --- a/webclient/src/websocket/events/game/createToken.ts +++ b/webclient/src/websocket/events/game/createToken.ts @@ -1,7 +1,6 @@ -import type { Event_CreateToken } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function createToken(data: Event_CreateToken, meta: GameEventMeta): void { +export function createToken(data: Data.Event_CreateToken, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.tokenCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/delCounter.ts b/webclient/src/websocket/events/game/delCounter.ts index 1a128b0ad..ac3d40345 100644 --- a/webclient/src/websocket/events/game/delCounter.ts +++ b/webclient/src/websocket/events/game/delCounter.ts @@ -1,7 +1,6 @@ -import type { Event_DelCounter } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void { +export function delCounter(data: Data.Event_DelCounter, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.counterDeleted(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/deleteArrow.ts b/webclient/src/websocket/events/game/deleteArrow.ts index df39f5d47..52e0d6bf0 100644 --- a/webclient/src/websocket/events/game/deleteArrow.ts +++ b/webclient/src/websocket/events/game/deleteArrow.ts @@ -1,7 +1,6 @@ -import type { Event_DeleteArrow } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void { +export function deleteArrow(data: Data.Event_DeleteArrow, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.arrowDeleted(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/destroyCard.ts b/webclient/src/websocket/events/game/destroyCard.ts index 65cb87d38..70d59582c 100644 --- a/webclient/src/websocket/events/game/destroyCard.ts +++ b/webclient/src/websocket/events/game/destroyCard.ts @@ -1,7 +1,6 @@ -import type { Event_DestroyCard } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void { +export function destroyCard(data: Data.Event_DestroyCard, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardDestroyed(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/drawCards.ts b/webclient/src/websocket/events/game/drawCards.ts index c9fed3078..2932ebe3b 100644 --- a/webclient/src/websocket/events/game/drawCards.ts +++ b/webclient/src/websocket/events/game/drawCards.ts @@ -1,7 +1,6 @@ -import type { Event_DrawCards } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void { +export function drawCards(data: Data.Event_DrawCards, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardsDrawn(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/dumpZone.ts b/webclient/src/websocket/events/game/dumpZone.ts index 8302877ef..c528aba2e 100644 --- a/webclient/src/websocket/events/game/dumpZone.ts +++ b/webclient/src/websocket/events/game/dumpZone.ts @@ -1,7 +1,6 @@ -import type { Event_DumpZone } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void { +export function dumpZone(data: Data.Event_DumpZone, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.zoneDumped(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/flipCard.ts b/webclient/src/websocket/events/game/flipCard.ts index 1c4e74cd3..9930580a0 100644 --- a/webclient/src/websocket/events/game/flipCard.ts +++ b/webclient/src/websocket/events/game/flipCard.ts @@ -1,7 +1,6 @@ -import type { Event_FlipCard } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function flipCard(data: Event_FlipCard, meta: GameEventMeta): void { +export function flipCard(data: Data.Event_FlipCard, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardFlipped(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/gameClosed.ts b/webclient/src/websocket/events/game/gameClosed.ts index 73fc445e2..9a4c0924d 100644 --- a/webclient/src/websocket/events/game/gameClosed.ts +++ b/webclient/src/websocket/events/game/gameClosed.ts @@ -1,6 +1,6 @@ -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import { Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function gameClosed(_data: {}, meta: GameEventMeta): void { +export function gameClosed(_data: {}, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.gameClosed(meta.gameId); } diff --git a/webclient/src/websocket/events/game/gameEvents.spec.ts b/webclient/src/websocket/events/game/gameEvents.spec.ts index 1276f1724..d5537fb23 100644 --- a/webclient/src/websocket/events/game/gameEvents.spec.ts +++ b/webclient/src/websocket/events/game/gameEvents.spec.ts @@ -1,32 +1,47 @@ -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 { create } from '@bufbuild/protobuf'; -import { - Event_AttachCardSchema, - Event_ChangeZonePropertiesSchema, - Event_CreateArrowSchema, - Event_CreateCounterSchema, - Event_CreateTokenSchema, - Event_DelCounterSchema, - Event_DeleteArrowSchema, - Event_DestroyCardSchema, - Event_DrawCardsSchema, - Event_DumpZoneSchema, - Event_FlipCardSchema, - Event_GameSaySchema, - Event_GameStateChangedSchema, - Event_MoveCardSchema, - Event_RevealCardsSchema, - Event_ReverseTurnSchema, - Event_RollDieSchema, - Event_SetActivePhaseSchema, - Event_SetActivePlayerSchema, - Event_SetCardAttrSchema, - Event_SetCardCounterSchema, - Event_SetCounterSchema, - Event_ShuffleSchema, - ServerInfo_PlayerPropertiesSchema, -} from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; + import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { createArrow } from './createArrow'; @@ -61,7 +76,7 @@ const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedB describe('joinGame event', () => { it('delegates to WebClient.instance.response.game.playerJoined with gameId from meta', () => { - const playerProperties = create(ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); + const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); const data = { playerProperties }; joinGame(data, meta); expect(WebClient.instance.response.game.playerJoined).toHaveBeenCalledWith(5, playerProperties); @@ -99,7 +114,7 @@ describe('kicked event', () => { describe('gameStateChanged event', () => { it('delegates to WebClient.instance.response.game.gameStateChanged with gameId and full data', () => { - const data = create(Event_GameStateChangedSchema, { playerList: [] }); + const data = create(Data.Event_GameStateChangedSchema, { playerList: [] }); gameStateChanged(data, meta); expect(WebClient.instance.response.game.gameStateChanged).toHaveBeenCalledWith(5, data); }); @@ -107,7 +122,7 @@ describe('gameStateChanged event', () => { describe('playerPropertiesChanged event', () => { it('delegates to WebClient.instance.response.game.playerPropertiesChanged with gameId, playerId, properties', () => { - const playerProperties = create(ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); + const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); const data = { playerProperties }; playerPropertiesChanged(data, meta); expect(WebClient.instance.response.game.playerPropertiesChanged).toHaveBeenCalledWith(5, 2, playerProperties); @@ -116,7 +131,7 @@ describe('playerPropertiesChanged event', () => { describe('gameSay event', () => { it('delegates to WebClient.instance.response.game.gameSay with gameId, playerId, message', () => { - const data = create(Event_GameSaySchema, { message: 'gg' }); + const data = create(Data.Event_GameSaySchema, { message: 'gg' }); gameSay(data, meta); expect(WebClient.instance.response.game.gameSay).toHaveBeenCalledWith(5, 2, 'gg'); }); @@ -124,7 +139,7 @@ describe('gameSay event', () => { describe('moveCard event', () => { it('delegates to WebClient.instance.response.game.cardMoved with gameId, playerId and data', () => { - const data = create(Event_MoveCardSchema, { cardId: 3 }); + const data = create(Data.Event_MoveCardSchema, { cardId: 3 }); moveCard(data, meta); expect(WebClient.instance.response.game.cardMoved).toHaveBeenCalledWith(5, 2, data); }); @@ -132,7 +147,7 @@ describe('moveCard event', () => { describe('flipCard event', () => { it('delegates to WebClient.instance.response.game.cardFlipped with gameId, playerId and data', () => { - const data = create(Event_FlipCardSchema, { cardId: 3 }); + const data = create(Data.Event_FlipCardSchema, { cardId: 3 }); flipCard(data, meta); expect(WebClient.instance.response.game.cardFlipped).toHaveBeenCalledWith(5, 2, data); }); @@ -140,7 +155,7 @@ describe('flipCard event', () => { describe('destroyCard event', () => { it('delegates to WebClient.instance.response.game.cardDestroyed with gameId, playerId and data', () => { - const data = create(Event_DestroyCardSchema, { cardId: 3 }); + const data = create(Data.Event_DestroyCardSchema, { cardId: 3 }); destroyCard(data, meta); expect(WebClient.instance.response.game.cardDestroyed).toHaveBeenCalledWith(5, 2, data); }); @@ -148,7 +163,7 @@ describe('destroyCard event', () => { describe('attachCard event', () => { it('delegates to WebClient.instance.response.game.cardAttached with gameId, playerId and data', () => { - const data = create(Event_AttachCardSchema, { cardId: 3 }); + const data = create(Data.Event_AttachCardSchema, { cardId: 3 }); attachCard(data, meta); expect(WebClient.instance.response.game.cardAttached).toHaveBeenCalledWith(5, 2, data); }); @@ -156,7 +171,7 @@ describe('attachCard event', () => { describe('createToken event', () => { it('delegates to WebClient.instance.response.game.tokenCreated with gameId, playerId and data', () => { - const data = create(Event_CreateTokenSchema, { cardId: 3 }); + const data = create(Data.Event_CreateTokenSchema, { cardId: 3 }); createToken(data, meta); expect(WebClient.instance.response.game.tokenCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -164,7 +179,7 @@ describe('createToken event', () => { describe('setCardAttr event', () => { it('delegates to WebClient.instance.response.game.cardAttrChanged with gameId, playerId and data', () => { - const data = create(Event_SetCardAttrSchema, { cardId: 3 }); + const data = create(Data.Event_SetCardAttrSchema, { cardId: 3 }); setCardAttr(data, meta); expect(WebClient.instance.response.game.cardAttrChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -172,7 +187,7 @@ describe('setCardAttr event', () => { describe('setCardCounter event', () => { it('delegates to WebClient.instance.response.game.cardCounterChanged with gameId, playerId and data', () => { - const data = create(Event_SetCardCounterSchema, { cardId: 3 }); + const data = create(Data.Event_SetCardCounterSchema, { cardId: 3 }); setCardCounter(data, meta); expect(WebClient.instance.response.game.cardCounterChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -180,7 +195,7 @@ describe('setCardCounter event', () => { describe('createArrow event', () => { it('delegates to WebClient.instance.response.game.arrowCreated with gameId, playerId and data', () => { - const data = create(Event_CreateArrowSchema, {}); + const data = create(Data.Event_CreateArrowSchema, {}); createArrow(data, meta); expect(WebClient.instance.response.game.arrowCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -188,7 +203,7 @@ describe('createArrow event', () => { describe('deleteArrow event', () => { it('delegates to WebClient.instance.response.game.arrowDeleted with gameId, playerId and data', () => { - const data = create(Event_DeleteArrowSchema, { arrowId: 9 }); + const data = create(Data.Event_DeleteArrowSchema, { arrowId: 9 }); deleteArrow(data, meta); expect(WebClient.instance.response.game.arrowDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -196,7 +211,7 @@ describe('deleteArrow event', () => { describe('createCounter event', () => { it('delegates to WebClient.instance.response.game.counterCreated with gameId, playerId and data', () => { - const data = create(Event_CreateCounterSchema, {}); + const data = create(Data.Event_CreateCounterSchema, {}); createCounter(data, meta); expect(WebClient.instance.response.game.counterCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -204,7 +219,7 @@ describe('createCounter event', () => { describe('setCounter event', () => { it('delegates to WebClient.instance.response.game.counterSet with gameId, playerId and data', () => { - const data = create(Event_SetCounterSchema, { counterId: 1, value: 20 }); + const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 20 }); setCounter(data, meta); expect(WebClient.instance.response.game.counterSet).toHaveBeenCalledWith(5, 2, data); }); @@ -212,7 +227,7 @@ describe('setCounter event', () => { describe('delCounter event', () => { it('delegates to WebClient.instance.response.game.counterDeleted with gameId, playerId and data', () => { - const data = create(Event_DelCounterSchema, { counterId: 1 }); + const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); delCounter(data, meta); expect(WebClient.instance.response.game.counterDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -220,7 +235,7 @@ describe('delCounter event', () => { describe('drawCards event', () => { it('delegates to WebClient.instance.response.game.cardsDrawn with gameId, playerId and data', () => { - const data = create(Event_DrawCardsSchema, { number: 2, cards: [] }); + const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [] }); drawCards(data, meta); expect(WebClient.instance.response.game.cardsDrawn).toHaveBeenCalledWith(5, 2, data); }); @@ -228,7 +243,7 @@ describe('drawCards event', () => { describe('revealCards event', () => { it('delegates to WebClient.instance.response.game.cardsRevealed with gameId, playerId and data', () => { - const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); + const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); revealCards(data, meta); expect(WebClient.instance.response.game.cardsRevealed).toHaveBeenCalledWith(5, 2, data); }); @@ -236,7 +251,7 @@ describe('revealCards event', () => { describe('shuffle event', () => { it('delegates to WebClient.instance.response.game.zoneShuffled with gameId, playerId and data', () => { - const data = create(Event_ShuffleSchema, { zoneName: 'deck' }); + const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck' }); shuffle(data, meta); expect(WebClient.instance.response.game.zoneShuffled).toHaveBeenCalledWith(5, 2, data); }); @@ -244,7 +259,7 @@ describe('shuffle event', () => { describe('rollDie event', () => { it('delegates to WebClient.instance.response.game.dieRolled with gameId, playerId and data', () => { - const data = create(Event_RollDieSchema, { die: 6, result: 4 }); + const data = create(Data.Event_RollDieSchema, { die: 6, result: 4 }); rollDie(data, meta); expect(WebClient.instance.response.game.dieRolled).toHaveBeenCalledWith(5, 2, data); }); @@ -252,7 +267,7 @@ describe('rollDie event', () => { describe('setActivePlayer event', () => { it('delegates to WebClient.instance.response.game.activePlayerSet with gameId and activePlayerId', () => { - const data = create(Event_SetActivePlayerSchema, { activePlayerId: 3 }); + const data = create(Data.Event_SetActivePlayerSchema, { activePlayerId: 3 }); setActivePlayer(data, meta); expect(WebClient.instance.response.game.activePlayerSet).toHaveBeenCalledWith(5, 3); }); @@ -260,7 +275,7 @@ describe('setActivePlayer event', () => { describe('setActivePhase event', () => { it('delegates to WebClient.instance.response.game.activePhaseSet with gameId and phase', () => { - const data = create(Event_SetActivePhaseSchema, { phase: 4 }); + const data = create(Data.Event_SetActivePhaseSchema, { phase: 4 }); setActivePhase(data, meta); expect(WebClient.instance.response.game.activePhaseSet).toHaveBeenCalledWith(5, 4); }); @@ -268,7 +283,7 @@ describe('setActivePhase event', () => { describe('reverseTurn event', () => { it('delegates to WebClient.instance.response.game.turnReversed with gameId and reversed', () => { - const data = create(Event_ReverseTurnSchema, { reversed: true }); + const data = create(Data.Event_ReverseTurnSchema, { reversed: true }); reverseTurn(data, meta); expect(WebClient.instance.response.game.turnReversed).toHaveBeenCalledWith(5, true); }); @@ -276,7 +291,7 @@ describe('reverseTurn event', () => { describe('dumpZone event', () => { it('delegates to WebClient.instance.response.game.zoneDumped with gameId, playerId and data', () => { - const data = create(Event_DumpZoneSchema, { zoneName: 'hand' }); + const data = create(Data.Event_DumpZoneSchema, { zoneName: 'hand' }); dumpZone(data, meta); expect(WebClient.instance.response.game.zoneDumped).toHaveBeenCalledWith(5, 2, data); }); @@ -284,7 +299,7 @@ describe('dumpZone event', () => { describe('changeZoneProperties event', () => { it('delegates to WebClient.instance.response.game.zonePropertiesChanged with gameId, playerId and data', () => { - const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); + const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); changeZoneProperties(data, meta); expect(WebClient.instance.response.game.zonePropertiesChanged).toHaveBeenCalledWith(5, 2, data); }); diff --git a/webclient/src/websocket/events/game/gameHostChanged.ts b/webclient/src/websocket/events/game/gameHostChanged.ts index 2cc7e5064..4c24f0373 100644 --- a/webclient/src/websocket/events/game/gameHostChanged.ts +++ b/webclient/src/websocket/events/game/gameHostChanged.ts @@ -1,10 +1,10 @@ -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import { Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; /** * Event_GameHostChanged carries no payload fields. * The new host is identified by GameEvent.player_id (meta.playerId). */ -export function gameHostChanged(_data: {}, meta: GameEventMeta): void { +export function gameHostChanged(_data: {}, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.gameHostChanged(meta.gameId, meta.playerId); } diff --git a/webclient/src/websocket/events/game/gameSay.ts b/webclient/src/websocket/events/game/gameSay.ts index 72a585fef..f84c54c0f 100644 --- a/webclient/src/websocket/events/game/gameSay.ts +++ b/webclient/src/websocket/events/game/gameSay.ts @@ -1,7 +1,6 @@ -import type { Event_GameSay } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function gameSay(data: Event_GameSay, meta: GameEventMeta): void { +export function gameSay(data: Data.Event_GameSay, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.gameSay(meta.gameId, meta.playerId, data.message); } diff --git a/webclient/src/websocket/events/game/gameStateChanged.ts b/webclient/src/websocket/events/game/gameStateChanged.ts index ff3d53fa3..533ffb3c9 100644 --- a/webclient/src/websocket/events/game/gameStateChanged.ts +++ b/webclient/src/websocket/events/game/gameStateChanged.ts @@ -1,7 +1,6 @@ -import type { Event_GameStateChanged } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void { +export function gameStateChanged(data: Data.Event_GameStateChanged, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.gameStateChanged(meta.gameId, data); } diff --git a/webclient/src/websocket/events/game/index.ts b/webclient/src/websocket/events/game/index.ts index d19292e80..e9931880c 100644 --- a/webclient/src/websocket/events/game/index.ts +++ b/webclient/src/websocket/events/game/index.ts @@ -1,41 +1,6 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { - type RegistryEntry, - type GameEvent, - makeEntry, - Event_Join_ext, - Event_Leave_ext, - Event_GameClosed_ext, - Event_GameHostChanged_ext, - Event_Kicked_ext, - Event_GameStateChanged_ext, - Event_PlayerPropertiesChanged_ext, - Event_GameSay_ext, - Event_CreateArrow_ext, - Event_DeleteArrow_ext, - Event_CreateCounter_ext, - Event_SetCounter_ext, - Event_DelCounter_ext, - Event_DrawCards_ext, - Event_RevealCards_ext, - Event_Shuffle_ext, - Event_RollDie_ext, - Event_MoveCard_ext, - Event_FlipCard_ext, - Event_DestroyCard_ext, - Event_AttachCard_ext, - Event_CreateToken_ext, - Event_SetCardAttr_ext, - Event_SetCardCounter_ext, - Event_SetActivePlayer_ext, - Event_SetActivePhase_ext, - Event_DumpZone_ext, - Event_ChangeZoneProperties_ext, - Event_ReverseTurn_ext, -} from '@app/generated'; - -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import { Data, Enriched } from '@app/types'; import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; @@ -67,44 +32,44 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; -type GameRegistryEntry = RegistryEntry; +type GameRegistryEntry = Data.RegistryEntry; export type GameExtensionRegistry = GameRegistryEntry[]; function makeGameEntry( - ext: GenExtension, - handler: (value: V, meta: GameEventMeta) => void, + ext: GenExtension, + handler: (value: V, meta: Enriched.GameEventMeta) => void, ): GameRegistryEntry { - return makeEntry(ext, handler); + return Data.makeEntry(ext, handler); } export const GameEvents: GameExtensionRegistry = [ - makeGameEntry(Event_Join_ext, joinGame), - makeGameEntry(Event_Leave_ext, leaveGame), - makeGameEntry(Event_GameClosed_ext, gameClosed), - makeGameEntry(Event_GameHostChanged_ext, gameHostChanged), - makeGameEntry(Event_Kicked_ext, kicked), - makeGameEntry(Event_GameStateChanged_ext, gameStateChanged), - makeGameEntry(Event_PlayerPropertiesChanged_ext, playerPropertiesChanged), - makeGameEntry(Event_GameSay_ext, gameSay), - makeGameEntry(Event_CreateArrow_ext, createArrow), - makeGameEntry(Event_DeleteArrow_ext, deleteArrow), - makeGameEntry(Event_CreateCounter_ext, createCounter), - makeGameEntry(Event_SetCounter_ext, setCounter), - makeGameEntry(Event_DelCounter_ext, delCounter), - makeGameEntry(Event_DrawCards_ext, drawCards), - makeGameEntry(Event_RevealCards_ext, revealCards), - makeGameEntry(Event_Shuffle_ext, shuffle), - makeGameEntry(Event_RollDie_ext, rollDie), - makeGameEntry(Event_MoveCard_ext, moveCard), - makeGameEntry(Event_FlipCard_ext, flipCard), - makeGameEntry(Event_DestroyCard_ext, destroyCard), - makeGameEntry(Event_AttachCard_ext, attachCard), - makeGameEntry(Event_CreateToken_ext, createToken), - makeGameEntry(Event_SetCardAttr_ext, setCardAttr), - makeGameEntry(Event_SetCardCounter_ext, setCardCounter), - makeGameEntry(Event_SetActivePlayer_ext, setActivePlayer), - makeGameEntry(Event_SetActivePhase_ext, setActivePhase), - makeGameEntry(Event_DumpZone_ext, dumpZone), - makeGameEntry(Event_ChangeZoneProperties_ext, changeZoneProperties), - makeGameEntry(Event_ReverseTurn_ext, reverseTurn), + makeGameEntry(Data.Event_Join_ext, joinGame), + makeGameEntry(Data.Event_Leave_ext, leaveGame), + makeGameEntry(Data.Event_GameClosed_ext, gameClosed), + makeGameEntry(Data.Event_GameHostChanged_ext, gameHostChanged), + makeGameEntry(Data.Event_Kicked_ext, kicked), + makeGameEntry(Data.Event_GameStateChanged_ext, gameStateChanged), + makeGameEntry(Data.Event_PlayerPropertiesChanged_ext, playerPropertiesChanged), + makeGameEntry(Data.Event_GameSay_ext, gameSay), + makeGameEntry(Data.Event_CreateArrow_ext, createArrow), + makeGameEntry(Data.Event_DeleteArrow_ext, deleteArrow), + makeGameEntry(Data.Event_CreateCounter_ext, createCounter), + makeGameEntry(Data.Event_SetCounter_ext, setCounter), + makeGameEntry(Data.Event_DelCounter_ext, delCounter), + makeGameEntry(Data.Event_DrawCards_ext, drawCards), + makeGameEntry(Data.Event_RevealCards_ext, revealCards), + makeGameEntry(Data.Event_Shuffle_ext, shuffle), + makeGameEntry(Data.Event_RollDie_ext, rollDie), + makeGameEntry(Data.Event_MoveCard_ext, moveCard), + makeGameEntry(Data.Event_FlipCard_ext, flipCard), + makeGameEntry(Data.Event_DestroyCard_ext, destroyCard), + makeGameEntry(Data.Event_AttachCard_ext, attachCard), + makeGameEntry(Data.Event_CreateToken_ext, createToken), + makeGameEntry(Data.Event_SetCardAttr_ext, setCardAttr), + makeGameEntry(Data.Event_SetCardCounter_ext, setCardCounter), + makeGameEntry(Data.Event_SetActivePlayer_ext, setActivePlayer), + makeGameEntry(Data.Event_SetActivePhase_ext, setActivePhase), + makeGameEntry(Data.Event_DumpZone_ext, dumpZone), + makeGameEntry(Data.Event_ChangeZoneProperties_ext, changeZoneProperties), + makeGameEntry(Data.Event_ReverseTurn_ext, reverseTurn), ]; diff --git a/webclient/src/websocket/events/game/joinGame.ts b/webclient/src/websocket/events/game/joinGame.ts index f0efd619b..1f3b627d0 100644 --- a/webclient/src/websocket/events/game/joinGame.ts +++ b/webclient/src/websocket/events/game/joinGame.ts @@ -1,8 +1,7 @@ -import type { Event_Join } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function joinGame(data: Event_Join, meta: GameEventMeta): void { +export function joinGame(data: Data.Event_Join, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.playerJoined(meta.gameId, data.playerProperties); } diff --git a/webclient/src/websocket/events/game/kicked.ts b/webclient/src/websocket/events/game/kicked.ts index f63951dcc..ca4a64d5e 100644 --- a/webclient/src/websocket/events/game/kicked.ts +++ b/webclient/src/websocket/events/game/kicked.ts @@ -1,6 +1,6 @@ -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import { Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function kicked(_data: {}, meta: GameEventMeta): void { +export function kicked(_data: {}, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.kicked(meta.gameId); } diff --git a/webclient/src/websocket/events/game/leaveGame.ts b/webclient/src/websocket/events/game/leaveGame.ts index 5d2df026e..2354e41ec 100644 --- a/webclient/src/websocket/events/game/leaveGame.ts +++ b/webclient/src/websocket/events/game/leaveGame.ts @@ -1,6 +1,6 @@ -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import { Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function leaveGame(data: { reason: number }, meta: GameEventMeta): void { +export function leaveGame(data: { reason: number }, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.playerLeft(meta.gameId, meta.playerId, data.reason ?? 1); } diff --git a/webclient/src/websocket/events/game/moveCard.ts b/webclient/src/websocket/events/game/moveCard.ts index a553e727a..d2f589d2b 100644 --- a/webclient/src/websocket/events/game/moveCard.ts +++ b/webclient/src/websocket/events/game/moveCard.ts @@ -1,7 +1,6 @@ -import type { Event_MoveCard } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void { +export function moveCard(data: Data.Event_MoveCard, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardMoved(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/playerPropertiesChanged.ts b/webclient/src/websocket/events/game/playerPropertiesChanged.ts index 073f5a408..5181d9ccc 100644 --- a/webclient/src/websocket/events/game/playerPropertiesChanged.ts +++ b/webclient/src/websocket/events/game/playerPropertiesChanged.ts @@ -1,7 +1,6 @@ -import type { Event_PlayerPropertiesChanged } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: GameEventMeta): void { +export function playerPropertiesChanged(data: Data.Event_PlayerPropertiesChanged, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.playerPropertiesChanged(meta.gameId, meta.playerId, data.playerProperties); } diff --git a/webclient/src/websocket/events/game/revealCards.ts b/webclient/src/websocket/events/game/revealCards.ts index 9eb08b82a..8e2f814f5 100644 --- a/webclient/src/websocket/events/game/revealCards.ts +++ b/webclient/src/websocket/events/game/revealCards.ts @@ -1,7 +1,6 @@ -import type { Event_RevealCards } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void { +export function revealCards(data: Data.Event_RevealCards, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardsRevealed(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/reverseTurn.ts b/webclient/src/websocket/events/game/reverseTurn.ts index ef953aba8..2b21074fa 100644 --- a/webclient/src/websocket/events/game/reverseTurn.ts +++ b/webclient/src/websocket/events/game/reverseTurn.ts @@ -1,7 +1,6 @@ -import type { Event_ReverseTurn } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void { +export function reverseTurn(data: Data.Event_ReverseTurn, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.turnReversed(meta.gameId, data.reversed); } diff --git a/webclient/src/websocket/events/game/rollDie.ts b/webclient/src/websocket/events/game/rollDie.ts index ca5365144..ee8bf8098 100644 --- a/webclient/src/websocket/events/game/rollDie.ts +++ b/webclient/src/websocket/events/game/rollDie.ts @@ -1,7 +1,6 @@ -import type { Event_RollDie } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function rollDie(data: Event_RollDie, meta: GameEventMeta): void { +export function rollDie(data: Data.Event_RollDie, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.dieRolled(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setActivePhase.ts b/webclient/src/websocket/events/game/setActivePhase.ts index 134651bdb..ad7c3ba88 100644 --- a/webclient/src/websocket/events/game/setActivePhase.ts +++ b/webclient/src/websocket/events/game/setActivePhase.ts @@ -1,7 +1,6 @@ -import type { Event_SetActivePhase } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void { +export function setActivePhase(data: Data.Event_SetActivePhase, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.activePhaseSet(meta.gameId, data.phase); } diff --git a/webclient/src/websocket/events/game/setActivePlayer.ts b/webclient/src/websocket/events/game/setActivePlayer.ts index e84f44920..569e1dadb 100644 --- a/webclient/src/websocket/events/game/setActivePlayer.ts +++ b/webclient/src/websocket/events/game/setActivePlayer.ts @@ -1,7 +1,6 @@ -import type { Event_SetActivePlayer } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void { +export function setActivePlayer(data: Data.Event_SetActivePlayer, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.activePlayerSet(meta.gameId, data.activePlayerId); } diff --git a/webclient/src/websocket/events/game/setCardAttr.ts b/webclient/src/websocket/events/game/setCardAttr.ts index 3733dc7b1..a3229df7f 100644 --- a/webclient/src/websocket/events/game/setCardAttr.ts +++ b/webclient/src/websocket/events/game/setCardAttr.ts @@ -1,7 +1,6 @@ -import type { Event_SetCardAttr } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void { +export function setCardAttr(data: Data.Event_SetCardAttr, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardAttrChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setCardCounter.ts b/webclient/src/websocket/events/game/setCardCounter.ts index c96d50ac6..622e1af54 100644 --- a/webclient/src/websocket/events/game/setCardCounter.ts +++ b/webclient/src/websocket/events/game/setCardCounter.ts @@ -1,7 +1,6 @@ -import type { Event_SetCardCounter } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void { +export function setCardCounter(data: Data.Event_SetCardCounter, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.cardCounterChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setCounter.ts b/webclient/src/websocket/events/game/setCounter.ts index 310fb9e28..e62f695ba 100644 --- a/webclient/src/websocket/events/game/setCounter.ts +++ b/webclient/src/websocket/events/game/setCounter.ts @@ -1,7 +1,6 @@ -import type { Event_SetCounter } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void { +export function setCounter(data: Data.Event_SetCounter, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.counterSet(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/shuffle.ts b/webclient/src/websocket/events/game/shuffle.ts index 9d1689472..b784bb634 100644 --- a/webclient/src/websocket/events/game/shuffle.ts +++ b/webclient/src/websocket/events/game/shuffle.ts @@ -1,7 +1,6 @@ -import type { Event_Shuffle } from '@app/generated'; -import type { GameEventMeta } from '../../interfaces/WebSocketConfig'; +import type { Data, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void { +export function shuffle(data: Data.Event_Shuffle, meta: Enriched.GameEventMeta): void { WebClient.instance.response.game.zoneShuffled(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/room/index.ts b/webclient/src/websocket/events/room/index.ts index 2b27954d2..c02805659 100644 --- a/webclient/src/websocket/events/room/index.ts +++ b/webclient/src/websocket/events/room/index.ts @@ -1,15 +1,6 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { - type RegistryEntry, - type RoomEvent, - makeEntry, - Event_JoinRoom_ext, - Event_LeaveRoom_ext, - Event_ListGames_ext, - Event_RemoveMessages_ext, - Event_RoomSay_ext, -} from '@app/generated'; +import { Data } from '@app/types'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -17,20 +8,20 @@ import { listGames } from './listGames'; import { roomSay } from './roomSay'; import { removeMessages } from './removeMessages'; -type RoomRegistryEntry = RegistryEntry; +type RoomRegistryEntry = Data.RegistryEntry; export type RoomExtensionRegistry = RoomRegistryEntry[]; function makeRoomEntry( - ext: GenExtension, - handler: (value: V, roomEvent: RoomEvent) => void, + ext: GenExtension, + handler: (value: V, roomEvent: Data.RoomEvent) => void, ): RoomRegistryEntry { - return makeEntry(ext, handler); + return Data.makeEntry(ext, handler); } export const RoomEvents: RoomExtensionRegistry = [ - makeRoomEntry(Event_JoinRoom_ext, joinRoom), - makeRoomEntry(Event_LeaveRoom_ext, leaveRoom), - makeRoomEntry(Event_ListGames_ext, listGames), - makeRoomEntry(Event_RemoveMessages_ext, removeMessages), - makeRoomEntry(Event_RoomSay_ext, roomSay), + makeRoomEntry(Data.Event_JoinRoom_ext, joinRoom), + makeRoomEntry(Data.Event_LeaveRoom_ext, leaveRoom), + makeRoomEntry(Data.Event_ListGames_ext, listGames), + makeRoomEntry(Data.Event_RemoveMessages_ext, removeMessages), + makeRoomEntry(Data.Event_RoomSay_ext, roomSay), ]; diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts index 203dcebe5..f4d31943c 100644 --- a/webclient/src/websocket/events/room/joinRoom.ts +++ b/webclient/src/websocket/events/room/joinRoom.ts @@ -1,6 +1,6 @@ -import type { Event_JoinRoom, RoomEvent } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function joinRoom({ userInfo }: Event_JoinRoom, { roomId }: RoomEvent): void { +export function joinRoom({ userInfo }: Data.Event_JoinRoom, { roomId }: Data.RoomEvent): void { WebClient.instance.response.room.userJoined(roomId, userInfo); } diff --git a/webclient/src/websocket/events/room/leaveRoom.ts b/webclient/src/websocket/events/room/leaveRoom.ts index 819e8f438..b03bda6e2 100644 --- a/webclient/src/websocket/events/room/leaveRoom.ts +++ b/webclient/src/websocket/events/room/leaveRoom.ts @@ -1,6 +1,6 @@ -import type { Event_LeaveRoom, RoomEvent } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function leaveRoom({ name }: Event_LeaveRoom, { roomId }: RoomEvent): void { +export function leaveRoom({ name }: Data.Event_LeaveRoom, { roomId }: Data.RoomEvent): void { WebClient.instance.response.room.userLeft(roomId, name); } diff --git a/webclient/src/websocket/events/room/listGames.ts b/webclient/src/websocket/events/room/listGames.ts index a06d4742f..b96596223 100644 --- a/webclient/src/websocket/events/room/listGames.ts +++ b/webclient/src/websocket/events/room/listGames.ts @@ -1,6 +1,6 @@ -import type { Event_ListGames, RoomEvent } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function listGames({ gameList }: Event_ListGames, { roomId }: RoomEvent): void { +export function listGames({ gameList }: Data.Event_ListGames, { roomId }: Data.RoomEvent): void { WebClient.instance.response.room.updateGames(roomId, gameList); } diff --git a/webclient/src/websocket/events/room/removeMessages.ts b/webclient/src/websocket/events/room/removeMessages.ts index 0c196438e..343635259 100644 --- a/webclient/src/websocket/events/room/removeMessages.ts +++ b/webclient/src/websocket/events/room/removeMessages.ts @@ -1,6 +1,6 @@ -import type { Event_RemoveMessages, RoomEvent } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function removeMessages({ name, amount }: Event_RemoveMessages, { roomId }: RoomEvent): void { +export function removeMessages({ name, amount }: Data.Event_RemoveMessages, { roomId }: Data.RoomEvent): void { WebClient.instance.response.room.removeMessages(roomId, name, amount); } diff --git a/webclient/src/websocket/events/room/roomEvents.spec.ts b/webclient/src/websocket/events/room/roomEvents.spec.ts index 3955b524d..142de91d1 100644 --- a/webclient/src/websocket/events/room/roomEvents.spec.ts +++ b/webclient/src/websocket/events/room/roomEvents.spec.ts @@ -1,14 +1,21 @@ -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 { create } from '@bufbuild/protobuf'; -import { - Event_JoinRoomSchema, - Event_LeaveRoomSchema, - Event_ListGamesSchema, - Event_RemoveMessagesSchema, - Event_RoomSaySchema, - RoomEventSchema, -} from '@app/generated'; +import { Data } from '@app/types'; import { WebClient } from '../../WebClient'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -16,12 +23,12 @@ import { listGames } from './listGames'; import { removeMessages } from './removeMessages'; import { roomSay } from './roomSay'; -const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId }); +const makeRoomEvent = (roomId: number) => create(Data.RoomEventSchema, { roomId }); describe('joinRoom room event', () => { it('calls response.room.userJoined with roomId and userInfo', () => { - const data = create(Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); + const data = create(Data.Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); joinRoom(data, makeRoomEvent(3)); expect(WebClient.instance.response.room.userJoined).toHaveBeenCalledWith(3, data.userInfo); }); @@ -30,7 +37,7 @@ describe('joinRoom room event', () => { describe('leaveRoom room event', () => { it('calls response.room.userLeft with roomId and name', () => { - leaveRoom(create(Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); + leaveRoom(create(Data.Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); expect(WebClient.instance.response.room.userLeft).toHaveBeenCalledWith(4, 'alice'); }); }); @@ -38,7 +45,7 @@ describe('leaveRoom room event', () => { describe('listGames room event', () => { it('calls response.room.updateGames with roomId and gameList', () => { - const data = create(Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); + const data = create(Data.Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); listGames(data, makeRoomEvent(5)); expect(WebClient.instance.response.room.updateGames).toHaveBeenCalledWith(5, data.gameList); }); @@ -47,7 +54,7 @@ describe('listGames room event', () => { describe('removeMessages room event', () => { it('calls response.room.removeMessages with roomId, name, amount', () => { - removeMessages(create(Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); + removeMessages(create(Data.Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); expect(WebClient.instance.response.room.removeMessages).toHaveBeenCalledWith(6, 'bob', 10); }); }); @@ -59,7 +66,7 @@ describe('roomSay room event', () => { afterEach(() => vi.useRealTimers()); it('calls response.room.addMessage with roomId and message', () => { - const data = create(Event_RoomSaySchema, { message: 'hello' }); + const data = create(Data.Event_RoomSaySchema, { message: 'hello' }); roomSay(data, makeRoomEvent(7)); expect(WebClient.instance.response.room.addMessage).toHaveBeenCalledWith(7, { ...data, timeReceived: 0 }); }); diff --git a/webclient/src/websocket/events/room/roomSay.ts b/webclient/src/websocket/events/room/roomSay.ts index 919189093..c712bd89c 100644 --- a/webclient/src/websocket/events/room/roomSay.ts +++ b/webclient/src/websocket/events/room/roomSay.ts @@ -1,7 +1,9 @@ -import type { Event_RoomSay, RoomEvent } from '@app/generated'; +import type { Data } from '@app/types'; +import { Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -export function roomSay(data: Event_RoomSay, { roomId }: RoomEvent): void { - const message = { ...data, timeReceived: Date.now() }; + +export function roomSay(data: Data.Event_RoomSay, { roomId }: Data.RoomEvent): void { + const message: Enriched.Message = { ...data, timeReceived: Date.now() }; WebClient.instance.response.room.addMessage(roomId, message); } diff --git a/webclient/src/websocket/events/session/addToList.ts b/webclient/src/websocket/events/session/addToList.ts index a8e27a654..819234e94 100644 --- a/webclient/src/websocket/events/session/addToList.ts +++ b/webclient/src/websocket/events/session/addToList.ts @@ -1,7 +1,7 @@ -import type { Event_AddToList } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function addToList({ listName, userInfo }: Event_AddToList): void { +export function addToList({ listName, userInfo }: Data.Event_AddToList): void { switch (listName) { case 'buddy': { WebClient.instance.response.session.addToBuddyList(userInfo); diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts index b3422172f..fcde2fceb 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -1,8 +1,7 @@ -import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated'; -import { StatusEnum } from '../../interfaces/StatusEnum'; +import { App, Data } from '@app/types'; import { updateStatus } from '../../commands/session'; -export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void { +export function connectionClosed({ reason, reasonStr, endTime }: Data.Event_ConnectionClosed): void { let message: string; // @TODO (5) @@ -10,35 +9,35 @@ export function connectionClosed({ reason, reasonStr, endTime }: Event_Connectio message = reasonStr; } else { switch (reason) { - case Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: + case Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: message = 'The server has reached its maximum user capacity'; break; - case Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: + case Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: message = 'There are too many concurrent connections from your address'; break; - case Event_ConnectionClosed_CloseReason.BANNED: + case Data.Event_ConnectionClosed_CloseReason.BANNED: message = typeof endTime === 'number' && endTime > 0 && Number.isFinite(endTime) ? `You are banned until ${new Date(endTime * 1000).toLocaleString()}` : 'You are banned'; break; - case Event_ConnectionClosed_CloseReason.DEMOTED: + case Data.Event_ConnectionClosed_CloseReason.DEMOTED: message = 'You were demoted'; break; - case Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: + case Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: message = 'Scheduled server shutdown'; break; - case Event_ConnectionClosed_CloseReason.USERNAMEINVALID: + case Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID: message = 'Invalid username'; break; - case Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: + case Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: message = 'You have been logged out due to logging in at another location'; break; - case Event_ConnectionClosed_CloseReason.OTHER: + case Data.Event_ConnectionClosed_CloseReason.OTHER: default: message = 'Unknown reason'; break; } } - updateStatus(StatusEnum.DISCONNECTED, message); + updateStatus(App.StatusEnum.DISCONNECTED, message); } diff --git a/webclient/src/websocket/events/session/gameJoined.ts b/webclient/src/websocket/events/session/gameJoined.ts index 83a555949..ed38a4ba1 100644 --- a/webclient/src/websocket/events/session/gameJoined.ts +++ b/webclient/src/websocket/events/session/gameJoined.ts @@ -1,6 +1,6 @@ -import type { Event_GameJoined } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function gameJoined(gameJoined: Event_GameJoined): void { +export function gameJoined(gameJoined: Data.Event_GameJoined): void { WebClient.instance.response.session.gameJoined(gameJoined); } diff --git a/webclient/src/websocket/events/session/index.ts b/webclient/src/websocket/events/session/index.ts index 847ea3246..3d5935c9f 100644 --- a/webclient/src/websocket/events/session/index.ts +++ b/webclient/src/websocket/events/session/index.ts @@ -1,24 +1,6 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { - type RegistryEntry, - type SessionEvent, - makeEntry, - Event_AddToList_ext, - Event_ConnectionClosed_ext, - Event_GameJoined_ext, - Event_ListRooms_ext, - Event_NotifyUser_ext, - Event_RemoveFromList_ext, - Event_ReplayAdded_ext, - Event_ServerCompleteList_ext, - Event_ServerIdentification_ext, - Event_ServerMessage_ext, - Event_ServerShutdown_ext, - Event_UserJoined_ext, - Event_UserLeft_ext, - Event_UserMessage_ext, -} from '@app/generated'; +import { Data } from '@app/types'; import { addToList } from './addToList'; import { connectionClosed } from './connectionClosed'; @@ -35,29 +17,29 @@ import { userLeft } from './userLeft'; import { userMessage } from './userMessage'; import { gameJoined } from './gameJoined'; -type SessionRegistryEntry = RegistryEntry; +type SessionRegistryEntry = Data.RegistryEntry; export type SessionExtensionRegistry = SessionRegistryEntry[]; function makeSessionEntry( - ext: GenExtension, + ext: GenExtension, handler: (value: V) => void, ): SessionRegistryEntry { - return makeEntry(ext, handler); + return Data.makeEntry(ext, handler); } export const SessionEvents: SessionExtensionRegistry = [ - makeSessionEntry(Event_AddToList_ext, addToList), - makeSessionEntry(Event_ConnectionClosed_ext, connectionClosed), - makeSessionEntry(Event_GameJoined_ext, gameJoined), - makeSessionEntry(Event_ListRooms_ext, listRooms), - makeSessionEntry(Event_NotifyUser_ext, notifyUser), - makeSessionEntry(Event_RemoveFromList_ext, removeFromList), - makeSessionEntry(Event_ReplayAdded_ext, replayAdded), - makeSessionEntry(Event_ServerCompleteList_ext, serverCompleteList), - makeSessionEntry(Event_ServerIdentification_ext, serverIdentification), - makeSessionEntry(Event_ServerMessage_ext, serverMessage), - makeSessionEntry(Event_ServerShutdown_ext, serverShutdown), - makeSessionEntry(Event_UserJoined_ext, userJoined), - makeSessionEntry(Event_UserLeft_ext, userLeft), - makeSessionEntry(Event_UserMessage_ext, userMessage), + makeSessionEntry(Data.Event_AddToList_ext, addToList), + makeSessionEntry(Data.Event_ConnectionClosed_ext, connectionClosed), + makeSessionEntry(Data.Event_GameJoined_ext, gameJoined), + makeSessionEntry(Data.Event_ListRooms_ext, listRooms), + makeSessionEntry(Data.Event_NotifyUser_ext, notifyUser), + makeSessionEntry(Data.Event_RemoveFromList_ext, removeFromList), + makeSessionEntry(Data.Event_ReplayAdded_ext, replayAdded), + makeSessionEntry(Data.Event_ServerCompleteList_ext, serverCompleteList), + makeSessionEntry(Data.Event_ServerIdentification_ext, serverIdentification), + makeSessionEntry(Data.Event_ServerMessage_ext, serverMessage), + makeSessionEntry(Data.Event_ServerShutdown_ext, serverShutdown), + makeSessionEntry(Data.Event_UserJoined_ext, userJoined), + makeSessionEntry(Data.Event_UserLeft_ext, userLeft), + makeSessionEntry(Data.Event_UserMessage_ext, userMessage), ]; diff --git a/webclient/src/websocket/events/session/listRooms.ts b/webclient/src/websocket/events/session/listRooms.ts index 8dac345a0..30f3285e6 100644 --- a/webclient/src/websocket/events/session/listRooms.ts +++ b/webclient/src/websocket/events/session/listRooms.ts @@ -1,9 +1,9 @@ -import type { Event_ListRooms } from '@app/generated'; +import type { Data } from '@app/types'; import { CLIENT_OPTIONS } from '../../config'; import { joinRoom } from '../../commands/session'; import { WebClient } from '../../WebClient'; -export function listRooms({ roomList }: Event_ListRooms): void { +export function listRooms({ roomList }: Data.Event_ListRooms): void { WebClient.instance.response.room.updateRooms(roomList); if (CLIENT_OPTIONS.autojoinrooms) { diff --git a/webclient/src/websocket/events/session/notifyUser.ts b/webclient/src/websocket/events/session/notifyUser.ts index df8840781..8a6184edf 100644 --- a/webclient/src/websocket/events/session/notifyUser.ts +++ b/webclient/src/websocket/events/session/notifyUser.ts @@ -1,6 +1,6 @@ -import type { Event_NotifyUser } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function notifyUser(payload: Event_NotifyUser): void { +export function notifyUser(payload: Data.Event_NotifyUser): void { WebClient.instance.response.session.notifyUser(payload); } diff --git a/webclient/src/websocket/events/session/removeFromList.ts b/webclient/src/websocket/events/session/removeFromList.ts index 8b4db5b21..3e5feae83 100644 --- a/webclient/src/websocket/events/session/removeFromList.ts +++ b/webclient/src/websocket/events/session/removeFromList.ts @@ -1,7 +1,7 @@ -import type { Event_RemoveFromList } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function removeFromList({ listName, userName }: Event_RemoveFromList): void { +export function removeFromList({ listName, userName }: Data.Event_RemoveFromList): void { switch (listName) { case 'buddy': { WebClient.instance.response.session.removeFromBuddyList(userName); diff --git a/webclient/src/websocket/events/session/replayAdded.ts b/webclient/src/websocket/events/session/replayAdded.ts index 8fe1e2b89..9acc7135b 100644 --- a/webclient/src/websocket/events/session/replayAdded.ts +++ b/webclient/src/websocket/events/session/replayAdded.ts @@ -1,6 +1,6 @@ -import type { Event_ReplayAdded } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function replayAdded({ matchInfo }: Event_ReplayAdded): void { +export function replayAdded({ matchInfo }: Data.Event_ReplayAdded): void { WebClient.instance.response.session.replayAdded(matchInfo); } diff --git a/webclient/src/websocket/events/session/serverCompleteList.ts b/webclient/src/websocket/events/session/serverCompleteList.ts index df2b0c05a..e8751bb32 100644 --- a/webclient/src/websocket/events/session/serverCompleteList.ts +++ b/webclient/src/websocket/events/session/serverCompleteList.ts @@ -1,7 +1,7 @@ -import type { Event_ServerCompleteList } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function serverCompleteList({ userList, roomList }: Event_ServerCompleteList): void { +export function serverCompleteList({ userList, roomList }: Data.Event_ServerCompleteList): void { WebClient.instance.response.session.updateUsers(userList); WebClient.instance.response.room.updateRooms(roomList); } diff --git a/webclient/src/websocket/events/session/serverIdentification.ts b/webclient/src/websocket/events/session/serverIdentification.ts index 6c7956f3f..79ac7a101 100644 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -1,93 +1,88 @@ -import type { Event_ServerIdentification } from '@app/generated'; +import { App, Data, Enriched } from '@app/types'; + 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 { + activate, + disconnect, + login, + register, + requestPasswordSalt, + forgotPasswordChallenge, + forgotPasswordRequest, + forgotPasswordReset, + updateStatus, +} from '../../commands/session'; import { generateSalt, passwordSaltSupported } from '../../utils'; -import * as SessionCommands from '../../commands/session'; - -export function serverIdentification(info: Event_ServerIdentification): void { +export function serverIdentification(info: Data.Event_ServerIdentification): void { 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(); + updateStatus(App.StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); + disconnect(); return; } const getPasswordSalt = passwordSaltSupported(serverOptions); - const options = consumePendingOptions(); + const options = WebClient.instance.options; if (!options) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Missing connection options'); - SessionCommands.disconnect(); + updateStatus(App.StatusEnum.DISCONNECTED, 'Missing connection options'); + disconnect(); return; } + // Strip credentials before handing off to session commands — they travel as + // separate function args so they can't accidentally ride along in the + // typed options object that flows downstream. switch (options.reason) { - case WebSocketConnectReason.LOGIN: { + case App.WebSocketConnectReason.LOGIN: { const { password, ...rest } = options; - SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); + updateStatus(App.StatusEnum.LOGGING_IN, 'Logging In...'); if (getPasswordSalt) { - SessionCommands.requestPasswordSalt(rest, - (salt) => SessionCommands.login(rest, password, salt), - () => { - response.session.loginFailed(); SessionCommands.disconnect(); - }, - ); + requestPasswordSalt(rest, password); } else { - SessionCommands.login(rest, password); + login(rest, password); } break; } - case WebSocketConnectReason.REGISTER: { + case App.WebSocketConnectReason.REGISTER: { const { password, ...rest } = options; const passwordSalt = getPasswordSalt ? generateSalt() : null; - SessionCommands.register(rest, password, passwordSalt); + register(rest, password, passwordSalt); break; } - case WebSocketConnectReason.ACTIVATE_ACCOUNT: { + 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(); - }, - ); + requestPasswordSalt(rest, password); } else { - SessionCommands.activate(rest, password); + activate(rest, password); } break; } - case WebSocketConnectReason.PASSWORD_RESET_REQUEST: - SessionCommands.forgotPasswordRequest(options); + case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: + forgotPasswordRequest(options); break; - case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - SessionCommands.forgotPasswordChallenge(options); + case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + forgotPasswordChallenge(options); break; - case WebSocketConnectReason.PASSWORD_RESET: { + 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(); - }, - ); + requestPasswordSalt(rest, undefined, newPassword); } else { - SessionCommands.forgotPasswordReset(rest, newPassword); + forgotPasswordReset(rest, newPassword); } break; } default: { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${(options as { reason: number }).reason}`); - SessionCommands.disconnect(); + const { reason } = options as Enriched.WebSocketConnectOptions; + updateStatus(App.StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${reason}`); + disconnect(); break; } } - response.session.updateInfo(serverName, serverVersion); + WebClient.instance.options = null; + WebClient.instance.response.session.updateInfo(serverName, serverVersion); } diff --git a/webclient/src/websocket/events/session/serverMessage.ts b/webclient/src/websocket/events/session/serverMessage.ts index c7f3b7b9d..42bb14f82 100644 --- a/webclient/src/websocket/events/session/serverMessage.ts +++ b/webclient/src/websocket/events/session/serverMessage.ts @@ -1,7 +1,7 @@ -import type { Event_ServerMessage } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; import { sanitizeHtml } from '../../utils'; -export function serverMessage({ message }: Event_ServerMessage): void { +export function serverMessage({ message }: Data.Event_ServerMessage): void { WebClient.instance.response.session.serverMessage(sanitizeHtml(message)); } diff --git a/webclient/src/websocket/events/session/serverShutdown.ts b/webclient/src/websocket/events/session/serverShutdown.ts index cdd404739..a0fce78c1 100644 --- a/webclient/src/websocket/events/session/serverShutdown.ts +++ b/webclient/src/websocket/events/session/serverShutdown.ts @@ -1,6 +1,6 @@ -import type { Event_ServerShutdown } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function serverShutdown(payload: Event_ServerShutdown): void { +export function serverShutdown(payload: Data.Event_ServerShutdown): void { WebClient.instance.response.session.serverShutdown(payload); } diff --git a/webclient/src/websocket/events/session/sessionEvents.spec.ts b/webclient/src/websocket/events/session/sessionEvents.spec.ts index c627a6856..4ac961a5d 100644 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ b/webclient/src/websocket/events/session/sessionEvents.spec.ts @@ -1,7 +1,35 @@ // Tests for simple session events that delegate 1:1 to SessionPersistence // or RoomPersistence with minimal logic. -vi.mock('../../WebClient'); +vi.mock('../../WebClient', () => ({ + WebClient: { + instance: { + options: null, + 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', () => ({ CLIENT_OPTIONS: { autojoinrooms: false }, @@ -15,52 +43,25 @@ vi.mock('../../commands/session', () => ({ login: vi.fn(), register: vi.fn(), activate: vi.fn(), + requestPasswordSalt: vi.fn(), forgotPasswordRequest: vi.fn(), forgotPasswordChallenge: vi.fn(), forgotPasswordReset: vi.fn(), - requestPasswordSalt: vi.fn(), })); vi.mock('../../utils', () => ({ - sanitizeHtml: vi.fn((msg: string) => msg), - generateSalt: vi.fn().mockReturnValue('randSalt'), + generateSalt: vi.fn().mockReturnValue('newSalt'), passwordSaltSupported: vi.fn().mockReturnValue(0), + sanitizeHtml: vi.fn((msg: string) => msg), })); -vi.mock('../../utils/connectionState', () => ({ - consumePendingOptions: vi.fn().mockReturnValue(null), -})); - -import { - Event_AddToListSchema, - Event_ConnectionClosedSchema, - Event_ConnectionClosed_CloseReason, - Event_GameJoinedSchema, - Event_ListRoomsSchema, - Event_NotifyUserSchema, - Event_RemoveFromListSchema, - Event_ReplayAddedSchema, - Event_ServerCompleteListSchema, - Event_ServerIdentificationSchema, - Event_ServerMessageSchema, - Event_ServerShutdownSchema, - Event_UserJoinedSchema, - Event_UserLeftSchema, - Event_UserMessageSchema, - ServerInfo_ReplayMatchSchema, - ServerInfo_RoomSchema, - ServerInfo_UserSchema, -} from '@app/generated'; +import { App, Data, Enriched } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; import * as Config from '../../config'; import * as SessionCmds from '../../commands/session'; -import { consumePendingOptions } from '../../utils/connectionState'; -import { passwordSaltSupported } from '../../utils'; -import { WebSocketConnectReason } from '../../interfaces/ConnectOptions'; -import { StatusEnum } from '../../interfaces/StatusEnum'; -import { Mock } from 'vitest'; +import * as Utils from '../../utils'; import { gameJoined } from './gameJoined'; import { notifyUser } from './notifyUser'; import { replayAdded } from './replayAdded'; @@ -75,16 +76,23 @@ import { removeFromList } from './removeFromList'; import { listRooms } from './listRooms'; import { connectionClosed } from './connectionClosed'; import { serverIdentification } from './serverIdentification'; +import { Mock } from 'vitest'; + const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] }; +beforeEach(() => { + (Utils.generateSalt as Mock).mockReturnValue('newSalt'); + (Utils.passwordSaltSupported as Mock).mockReturnValue(0); +}); + // ---------------------------------------------------------------- // gameJoined // ---------------------------------------------------------------- describe('gameJoined', () => { it('calls WebClient.instance.response.session.gameJoined', () => { - const data = create(Event_GameJoinedSchema, { playerId: 1 }); + const data = create(Data.Event_GameJoinedSchema, { playerId: 1 }); gameJoined(data); expect(WebClient.instance.response.session.gameJoined).toHaveBeenCalledWith(data); }); @@ -96,7 +104,7 @@ describe('gameJoined', () => { describe('notifyUser', () => { it('calls WebClient.instance.response.session.notifyUser', () => { - const data = create(Event_NotifyUserSchema, { warningReason: 'yo' }); + const data = create(Data.Event_NotifyUserSchema, { warningReason: 'yo' }); notifyUser(data); expect(WebClient.instance.response.session.notifyUser).toHaveBeenCalledWith(data); }); @@ -108,8 +116,8 @@ describe('notifyUser', () => { describe('replayAdded', () => { it('calls WebClient.instance.response.session.replayAdded with matchInfo', () => { - const data = create(Event_ReplayAddedSchema, { - matchInfo: create(ServerInfo_ReplayMatchSchema, { gameId: 42 }), + const data = create(Data.Event_ReplayAddedSchema, { + matchInfo: create(Data.ServerInfo_ReplayMatchSchema, { gameId: 42 }), }); replayAdded(data); expect(WebClient.instance.response.session.replayAdded).toHaveBeenCalledWith(data.matchInfo); @@ -122,7 +130,7 @@ describe('replayAdded', () => { describe('serverCompleteList', () => { it('calls WebClient.instance.response.session.updateUsers and WebClient.instance.response.room.updateRooms', () => { - const data = create(Event_ServerCompleteListSchema, { userList: [], roomList: [] }); + const data = create(Data.Event_ServerCompleteListSchema, { userList: [], roomList: [] }); serverCompleteList(data); expect(WebClient.instance.response.session.updateUsers).toHaveBeenCalledWith(data.userList); expect(WebClient.instance.response.room.updateRooms).toHaveBeenCalledWith(data.roomList); @@ -135,7 +143,7 @@ describe('serverCompleteList', () => { describe('serverMessage', () => { it('calls WebClient.instance.response.session.serverMessage with message', () => { - serverMessage(create(Event_ServerMessageSchema, { message: 'hello server' })); + serverMessage(create(Data.Event_ServerMessageSchema, { message: 'hello server' })); expect(WebClient.instance.response.session.serverMessage).toHaveBeenCalledWith('hello server'); }); }); @@ -146,7 +154,7 @@ describe('serverMessage', () => { describe('serverShutdown', () => { it('calls WebClient.instance.response.session.serverShutdown', () => { - const payload = create(Event_ServerShutdownSchema, { reason: 'maintenance' }); + const payload = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance' }); serverShutdown(payload); expect(WebClient.instance.response.session.serverShutdown).toHaveBeenCalledWith(payload); }); @@ -158,8 +166,8 @@ describe('serverShutdown', () => { describe('userJoined', () => { it('calls WebClient.instance.response.session.userJoined with userInfo', () => { - const data = create(Event_UserJoinedSchema, { - userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), + const data = create(Data.Event_UserJoinedSchema, { + userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), }); userJoined(data); expect(WebClient.instance.response.session.userJoined).toHaveBeenCalledWith(data.userInfo); @@ -172,7 +180,7 @@ describe('userJoined', () => { describe('userLeft', () => { it('calls WebClient.instance.response.session.userLeft with name', () => { - userLeft(create(Event_UserLeftSchema, { name: 'bob' })); + userLeft(create(Data.Event_UserLeftSchema, { name: 'bob' })); expect(WebClient.instance.response.session.userLeft).toHaveBeenCalledWith('bob'); }); }); @@ -183,7 +191,7 @@ describe('userLeft', () => { describe('userMessage', () => { it('calls WebClient.instance.response.session.userMessage', () => { - const payload = create(Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); + const payload = create(Data.Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); userMessage(payload); expect(WebClient.instance.response.session.userMessage).toHaveBeenCalledWith(payload); }); @@ -202,25 +210,25 @@ describe('addToList', () => { }); it('buddy list → addToBuddyList', () => { - const data = create(Event_AddToListSchema, { + const data = create(Data.Event_AddToListSchema, { listName: 'buddy', - userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), + userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), }); addToList(data); expect(WebClient.instance.response.session.addToBuddyList).toHaveBeenCalledWith(data.userInfo); }); it('ignore list → addToIgnoreList', () => { - const data = create(Event_AddToListSchema, { + const data = create(Data.Event_AddToListSchema, { listName: 'ignore', - userInfo: create(ServerInfo_UserSchema, { name: 'bob' }), + userInfo: create(Data.ServerInfo_UserSchema, { name: 'bob' }), }); addToList(data); expect(WebClient.instance.response.session.addToIgnoreList).toHaveBeenCalledWith(data.userInfo); }); it('unknown list → console.log', () => { - addToList(create(Event_AddToListSchema, { listName: 'unknown' })); + addToList(create(Data.Event_AddToListSchema, { listName: 'unknown' })); expect(logSpy).toHaveBeenCalled(); }); }); @@ -231,18 +239,18 @@ describe('addToList', () => { describe('removeFromList', () => { it('buddy list → removeFromBuddyList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); expect(WebClient.instance.response.session.removeFromBuddyList).toHaveBeenCalledWith('alice'); }); it('ignore list → removeFromIgnoreList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); expect(WebClient.instance.response.session.removeFromIgnoreList).toHaveBeenCalledWith('bob'); }); it('unknown list → console.log', () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - removeFromList(create(Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); expect(logSpy).toHaveBeenCalled(); logSpy.mockRestore(); }); @@ -254,24 +262,24 @@ describe('removeFromList', () => { describe('listRooms', () => { it('calls WebClient.instance.response.room.updateRooms', () => { - listRooms(create(Event_ListRoomsSchema, { roomList: [] })); + listRooms(create(Data.Event_ListRoomsSchema, { roomList: [] })); expect(WebClient.instance.response.room.updateRooms).toHaveBeenCalledWith([]); }); it('does not call joinRoom when autojoinrooms is false', () => { ConfigMock.CLIENT_OPTIONS = { autojoinrooms: false }; - listRooms(create(Event_ListRoomsSchema, { - roomList: [create(ServerInfo_RoomSchema, { autoJoin: true, roomId: 1 })] + listRooms(create(Data.Event_ListRoomsSchema, { + roomList: [create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 1 })] })); expect(SessionCmds.joinRoom).not.toHaveBeenCalled(); }); it('calls joinRoom for autoJoin rooms when autojoinrooms is true', () => { ConfigMock.CLIENT_OPTIONS = { autojoinrooms: true }; - listRooms(create(Event_ListRoomsSchema, { + listRooms(create(Data.Event_ListRoomsSchema, { roomList: [ - create(ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), - create(ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) + create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), + create(Data.ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) ] })); expect(SessionCmds.joinRoom).toHaveBeenCalledTimes(1); @@ -285,12 +293,12 @@ describe('listRooms', () => { describe('connectionClosed', () => { it('uses reasonStr when provided', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom'); }); it('USER_LIMIT_REACHED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('maximum user capacity') @@ -298,43 +306,43 @@ describe('connectionClosed', () => { }); it('TOO_MANY_CONNECTIONS → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('too many concurrent')); }); it('BANNED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('DEMOTED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.DEMOTED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.DEMOTED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted')); }); it('SERVER_SHUTDOWN → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('shutdown')); }); it('USERNAMEINVALID → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username')); }); it('LOGGEDINELSEWERE → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out')); }); it('OTHER → "Unknown reason"', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.OTHER })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.OTHER })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'Unknown reason'); }); it('BANNED with valid positive endTime → shows formatted date', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { - reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, + connectionClosed(create(Data.Event_ConnectionClosedSchema, { + reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), @@ -343,30 +351,30 @@ describe('connectionClosed', () => { }); it('BANNED with endTime = 0 → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = -1 → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = NaN → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = Infinity → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { - reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity, + connectionClosed(create(Data.Event_ConnectionClosedSchema, { + reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity, })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with reasonStr → uses reasonStr regardless of endTime', () => { - connectionClosed(create(Event_ConnectionClosedSchema, - { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, + { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason'); }); }); @@ -375,149 +383,157 @@ describe('connectionClosed', () => { // serverIdentification // ---------------------------------------------------------------- describe('serverIdentification', () => { - const makeInfo = (overrides: Record = {}) => - create(Event_ServerIdentificationSchema, { - serverName: 'TestServer', - serverVersion: '1.0', - protocolVersion: 14, - serverOptions: 0, - ...overrides, - }); - - const makeLoginOptions = () => ({ - host: 'h', port: '1', userName: 'alice', password: 'pw', - reason: WebSocketConnectReason.LOGIN as const, - }); beforeEach(() => { - (consumePendingOptions as Mock).mockReturnValue(null); - (passwordSaltSupported as Mock).mockReturnValue(0); + ConfigMock.PROTOCOL_VERSION = 14; + WebClient.instance.options = null; }); - it('disconnects on protocol version mismatch', () => { - serverIdentification(makeInfo({ protocolVersion: 99 })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.stringContaining('Protocol version mismatch')); + it('disconnects when protocolVersion mismatches', () => { + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 99, serverOptions: 0 })); + expect(SessionCmds.updateStatus).toHaveBeenCalled(); 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 reason without salt → calls login with password as separate param', () => { + WebClient.instance.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN, password: 'secret' }; + (Utils.passwordSaltSupported as Mock).mockReturnValue(0); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); + expect(SessionCmds.login).toHaveBeenCalledWith( + expect.not.objectContaining({ password: expect.anything() }), + 'secret' + ); }); - 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 })); + it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => { + WebClient.instance.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN, password: 'secret' }; + (Utils.passwordSaltSupported as Mock).mockReturnValue(1); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), - expect.any(Function), - expect.any(Function), + expect.not.objectContaining({ password: expect.anything() }), + 'secret' ); }); - 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, + it('REGISTER reason without salt → calls register with password and null salt', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', email: 'e', country: 'US', realName: 'R', + reason: App.WebSocketConnectReason.REGISTER, password: 'secret', }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); + (Utils.passwordSaltSupported as Mock).mockReturnValue(0); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.register).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), 'pw', null, + expect.not.objectContaining({ password: expect.anything() }), + 'secret', + 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, + it('REGISTER reason with salt → calls register with password and generated salt', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', email: 'e', country: 'US', realName: 'R', + reason: App.WebSocketConnectReason.REGISTER, password: 'secret', }; - (consumePendingOptions as Mock).mockReturnValue(opts); - (passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(makeInfo({ serverOptions: 1 })); + (Utils.passwordSaltSupported as Mock).mockReturnValue(1); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.register).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), 'pw', 'randSalt', + expect.not.objectContaining({ password: expect.anything() }), + 'secret', + 'newSalt' ); }); - it('ACTIVATE_ACCOUNT → calls activate', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', token: 'tok', password: 'pw', - reason: WebSocketConnectReason.ACTIVATE_ACCOUNT as const, + it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret', }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); + (Utils.passwordSaltSupported as Mock).mockReturnValue(0); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.activate).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice', token: 'tok' }), 'pw', + expect.not.objectContaining({ password: expect.anything() }), + 'secret' ); }); - it('PASSWORD_RESET_REQUEST → calls forgotPasswordRequest', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', - reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST as const, + it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret', }; - (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 })); + (Utils.passwordSaltSupported as Mock).mockReturnValue(1); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), - expect.any(Function), - expect.any(Function), + expect.not.objectContaining({ password: expect.anything() }), + 'secret' ); }); - it('always calls updateInfo after successful routing', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - serverIdentification(makeInfo()); - expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('TestServer', '1.0'); + it('PASSWORD_RESET_REQUEST reason → calls forgotPasswordRequest', () => { + WebClient.instance.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); + expect(SessionCmds.forgotPasswordRequest).toHaveBeenCalled(); + }); + + it('PASSWORD_RESET_CHALLENGE reason → calls forgotPasswordChallenge', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', email: 'e', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE, + }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); + expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled(); + }); + + it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw', + }; + (Utils.passwordSaltSupported as Mock).mockReturnValue(0); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); + expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith( + expect.not.objectContaining({ newPassword: expect.anything() }), + 'newpw' + ); + }); + + it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => { + WebClient.instance.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw', + }; + (Utils.passwordSaltSupported as Mock).mockReturnValue(1); + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); + expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( + expect.not.objectContaining({ newPassword: expect.anything() }), + undefined, + 'newpw' + ); + }); + + it('unknown reason → updateStatus DISCONNECTED and disconnect', () => { + WebClient.instance.options = { host: 'h', port: '1', reason: 999 as App.WebSocketConnectReason } as Enriched.WebSocketConnectOptions; + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); + expect(SessionCmds.updateStatus).toHaveBeenCalled(); + expect(SessionCmds.disconnect).toHaveBeenCalled(); + }); + + it('resets WebClient.instance.options and calls WebClient.instance.response.session.updateInfo', () => { + WebClient.instance.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, + { serverName: 'myServer', serverVersion: '2.0', protocolVersion: 14, serverOptions: 0 })); + expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('myServer', '2.0'); + expect(WebClient.instance.options).toBeNull(); }); }); diff --git a/webclient/src/websocket/events/session/userJoined.ts b/webclient/src/websocket/events/session/userJoined.ts index 5f0e6014d..e474bf6e0 100644 --- a/webclient/src/websocket/events/session/userJoined.ts +++ b/webclient/src/websocket/events/session/userJoined.ts @@ -1,6 +1,6 @@ -import type { Event_UserJoined } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function userJoined({ userInfo }: Event_UserJoined): void { +export function userJoined({ userInfo }: Data.Event_UserJoined): void { WebClient.instance.response.session.userJoined(userInfo); } diff --git a/webclient/src/websocket/events/session/userLeft.ts b/webclient/src/websocket/events/session/userLeft.ts index f53bff2d4..fe20593da 100644 --- a/webclient/src/websocket/events/session/userLeft.ts +++ b/webclient/src/websocket/events/session/userLeft.ts @@ -1,6 +1,6 @@ -import type { Event_UserLeft } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function userLeft({ name }: Event_UserLeft): void { +export function userLeft({ name }: Data.Event_UserLeft): void { WebClient.instance.response.session.userLeft(name); } diff --git a/webclient/src/websocket/events/session/userMessage.ts b/webclient/src/websocket/events/session/userMessage.ts index 5b3ca5905..aecbd01b0 100644 --- a/webclient/src/websocket/events/session/userMessage.ts +++ b/webclient/src/websocket/events/session/userMessage.ts @@ -1,6 +1,6 @@ -import type { Event_UserMessage } from '@app/generated'; +import type { Data } from '@app/types'; import { WebClient } from '../../WebClient'; -export function userMessage(payload: Event_UserMessage): void { +export function userMessage(payload: Data.Event_UserMessage): void { WebClient.instance.response.session.userMessage(payload); } diff --git a/webclient/src/websocket/index.ts b/webclient/src/websocket/index.ts index 9d0cd9481..61a67c4db 100644 --- a/webclient/src/websocket/index.ts +++ b/webclient/src/websocket/index.ts @@ -2,31 +2,3 @@ export * from './commands'; export * from './interfaces'; export { WebClient } from './WebClient'; -export { StatusEnum } from './interfaces/StatusEnum'; -export type { WebClientConfig, ConnectTarget } from './interfaces/WebClientConfig'; -export type { - KeyOf, - GameEventMeta, - WebSocketSessionResponseOverrides, - WebSocketRoomResponseOverrides, -} from './interfaces/WebSocketConfig'; - -export { SessionEvents } from './events/session'; -export { RoomEvents } from './events/room'; -export { GameEvents } from './events/game'; - -export { generateSalt, passwordSaltSupported, hashPassword } from './utils'; - -export { WebSocketConnectReason } from './interfaces/ConnectOptions'; -export type { - LoginConnectOptions, - RegisterConnectOptions, - ActivateConnectOptions, - PasswordResetRequestConnectOptions, - PasswordResetChallengeConnectOptions, - PasswordResetConnectOptions, - TestConnectionOptions, - WebSocketConnectOptions, -} from './interfaces/ConnectOptions'; - -export { setPendingOptions, consumePendingOptions } from './utils/connectionState'; diff --git a/webclient/src/websocket/interfaces/ConnectOptions.ts b/webclient/src/websocket/interfaces/ConnectOptions.ts deleted file mode 100644 index a74402fb4..000000000 --- a/webclient/src/websocket/interfaces/ConnectOptions.ts +++ /dev/null @@ -1,82 +0,0 @@ -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; diff --git a/webclient/src/websocket/interfaces/StatusEnum.ts b/webclient/src/websocket/interfaces/StatusEnum.ts deleted file mode 100644 index 179bdf675..000000000 --- a/webclient/src/websocket/interfaces/StatusEnum.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum StatusEnum { - DISCONNECTED, - CONNECTING, - CONNECTED, - LOGGING_IN, - LOGGED_IN, - DISCONNECTING = 99 -} diff --git a/webclient/src/websocket/interfaces/WebClientConfig.ts b/webclient/src/websocket/interfaces/WebClientConfig.ts deleted file mode 100644 index 6ff860009..000000000 --- a/webclient/src/websocket/interfaces/WebClientConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - RegistryEntry, - SessionEvent, - RoomEvent, - GameEvent, -} from '@app/generated'; - -import type { GameEventMeta } from './WebSocketConfig'; -import type { IWebClientResponse } from '.'; - -export interface ConnectTarget { - host: string; - port: string; -} - -export interface WebClientConfig { - response: IWebClientResponse; - - sessionEvents: RegistryEntry[]; - roomEvents: RegistryEntry[]; - gameEvents: RegistryEntry[]; - - keepAliveFn(pingReceived: () => void): void; -} diff --git a/webclient/src/websocket/interfaces/WebClientRequest.ts b/webclient/src/websocket/interfaces/WebClientRequest.ts index 16db5fd08..ba44e9841 100644 --- a/webclient/src/websocket/interfaces/WebClientRequest.ts +++ b/webclient/src/websocket/interfaces/WebClientRequest.ts @@ -1,68 +1,13 @@ -import type { - LoginParams, - RegisterParams, - ActivateParams, - ForgotPasswordRequestParams, - ForgotPasswordChallengeParams, - ForgotPasswordResetParams, - ViewLogHistoryParams, - KickFromGameParams, - GameSayParams, - ReadyStartParams, - SetActivePhaseParams, - MoveCardParams, - FlipCardParams, - AttachCardParams, - CreateTokenParams, - SetCardAttrParams, - SetCardCounterParams, - IncCardCounterParams, - DrawCardsParams, - CreateArrowParams, - DeleteArrowParams, - CreateCounterParams, - SetCounterParams, - IncCounterParams, - DelCounterParams, - ShuffleParams, - DumpZoneParams, - RevealCardsParams, - ChangeZonePropertiesParams, - DeckSelectParams, - SetSideboardPlanParams, - SetSideboardLockParams, - MulliganParams, - RollDieParams, - GameCommand, -} from '@app/generated'; +import { Data, Enriched } from '@app/types'; -import type { ConnectTarget } from './WebClientConfig'; -import type { KeyOf } from './WebSocketConfig'; - -// ── Auth request type map ──────────────────────────────────────────────────── -// Keys = generated *Params type names composed with ConnectTarget. -// @app/api overrides these with Enriched connect option types. - -export interface AuthRequestMap { - LoginParams: ConnectTarget & LoginParams; - ConnectTarget: ConnectTarget; - RegisterParams: ConnectTarget & RegisterParams; - ActivateParams: ConnectTarget & ActivateParams; - ForgotPasswordRequestParams: ConnectTarget & ForgotPasswordRequestParams; - ForgotPasswordChallengeParams: ConnectTarget & ForgotPasswordChallengeParams; - ForgotPasswordResetParams: ConnectTarget & ForgotPasswordResetParams; -} - -type AK = KeyOf; - -export interface IAuthenticationRequest { - login(options: T[AK]): void; - testConnection(options: T[AK]): void; - register(options: T[AK]): void; - activateAccount(options: T[AK]): void; - resetPasswordRequest(options: T[AK]): void; - resetPasswordChallenge(options: T[AK]): void; - resetPassword(options: T[AK]): void; +export interface IAuthenticationRequest { + login(options: Omit): void; + testConnection(options: Omit): void; + register(options: Omit): void; + activateAccount(options: Omit): void; + resetPasswordRequest(options: Omit): void; + resetPasswordChallenge(options: Omit): void; + resetPassword(options: Omit): void; disconnect(): void; } @@ -107,51 +52,49 @@ export interface IModeratorRequest { getBanHistory(userName: string): void; getWarnHistory(userName: string): void; getWarnList(modName: string, userName: string, userClientid: string): void; - viewLogHistory(filters: ViewLogHistoryParams): void; + viewLogHistory(filters: Data.ViewLogHistoryParams): void; warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void; } export interface IGameRequest { leaveGame(gameId: number): void; - kickFromGame(gameId: number, params: KickFromGameParams): void; - gameSay(gameId: number, params: GameSayParams): void; - readyStart(gameId: number, params: ReadyStartParams): void; + kickFromGame(gameId: number, params: Data.KickFromGameParams): void; + gameSay(gameId: number, params: Data.GameSayParams): void; + readyStart(gameId: number, params: Data.ReadyStartParams): void; concede(gameId: number): void; unconcede(gameId: number): void; - judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void; + judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void; nextTurn(gameId: number): void; - setActivePhase(gameId: number, params: SetActivePhaseParams): void; + setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void; reverseTurn(gameId: number): void; - moveCard(gameId: number, params: MoveCardParams): void; - flipCard(gameId: number, params: FlipCardParams): void; - attachCard(gameId: number, params: AttachCardParams): void; - createToken(gameId: number, params: CreateTokenParams): void; - setCardAttr(gameId: number, params: SetCardAttrParams): void; - setCardCounter(gameId: number, params: SetCardCounterParams): void; - incCardCounter(gameId: number, params: IncCardCounterParams): void; - drawCards(gameId: number, params: DrawCardsParams): void; + moveCard(gameId: number, params: Data.MoveCardParams): void; + flipCard(gameId: number, params: Data.FlipCardParams): void; + attachCard(gameId: number, params: Data.AttachCardParams): void; + createToken(gameId: number, params: Data.CreateTokenParams): void; + setCardAttr(gameId: number, params: Data.SetCardAttrParams): void; + setCardCounter(gameId: number, params: Data.SetCardCounterParams): void; + incCardCounter(gameId: number, params: Data.IncCardCounterParams): void; + drawCards(gameId: number, params: Data.DrawCardsParams): void; undoDraw(gameId: number): void; - createArrow(gameId: number, params: CreateArrowParams): void; - deleteArrow(gameId: number, params: DeleteArrowParams): void; - createCounter(gameId: number, params: CreateCounterParams): void; - setCounter(gameId: number, params: SetCounterParams): void; - incCounter(gameId: number, params: IncCounterParams): void; - delCounter(gameId: number, params: DelCounterParams): void; - shuffle(gameId: number, params: ShuffleParams): void; - dumpZone(gameId: number, params: DumpZoneParams): void; - revealCards(gameId: number, params: RevealCardsParams): void; - changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void; - deckSelect(gameId: number, params: DeckSelectParams): void; - setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void; - setSideboardLock(gameId: number, params: SetSideboardLockParams): void; - mulligan(gameId: number, params: MulliganParams): void; - rollDie(gameId: number, params: RollDieParams): void; + createArrow(gameId: number, params: Data.CreateArrowParams): void; + deleteArrow(gameId: number, params: Data.DeleteArrowParams): void; + createCounter(gameId: number, params: Data.CreateCounterParams): void; + setCounter(gameId: number, params: Data.SetCounterParams): void; + incCounter(gameId: number, params: Data.IncCounterParams): void; + delCounter(gameId: number, params: Data.DelCounterParams): void; + shuffle(gameId: number, params: Data.ShuffleParams): void; + dumpZone(gameId: number, params: Data.DumpZoneParams): void; + revealCards(gameId: number, params: Data.RevealCardsParams): void; + changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void; + deckSelect(gameId: number, params: Data.DeckSelectParams): void; + setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void; + setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void; + mulligan(gameId: number, params: Data.MulliganParams): void; + rollDie(gameId: number, params: Data.RollDieParams): void; } -export interface IWebClientRequest< - A extends AuthRequestMap = AuthRequestMap, -> { - authentication: IAuthenticationRequest; +export interface IWebClientRequest { + authentication: IAuthenticationRequest; session: ISessionRequest; rooms: IRoomsRequest; game: IGameRequest; diff --git a/webclient/src/websocket/interfaces/WebClientResponse.ts b/webclient/src/websocket/interfaces/WebClientResponse.ts index cf3bd40d5..8a60fc4e9 100644 --- a/webclient/src/websocket/interfaces/WebClientResponse.ts +++ b/webclient/src/websocket/interfaces/WebClientResponse.ts @@ -1,79 +1,28 @@ -import type { - Response_Login, - Response, - Response_GetGamesOfUser, - Response_DeckList, - Response_DeckDownload, - Response_ReplayDownload, - Response_WarnList, - ResponseMap, - Event_RoomSay, - Event_GameJoined, - Event_GameStateChanged, - Event_MoveCard, - Event_FlipCard, - Event_DestroyCard, - Event_AttachCard, - Event_CreateToken, - Event_SetCardAttr, - Event_SetCardCounter, - Event_CreateArrow, - Event_DeleteArrow, - Event_CreateCounter, - Event_SetCounter, - Event_DelCounter, - Event_DrawCards, - Event_RevealCards, - Event_Shuffle, - Event_RollDie, - Event_DumpZone, - Event_ChangeZoneProperties, - Event_NotifyUser, - Event_PlayerPropertiesChanged, - Event_ServerShutdown, - Event_UserMessage, - RoomEventMap, - ServerInfo_User, - ServerInfo_Room, - ServerInfo_Game, - ServerInfo_PlayerProperties, - ServerInfo_Ban, - ServerInfo_ChatMessage, - ServerInfo_Warning, - ServerInfo_DeckStorage_TreeItem, - ServerInfo_ReplayMatch, -} from '@app/generated'; +import { App, Data, Enriched } from '@app/types'; -import type { StatusEnum } from './StatusEnum'; -import type { - KeyOf, - WebSocketSessionResponseOverrides, - WebSocketRoomResponseOverrides, -} from './WebSocketConfig'; - -export interface ISessionResponse { +export interface ISessionResponse { initialized(): void; connectionAttempted(): void; clearStore(): void; - loginSuccessful(result: T[KeyOf]): void; + loginSuccessful(options: Enriched.LoginSuccessContext): void; loginFailed(): void; connectionFailed(): void; testConnectionSuccessful(): void; testConnectionFailed(): void; - updateBuddyList(buddyList: ServerInfo_User[]): void; - addToBuddyList(user: ServerInfo_User): void; + updateBuddyList(buddyList: Data.ServerInfo_User[]): void; + addToBuddyList(user: Data.ServerInfo_User): void; removeFromBuddyList(userName: string): void; - updateIgnoreList(ignoreList: ServerInfo_User[]): void; - addToIgnoreList(user: ServerInfo_User): void; + updateIgnoreList(ignoreList: Data.ServerInfo_User[]): void; + addToIgnoreList(user: Data.ServerInfo_User): void; removeFromIgnoreList(userName: string): void; updateInfo(name: string, version: string): void; - updateStatus(state: StatusEnum, description: string): void; - updateUser(user: ServerInfo_User): void; - updateUsers(users: ServerInfo_User[]): void; - userJoined(user: ServerInfo_User): void; + updateStatus(state: App.StatusEnum, description: string): void; + updateUser(user: Data.ServerInfo_User): void; + updateUsers(users: Data.ServerInfo_User[]): void; + userJoined(user: Data.ServerInfo_User): void; userLeft(userName: string): void; serverMessage(message: string): void; - accountAwaitingActivation(result: T[KeyOf]): void; + accountAwaitingActivation(options: Enriched.PendingActivationContext): void; accountActivationSuccess(): void; accountActivationFailed(): void; registrationRequiresEmail(): void; @@ -89,36 +38,36 @@ export interface ISessionResponse { +export interface IRoomResponse { clearStore(): void; - joinRoom(roomInfo: ServerInfo_Room): void; + joinRoom(roomInfo: Data.ServerInfo_Room): void; leaveRoom(roomId: number): void; - updateRooms(rooms: ServerInfo_Room[]): void; - updateGames(roomId: number, gameList: ServerInfo_Game[]): void; - addMessage(roomId: number, message: T[KeyOf]): void; - userJoined(roomId: number, user: ServerInfo_User): void; + updateRooms(rooms: Data.ServerInfo_Room[]): void; + updateGames(roomId: number, gameList: Data.ServerInfo_Game[]): void; + addMessage(roomId: number, message: Enriched.Message): void; + userJoined(roomId: number, user: Data.ServerInfo_User): void; userLeft(roomId: number, name: string): void; removeMessages(roomId: number, name: string, amount: number): void; gameCreated(roomId: number): void; @@ -127,35 +76,35 @@ export interface IRoomResponse { - session: ISessionResponse; - room: IRoomResponse; +export interface IWebClientResponse { + session: ISessionResponse; + room: IRoomResponse; game: IGameResponse; admin: IAdminResponse; moderator: IModeratorResponse; diff --git a/webclient/src/websocket/interfaces/WebSocketConfig.ts b/webclient/src/websocket/interfaces/WebSocketConfig.ts deleted file mode 100644 index cc1dface5..000000000 --- a/webclient/src/websocket/interfaces/WebSocketConfig.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { - GameEventContext, - Response_Login, - Response, - Event_RoomSay, - ResponseMap, - RoomEventMap, -} from '@app/generated'; - -// ── KeyOf utility ──────────────────────────────────────────────────────────── -// Derives a type map key from a generated type. Allows interface methods to -// reference generated types instead of hardcoded string keys. -// -// T[KeyOf] -// ↓ resolves to ↓ -// T['Response_Login'] - -export type KeyOf = { [K in keyof Map]: Map[K] extends V ? K : never }[keyof Map]; - -// ── GameEventMeta ──────────────────────────────────────────────────────────── -// Per-container metadata passed to every game event handler alongside the -// event payload. Constructed by ProtobufService.processGameEvent from the -// GameEventContainer fields. Structurally identical to Enriched.GameEventMeta. - -export interface GameEventMeta { - gameId: number; - playerId: number; - context: GameEventContext | null; - secondsElapsed: number; - forcedByJudge: number; -} - -// ── Websocket-layer enrichments ────────────────────────────────────────────── -// Protocol-level enrichments of proto types — these are websocket concerns, -// not app concerns. Used as the DEFAULT generic on the response interfaces. - -export interface WebSocketSessionResponseOverrides extends ResponseMap { - Response_Login: Response_Login & { hashedPassword?: string }; - Response: Response & { host: string; port: string; userName: string }; -} - -export interface WebSocketRoomResponseOverrides extends RoomEventMap { - Event_RoomSay: Event_RoomSay & { timeReceived: number }; -} diff --git a/webclient/src/websocket/interfaces/index.ts b/webclient/src/websocket/interfaces/index.ts index 6c4459c91..abe347cd5 100644 --- a/webclient/src/websocket/interfaces/index.ts +++ b/webclient/src/websocket/interfaces/index.ts @@ -8,7 +8,6 @@ export type { } from './WebClientResponse'; export type { - AuthRequestMap, IAuthenticationRequest, ISessionRequest, IRoomsRequest, @@ -17,7 +16,3 @@ export type { IModeratorRequest, IWebClientRequest, } from './WebClientRequest'; - -export * from './WebClientConfig'; -export * from './WebSocketConfig'; -export * from './StatusEnum'; diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts index 7880b9099..098869e82 100644 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ b/webclient/src/websocket/services/ProtobufService.spec.ts @@ -7,47 +7,32 @@ vi.mock('@bufbuild/protobuf', async (importOriginal) => ({ setExtension: vi.fn(), })); -vi.mock('../events/game', () => ({ +vi.mock('../events', () => ({ GameEvents: [], -})); - -vi.mock('../events/room', () => ({ RoomEvents: [], + SessionEvents: [], })); -vi.mock('../events/session', () => ({ - SessionEvents: [], +vi.mock('../WebClient', () => ({ + __esModule: true, + default: {}, })); import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; import { ProtobufService } from './ProtobufService'; -import { GameEvents } from '../events/game'; -import { RoomEvents } from '../events/room'; -import { SessionEvents } from '../events/session'; +import { GameEvents, RoomEvents, SessionEvents } from '../events'; +import type { GameExtensionRegistry } from '../events/game'; +import type { RoomExtensionRegistry } from '../events/room'; +import type { SessionExtensionRegistry } from '../events/session'; +import { withEventRegistry } from '../../__test-utils__'; -import type { - AdminCommand, - GameCommand, - GameEvent, - ModeratorCommand, - Response, - RoomCommand, - RoomEvent, - SessionCommand, - SessionEvent, -} from '@app/generated'; -import { - CommandContainerSchema, - ResponseSchema, - ServerMessageSchema, - ServerMessage_MessageType, -} from '@app/generated'; +import { Data } from '@app/types'; type ProtobufInternal = ProtobufService & { cmdId: number; - pendingCommands: Map void>; + pendingCommands: Map void>; processGameEvent(container: unknown, extra?: unknown): void; processRoomEvent(event: unknown): void; processSessionEvent(event: unknown): void; @@ -55,26 +40,29 @@ type ProtobufInternal = ProtobufService & { }; let mockSocket: { isOpen: ReturnType; send: ReturnType }; +let registryTeardowns: Array<() => void>; beforeEach(() => { mockSocket = { isOpen: vi.fn().mockReturnValue(true), send: vi.fn(), }; + registryTeardowns = []; +}); - // Reset event registries - (GameEvents as any).length = 0; - (RoomEvents as any).length = 0; - (SessionEvents as any).length = 0; +afterEach(() => { + while (registryTeardowns.length > 0) { + registryTeardowns.pop()!(); + } }); describe('ProtobufService', () => { // Mock extensions for send*Command tests — @bufbuild/protobuf is fully mocked so these are never invoked - const sessionExt = {} as GenExtension>; - const roomExt = {} as GenExtension>; - const gameExt = {} as GenExtension>; - const moderatorExt = {} as GenExtension>; - const adminExt = {} as GenExtension>; + const sessionExt = {} as GenExtension>; + const roomExt = {} as GenExtension>; + const gameExt = {} as GenExtension>; + const moderatorExt = {} as GenExtension>; + const adminExt = {} as GenExtension>; describe('resetCommands', () => { it('resets cmdId and pendingCommands', () => { @@ -91,7 +79,7 @@ describe('ProtobufService', () => { it('increments cmdId and stores callback', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendCommand(create(CommandContainerSchema), cb); + service.sendCommand(create(Data.CommandContainerSchema), cb); expect((service as ProtobufInternal).cmdId).toBe(1); expect((service as ProtobufInternal).pendingCommands.get(1)).toBe(cb); }); @@ -99,7 +87,7 @@ describe('ProtobufService', () => { it('sends encoded data when socket is OPEN', () => { const service = new ProtobufService(mockSocket); mockSocket.isOpen.mockReturnValue(true); - service.sendCommand(create(CommandContainerSchema), vi.fn()); + service.sendCommand(create(Data.CommandContainerSchema), vi.fn()); expect(mockSocket.send).toHaveBeenCalled(); }); @@ -107,7 +95,7 @@ describe('ProtobufService', () => { const service = new ProtobufService(mockSocket); mockSocket.isOpen.mockReturnValue(false); const cb = vi.fn(); - service.sendCommand(create(CommandContainerSchema), cb); + service.sendCommand(create(Data.CommandContainerSchema), cb); expect(mockSocket.send).not.toHaveBeenCalled(); expect((service as ProtobufInternal).cmdId).toBe(0); expect((service as ProtobufInternal).pendingCommands.size).toBe(0); @@ -128,9 +116,9 @@ describe('ProtobufService', () => { service.sendSessionCommand(sessionExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { @@ -138,7 +126,7 @@ describe('ProtobufService', () => { service.sendSessionCommand(sessionExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -155,9 +143,9 @@ describe('ProtobufService', () => { service.sendRoomCommand(42, roomExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { @@ -165,7 +153,7 @@ describe('ProtobufService', () => { service.sendRoomCommand(42, roomExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -182,9 +170,9 @@ describe('ProtobufService', () => { service.sendGameCommand(7, gameExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { @@ -192,7 +180,7 @@ describe('ProtobufService', () => { service.sendGameCommand(7, gameExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -209,9 +197,9 @@ describe('ProtobufService', () => { service.sendModeratorCommand(moderatorExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { @@ -219,7 +207,7 @@ describe('ProtobufService', () => { service.sendModeratorCommand(moderatorExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -236,9 +224,9 @@ describe('ProtobufService', () => { service.sendAdminCommand(adminExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { @@ -246,7 +234,7 @@ describe('ProtobufService', () => { service.sendAdminCommand(adminExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -258,9 +246,9 @@ describe('ProtobufService', () => { (service as ProtobufInternal).pendingCommands.set(1, cb); vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.RESPONSE, - response: create(ResponseSchema, { cmdId: BigInt(1) }), + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.RESPONSE, + response: create(Data.ResponseSchema, { cmdId: BigInt(1) }), }) ); @@ -274,8 +262,8 @@ describe('ProtobufService', () => { const processRoomEvent = vi.spyOn(service as ProtobufInternal, 'processRoomEvent'); vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.ROOM_EVENT, + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.ROOM_EVENT, }) ); @@ -288,8 +276,8 @@ describe('ProtobufService', () => { const processSessionEvent = vi.spyOn(service as ProtobufInternal, 'processSessionEvent'); vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.SESSION_EVENT, + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.SESSION_EVENT, }) ); @@ -302,8 +290,8 @@ describe('ProtobufService', () => { const processGameEvent = vi.spyOn(service as ProtobufInternal, 'processGameEvent'); vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER, + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER, }) ); @@ -316,7 +304,7 @@ describe('ProtobufService', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { + create(Data.ServerMessageSchema, { messageType: 999, }) ); @@ -353,12 +341,12 @@ describe('ProtobufService', () => { }); it('dispatches to a GameEvents handler when hasExtension returns true', () => { + const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {} as GenExtension; + const mockExt = {} as GenExtension; const payload = { someData: 1 }; - (GameEvents as any).push([mockExt, handler]); - const service = new ProtobufService(mockSocket); + registryTeardowns.push(withEventRegistry(GameEvents as GameExtensionRegistry, [mockExt, handler])); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -371,12 +359,12 @@ describe('ProtobufService', () => { }); it('defaults gameId and playerId to -1 when undefined', () => { + const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {} as GenExtension; + const mockExt = {} as GenExtension; const payload = { someData: 1 }; - (GameEvents as any).push([mockExt, handler]); - const service = new ProtobufService(mockSocket); + registryTeardowns.push(withEventRegistry(GameEvents as GameExtensionRegistry, [mockExt, handler])); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -407,12 +395,12 @@ describe('ProtobufService', () => { }); it('dispatches to a RoomEvents handler when hasExtension returns true', () => { + const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {} as GenExtension; + const mockExt = {} as GenExtension; const payload = { roomData: 1 }; - (RoomEvents as any).push([mockExt, handler]); - const service = new ProtobufService(mockSocket); + registryTeardowns.push(withEventRegistry(RoomEvents as RoomExtensionRegistry, [mockExt, handler])); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -432,12 +420,12 @@ describe('ProtobufService', () => { }); it('dispatches to a SessionEvents handler when hasExtension returns true', () => { + const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {} as GenExtension; + const mockExt = {} as GenExtension; const payload = { sessionData: 1 }; - (SessionEvents as any).push([mockExt, handler]); - const service = new ProtobufService(mockSocket); + registryTeardowns.push(withEventRegistry(SessionEvents as SessionExtensionRegistry, [mockExt, handler])); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -448,72 +436,3 @@ 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('@bufbuild/protobuf'); - const { CommandContainerSchema } = - await vi.importActual('@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('@bufbuild/protobuf'); - const { ServerMessageSchema, ServerMessage_MessageType, ResponseSchema, Response_ResponseCode } = - await vi.importActual('@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('@bufbuild/protobuf'); - const { - CommandContainerSchema, SessionCommandSchema, - Command_Ping_ext, Command_PingSchema, - } = await vi.importActual('@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(); - }); -}); diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index 05437f80e..c4b6cd3ce 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,32 +1,11 @@ import { create, fromBinary, hasExtension, getExtension, setExtension, toBinary } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { - CommandContainerSchema, - GameCommandSchema, - SessionCommandSchema, - RoomCommandSchema, - ModeratorCommandSchema, - AdminCommandSchema, - ServerMessageSchema, - ServerMessage_MessageType, - type Response, - type CommandContainer, - type GameCommand, - type SessionCommand, - type RoomCommand, - type ModeratorCommand, - type AdminCommand, - type ServerMessage, - type GameEventContainer, - type SessionEvent, - type RoomEvent, -} from '@app/generated'; -import { GameEvents } from '../events/game'; -import { RoomEvents } from '../events/room'; -import { SessionEvents } from '../events/session'; -import type { GameEventMeta } from '../interfaces/WebSocketConfig'; +import { GameEvents, RoomEvents, SessionEvents } from '../events'; +import { Data, Enriched } from '@app/types'; + + import { type CommandOptions, handleResponse } from './command-options'; export interface SocketTransport { @@ -36,9 +15,13 @@ export interface SocketTransport { export class ProtobufService { private cmdId = 0; - private pendingCommands = new Map void>(); + private pendingCommands = new Map void>(); - constructor(private transport: SocketTransport) {} + private transport: SocketTransport; + + constructor(transport: SocketTransport) { + this.transport = transport; + } public resetCommands() { this.cmdId = 0; @@ -47,13 +30,13 @@ export class ProtobufService { public sendGameCommand( gameId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const gameCmd = create(GameCommandSchema); + const gameCmd = create(Data.GameCommandSchema); setExtension(gameCmd, ext, value); - const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); + const cmd = create(Data.CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -63,13 +46,13 @@ export class ProtobufService { public sendRoomCommand( roomId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const roomCmd = create(RoomCommandSchema); + const roomCmd = create(Data.RoomCommandSchema); setExtension(roomCmd, ext, value); - const cmd = create(CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); + const cmd = create(Data.CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -78,13 +61,13 @@ export class ProtobufService { } public sendSessionCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const sesCmd = create(SessionCommandSchema); + const sesCmd = create(Data.SessionCommandSchema); setExtension(sesCmd, ext, value); - const cmd = create(CommandContainerSchema, { sessionCommand: [sesCmd] }); + const cmd = create(Data.CommandContainerSchema, { sessionCommand: [sesCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -93,13 +76,13 @@ export class ProtobufService { } public sendModeratorCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const modCmd = create(ModeratorCommandSchema); + const modCmd = create(Data.ModeratorCommandSchema); setExtension(modCmd, ext, value); - const cmd = create(CommandContainerSchema, { moderatorCommand: [modCmd] }); + const cmd = create(Data.CommandContainerSchema, { moderatorCommand: [modCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -108,13 +91,13 @@ export class ProtobufService { } public sendAdminCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const adminCmd = create(AdminCommandSchema); + const adminCmd = create(Data.AdminCommandSchema); setExtension(adminCmd, ext, value); - const cmd = create(CommandContainerSchema, { adminCommand: [adminCmd] }); + const cmd = create(Data.CommandContainerSchema, { adminCommand: [adminCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -122,7 +105,7 @@ export class ProtobufService { }); } - public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void) { + public sendCommand(cmd: Data.CommandContainer, callback: (raw: Data.Response) => void) { if (!this.transport.isOpen()) { return; } @@ -130,26 +113,26 @@ export class ProtobufService { this.cmdId++; cmd.cmdId = BigInt(this.cmdId); this.pendingCommands.set(this.cmdId, callback); - this.transport.send(toBinary(CommandContainerSchema, cmd)); + this.transport.send(toBinary(Data.CommandContainerSchema, cmd)); } public handleMessageEvent({ data }: MessageEvent): void { try { const uint8msg = new Uint8Array(data); - const msg: ServerMessage = fromBinary(ServerMessageSchema, uint8msg); + const msg: Data.ServerMessage = fromBinary(Data.ServerMessageSchema, uint8msg); if (msg) { switch (msg.messageType) { - case ServerMessage_MessageType.RESPONSE: + case Data.ServerMessage_MessageType.RESPONSE: this.processServerResponse(msg.response); break; - case ServerMessage_MessageType.ROOM_EVENT: + case Data.ServerMessage_MessageType.ROOM_EVENT: this.processRoomEvent(msg.roomEvent); break; - case ServerMessage_MessageType.SESSION_EVENT: + case Data.ServerMessage_MessageType.SESSION_EVENT: this.processSessionEvent(msg.sessionEvent); break; - case ServerMessage_MessageType.GAME_EVENT_CONTAINER: + case Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER: this.processGameEvent(msg.gameEventContainer); break; default: @@ -162,7 +145,7 @@ export class ProtobufService { } } - private processServerResponse(response: Response | undefined) { + private processServerResponse(response: Data.Response | undefined) { if (!response) { return; } @@ -174,7 +157,7 @@ export class ProtobufService { } } - private processRoomEvent(event: RoomEvent | undefined) { + private processRoomEvent(event: Data.RoomEvent | undefined) { if (!event) { return; } @@ -186,7 +169,7 @@ export class ProtobufService { } } - private processSessionEvent(event: SessionEvent | undefined) { + private processSessionEvent(event: Data.SessionEvent | undefined) { if (!event) { return; } @@ -198,7 +181,7 @@ export class ProtobufService { } } - private processGameEvent(container: GameEventContainer | undefined): void { + private processGameEvent(container: Data.GameEventContainer | undefined): void { if (!container?.eventList?.length) { return; } @@ -206,7 +189,7 @@ export class ProtobufService { const { gameId, context, secondsElapsed, forcedByJudge } = container; for (const event of container.eventList) { - const meta: GameEventMeta = { + const meta: Enriched.GameEventMeta = { gameId: gameId ?? -1, playerId: event.playerId ?? -1, context, @@ -224,3 +207,4 @@ export class ProtobufService { } } + diff --git a/webclient/src/websocket/services/WebSocketService.spec.ts b/webclient/src/websocket/services/WebSocketService.spec.ts index c565ac74e..e8a25802c 100644 --- a/webclient/src/websocket/services/WebSocketService.spec.ts +++ b/webclient/src/websocket/services/WebSocketService.spec.ts @@ -9,7 +9,7 @@ vi.mock('../config', () => ({ import { WebSocketService } from './WebSocketService'; import type { WebSocketServiceConfig } from './WebSocketService'; import { KeepAliveService } from './KeepAliveService'; -import { StatusEnum } from '../interfaces/StatusEnum'; +import { App } from '@app/types'; type WebSocketInternal = WebSocketService & { keepAliveService: KeepAliveService; @@ -19,7 +19,11 @@ let MockWS: Mock; let mockInstance: ReturnType['mockInstance']; let restoreWebSocket: ReturnType['restore']; let mockConfig: WebSocketServiceConfig; -let mockOnConnectionFailed: Mock; +let mockResponse: { + session: { + connectionFailed: Mock; + }; +}; let mockOnStatusChange: Mock; let locationRestores: Array<() => void>; @@ -31,12 +35,16 @@ beforeEach(() => { mockInstance = installed.mockInstance; restoreWebSocket = installed.restore; - mockOnConnectionFailed = vi.fn(); + mockResponse = { + session: { + connectionFailed: vi.fn(), + }, + }; mockOnStatusChange = vi.fn(); mockConfig = { keepAliveFn: vi.fn(), - onConnectionFailed: mockOnConnectionFailed, + response: mockResponse as unknown as WebSocketServiceConfig['response'], onStatusChange: mockOnStatusChange, }; @@ -70,7 +78,7 @@ describe('WebSocketService', () => { // trigger keepAliveService.disconnected$ (service as WebSocketInternal).keepAliveService.disconnected$.next(); expect(mockInstance.close).toHaveBeenCalled(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection timeout'); + expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection timeout'); }); }); @@ -112,7 +120,7 @@ describe('WebSocketService', () => { it('calls onStatusChange CONNECTED on open', () => { createConnectedService(); mockInstance.onopen(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); + expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'Connected'); }); it('starts the ping loop with the keepalive interval', () => { @@ -139,14 +147,14 @@ describe('WebSocketService', () => { it('calls onStatusChange DISCONNECTED on close when not already DISCONNECTED', () => { createConnectedService(); mockInstance.onclose(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('does not overwrite status if already DISCONNECTED', () => { createConnectedService(); mockInstance.onerror(); mockInstance.onclose(); - expect(mockOnStatusChange).not.toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(mockOnStatusChange).not.toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('ends the ping loop on close', () => { @@ -162,13 +170,13 @@ describe('WebSocketService', () => { it('calls onStatusChange DISCONNECTED on error', () => { createConnectedService(); mockInstance.onerror(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Failed'); + expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Failed'); }); - it('calls onConnectionFailed on error', () => { + it('calls response.session.connectionFailed on error', () => { createConnectedService(); mockInstance.onerror(); - expect(mockOnConnectionFailed).toHaveBeenCalled(); + expect(mockResponse.session.connectionFailed).toHaveBeenCalled(); }); }); diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts index cf2321667..3e93ad19e 100644 --- a/webclient/src/websocket/services/WebSocketService.ts +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -1,20 +1,22 @@ import { Subject } from 'rxjs'; -import { StatusEnum } from '../interfaces/StatusEnum'; +import { App, Enriched } from '@app/types'; + import { KeepAliveService } from './KeepAliveService'; import { CLIENT_OPTIONS } from '../config'; -import type { ConnectTarget } from '../interfaces/WebClientConfig'; +import { IWebClientResponse } from '../interfaces'; export interface WebSocketServiceConfig { keepAliveFn: (pingReceived: () => void) => void; - onStatusChange: (status: StatusEnum, description: string) => void; - onConnectionFailed: () => void; + response: IWebClientResponse; + onStatusChange: (status: App.StatusEnum, description: string) => void; } export class WebSocketService { private socket: WebSocket; private config: WebSocketServiceConfig; + private response: IWebClientResponse; private keepAliveService: KeepAliveService; private errorFired = false; @@ -24,20 +26,21 @@ export class WebSocketService { constructor(config: WebSocketServiceConfig) { this.config = config; + this.response = config.response; this.keepAliveService = new KeepAliveService(() => this.checkReadyState(WebSocket.OPEN)); this.keepAliveService.disconnected$.subscribe(() => { this.disconnect(); - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection timeout'); + this.config.onStatusChange(App.StatusEnum.DISCONNECTED, 'Connection timeout'); }); } - public connect(target: ConnectTarget, protocol: string = 'wss'): void { + public connect(options: Enriched.WebSocketConnectOptions, protocol: string = 'wss'): void { if (window.location.hostname === 'localhost') { protocol = 'ws'; } - const { host, port } = target; + const { host, port } = options; this.keepalive = CLIENT_OPTIONS.keepalive; this.socket = this.createWebSocket(`${protocol}://${host}:${port}`); @@ -66,7 +69,7 @@ export class WebSocketService { socket.onopen = () => { clearTimeout(connectionTimer); this.errorFired = false; - this.config.onStatusChange(StatusEnum.CONNECTED, 'Connected'); + this.config.onStatusChange(App.StatusEnum.CONNECTED, 'Connected'); this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: () => void) => { this.config.keepAliveFn(pingReceived); @@ -76,7 +79,7 @@ export class WebSocketService { socket.onclose = () => { // dont overwrite failure messages if (!this.errorFired) { - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Closed'); + this.config.onStatusChange(App.StatusEnum.DISCONNECTED, 'Connection Closed'); } this.errorFired = false; this.keepAliveService.endPingLoop(); @@ -84,8 +87,8 @@ export class WebSocketService { socket.onerror = () => { this.errorFired = true; - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Failed'); - this.config.onConnectionFailed(); + this.config.onStatusChange(App.StatusEnum.DISCONNECTED, 'Connection Failed'); + this.response.session.connectionFailed(); }; socket.onmessage = (event: MessageEvent) => { diff --git a/webclient/src/websocket/services/command-options.spec.ts b/webclient/src/websocket/services/command-options.spec.ts index 19dd644be..23065d8e6 100644 --- a/webclient/src/websocket/services/command-options.spec.ts +++ b/webclient/src/websocket/services/command-options.spec.ts @@ -1,6 +1,5 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import type { Response } from '@app/generated'; -import { Response_ResponseCode, ResponseSchema } from '@app/generated'; +import { Data } from '@app/types'; vi.mock('@bufbuild/protobuf', async () => { const actual = await vi.importActual('@bufbuild/protobuf'); return { ...actual, getExtension: vi.fn() }; @@ -20,14 +19,14 @@ describe('handleResponse', () => { it('calls onResponse and returns early when provided', () => { const onResponse = vi.fn(); const onSuccess = vi.fn(); - handleResponse('test', create(ResponseSchema, { responseCode: 99 }), { onResponse, onSuccess }); + handleResponse('test', create(Data.ResponseSchema, { responseCode: 99 }), { onResponse, onSuccess }); expect(onResponse).toHaveBeenCalled(); expect(onSuccess).not.toHaveBeenCalled(); }); it('calls onSuccess when responseCode is RespOk and no responseExt', () => { const onSuccess = vi.fn(); - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespOk }); + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); handleResponse('test', raw, { onSuccess }); expect(onSuccess).toHaveBeenCalledWith(); }); @@ -35,28 +34,28 @@ describe('handleResponse', () => { it('calls onSuccess with nested response when responseExt is set', () => { vi.mocked(getExtension).mockReturnValue({ nested: true }); const onSuccess = vi.fn(); - const fakeExt = {} as unknown as GenExtension; - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespOk }); + const fakeExt = {} as unknown as GenExtension; + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); handleResponse('test', raw, { onSuccess, responseExt: fakeExt }); expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw); }); it('calls onResponseCode handler when code matches', () => { const specificHandler = vi.fn(); - handleResponse('test', create(ResponseSchema, { responseCode: 5 }), { onResponseCode: { 5: specificHandler } }); + handleResponse('test', create(Data.ResponseSchema, { responseCode: 5 }), { onResponseCode: { 5: specificHandler } }); expect(specificHandler).toHaveBeenCalled(); }); it('calls onError when responseCode is not RespOk and no specific handler', () => { const onError = vi.fn(); - const raw = create(ResponseSchema, { responseCode: 99 }); + const raw = create(Data.ResponseSchema, { responseCode: 99 }); handleResponse('test', raw, { onError }); expect(onError).toHaveBeenCalledWith(99, raw); }); it('logs error to console when no callbacks for non-RespOk response', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - handleResponse('test.Type', create(ResponseSchema, { responseCode: 42 }), {}); + handleResponse('test.Type', create(Data.ResponseSchema, { responseCode: 42 }), {}); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); diff --git a/webclient/src/websocket/services/command-options.ts b/webclient/src/websocket/services/command-options.ts index 6d1138886..671d0a02b 100644 --- a/webclient/src/websocket/services/command-options.ts +++ b/webclient/src/websocket/services/command-options.ts @@ -1,16 +1,16 @@ import { getExtension } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Response_ResponseCode, type Response } from '@app/generated'; +import { Data } from '@app/types'; interface CommandOptionsBase { - onError?: (responseCode: number, raw: Response) => void; - onResponseCode?: { [code: number]: (raw: Response) => void }; - onResponse?: (raw: Response) => void; + onError?: (responseCode: number, raw: Data.Response) => void; + onResponseCode?: { [code: number]: (raw: Data.Response) => void }; + onResponse?: (raw: Data.Response) => void; } export interface CommandOptionsWithResponse extends CommandOptionsBase { - responseExt: GenExtension; - onSuccess?: (response: R, raw: Response) => void; + responseExt: GenExtension; + onSuccess?: (response: R, raw: Data.Response) => void; } export interface CommandOptionsWithoutResponse extends CommandOptionsBase { @@ -24,7 +24,7 @@ export function hasResponseExt(options: CommandOptions): options is Comman return options.responseExt !== undefined; } -export function handleResponse(typeName: string, raw: Response, options: CommandOptions): void { +export function handleResponse(typeName: string, raw: Data.Response, options: CommandOptions): void { if (options.onResponse) { options.onResponse(raw); return; @@ -32,7 +32,7 @@ export function handleResponse(typeName: string, raw: Response, options: Comm const { responseCode } = raw; - if (responseCode === Response_ResponseCode.RespOk) { + if (responseCode === Data.Response_ResponseCode.RespOk) { if (hasResponseExt(options)) { options.onSuccess?.(getExtension(raw, options.responseExt), raw); } else { diff --git a/webclient/src/websocket/utils/connectionState.ts b/webclient/src/websocket/utils/connectionState.ts deleted file mode 100644 index de4866462..000000000 --- a/webclient/src/websocket/utils/connectionState.ts +++ /dev/null @@ -1,13 +0,0 @@ -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; -} diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts index 6a230d9ea..22951ce49 100644 --- a/webclient/src/websocket/utils/passwordHasher.ts +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -1,6 +1,6 @@ import sha512 from 'crypto-js/sha512'; import Base64 from 'crypto-js/enc-base64'; -import { Event_ServerIdentification_ServerOptions } from '@app/generated'; +import { Data } from '@app/types'; const HASH_ROUNDS = 1_000; const SALT_LENGTH = 16; @@ -28,5 +28,5 @@ export const generateSalt = (): string => { export const passwordSaltSupported = (serverOptions: number): number => { // Intentional use of Bitwise operator b/c of how Servatrice Enums work - return serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash; + return serverOptions & Data.Event_ServerIdentification_ServerOptions.SupportsPasswordHash; } diff --git a/webclient/vite.config.ts b/webclient/vite.config.ts index 1c1157f0a..86c243058 100644 --- a/webclient/vite.config.ts +++ b/webclient/vite.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./src/setupTests.ts'], include: ['src/**/*.spec.{ts,tsx}'], - isolate: true, + isolate: false, coverage: { provider: 'v8', reporter: ['text', 'html'], diff --git a/webclient/vitest.integration.config.ts b/webclient/vitest.integration.config.ts index 0b88f927a..dfba71824 100644 --- a/webclient/vitest.integration.config.ts +++ b/webclient/vitest.integration.config.ts @@ -16,15 +16,12 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./integration/src/helpers/setup.ts'], include: ['integration/src/**/*.spec.ts'], + isolate: false, coverage: { provider: 'v8', reporter: ['text', 'html'], reportsDirectory: './coverage/integration', - include: [ - 'src/websocket/**/*.{ts,tsx}', - 'src/store/**/*.{ts,tsx}', - 'src/api/**/*.{ts,tsx}', - ], + include: ['src/**/*.{ts,tsx}'], exclude: [ 'src/generated/**', 'src/**/*.spec.{ts,tsx}',