diff --git a/webclient/buf.gen.plugin.mjs b/webclient/buf.gen.plugin.mjs index b618761db..35354ce4c 100644 --- a/webclient/buf.gen.plugin.mjs +++ b/webclient/buf.gen.plugin.mjs @@ -11,7 +11,8 @@ 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_*.', + '// Rollup of all proto modules + MessageInitShape param aliases for every Command_*,', + '// plus type maps for Response/Event extensions grouped by scope.', '/* eslint-disable */', '', '', @@ -55,6 +56,71 @@ 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 0d67c6ae9..164dc6ed6 100644 --- a/webclient/eslint.boundaries.mjs +++ b/webclient/eslint.boundaries.mjs @@ -19,9 +19,9 @@ const types = (...types) => types.map((type) => ({ to: { type } })); const rules = [ { from: { type: 'generated' }, allow: [] }, - { from: { type: 'types' }, allow: types('generated') }, + { from: { type: 'types' }, allow: types('generated', 'websocket') }, - { from: { type: 'websocket' }, allow: types('types') }, + { from: { type: 'websocket' }, allow: types('generated') }, { from: { type: 'store' }, allow: types('types') }, { from: { type: 'api' }, allow: types('types', 'store', 'websocket') }, @@ -35,21 +35,27 @@ const rules = [ { 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, + }], }, }, - rules: { - 'boundaries/dependencies': ['error', { - default: 'disallow', - rules, - }], + { + files: ['**/*.spec.*'], + rules: { 'boundaries/dependencies': 'off' }, }, -}; +]; diff --git a/webclient/eslint.config.mjs b/webclient/eslint.config.mjs index 28a00a0ae..f8885d58e 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 new file mode 100644 index 000000000..f67bcfc6f --- /dev/null +++ b/webclient/integration/src/admin.spec.ts @@ -0,0 +1,66 @@ +// 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 fb852b565..c5149c809 100644 --- a/webclient/integration/src/authentication.spec.ts +++ b/webclient/integration/src/authentication.spec.ts @@ -1,12 +1,14 @@ -// Authentication scenarios — login success/failure, register, and activate. +// Authentication scenarios — login success/failure, register, activate, +// and the hashed-password (salt) login path. import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; -import { App, Data } from '@app/types'; +import { Data } from '@app/types'; import { store } from '@app/store'; +import { StatusEnum, WebSocketConnectReason } from '@app/websocket'; -import { connectAndHandshake } from './helpers/setup'; +import { connectAndHandshake, connectAndHandshakeWithSalt } from './helpers/setup'; import { buildResponse, buildResponseMessage, @@ -42,7 +44,7 @@ describe('authentication', () => { }))); const state = store.getState().server; - expect(state.status.state).toBe(App.StatusEnum.LOGGED_IN); + expect(state.status.state).toBe(StatusEnum.LOGGED_IN); expect(state.status.description).toBe('Logged in.'); expect(state.user?.name).toBe('alice'); expect(Object.keys(state.buddyList)).toEqual(['bob']); @@ -62,7 +64,7 @@ describe('authentication', () => { }))); const state = store.getState().server; - expect(state.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(state.status.state).toBe(StatusEnum.DISCONNECTED); expect(state.user).toBeNull(); expect(state.buddyList).toEqual({}); }); @@ -70,7 +72,7 @@ describe('authentication', () => { describe('register', () => { const registerOptions = { - reason: App.WebSocketConnectReason.REGISTER, + reason: WebSocketConnectReason.REGISTER as const, host: 'localhost', port: '4748', userName: 'newbie', @@ -78,10 +80,10 @@ describe('authentication', () => { email: 'newbie@example.com', country: 'US', realName: 'New Bie', - } as const; + }; it('auto-logs-in on RespRegistrationAccepted', () => { - connectAndHandshake(registerOptions as any); + connectAndHandshake(registerOptions); const register = findLastSessionCommand(Data.Command_Register_ext); expect(register.value.userName).toBe('newbie'); @@ -97,7 +99,7 @@ describe('authentication', () => { }); it('parks registration in awaiting-activation on RespRegistrationAcceptedNeedsActivation', () => { - connectAndHandshake(registerOptions as any); + connectAndHandshake(registerOptions); const register = findLastSessionCommand(Data.Command_Register_ext); deliverMessage(buildResponseMessage(buildResponse({ @@ -105,7 +107,7 @@ describe('authentication', () => { responseCode: Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation, }))); - expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow(); }); }); @@ -113,13 +115,13 @@ describe('authentication', () => { describe('activate', () => { it('auto-logs-in on RespActivationAccepted', () => { connectAndHandshake({ - reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, + reason: WebSocketConnectReason.ACTIVATE_ACCOUNT as const, 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'); @@ -133,4 +135,43 @@ 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 d81c35f49..31cd41b83 100644 --- a/webclient/integration/src/connection.spec.ts +++ b/webclient/integration/src/connection.spec.ts @@ -1,30 +1,44 @@ // 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 { App, Data } from '@app/types'; +import { 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 } from './helpers/setup'; +import { + getMockWebSocket, + getWebClient, + openMockWebSocket, + setPendingOptions, + connectAndHandshake, +} from './helpers/setup'; +import type { WebSocketConnectOptions } from '@app/websocket'; +import { WebSocketConnectReason } from '@app/websocket'; import { buildSessionEventMessage, deliverMessage, } from './helpers/protobuf-builders'; import { findLastSessionCommand } from './helpers/command-capture'; -function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}) { +function loginOptions(overrides: Partial<{ userName: string; password: string }> = {}): WebSocketConnectOptions { return { - reason: App.WebSocketConnectReason.LOGIN, + reason: WebSocketConnectReason.LOGIN, host: 'localhost', port: '4748', userName: overrides.userName ?? 'alice', password: overrides.password ?? 'secret', - } as const; + }; +} + +function connectWithOptions(opts: WebSocketConnectOptions): void { + setPendingOptions(opts); + getWebClient().connect({ host: opts.host, port: opts.port }); } function serverIdentification( @@ -43,47 +57,45 @@ function serverIdentification( describe('connection lifecycle', () => { it('flips status through CONNECTING → CONNECTED on socket open', () => { - getWebClient().connect(loginOptions()); + connectWithOptions(loginOptions()); expect(store.getState().server.status.connectionAttemptMade).toBe(true); openMockWebSocket(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); expect(store.getState().server.status.description).toBe('Connected'); }); it('routes a matching ServerIdentification into LOGGING_IN and sends Command_Login', () => { - getWebClient().connect(loginOptions({ userName: 'alice' })); + connectWithOptions(loginOptions({ userName: 'alice' })); openMockWebSocket(); deliverMessage(serverIdentification()); - expect(store.getState().server.status.state).toBe(App.StatusEnum.LOGGING_IN); + expect(store.getState().server.status.state).toBe(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', () => { - getWebClient().connect(loginOptions()); + connectWithOptions(loginOptions()); openMockWebSocket(); deliverMessage(serverIdentification(PROTOCOL_VERSION + 1)); const mock = getMockWebSocket(); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); expect(() => findLastSessionCommand(Data.Command_Login_ext)).toThrow(); }); it('times out when onopen never fires within the keepalive window', () => { - getWebClient().connect(loginOptions()); + connectWithOptions(loginOptions()); const mock = getMockWebSocket(); expect(mock.close).not.toHaveBeenCalled(); @@ -91,11 +103,11 @@ describe('connection lifecycle', () => { vi.advanceTimersByTime(5000); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); }); it('releases keep-alive ping loop on explicit disconnect', () => { - getWebClient().connect(loginOptions()); + connectWithOptions(loginOptions()); openMockWebSocket(); deliverMessage(serverIdentification()); @@ -103,6 +115,20 @@ describe('connection lifecycle', () => { getWebClient().disconnect(); expect(mock.close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(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 new file mode 100644 index 000000000..76ed032be --- /dev/null +++ b/webclient/integration/src/deck.spec.ts @@ -0,0 +1,119 @@ +// 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 new file mode 100644 index 000000000..eb88e6b2b --- /dev/null +++ b/webclient/integration/src/game.spec.ts @@ -0,0 +1,416 @@ +// 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 0b48f4df6..b0d93cd2f 100644 --- a/webclient/integration/src/helpers/command-capture.ts +++ b/webclient/integration/src/helpers/command-capture.ts @@ -11,10 +11,12 @@ import { Data } from '@app/types'; import { getMockWebSocket } from './setup'; -/** The three command scopes a CommandContainer can carry in practice. */ +/** The 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[] { @@ -110,3 +112,47 @@ 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 99d351311..4c58dbcca 100644 --- a/webclient/integration/src/helpers/setup.ts +++ b/webclient/integration/src/helpers/setup.ts @@ -14,10 +14,16 @@ import { create } from '@bufbuild/protobuf'; import { afterEach, beforeEach, vi } from 'vitest'; import { ServerDispatch, RoomsDispatch, GameDispatch } from '@app/store'; -import { App, Data, Enriched } from '@app/types'; -import { WebClient } from '@app/websocket'; +import { Data } from '@app/types'; +import { + WebClient, + StatusEnum, + WebSocketConnectReason, + setPendingOptions, +} from '@app/websocket'; +import type { WebSocketConnectOptions } from '@app/websocket'; import { PROTOCOL_VERSION } from '../../../src/websocket/config'; -import { createWebClientResponse, createWebClientRequest } from '@app/api'; +import { initWebClient } from '@app/api'; import { buildResponse, @@ -27,6 +33,8 @@ import { } from './protobuf-builders'; import { findLastSessionCommand } from './command-capture'; +export { setPendingOptions }; + export interface MockWebSocketInstance { send: ReturnType; close: ReturnType; @@ -97,8 +105,7 @@ function resetAll(): void { } client.protobuf.resetCommands(); - client.options = null; - client.status = App.StatusEnum.DISCONNECTED; + client.status = StatusEnum.DISCONNECTED; ServerDispatch.clearStore(); RoomsDispatch.clearStore(); @@ -117,8 +124,8 @@ function resetAll(): void { // ── Shared connect helpers ────────────────────────────────────────────────── -const DEFAULT_LOGIN_OPTIONS: Enriched.LoginConnectOptions = { - reason: App.WebSocketConnectReason.LOGIN, +const DEFAULT_LOGIN_OPTIONS: WebSocketConnectOptions = { + reason: WebSocketConnectReason.LOGIN, host: 'localhost', port: '4748', userName: 'alice', @@ -126,14 +133,16 @@ const DEFAULT_LOGIN_OPTIONS: Enriched.LoginConnectOptions = { }; export function connectRaw( - overrides: Partial = {} + overrides: Partial = {} ): void { - getWebClient().connect({ ...DEFAULT_LOGIN_OPTIONS, ...overrides }); + const opts = { ...DEFAULT_LOGIN_OPTIONS, ...overrides }; + setPendingOptions(opts as WebSocketConnectOptions); + getWebClient().connect({ host: opts.host, port: opts.port }); openMockWebSocket(); } export function connectAndHandshake( - overrides: Partial = {} + overrides: Partial = {} ): void { connectRaw(overrides); deliverMessage(buildSessionEventMessage( @@ -146,6 +155,21 @@ 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 }); @@ -172,7 +196,7 @@ installMockWebSocket(); beforeEach(() => { vi.useFakeTimers(); - new WebClient(createWebClientResponse(), createWebClientRequest()); + initWebClient(); }); afterEach(() => { diff --git a/webclient/integration/src/keep-alive.spec.ts b/webclient/integration/src/keep-alive.spec.ts index 521d4dce9..c4889e0fc 100644 --- a/webclient/integration/src/keep-alive.spec.ts +++ b/webclient/integration/src/keep-alive.spec.ts @@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest'; -import { App, Data } from '@app/types'; +import { Data } from '@app/types'; import { store } from '@app/store'; +import { StatusEnum } from '@app/websocket'; import { connectRaw, getMockWebSocket } from './helpers/setup'; import { @@ -31,7 +32,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(App.StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); }); it('stays CONNECTED while pongs arrive before the next tick', () => { @@ -46,7 +47,7 @@ describe('keep-alive', () => { }))); } - expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); expect(getMockWebSocket().close).not.toHaveBeenCalled(); }); @@ -55,11 +56,11 @@ describe('keep-alive', () => { vi.advanceTimersByTime(5000); expect(() => findLastSessionCommand(Data.Command_Ping_ext)).not.toThrow(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.CONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.CONNECTED); vi.advanceTimersByTime(5000); expect(getMockWebSocket().close).toHaveBeenCalled(); - expect(store.getState().server.status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(store.getState().server.status.state).toBe(StatusEnum.DISCONNECTED); }); }); diff --git a/webclient/integration/src/moderator.spec.ts b/webclient/integration/src/moderator.spec.ts new file mode 100644 index 000000000..c90c3b026 --- /dev/null +++ b/webclient/integration/src/moderator.spec.ts @@ -0,0 +1,104 @@ +// 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 new file mode 100644 index 000000000..ec842c3ec --- /dev/null +++ b/webclient/integration/src/password-reset.spec.ts @@ -0,0 +1,86 @@ +// 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 7e9ed519f..5bd776d08 100644 --- a/webclient/integration/src/rooms.spec.ts +++ b/webclient/integration/src/rooms.spec.ts @@ -1,11 +1,12 @@ // Room scenarios — Event_ListRooms handling, auto-join, Response_JoinRoom, -// room chat, and in-room game list updates. +// room chat (inbound + outbound), game list updates, and leaveRoom. 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 { @@ -15,7 +16,8 @@ import { buildSessionEventMessage, deliverMessage, } from './helpers/protobuf-builders'; -import { findLastSessionCommand } from './helpers/command-capture'; +import { findLastSessionCommand, findLastRoomCommand, captureAllOutbound } from './helpers/command-capture'; +import { fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; function makeRoom(overrides: Partial<{ roomId: number; @@ -35,6 +37,21 @@ 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(); @@ -81,18 +98,7 @@ describe('rooms', () => { it('appends a room chat message on Event_RoomSay', () => { connectAndHandshake(); - - 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 }) }), - }))); + setupJoinedRoom(1); const say = create(Data.Event_RoomSaySchema, { name: 'bob', @@ -109,18 +115,7 @@ describe('rooms', () => { it('updates the game list on Event_ListGames', () => { connectAndHandshake(); - - 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 }) }), - }))); + setupJoinedRoom(1); const game = create(Data.ServerInfo_GameSchema, { gameId: 42, @@ -137,4 +132,102 @@ 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 faa42f11a..16ac3a6bf 100644 --- a/webclient/integration/src/server-events.spec.ts +++ b/webclient/integration/src/server-events.spec.ts @@ -4,8 +4,9 @@ import { create } from '@bufbuild/protobuf'; import { describe, expect, it } from 'vitest'; -import { App, Data } from '@app/types'; +import { Data } from '@app/types'; import { store } from '@app/store'; +import { StatusEnum } from '@app/websocket'; import { connectAndHandshake } from './helpers/setup'; import { @@ -72,7 +73,7 @@ describe('server events', () => { )); const status = store.getState().server.status; - expect(status.state).toBe(App.StatusEnum.DISCONNECTED); + expect(status.state).toBe(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 03005abba..36062963d 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/config.ts b/webclient/src/api/config.ts new file mode 100644 index 000000000..42ed9fd77 --- /dev/null +++ b/webclient/src/api/config.ts @@ -0,0 +1 @@ +export const PROTOCOL_VERSION = 14; diff --git a/webclient/src/api/connectionState.ts b/webclient/src/api/connectionState.ts new file mode 100644 index 000000000..ddf7a90b5 --- /dev/null +++ b/webclient/src/api/connectionState.ts @@ -0,0 +1,13 @@ +import type { Enriched } from '@app/types'; + +let pendingOptions: Enriched.WebSocketConnectOptions | null = null; + +export function setPendingOptions(options: Enriched.WebSocketConnectOptions) { + pendingOptions = options; +} + +export function consumePendingOptions(): Enriched.WebSocketConnectOptions | null { + const opts = pendingOptions; + pendingOptions = null; + return opts; +} diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts index 0acaf2d93..618e6fc71 100644 --- a/webclient/src/api/index.ts +++ b/webclient/src/api/index.ts @@ -1,35 +1,13 @@ -import { WebClient } from '@app/websocket'; -import type { IWebClientRequest } from '@app/websocket'; - +export { initWebClient } from './initWebClient'; export { createWebClientResponse } from './response'; export { createWebClientRequest } from './request'; +import { 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. + * UI-facing request surface. The request implementations are created once + * at module load. They access `WebClient.instance` at call time (via lazy + * internal references), so the singleton only needs to exist by the first + * actual command send. */ -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; - }, -}; +export const request = createWebClientRequest(); diff --git a/webclient/src/api/initWebClient.ts b/webclient/src/api/initWebClient.ts new file mode 100644 index 000000000..89858fa4d --- /dev/null +++ b/webclient/src/api/initWebClient.ts @@ -0,0 +1,114 @@ +import { App } from '@app/types'; +import { + WebClient, + StatusEnum, + SessionEvents, + RoomEvents, + GameEvents, + SessionCommands, + generateSalt, + passwordSaltSupported, +} from '@app/websocket'; +import type { WebClientConfig } from '@app/websocket'; + +import { createWebClientResponse } from './response'; +import { consumePendingOptions } from './connectionState'; +import { PROTOCOL_VERSION } from './config'; + +export function initWebClient(): void { + const response = createWebClientResponse(); + + const config: WebClientConfig = { + response, + + onServerIdentified: (info) => { + const { serverName, serverVersion, protocolVersion, serverOptions } = info; + if (protocolVersion !== PROTOCOL_VERSION) { + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); + SessionCommands.disconnect(); + return; + } + + const getPasswordSalt = passwordSaltSupported(serverOptions); + const options = consumePendingOptions(); + + if (!options) { + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Missing connection options'); + SessionCommands.disconnect(); + return; + } + + switch (options.reason) { + case App.WebSocketConnectReason.LOGIN: { + const { password, ...rest } = options; + SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); + if (getPasswordSalt) { + SessionCommands.requestPasswordSalt(rest, + (salt) => SessionCommands.login(rest, password, salt), + () => { + response.session.loginFailed(); SessionCommands.disconnect(); + }, + ); + } else { + SessionCommands.login(rest, password); + } + break; + } + case App.WebSocketConnectReason.REGISTER: { + const { password, ...rest } = options; + const passwordSalt = getPasswordSalt ? generateSalt() : null; + SessionCommands.register(rest, password, passwordSalt); + break; + } + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: { + const { password, ...rest } = options; + if (getPasswordSalt) { + SessionCommands.requestPasswordSalt(rest, + (salt) => SessionCommands.activate(rest, password, salt), + () => { + response.session.accountActivationFailed(); SessionCommands.disconnect(); + }, + ); + } else { + SessionCommands.activate(rest, password); + } + break; + } + case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: + SessionCommands.forgotPasswordRequest(options); + break; + case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + SessionCommands.forgotPasswordChallenge(options); + break; + case App.WebSocketConnectReason.PASSWORD_RESET: { + const { newPassword, ...rest } = options; + if (getPasswordSalt) { + SessionCommands.requestPasswordSalt(rest, + (salt) => SessionCommands.forgotPasswordReset(rest, newPassword, salt), + () => { + response.session.resetPasswordFailed(); SessionCommands.disconnect(); + }, + ); + } else { + SessionCommands.forgotPasswordReset(rest, newPassword); + } + break; + } + default: { + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${options.reason}`); + SessionCommands.disconnect(); + break; + } + } + + response.session.updateInfo(serverName, serverVersion); + }, + + sessionEvents: SessionEvents, + roomEvents: RoomEvents, + gameEvents: GameEvents, + keepAliveFn: (cb) => SessionCommands.ping(cb), + }; + + new WebClient(config); +} diff --git a/webclient/src/api/request/AuthenticationRequestImpl.ts b/webclient/src/api/request/AuthenticationRequestImpl.ts index 9157d736f..7ac5de137 100644 --- a/webclient/src/api/request/AuthenticationRequestImpl.ts +++ b/webclient/src/api/request/AuthenticationRequestImpl.ts @@ -1,34 +1,58 @@ import { App, Enriched } from '@app/types'; -import type { IAuthenticationRequest } from '@app/websocket'; -import { SessionCommands } from '@app/websocket'; +import { WebClient, StatusEnum, SessionCommands } from '@app/websocket'; +import type { IAuthenticationRequest, AuthRequestMap } from '@app/websocket'; -export class AuthenticationRequestImpl implements IAuthenticationRequest { +import { setPendingOptions } from '../connectionState'; + +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 { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.LOGIN }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.LOGIN }); + SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); + WebClient.instance.connect({ host: options.host, port: options.port }); } testConnection(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.TEST_CONNECTION }); + WebClient.instance.testConnect({ host: options.host, port: options.port }); } register(options: Omit): void { - SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.REGISTER }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.REGISTER }); + 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 }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); + 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 }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); + 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 }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); + 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 }); + setPendingOptions({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET }); + SessionCommands.updateStatus(StatusEnum.CONNECTING, 'Connecting...'); + WebClient.instance.connect({ host: options.host, port: options.port }); } disconnect(): void { diff --git a/webclient/src/api/response/RoomResponseImpl.ts b/webclient/src/api/response/RoomResponseImpl.ts index 1a11bc4c5..d450158c0 100644 --- a/webclient/src/api/response/RoomResponseImpl.ts +++ b/webclient/src/api/response/RoomResponseImpl.ts @@ -1,8 +1,10 @@ -import { Data, Enriched } from '@app/types'; -import type { IRoomResponse } from '@app/websocket'; +import { Data } from '@app/types'; +import type { IRoomResponse, WebSocketRoomResponseOverrides } from '@app/websocket'; import { RoomsDispatch } from '@app/store'; -export class RoomResponseImpl implements IRoomResponse { +type Message = WebSocketRoomResponseOverrides['Event_RoomSay']; + +export class RoomResponseImpl implements IRoomResponse { clearStore(): void { RoomsDispatch.clearStore(); } @@ -23,7 +25,7 @@ export class RoomResponseImpl implements IRoomResponse { RoomsDispatch.updateGames(roomId, gameList); } - addMessage(roomId: number, message: Enriched.Message): void { + addMessage(roomId: number, message: Message): void { RoomsDispatch.addMessage(roomId, message); } diff --git a/webclient/src/api/response/SessionResponseImpl.ts b/webclient/src/api/response/SessionResponseImpl.ts index 7d6c16b47..59892d08d 100644 --- a/webclient/src/api/response/SessionResponseImpl.ts +++ b/webclient/src/api/response/SessionResponseImpl.ts @@ -1,8 +1,12 @@ -import { App, Data, Enriched } from '@app/types'; -import type { ISessionResponse } from '@app/websocket'; +import { Data } from '@app/types'; +import type { ISessionResponse, WebSocketSessionResponseOverrides } from '@app/websocket'; +import { StatusEnum } from '@app/websocket'; import { GameDispatch, RoomsDispatch, ServerDispatch } from '@app/store'; -export class SessionResponseImpl implements ISessionResponse { +type LoginSuccess = WebSocketSessionResponseOverrides['Response_Login']; +type PendingActivation = WebSocketSessionResponseOverrides['Response']; + +export class SessionResponseImpl implements ISessionResponse { initialized(): void { ServerDispatch.initialized(); } @@ -15,7 +19,7 @@ export class SessionResponseImpl implements ISessionResponse { ServerDispatch.clearStore(); } - loginSuccessful(options: Enriched.LoginSuccessContext): void { + loginSuccessful(options: LoginSuccess): void { ServerDispatch.loginSuccessful(options); } @@ -63,8 +67,8 @@ export class SessionResponseImpl implements ISessionResponse { ServerDispatch.updateInfo(name, version); } - updateStatus(state: App.StatusEnum, description: string): void { - if (state === App.StatusEnum.DISCONNECTED) { + updateStatus(state: StatusEnum, description: string): void { + if (state === StatusEnum.DISCONNECTED) { GameDispatch.clearStore(); RoomsDispatch.clearStore(); ServerDispatch.clearStore(); @@ -92,7 +96,7 @@ export class SessionResponseImpl implements ISessionResponse { ServerDispatch.serverMessage(message); } - accountAwaitingActivation(options: Enriched.PendingActivationContext): void { + accountAwaitingActivation(options: PendingActivation): void { ServerDispatch.accountAwaitingActivation(options); } diff --git a/webclient/src/components/Message/Message.tsx b/webclient/src/components/Message/Message.tsx index bc9d640ec..bb5617242 100644 --- a/webclient/src/components/Message/Message.tsx +++ b/webclient/src/components/Message/Message.tsx @@ -1,5 +1,5 @@ -// eslint-disable-next-line -import React, { useEffect, useMemo, useState } from 'react'; + +import React, { useEffect, 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); - useMemo(() => { + useEffect(() => { const name = message.match(App.MESSAGE_SENDER_REGEX); if (name) { diff --git a/webclient/src/index.tsx b/webclient/src/index.tsx index 57165f173..d90364a30 100644 --- a/webclient/src/index.tsx +++ b/webclient/src/index.tsx @@ -7,27 +7,26 @@ import { createRoot } from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material'; import { ThemeProvider } from '@mui/material/styles'; -import { WebClient } from '@app/websocket'; -import { createWebClientResponse, createWebClientRequest } from '@app/api'; +import { initWebClient } from '@app/api'; import { AppShell } from '@app/containers'; import { materialTheme } from './material-theme'; import './i18n'; import './index.css'; -function initWebClient() { +function useInitWebClient() { const initialized = useRef(false); if (!initialized.current) { initialized.current = true; - new WebClient(createWebClientResponse(), createWebClientRequest()); + initWebClient(); } } const AppWithMaterialTheme = () => { // Instantiate the WebClient singleton before any container renders or any // hook touches WebClient.instance. - initWebClient(); + useInitWebClient(); return ( diff --git a/webclient/src/types/server.ts b/webclient/src/types/server.ts index 6c3fa8006..4ad79e6eb 100644 --- a/webclient/src/types/server.ts +++ b/webclient/src/types/server.ts @@ -1,17 +1,11 @@ +export { StatusEnum } 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, diff --git a/webclient/src/websocket/StatusEnum.ts b/webclient/src/websocket/StatusEnum.ts new file mode 100644 index 000000000..179bdf675 --- /dev/null +++ b/webclient/src/websocket/StatusEnum.ts @@ -0,0 +1,8 @@ +export enum StatusEnum { + DISCONNECTED, + CONNECTING, + CONNECTED, + LOGGING_IN, + LOGGED_IN, + DISCONNECTING = 99 +} diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts index 0c5010ec1..d4e9cf696 100644 --- a/webclient/src/websocket/WebClient.spec.ts +++ b/webclient/src/websocket/WebClient.spec.ts @@ -1,6 +1,7 @@ const captured = vi.hoisted(() => ({ wsOptions: null as WebSocketServiceConfig | null, pbOptions: null as SocketTransport | null, + pbEvents: null as EventRegistries | null, })); vi.mock('./services/WebSocketService', () => ({ @@ -17,8 +18,9 @@ vi.mock('./services/WebSocketService', () => ({ })); vi.mock('./services/ProtobufService', () => ({ - ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { - captured.pbOptions = options; + ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(transport: SocketTransport, events: EventRegistries) { + captured.pbOptions = transport; + captured.pbEvents = events; return { handleMessageEvent: vi.fn(), resetCommands: vi.fn(), @@ -26,29 +28,28 @@ 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 { ping } from './commands/session'; -import { App, Enriched } from '@app/types'; +import { StatusEnum } from './StatusEnum'; import { Subject } from 'rxjs'; import { Mock } from 'vitest'; -import { SocketTransport } from './services/ProtobufService'; +import { SocketTransport, EventRegistries } from './services/ProtobufService'; import { WebSocketServiceConfig } from './services/WebSocketService'; -import type { IWebClientResponse, IWebClientRequest } from './interfaces'; -import { installMockWebSocket } from './__mocks__/helpers'; +import type { IWebClientResponse } from './interfaces'; +import type { WebClientConfig, ConnectTarget } from './WebClientConfig'; +import { installMockWebSocket, useWebClientCleanup } from './__mocks__/helpers'; function makeMockResponse(): IWebClientResponse { return { 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,28 +58,35 @@ function makeMockResponse(): IWebClientResponse { } as unknown as IWebClientResponse; } -function makeMockRequest(): IWebClientRequest { +function makeMockConfig(response: IWebClientResponse): WebClientConfig { return { - authentication: {}, - session: {}, - rooms: {}, - admin: {}, - moderator: {}, - } as unknown as IWebClientRequest; + response, + onServerIdentified: vi.fn(), + sessionEvents: [], + roomEvents: [], + gameEvents: [], + keepAliveFn: vi.fn(), + }; } +useWebClientCleanup(); + describe('WebClient', () => { let client: WebClient; let mockResponse: IWebClientResponse; - let mockRequest: IWebClientRequest; + let mockConfig: WebClientConfig; let messageSubject: Subject; beforeEach(() => { // Reset the singleton so each test starts fresh. + // This direct reset is needed in addition to useWebClientCleanup() because + // this file imports the real WebClient (not a mock), and with isolate:false + // the helper's import may resolve to a different (mocked) module reference. (WebClient as unknown as { _instance: WebClient | null })._instance = null; - (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { - captured.pbOptions = options; + (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport, events: EventRegistries) { + captured.pbOptions = transport; + captured.pbEvents = events; return { handleMessageEvent: vi.fn(), resetCommands: vi.fn(), @@ -98,8 +106,8 @@ describe('WebClient', () => { vi.spyOn(console, 'log').mockImplementation(() => {}); mockResponse = makeMockResponse(); - mockRequest = makeMockRequest(); - client = new WebClient(mockResponse, mockRequest); + mockConfig = makeMockConfig(mockResponse); + client = new WebClient(mockConfig); }); afterEach(() => { @@ -108,9 +116,9 @@ describe('WebClient', () => { }); describe('constructor', () => { - it('stores the response and request on the instance', () => { + it('stores the response and config on the instance', () => { expect(client.response).toBe(mockResponse); - expect(client.request).toBe(mockRequest); + expect(client.config).toBe(mockConfig); }); it('subscribes socket.message$ to protobuf.handleMessageEvent', () => { @@ -128,7 +136,7 @@ describe('WebClient', () => { }); it('throws when instantiated more than once', () => { - expect(() => new WebClient(makeMockResponse(), makeMockRequest())).toThrow(/singleton/); + expect(() => new WebClient(makeMockConfig(makeMockResponse()))).toThrow(/singleton/); }); }); @@ -141,16 +149,15 @@ describe('WebClient', () => { describe('connect', () => { it('calls response.session.connectionAttempted', () => { - const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; - client.connect(opts); + const target: ConnectTarget = { host: 'h', port: '1' }; + client.connect(target); expect(mockResponse.session.connectionAttempted).toHaveBeenCalled(); }); - 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); + it('calls socket.connect with target', () => { + const target: ConnectTarget = { host: 'h', port: '1' }; + client.connect(target); + expect(client.socket.connect).toHaveBeenCalledWith(target); }); }); @@ -172,30 +179,28 @@ describe('WebClient', () => { vi.useRealTimers(); }); - const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; + const target: ConnectTarget = { host: 'h', port: '1' }; it('creates a WebSocket with the correct URL', () => { - client.testConnect(opts); + client.testConnect(target); expect(MockWS).toHaveBeenCalledWith(expect.stringContaining('://h:1')); }); it('calls testConnectionSuccessful and closes on open', () => { - (mockResponse.session as any).testConnectionSuccessful = vi.fn(); - client.testConnect(opts); + client.testConnect(target); wsMockInstance.onopen(); - expect((mockResponse.session as any).testConnectionSuccessful).toHaveBeenCalled(); + expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalled(); expect(wsMockInstance.close).toHaveBeenCalled(); }); it('calls testConnectionFailed on error', () => { - (mockResponse.session as any).testConnectionFailed = vi.fn(); - client.testConnect(opts); + client.testConnect(target); wsMockInstance.onerror(); - expect((mockResponse.session as any).testConnectionFailed).toHaveBeenCalled(); + expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled(); }); it('closes socket after keepalive timeout', () => { - client.testConnect(opts); + client.testConnect(target); vi.advanceTimersByTime(5000); expect(wsMockInstance.close).toHaveBeenCalled(); }); @@ -210,32 +215,43 @@ describe('WebClient', () => { describe('updateStatus', () => { it('sets the status', () => { - client.updateStatus(App.StatusEnum.CONNECTED); - expect(client.status).toBe(App.StatusEnum.CONNECTED); + client.updateStatus(StatusEnum.CONNECTED); + expect(client.status).toBe(StatusEnum.CONNECTED); }); it('calls protobuf.resetCommands on DISCONNECTED', () => { - client.updateStatus(App.StatusEnum.DISCONNECTED); + client.updateStatus(StatusEnum.DISCONNECTED); expect(client.protobuf.resetCommands).toHaveBeenCalled(); }); it('does not reset protobuf when status is not DISCONNECTED', () => { - client.updateStatus(App.StatusEnum.CONNECTED); + client.updateStatus(StatusEnum.CONNECTED); expect(client.protobuf.resetCommands).not.toHaveBeenCalled(); }); }); describe('constructor closures', () => { - it('keepAliveFn calls ping with the callback', () => { + it('keepAliveFn forwards from config to WebSocketService', () => { const cb = vi.fn(); captured.wsOptions!.keepAliveFn(cb); - expect(ping).toHaveBeenCalledWith(cb); + expect(mockConfig.keepAliveFn).toHaveBeenCalledWith(cb); }); it('onStatusChange routes to response.session.updateStatus and updates own status', () => { - captured.wsOptions!.onStatusChange(App.StatusEnum.CONNECTED, 'Connected'); - expect(mockResponse.session.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'Connected'); - expect(client.status).toBe(App.StatusEnum.CONNECTED); + 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(); + }); + + it('passes event registries from config to ProtobufService', () => { + expect(captured.pbEvents!.sessionEvents).toBe(mockConfig.sessionEvents); + expect(captured.pbEvents!.roomEvents).toBe(mockConfig.roomEvents); + expect(captured.pbEvents!.gameEvents).toBe(mockConfig.gameEvents); }); it('send closure delegates to socket.send', () => { diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index 310ffae40..5040e6b69 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -1,10 +1,9 @@ -import { App, Enriched } from '@app/types'; - +import { StatusEnum } from './StatusEnum'; 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'; +import type { IWebClientResponse } from './interfaces'; +import type { WebClientConfig, ConnectTarget } from './WebClientConfig'; export class WebClient { private static _instance: WebClient | null = null; @@ -12,7 +11,7 @@ export class WebClient { public static get instance(): WebClient { if (!WebClient._instance) { throw new Error( - 'WebClient has not been initialized. Instantiate it via `new WebClient(response, request)` before accessing `WebClient.instance`.' + 'WebClient has not been initialized. Instantiate it via `new WebClient(config)` before accessing `WebClient.instance`.' ); } return WebClient._instance; @@ -21,32 +20,40 @@ export class WebClient { public socket: WebSocketService; public protobuf: ProtobufService; public response: IWebClientResponse; - public request: IWebClientRequest; + public config: WebClientConfig; - public options: Enriched.WebSocketConnectOptions | null = null; - public status: App.StatusEnum; + public status: StatusEnum; - constructor(response: IWebClientResponse, request: IWebClientRequest) { + constructor(config: WebClientConfig) { if (WebClient._instance) { throw new Error('WebClient is a singleton and has already been initialized.'); } - this.response = response; - this.request = request; + this.config = config; + this.response = config.response; this.socket = new WebSocketService({ - keepAliveFn: (cb) => ping(cb), - response, + keepAliveFn: config.keepAliveFn, 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), + }, + { + sessionEvents: config.sessionEvents, + roomEvents: config.roomEvents, + gameEvents: config.gameEvents, + }, + ); this.socket.message$.subscribe((message: MessageEvent) => { this.protobuf.handleMessageEvent(message); @@ -57,15 +64,14 @@ export class WebClient { this.response.session.initialized(); } - public connect(options: Enriched.WebSocketConnectOptions) { + public connect(target: ConnectTarget) { this.response.session.connectionAttempted(); - this.options = options; - this.socket.connect(options); + this.socket.connect(target); } - public testConnect(options: Enriched.WebSocketConnectOptions) { + public testConnect(target: ConnectTarget) { const protocol = window.location.hostname === 'localhost' ? 'ws' : 'wss'; - const { host, port } = options; + const { host, port } = target; const socket = new WebSocket(`${protocol}://${host}:${port}`); socket.binaryType = 'arraybuffer'; @@ -88,10 +94,10 @@ export class WebClient { this.socket.disconnect(); } - public updateStatus(status: App.StatusEnum) { + public updateStatus(status: StatusEnum) { this.status = status; - if (status === App.StatusEnum.DISCONNECTED) { + if (status === StatusEnum.DISCONNECTED) { this.protobuf.resetCommands(); } } diff --git a/webclient/src/websocket/WebClientConfig.ts b/webclient/src/websocket/WebClientConfig.ts new file mode 100644 index 000000000..5e562f869 --- /dev/null +++ b/webclient/src/websocket/WebClientConfig.ts @@ -0,0 +1,27 @@ +import type { + RegistryEntry, + SessionEvent, + RoomEvent, + GameEvent, + Event_ServerIdentification, +} from '@app/generated'; + +import type { GameEventMeta } from './types'; +import type { IWebClientResponse } from './interfaces'; + +export interface ConnectTarget { + host: string; + port: string; +} + +export interface WebClientConfig { + response: IWebClientResponse; + + onServerIdentified(info: Event_ServerIdentification): void; + + sessionEvents: RegistryEntry[]; + roomEvents: RegistryEntry[]; + gameEvents: RegistryEntry[]; + + keepAliveFn(pingReceived: () => void): void; +} diff --git a/webclient/src/websocket/__mocks__/helpers.ts b/webclient/src/websocket/__mocks__/helpers.ts index ec05d87d2..c76fd9f17 100644 --- a/webclient/src/websocket/__mocks__/helpers.ts +++ b/webclient/src/websocket/__mocks__/helpers.ts @@ -1,8 +1,27 @@ /** * Shared mock factories for websocket layer unit tests. * Import the helpers you need in each spec file via: - * import { makeMockWebSocket } from '../__mocks__/helpers'; + * import { makeMockWebSocket, useWebClientCleanup } 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 e12d89ccf..7e0bda6cc 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(), - options: {}, + config: { onServerIdentified: vi.fn() }, 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 1641f64ce..7d9562506 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 { Data } from '@app/types'; +import { Command_AdjustMod_ext, Command_AdjustModSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { WebClient.instance.protobuf.sendAdminCommand( - Data.Command_AdjustMod_ext, - create(Data.Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), + Command_AdjustMod_ext, + create(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 88a055f1a..04da773f3 100644 --- a/webclient/src/websocket/commands/admin/adminCommands.spec.ts +++ b/webclient/src/websocket/commands/admin/adminCommands.spec.ts @@ -15,6 +15,7 @@ vi.mock('../../WebClient', () => ({ })); import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { WebClient } from '../../WebClient'; import { adjustMod } from './adjustMod'; import { reloadConfig } from './reloadConfig'; @@ -23,6 +24,8 @@ import { updateServerMessage } from './updateServerMessage'; import { Mock } from 'vitest'; +useWebClientCleanup(); + const { invokeOnSuccess } = makeCallbackHelpers( WebClient.instance.protobuf.sendAdminCommand as Mock, 2 diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts index 25ad85b1f..0856db673 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 { Data } from '@app/types'; +import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; export function reloadConfig(): void { - WebClient.instance.protobuf.sendAdminCommand(Data.Command_ReloadConfig_ext, create(Data.Command_ReloadConfigSchema), { + WebClient.instance.protobuf.sendAdminCommand(Command_ReloadConfig_ext, create(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 69a2e56d7..a341ffa81 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 { Data } from '@app/types'; +import { Command_ShutdownServer_ext, Command_ShutdownServerSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; export function shutdownServer(reason: string, minutes: number): void { WebClient.instance.protobuf.sendAdminCommand( - Data.Command_ShutdownServer_ext, - create(Data.Command_ShutdownServerSchema, { reason, minutes }), + Command_ShutdownServer_ext, + create(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 b56b351b0..7c99ddd5b 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 { Data } from '@app/types'; +import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from '@app/generated'; import { WebClient } from '../../WebClient'; export function updateServerMessage(): void { - WebClient.instance.protobuf.sendAdminCommand(Data.Command_UpdateServerMessage_ext, create(Data.Command_UpdateServerMessageSchema), { + WebClient.instance.protobuf.sendAdminCommand(Command_UpdateServerMessage_ext, create(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 ac9128436..5864a331a 100644 --- a/webclient/src/websocket/commands/game/attachCard.ts +++ b/webclient/src/websocket/commands/game/attachCard.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_AttachCard_ext, Command_AttachCardSchema, type AttachCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function attachCard(gameId: number, params: AttachCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/changeZoneProperties.ts b/webclient/src/websocket/commands/game/changeZoneProperties.ts index ad10f0978..e186bba91 100644 --- a/webclient/src/websocket/commands/game/changeZoneProperties.ts +++ b/webclient/src/websocket/commands/game/changeZoneProperties.ts @@ -1,12 +1,11 @@ import { create } from '@bufbuild/protobuf'; +import { Command_ChangeZoneProperties_ext, Command_ChangeZonePropertiesSchema, type ChangeZonePropertiesParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; - -export function changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void { +export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Data.Command_ChangeZoneProperties_ext, - create(Data.Command_ChangeZonePropertiesSchema, params) + Command_ChangeZoneProperties_ext, + create(Command_ChangeZonePropertiesSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/concede.ts b/webclient/src/websocket/commands/game/concede.ts index fb9634a34..9007e1223 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, Data.Command_Concede_ext, create(Data.Command_ConcedeSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_Concede_ext, create(Command_ConcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/createArrow.ts b/webclient/src/websocket/commands/game/createArrow.ts index 6b727d631..87aaf094c 100644 --- a/webclient/src/websocket/commands/game/createArrow.ts +++ b/webclient/src/websocket/commands/game/createArrow.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_CreateArrow_ext, Command_CreateArrowSchema, type CreateArrowParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function createArrow(gameId: number, params: CreateArrowParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createCounter.ts b/webclient/src/websocket/commands/game/createCounter.ts index 28f897944..886c63b3b 100644 --- a/webclient/src/websocket/commands/game/createCounter.ts +++ b/webclient/src/websocket/commands/game/createCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_CreateCounter_ext, Command_CreateCounterSchema, type CreateCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function createCounter(gameId: number, params: CreateCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createToken.ts b/webclient/src/websocket/commands/game/createToken.ts index 2e8902981..b4c837417 100644 --- a/webclient/src/websocket/commands/game/createToken.ts +++ b/webclient/src/websocket/commands/game/createToken.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_CreateToken_ext, Command_CreateTokenSchema, type CreateTokenParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function createToken(gameId: number, params: CreateTokenParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deckSelect.ts b/webclient/src/websocket/commands/game/deckSelect.ts index ea4c7f455..d2f28a7c0 100644 --- a/webclient/src/websocket/commands/game/deckSelect.ts +++ b/webclient/src/websocket/commands/game/deckSelect.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_DeckSelect_ext, Command_DeckSelectSchema, type DeckSelectParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function deckSelect(gameId: number, params: DeckSelectParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params)); } diff --git a/webclient/src/websocket/commands/game/delCounter.ts b/webclient/src/websocket/commands/game/delCounter.ts index fec44bd68..6b215d123 100644 --- a/webclient/src/websocket/commands/game/delCounter.ts +++ b/webclient/src/websocket/commands/game/delCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_DelCounter_ext, Command_DelCounterSchema, type DelCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function delCounter(gameId: number, params: DelCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deleteArrow.ts b/webclient/src/websocket/commands/game/deleteArrow.ts index 8e8dc9be3..8391277f7 100644 --- a/webclient/src/websocket/commands/game/deleteArrow.ts +++ b/webclient/src/websocket/commands/game/deleteArrow.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_DeleteArrow_ext, Command_DeleteArrowSchema, type DeleteArrowParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function deleteArrow(gameId: number, params: DeleteArrowParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/drawCards.ts b/webclient/src/websocket/commands/game/drawCards.ts index 04309bf63..5c7ed8c0a 100644 --- a/webclient/src/websocket/commands/game/drawCards.ts +++ b/webclient/src/websocket/commands/game/drawCards.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_DrawCards_ext, Command_DrawCardsSchema, type DrawCardsParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function drawCards(gameId: number, params: DrawCardsParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/dumpZone.ts b/webclient/src/websocket/commands/game/dumpZone.ts index c5663a732..62d792d2e 100644 --- a/webclient/src/websocket/commands/game/dumpZone.ts +++ b/webclient/src/websocket/commands/game/dumpZone.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_DumpZone_ext, Command_DumpZoneSchema, type DumpZoneParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function dumpZone(gameId: number, params: DumpZoneParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params)); } diff --git a/webclient/src/websocket/commands/game/flipCard.ts b/webclient/src/websocket/commands/game/flipCard.ts index ee16a938a..b4b894962 100644 --- a/webclient/src/websocket/commands/game/flipCard.ts +++ b/webclient/src/websocket/commands/game/flipCard.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_FlipCard_ext, Command_FlipCardSchema, type FlipCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function flipCard(gameId: number, params: FlipCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts index dee19f57c..deba8e1a2 100644 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ b/webclient/src/websocket/commands/game/gameCommands.spec.ts @@ -8,8 +8,48 @@ vi.mock('../../WebClient', () => ({ })); import { WebClient } from '../../WebClient'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; + +useWebClientCleanup(); import { create, setExtension } from '@bufbuild/protobuf'; -import { Data } from '@app/types'; +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 { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; @@ -52,122 +92,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, Data.Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) + gameId, 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, Data.Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) + gameId, Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) ); }); it('concede sends Command_Concede with empty object', () => { concede(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Concede_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Concede_ext, expect.any(Object)); }); it('createArrow sends Command_CreateArrow', () => { createArrow(gameId, { startPlayerId: 1, startZone: 'hand' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) + gameId, 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, Data.Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) + gameId, 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, Data.Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) + gameId, 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, Data.Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) + gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) ); }); it('delCounter sends Command_DelCounter', () => { delCounter(gameId, { counterId: 3 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) + gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) ); }); it('deleteArrow sends Command_DeleteArrow', () => { deleteArrow(gameId, { arrowId: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) + gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) ); }); it('drawCards sends Command_DrawCards', () => { drawCards(gameId, { number: 3 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_DrawCards_ext, expect.objectContaining({ number: 3 }) + gameId, 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, Data.Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) + gameId, 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, Data.Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) + gameId, 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, Data.Command_GameSay_ext, expect.objectContaining({ message: 'hello' }) + gameId, 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, Data.Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, 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, Data.Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) + gameId, 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, Data.Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) + gameId, Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) ); }); it('leaveGame sends Command_LeaveGame with empty object', () => { leaveGame(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_LeaveGame_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_LeaveGame_ext, expect.any(Object)); }); it('moveCard sends Command_MoveCard', () => { moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_MoveCard_ext, + gameId, Command_MoveCard_ext, expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' }) ); }); @@ -175,45 +215,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, Data.Command_Mulligan_ext, expect.objectContaining({ number: 7 }) + gameId, Command_Mulligan_ext, expect.objectContaining({ number: 7 }) ); }); it('nextTurn sends Command_NextTurn with empty object', () => { nextTurn(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_NextTurn_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_NextTurn_ext, expect.any(Object)); }); it('readyStart sends Command_ReadyStart', () => { readyStart(gameId, { ready: true }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_ReadyStart_ext, expect.objectContaining({ ready: true }) + gameId, 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, Data.Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) + gameId, 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, Data.Command_ReverseTurn_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReverseTurn_ext, expect.any(Object)); }); it('setActivePhase sends Command_SetActivePhase', () => { setActivePhase(gameId, { phase: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) + gameId, 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, Data.Command_SetCardAttr_ext, + gameId, Command_SetCardAttr_ext, expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' }) ); }); @@ -221,63 +261,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, Data.Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, 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, Data.Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) + gameId, 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, Data.Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) + gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) ); }); it('setSideboardPlan sends Command_SetSideboardPlan', () => { setSideboardPlan(gameId, { moveList: [] }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) + gameId, 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, Data.Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) + gameId, Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) ); }); it('undoDraw sends Command_UndoDraw with empty object', () => { undoDraw(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_UndoDraw_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_UndoDraw_ext, expect.any(Object)); }); it('unconcede sends Command_Unconcede with empty object', () => { unconcede(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Unconcede_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object)); }); it('rollDie sends Command_RollDie', () => { rollDie(gameId, { sides: 6, count: 2 }); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Data.Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 }) + gameId, 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(Data.GameCommandSchema); - setExtension(innerCmd, Data.Command_DrawCards_ext, create(Data.Command_DrawCardsSchema, { number: 2 })); + const innerCmd = create(GameCommandSchema); + setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 })); judge(gameId, targetId, innerCmd); expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( gameId, - Data.Command_Judge_ext, + 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 371a3e19d..7ae7263e9 100644 --- a/webclient/src/websocket/commands/game/gameSay.ts +++ b/webclient/src/websocket/commands/game/gameSay.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_GameSay_ext, Command_GameSaySchema, type GameSayParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function gameSay(gameId: number, params: GameSayParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCardCounter.ts b/webclient/src/websocket/commands/game/incCardCounter.ts index 7374c8ab9..54349d920 100644 --- a/webclient/src/websocket/commands/game/incCardCounter.ts +++ b/webclient/src/websocket/commands/game/incCardCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_IncCardCounter_ext, Command_IncCardCounterSchema, type IncCardCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function incCardCounter(gameId: number, params: IncCardCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCounter.ts b/webclient/src/websocket/commands/game/incCounter.ts index 038dbfb42..4ca0b8558 100644 --- a/webclient/src/websocket/commands/game/incCounter.ts +++ b/webclient/src/websocket/commands/game/incCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_IncCounter_ext, Command_IncCounterSchema, type IncCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function incCounter(gameId: number, params: IncCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/judge.ts b/webclient/src/websocket/commands/game/judge.ts index 274945428..c61a1092d 100644 --- a/webclient/src/websocket/commands/game/judge.ts +++ b/webclient/src/websocket/commands/game/judge.ts @@ -1,11 +1,10 @@ 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: Data.GameCommand): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Data.Command_Judge_ext, create(Data.Command_JudgeSchema, { +export function judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_Judge_ext, create(Command_JudgeSchema, { targetId, gameCommand: [innerGameCommand], })); } - diff --git a/webclient/src/websocket/commands/game/kickFromGame.ts b/webclient/src/websocket/commands/game/kickFromGame.ts index 3f55e9078..c7b724b88 100644 --- a/webclient/src/websocket/commands/game/kickFromGame.ts +++ b/webclient/src/websocket/commands/game/kickFromGame.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_KickFromGame_ext, Command_KickFromGameSchema, type KickFromGameParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function kickFromGame(gameId: number, params: KickFromGameParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params)); } diff --git a/webclient/src/websocket/commands/game/leaveGame.ts b/webclient/src/websocket/commands/game/leaveGame.ts index ad8b40634..05d160059 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, Data.Command_LeaveGame_ext, create(Data.Command_LeaveGameSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_LeaveGame_ext, create(Command_LeaveGameSchema)); } diff --git a/webclient/src/websocket/commands/game/moveCard.ts b/webclient/src/websocket/commands/game/moveCard.ts index 16390d3a5..9b5dc52b5 100644 --- a/webclient/src/websocket/commands/game/moveCard.ts +++ b/webclient/src/websocket/commands/game/moveCard.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_MoveCard_ext, Command_MoveCardSchema, type MoveCardParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function moveCard(gameId: number, params: MoveCardParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/mulligan.ts b/webclient/src/websocket/commands/game/mulligan.ts index 5d690b754..b6ac92cb9 100644 --- a/webclient/src/websocket/commands/game/mulligan.ts +++ b/webclient/src/websocket/commands/game/mulligan.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_Mulligan_ext, Command_MulliganSchema, type MulliganParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function mulligan(gameId: number, params: MulliganParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params)); } diff --git a/webclient/src/websocket/commands/game/nextTurn.ts b/webclient/src/websocket/commands/game/nextTurn.ts index 08bf084d8..4633e9233 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, Data.Command_NextTurn_ext, create(Data.Command_NextTurnSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_NextTurn_ext, create(Command_NextTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/readyStart.ts b/webclient/src/websocket/commands/game/readyStart.ts index f6f32c401..29c65c2a8 100644 --- a/webclient/src/websocket/commands/game/readyStart.ts +++ b/webclient/src/websocket/commands/game/readyStart.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_ReadyStart_ext, Command_ReadyStartSchema, type ReadyStartParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function readyStart(gameId: number, params: ReadyStartParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params)); } diff --git a/webclient/src/websocket/commands/game/revealCards.ts b/webclient/src/websocket/commands/game/revealCards.ts index 774074378..e4a1cf715 100644 --- a/webclient/src/websocket/commands/game/revealCards.ts +++ b/webclient/src/websocket/commands/game/revealCards.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_RevealCards_ext, Command_RevealCardsSchema, type RevealCardsParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function revealCards(gameId: number, params: RevealCardsParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/reverseTurn.ts b/webclient/src/websocket/commands/game/reverseTurn.ts index 5b03e8e53..7ce90a669 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, Data.Command_ReverseTurn_ext, create(Data.Command_ReverseTurnSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReverseTurn_ext, create(Command_ReverseTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/rollDie.ts b/webclient/src/websocket/commands/game/rollDie.ts index c568e2de4..e76bc2445 100644 --- a/webclient/src/websocket/commands/game/rollDie.ts +++ b/webclient/src/websocket/commands/game/rollDie.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_RollDie_ext, Command_RollDieSchema, type RollDieParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function rollDie(gameId: number, params: RollDieParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_RollDie_ext, create(Command_RollDieSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setActivePhase.ts b/webclient/src/websocket/commands/game/setActivePhase.ts index 8987ff9d2..c56d42d37 100644 --- a/webclient/src/websocket/commands/game/setActivePhase.ts +++ b/webclient/src/websocket/commands/game/setActivePhase.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetActivePhase_ext, Command_SetActivePhaseSchema, type SetActivePhaseParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function setActivePhase(gameId: number, params: SetActivePhaseParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardAttr.ts b/webclient/src/websocket/commands/game/setCardAttr.ts index 4808fa4e1..3f62f82a6 100644 --- a/webclient/src/websocket/commands/game/setCardAttr.ts +++ b/webclient/src/websocket/commands/game/setCardAttr.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetCardAttr_ext, Command_SetCardAttrSchema, type SetCardAttrParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function setCardAttr(gameId: number, params: SetCardAttrParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardCounter.ts b/webclient/src/websocket/commands/game/setCardCounter.ts index 47ebc6f60..e70631ed3 100644 --- a/webclient/src/websocket/commands/game/setCardCounter.ts +++ b/webclient/src/websocket/commands/game/setCardCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetCardCounter_ext, Command_SetCardCounterSchema, type SetCardCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function setCardCounter(gameId: number, params: SetCardCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCounter.ts b/webclient/src/websocket/commands/game/setCounter.ts index 8dc163e93..290c1b515 100644 --- a/webclient/src/websocket/commands/game/setCounter.ts +++ b/webclient/src/websocket/commands/game/setCounter.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetCounter_ext, Command_SetCounterSchema, type SetCounterParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function setCounter(gameId: number, params: SetCounterParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardLock.ts b/webclient/src/websocket/commands/game/setSideboardLock.ts index 3a5ea21ab..cc7844d36 100644 --- a/webclient/src/websocket/commands/game/setSideboardLock.ts +++ b/webclient/src/websocket/commands/game/setSideboardLock.ts @@ -1,12 +1,11 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetSideboardLock_ext, Command_SetSideboardLockSchema, type SetSideboardLockParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; - -export function setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void { +export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Data.Command_SetSideboardLock_ext, - create(Data.Command_SetSideboardLockSchema, params) + Command_SetSideboardLock_ext, + create(Command_SetSideboardLockSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/setSideboardPlan.ts b/webclient/src/websocket/commands/game/setSideboardPlan.ts index 9472a0df6..8caa495c2 100644 --- a/webclient/src/websocket/commands/game/setSideboardPlan.ts +++ b/webclient/src/websocket/commands/game/setSideboardPlan.ts @@ -1,12 +1,11 @@ import { create } from '@bufbuild/protobuf'; +import { Command_SetSideboardPlan_ext, Command_SetSideboardPlanSchema, type SetSideboardPlanParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; - -export function setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void { +export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void { WebClient.instance.protobuf.sendGameCommand( gameId, - Data.Command_SetSideboardPlan_ext, - create(Data.Command_SetSideboardPlanSchema, params) + Command_SetSideboardPlan_ext, + create(Command_SetSideboardPlanSchema, params) ); } diff --git a/webclient/src/websocket/commands/game/shuffle.ts b/webclient/src/websocket/commands/game/shuffle.ts index 688bd6bb0..9e9a6772c 100644 --- a/webclient/src/websocket/commands/game/shuffle.ts +++ b/webclient/src/websocket/commands/game/shuffle.ts @@ -1,8 +1,7 @@ import { create } from '@bufbuild/protobuf'; +import { Command_Shuffle_ext, Command_ShuffleSchema, type ShuffleParams } from '@app/generated'; import { WebClient } from '../../WebClient'; -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)); +export function shuffle(gameId: number, params: ShuffleParams): void { + WebClient.instance.protobuf.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params)); } diff --git a/webclient/src/websocket/commands/game/unconcede.ts b/webclient/src/websocket/commands/game/unconcede.ts index 5569e76ed..505b5991c 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, Data.Command_Unconcede_ext, create(Data.Command_UnconcedeSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_Unconcede_ext, create(Command_UnconcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/undoDraw.ts b/webclient/src/websocket/commands/game/undoDraw.ts index 0f0677352..b11e76d29 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, Data.Command_UndoDraw_ext, create(Data.Command_UndoDrawSchema)); + WebClient.instance.protobuf.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema)); } diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts index 7bb0f9989..d2c2bae64 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 { Data } from '@app/types'; +import { Command_BanFromServer_ext, Command_BanFromServerSchema } from '@app/generated'; export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, visibleReason?: string, clientid?: string, removeMessages?: number): void { - WebClient.instance.protobuf.sendModeratorCommand(Data.Command_BanFromServer_ext, create(Data.Command_BanFromServerSchema, { + WebClient.instance.protobuf.sendModeratorCommand(Command_BanFromServer_ext, create(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 53ff315ba..6b7625809 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 { Data } from '@app/types'; +import { Command_ForceActivateUser_ext, Command_ForceActivateUserSchema } from '@app/generated'; export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { - const cmd = create(Data.Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); - WebClient.instance.protobuf.sendModeratorCommand(Data.Command_ForceActivateUser_ext, cmd, { + const cmd = create(Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); + WebClient.instance.protobuf.sendModeratorCommand(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 734a9d041..6339b8b77 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 { Data } from '@app/types'; +import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema, Response_GetAdminNotes_ext } from '@app/generated'; export function getAdminNotes(userName: string): void { - WebClient.instance.protobuf.sendModeratorCommand(Data.Command_GetAdminNotes_ext, create(Data.Command_GetAdminNotesSchema, { userName }), { - responseExt: Data.Response_GetAdminNotes_ext, + WebClient.instance.protobuf.sendModeratorCommand(Command_GetAdminNotes_ext, create(Command_GetAdminNotesSchema, { userName }), { + responseExt: 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 c22e0d624..ecc48fb26 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 { Data } from '@app/types'; +import { Command_GetBanHistory_ext, Command_GetBanHistorySchema, Response_BanHistory_ext } from '@app/generated'; export function getBanHistory(userName: string): void { - WebClient.instance.protobuf.sendModeratorCommand(Data.Command_GetBanHistory_ext, create(Data.Command_GetBanHistorySchema, { userName }), { - responseExt: Data.Response_BanHistory_ext, + WebClient.instance.protobuf.sendModeratorCommand(Command_GetBanHistory_ext, create(Command_GetBanHistorySchema, { userName }), { + responseExt: 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 0aa4f6646..9f27cf2c6 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 { Data } from '@app/types'; +import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema, Response_WarnHistory_ext } from '@app/generated'; export function getWarnHistory(userName: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Data.Command_GetWarnHistory_ext, - create(Data.Command_GetWarnHistorySchema, { userName }), + Command_GetWarnHistory_ext, + create(Command_GetWarnHistorySchema, { userName }), { - responseExt: Data.Response_WarnHistory_ext, + responseExt: 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 9a734727c..262cba2df 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 { Data } from '@app/types'; +import { Command_GetWarnList_ext, Command_GetWarnListSchema, Response_WarnList_ext } from '@app/generated'; export function getWarnList(modName: string, userName: string, userClientid: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Data.Command_GetWarnList_ext, - create(Data.Command_GetWarnListSchema, { modName, userName, userClientid }), + Command_GetWarnList_ext, + create(Command_GetWarnListSchema, { modName, userName, userClientid }), { - responseExt: Data.Response_WarnList_ext, + responseExt: 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 47def64d0..0b288efc7 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 { Data } from '@app/types'; +import { Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema } from '@app/generated'; export function grantReplayAccess(replayId: number, moderatorName: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Data.Command_GrantReplayAccess_ext, - create(Data.Command_GrantReplayAccessSchema, { replayId, moderatorName }), + Command_GrantReplayAccess_ext, + create(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 dea409034..6af468c28 100644 --- a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts +++ b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts @@ -21,8 +21,26 @@ vi.mock('../../WebClient', () => ({ })); import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; +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 { banFromServer } from './banFromServer'; import { forceActivateUser } from './forceActivateUser'; @@ -37,6 +55,8 @@ import { warnUser } from './warnUser'; import { create } from '@bufbuild/protobuf'; import { Mock } from 'vitest'; +useWebClientCleanup(); + const { invokeOnSuccess } = makeCallbackHelpers( WebClient.instance.protobuf.sendModeratorCommand as Mock, 2 @@ -50,7 +70,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( - Data.Command_BanFromServer_ext, + Command_BanFromServer_ext, expect.objectContaining({ minutes: 30, userName: 'alice' }), expect.any(Object) ); @@ -71,7 +91,7 @@ describe('forceActivateUser', () => { it('calls sendModeratorCommand with Command_ForceActivateUser', () => { forceActivateUser('alice', 'mod1'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) + Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) ); }); @@ -90,9 +110,9 @@ describe('getAdminNotes', () => { it('calls sendModeratorCommand with Command_GetAdminNotes', () => { getAdminNotes('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_GetAdminNotes_ext, + Command_GetAdminNotes_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_GetAdminNotes_ext }) + expect.objectContaining({ responseExt: Response_GetAdminNotes_ext }) ); }); @@ -112,9 +132,9 @@ describe('getBanHistory', () => { it('calls sendModeratorCommand with Command_GetBanHistory', () => { getBanHistory('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_GetBanHistory_ext, + Command_GetBanHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_BanHistory_ext }) + expect.objectContaining({ responseExt: Response_BanHistory_ext }) ); }); @@ -134,9 +154,9 @@ describe('getWarnHistory', () => { it('calls sendModeratorCommand with Command_GetWarnHistory', () => { getWarnHistory('alice'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_GetWarnHistory_ext, + Command_GetWarnHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_WarnHistory_ext }) + expect.objectContaining({ responseExt: Response_WarnHistory_ext }) ); }); @@ -156,9 +176,9 @@ describe('getWarnList', () => { it('calls sendModeratorCommand with Command_GetWarnList', () => { getWarnList('mod1', 'alice', 'US'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_GetWarnList_ext, + Command_GetWarnList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_WarnList_ext }) + expect.objectContaining({ responseExt: Response_WarnList_ext }) ); }); @@ -178,7 +198,7 @@ describe('grantReplayAccess', () => { it('calls sendModeratorCommand with Command_GrantReplayAccess', () => { grantReplayAccess(10, 'mod1'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) + Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) ); }); @@ -197,7 +217,7 @@ describe('updateAdminNotes', () => { it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => { updateAdminNotes('alice', 'new notes'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) + Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) ); }); @@ -214,17 +234,17 @@ describe('updateAdminNotes', () => { describe('viewLogHistory', () => { it('calls sendModeratorCommand with Command_ViewLogHistory', () => { - const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); + const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); viewLogHistory(filters); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_ViewLogHistory_ext, + Command_ViewLogHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_ViewLogHistory_ext }) + expect.objectContaining({ responseExt: Response_ViewLogHistory_ext }) ); }); it('onSuccess calls response.moderator.viewLogs with logMessage', () => { - const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); + const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); viewLogHistory(filters); const resp = { logMessage: ['log1'] }; invokeOnSuccess(resp, { responseCode: 0 }); @@ -240,7 +260,7 @@ describe('warnUser', () => { it('calls sendModeratorCommand with Command_WarnUser', () => { warnUser('alice', 'bad behavior', 'cid'); expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Data.Command_WarnUser_ext, expect.any(Object), expect.any(Object) + 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 dba02650d..7d760938d 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 { Data } from '@app/types'; +import { Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema } from '@app/generated'; export function updateAdminNotes(userName: string, notes: string): void { WebClient.instance.protobuf.sendModeratorCommand( - Data.Command_UpdateAdminNotes_ext, - create(Data.Command_UpdateAdminNotesSchema, { userName, notes }), + Command_UpdateAdminNotes_ext, + create(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 956af083e..e222899f0 100644 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; +import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema, Response_ViewLogHistory_ext } from '@app/generated'; +import type { ViewLogHistoryParams } from '@app/generated'; -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, +export function viewLogHistory(filters: ViewLogHistoryParams): void { + WebClient.instance.protobuf.sendModeratorCommand(Command_ViewLogHistory_ext, create(Command_ViewLogHistorySchema, filters), { + responseExt: 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 13a521e59..22d25ac75 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 { Data } from '@app/types'; +import { Command_WarnUser_ext, Command_WarnUserSchema } from '@app/generated'; export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - const cmd = create(Data.Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); - WebClient.instance.protobuf.sendModeratorCommand(Data.Command_WarnUser_ext, cmd, { + const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); + WebClient.instance.protobuf.sendModeratorCommand(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 045cb0175..133e8a396 100644 --- a/webclient/src/websocket/commands/room/createGame.ts +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; +import { Command_CreateGame_ext, Command_CreateGameSchema } from '@app/generated'; +import type { CreateGameParams } from '@app/generated'; -export function createGame(roomId: number, gameConfig: Data.CreateGameParams): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_CreateGame_ext, create(Data.Command_CreateGameSchema, gameConfig), { +export function createGame(roomId: number, gameConfig: CreateGameParams): void { + WebClient.instance.protobuf.sendRoomCommand(roomId, Command_CreateGame_ext, create(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 dbaa6ea95..a19721746 100644 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; +import { Command_JoinGame_ext, Command_JoinGameSchema } from '@app/generated'; +import type { JoinGameParams } from '@app/generated'; -export function joinGame(roomId: number, joinGameParams: Data.JoinGameParams): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_JoinGame_ext, create(Data.Command_JoinGameSchema, joinGameParams), { +export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { + WebClient.instance.protobuf.sendRoomCommand(roomId, Command_JoinGame_ext, create(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 8f61ee652..004e704c0 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 { Data } from '@app/types'; +import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from '@app/generated'; export function leaveRoom(roomId: number): void { - WebClient.instance.protobuf.sendRoomCommand(roomId, Data.Command_LeaveRoom_ext, create(Data.Command_LeaveRoomSchema), { + WebClient.instance.protobuf.sendRoomCommand(roomId, Command_LeaveRoom_ext, create(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 5c432f9d6..833b1deff 100644 --- a/webclient/src/websocket/commands/room/roomCommands.spec.ts +++ b/webclient/src/websocket/commands/room/roomCommands.spec.ts @@ -14,8 +14,16 @@ vi.mock('../../WebClient', () => ({ })); import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { WebClient } from '../../WebClient'; -import { Data } from '@app/types'; +import { + Command_CreateGame_ext, + Command_CreateGameSchema, + Command_JoinGame_ext, + Command_JoinGameSchema, + Command_LeaveRoom_ext, + Command_RoomSay_ext, +} from '@app/generated'; import { createGame } from './createGame'; import { joinGame } from './joinGame'; @@ -24,6 +32,8 @@ import { roomSay } from './roomSay'; import { create } from '@bufbuild/protobuf'; import { Mock } from 'vitest'; +useWebClientCleanup(); + const { invokeOnSuccess } = makeCallbackHelpers( WebClient.instance.protobuf.sendRoomCommand as Mock, // sendRoomCommand(roomId, ext, value, options) — options at index 3 @@ -36,14 +46,14 @@ const { invokeOnSuccess } = makeCallbackHelpers( describe('createGame', () => { it('calls sendRoomCommand with Command_CreateGame', () => { - createGame(5, create(Data.Command_CreateGameSchema, { maxPlayers: 4 })); + createGame(5, create(Command_CreateGameSchema, { maxPlayers: 4 })); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 5, Data.Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) + 5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) ); }); it('onSuccess calls response.room.gameCreated with roomId', () => { - createGame(5, create(Data.Command_CreateGameSchema, {})); + createGame(5, create(Command_CreateGameSchema, {})); invokeOnSuccess(); expect(WebClient.instance.response.room.gameCreated).toHaveBeenCalledWith(5); }); @@ -55,14 +65,14 @@ describe('createGame', () => { describe('joinGame', () => { it('calls sendRoomCommand with Command_JoinGame', () => { - joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42, password: '' })); + joinGame(7, create(Command_JoinGameSchema, { gameId: 42, password: '' })); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 7, Data.Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) + 7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) ); }); it('onSuccess calls response.room.joinedGame with roomId and gameId', () => { - joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42 })); + joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); invokeOnSuccess(); expect(WebClient.instance.response.room.joinedGame).toHaveBeenCalledWith(7, 42); }); @@ -76,7 +86,7 @@ describe('leaveRoom', () => { it('calls sendRoomCommand with Command_LeaveRoom', () => { leaveRoom(3); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 3, Data.Command_LeaveRoom_ext, expect.any(Object), expect.any(Object) + 3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object) ); }); @@ -96,7 +106,7 @@ describe('roomSay', () => { roomSay(2, ' hello '); expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( 2, - Data.Command_RoomSay_ext, + 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 ac5f964d2..2b3618420 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 { Data } from '@app/types'; +import { Command_RoomSay_ext, Command_RoomSaySchema } from '@app/generated'; 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, Data.Command_RoomSay_ext, create(Data.Command_RoomSaySchema, { message: trimmed })); + WebClient.instance.protobuf.sendRoomCommand(roomId, Command_RoomSay_ext, create(Command_RoomSaySchema, { message: trimmed })); } diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts index 60b361c57..be44b19e8 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 { Data } from '@app/types'; +import { Command_AccountEdit_ext, Command_AccountEditSchema } from '@app/generated'; export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - const cmd = create(Data.Command_AccountEditSchema, { passwordCheck, realName, email, country }); - WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountEdit_ext, cmd, { + const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country }); + WebClient.instance.protobuf.sendSessionCommand(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 b14181ff2..47df13c92 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 { Data } from '@app/types'; +import { Command_AccountImage_ext, Command_AccountImageSchema } from '@app/generated'; export function accountImage(image: Uint8Array): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountImage_ext, create(Data.Command_AccountImageSchema, { image }), { + WebClient.instance.protobuf.sendSessionCommand(Command_AccountImage_ext, create(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 2f5476461..236c2e162 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 { Data } from '@app/types'; +import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from '@app/generated'; export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - const cmd = create(Data.Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); - WebClient.instance.protobuf.sendSessionCommand(Data.Command_AccountPassword_ext, cmd, { + const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); + WebClient.instance.protobuf.sendSessionCommand(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 b9dffea40..5972cfcb2 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -1,32 +1,37 @@ -import { App, Enriched, Data } from '@app/types'; - import { create } from '@bufbuild/protobuf'; +import { + Command_Activate_ext, + Command_ActivateSchema, + Response_ResponseCode, + type ActivateParams, +} from '@app/generated'; + +import { StatusEnum } from '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { disconnect, login, updateStatus } from './'; -export function activate(options: Omit, password?: string, passwordSalt?: string): void { +export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void { const { userName, token } = options; - WebClient.instance.protobuf.sendSessionCommand(Data.Command_Activate_ext, create(Data.Command_ActivateSchema, { + WebClient.instance.protobuf.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, { ...CLIENT_CONFIG, userName, token, }), { onResponseCode: { - [Data.Response_ResponseCode.RespActivationAccepted]: () => { + [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(App.StatusEnum.DISCONNECTED, 'Account Activation Failed'); + updateStatus(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 d7d36db57..76836cb38 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 { Data } from '@app/types'; +import { Command_AddToList_ext, Command_AddToListSchema } from '@app/generated'; 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(Data.Command_AddToList_ext, create(Data.Command_AddToListSchema, { list, userName }), { + WebClient.instance.protobuf.sendSessionCommand(Command_AddToList_ext, create(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 df494cbbb..f9221a6a0 100644 --- a/webclient/src/websocket/commands/session/connect.ts +++ b/webclient/src/websocket/commands/session/connect.ts @@ -1,25 +1,10 @@ -import { App, Enriched } from '@app/types'; import { WebClient } from '../../WebClient'; -import { updateStatus } from './'; +import type { ConnectTarget } from '../../WebClientConfig'; -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; - } - } +export function connect(target: ConnectTarget): void { + WebClient.instance.connect(target); +} + +export function testConnect(target: ConnectTarget): void { + WebClient.instance.testConnect(target); } diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts index bff1bf859..e5cc88483 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 { Data } from '@app/types'; +import { Command_DeckDel_ext, Command_DeckDelSchema } from '@app/generated'; export function deckDel(deckId: number): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckDel_ext, create(Data.Command_DeckDelSchema, { deckId }), { + WebClient.instance.protobuf.sendSessionCommand(Command_DeckDel_ext, create(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 310f18907..740bbd02b 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 { Data } from '@app/types'; +import { Command_DeckDelDir_ext, Command_DeckDelDirSchema } from '@app/generated'; export function deckDelDir(path: string): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckDelDir_ext, create(Data.Command_DeckDelDirSchema, { path }), { + WebClient.instance.protobuf.sendSessionCommand(Command_DeckDelDir_ext, create(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 baffbf262..566e56272 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 { Data } from '@app/types'; +import { Command_DeckDownload_ext, Command_DeckDownloadSchema, Response_DeckDownload_ext } from '@app/generated'; export function deckDownload(deckId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_DeckDownload_ext, - create(Data.Command_DeckDownloadSchema, { deckId }), + Command_DeckDownload_ext, + create(Command_DeckDownloadSchema, { deckId }), { - responseExt: Data.Response_DeckDownload_ext, + responseExt: 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 b9afc6440..e0940010b 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 { Data } from '@app/types'; +import { Command_DeckList_ext, Command_DeckListSchema, Response_DeckList_ext } from '@app/generated'; export function deckList(): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckList_ext, create(Data.Command_DeckListSchema), { - responseExt: Data.Response_DeckList_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_DeckList_ext, create(Command_DeckListSchema), { + responseExt: 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 0786936da..dc27fb0ca 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 { Data } from '@app/types'; +import { Command_DeckNewDir_ext, Command_DeckNewDirSchema } from '@app/generated'; export function deckNewDir(path: string, dirName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_DeckNewDir_ext, create(Data.Command_DeckNewDirSchema, { path, dirName }), { + WebClient.instance.protobuf.sendSessionCommand(Command_DeckNewDir_ext, create(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 7f0c0de67..8b4c647ba 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 { Data } from '@app/types'; +import { Command_DeckUpload_ext, Command_DeckUploadSchema, Response_DeckUpload_ext } from '@app/generated'; export function deckUpload(path: string, deckId: number, deckList: string): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_DeckUpload_ext, - create(Data.Command_DeckUploadSchema, { path, deckId, deckList }), + Command_DeckUpload_ext, + create(Command_DeckUploadSchema, { path, deckId, deckList }), { - responseExt: Data.Response_DeckUpload_ext, + responseExt: 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 fb4e7d468..f5ca0212b 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -1,29 +1,34 @@ -import { App, Enriched, Data } from '@app/types'; - import { create } from '@bufbuild/protobuf'; +import { + Command_ForgotPasswordChallenge_ext, + Command_ForgotPasswordChallengeSchema, + type ForgotPasswordChallengeParams, +} from '@app/generated'; + +import { StatusEnum } from '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { disconnect, updateStatus } from './'; -export function forgotPasswordChallenge(options: Enriched.PasswordResetChallengeConnectOptions): void { +export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void { const { userName, email } = options; WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ForgotPasswordChallenge_ext, - create(Data.Command_ForgotPasswordChallengeSchema, { + Command_ForgotPasswordChallenge_ext, + create(Command_ForgotPasswordChallengeSchema, { ...CLIENT_CONFIG, userName, email, }), { onSuccess: () => { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPassword(); disconnect(); }, onError: () => { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(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 ddc268c5a..e6255104f 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -1,31 +1,37 @@ -import { App, Enriched, Data } from '@app/types'; - import { create } from '@bufbuild/protobuf'; +import { + Command_ForgotPasswordRequest_ext, + Command_ForgotPasswordRequestSchema, + Response_ForgotPasswordRequest_ext, + type ForgotPasswordRequestParams, +} from '@app/generated'; + +import { StatusEnum } from '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { disconnect, updateStatus } from './'; -export function forgotPasswordRequest(options: Enriched.PasswordResetRequestConnectOptions): void { +export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void { const { userName } = options; - WebClient.instance.protobuf.sendSessionCommand(Data.Command_ForgotPasswordRequest_ext, create(Data.Command_ForgotPasswordRequestSchema, { + WebClient.instance.protobuf.sendSessionCommand(Command_ForgotPasswordRequest_ext, create(Command_ForgotPasswordRequestSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Data.Response_ForgotPasswordRequest_ext, + responseExt: Response_ForgotPasswordRequest_ext, onSuccess: (resp) => { if (resp?.challengeEmail) { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordChallenge(); } else { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPassword(); } disconnect(); }, onError: () => { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(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 62b3ba84f..eaa2cbfea 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -1,22 +1,26 @@ -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 '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { hashPassword } from '../../utils'; - import { disconnect, updateStatus } from '.'; export function forgotPasswordReset( - options: Omit, + options: ConnectTarget & ForgotPasswordResetParams, newPassword?: string, passwordSalt?: string ): void { const { userName, token } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, token, @@ -26,16 +30,16 @@ export function forgotPasswordReset( }; WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ForgotPasswordReset_ext, - create(Data.Command_ForgotPasswordResetSchema, params), + Command_ForgotPasswordReset_ext, + create(Command_ForgotPasswordResetSchema, params), { onSuccess: () => { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(StatusEnum.DISCONNECTED, null); WebClient.instance.response.session.resetPasswordSuccess(); disconnect(); }, onError: () => { - updateStatus(App.StatusEnum.DISCONNECTED, null); + updateStatus(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 23ec2f784..91cb2c3ec 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 { Data } from '@app/types'; +import { Command_GetGamesOfUser_ext, Command_GetGamesOfUserSchema, Response_GetGamesOfUser_ext } from '@app/generated'; export function getGamesOfUser(userName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_GetGamesOfUser_ext, create(Data.Command_GetGamesOfUserSchema, { userName }), { - responseExt: Data.Response_GetGamesOfUser_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_GetGamesOfUser_ext, create(Command_GetGamesOfUserSchema, { userName }), { + responseExt: 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 f7033d451..ff266670f 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 { Data } from '@app/types'; +import { Command_GetUserInfo_ext, Command_GetUserInfoSchema, Response_GetUserInfo_ext } from '@app/generated'; export function getUserInfo(userName: string): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_GetUserInfo_ext, create(Data.Command_GetUserInfoSchema, { userName }), { - responseExt: Data.Response_GetUserInfo_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_GetUserInfo_ext, create(Command_GetUserInfoSchema, { userName }), { + responseExt: 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 9c67d818d..494819208 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 { Data } from '@app/types'; +import { Command_JoinRoom_ext, Command_JoinRoomSchema, Response_JoinRoom_ext } from '@app/generated'; export function joinRoom(roomId: number): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_JoinRoom_ext, create(Data.Command_JoinRoomSchema, { roomId }), { - responseExt: Data.Response_JoinRoom_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_JoinRoom_ext, create(Command_JoinRoomSchema, { roomId }), { + responseExt: 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 a54d12ad1..25b0837f3 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 { Data } from '@app/types'; +import { Command_ListRooms_ext, Command_ListRoomsSchema } from '@app/generated'; export function listRooms(): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_ListRooms_ext, create(Data.Command_ListRoomsSchema)); + WebClient.instance.protobuf.sendSessionCommand(Command_ListRooms_ext, create(Command_ListRoomsSchema)); } diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts index 7e867754c..9978cca48 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 { Data } from '@app/types'; +import { Command_ListUsers_ext, Command_ListUsersSchema, Response_ListUsers_ext } from '@app/generated'; export function listUsers(): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_ListUsers_ext, create(Data.Command_ListUsersSchema), { - responseExt: Data.Response_ListUsers_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_ListUsers_ext, create(Command_ListUsersSchema), { + responseExt: 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 e2e759612..b3e43b144 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -1,9 +1,17 @@ -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 '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { hashPassword } from '../../utils'; import { disconnect, @@ -12,7 +20,7 @@ import { updateStatus, } from './'; -export function login(options: Omit, password?: string, passwordSalt?: string): void { +export function login(options: ConnectTarget & LoginParams, password?: string, passwordSalt?: string): void { const { userName, hashedPassword } = options; const loginConfig = { @@ -22,51 +30,52 @@ export function login(options: Omit, p ...(passwordSalt ? { hashedPassword: hashedPassword || hashPassword(passwordSalt, password) } : { password }), - } satisfies MessageInitShape; + } satisfies MessageInitShape; const onLoginError = (message: string, extra?: () => void) => { - updateStatus(App.StatusEnum.DISCONNECTED, message); + updateStatus(StatusEnum.DISCONNECTED, message); extra?.(); WebClient.instance.response.session.loginFailed(); disconnect(); }; - WebClient.instance.protobuf.sendSessionCommand(Data.Command_Login_ext, create(Data.Command_LoginSchema, loginConfig), { - responseExt: Data.Response_Login_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_Login_ext, create(Command_LoginSchema, loginConfig), { + responseExt: 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({ hashedPassword: loginConfig.hashedPassword }); + WebClient.instance.response.session.loginSuccessful({ ...resp, hashedPassword: loginConfig.hashedPassword }); listUsers(); listRooms(); - updateStatus(App.StatusEnum.LOGGED_IN, 'Logged in.'); + updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); }, onResponseCode: { - [Data.Response_ResponseCode.RespClientUpdateRequired]: () => + [Response_ResponseCode.RespClientUpdateRequired]: () => onLoginError('Login failed: missing features'), - [Data.Response_ResponseCode.RespWrongPassword]: () => + [Response_ResponseCode.RespWrongPassword]: () => onLoginError('Login failed: incorrect username or password'), - [Data.Response_ResponseCode.RespUsernameInvalid]: () => + [Response_ResponseCode.RespUsernameInvalid]: () => onLoginError('Login failed: incorrect username or password'), - [Data.Response_ResponseCode.RespWouldOverwriteOldSession]: () => + [Response_ResponseCode.RespWouldOverwriteOldSession]: () => onLoginError('Login failed: duplicated user session'), - [Data.Response_ResponseCode.RespUserIsBanned]: () => + [Response_ResponseCode.RespUserIsBanned]: () => onLoginError('Login failed: banned user'), - [Data.Response_ResponseCode.RespRegistrationRequired]: () => + [Response_ResponseCode.RespRegistrationRequired]: () => onLoginError('Login failed: registration required'), - [Data.Response_ResponseCode.RespClientIdRequired]: () => + [Response_ResponseCode.RespClientIdRequired]: () => onLoginError('Login failed: missing client ID'), - [Data.Response_ResponseCode.RespContextError]: () => + [Response_ResponseCode.RespContextError]: () => onLoginError('Login failed: server error'), - [Data.Response_ResponseCode.RespAccountNotActivated]: () => + [Response_ResponseCode.RespAccountNotActivated]: (raw) => 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 3396a3947..94afefc46 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 { Data } from '@app/types'; +import { Command_Message_ext, Command_MessageSchema } from '@app/generated'; export function message(userName: string, message: string): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_Message_ext, create(Data.Command_MessageSchema, { userName, message })); + WebClient.instance.protobuf.sendSessionCommand(Command_Message_ext, create(Command_MessageSchema, { userName, message })); } diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts index f60687225..fab3c9272 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 { Data } from '@app/types'; +import { Command_Ping_ext, Command_PingSchema } from '@app/generated'; export function ping(pingReceived: () => void): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_Ping_ext, create(Data.Command_PingSchema), { + WebClient.instance.protobuf.sendSessionCommand(Command_Ping_ext, create(Command_PingSchema), { onResponse: () => pingReceived(), }); } diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index 5b35a1207..7badfc4ae 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -1,18 +1,24 @@ -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 '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; - +import type { ConnectTarget } from '../../WebClientConfig'; import { hashPassword } from '../../utils'; - import { login, disconnect, updateStatus } from './'; -export function register(options: Omit, password?: string, passwordSalt?: string): void { +export function register(options: ConnectTarget & RegisterParams, password?: string, passwordSalt?: string): void { const { userName, email, country, realName } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, email, @@ -25,53 +31,53 @@ export function register(options: Omit void) => { action(); - updateStatus(App.StatusEnum.DISCONNECTED, 'Registration failed'); + updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); disconnect(); }; - WebClient.instance.protobuf.sendSessionCommand(Data.Command_Register_ext, create(Data.Command_RegisterSchema, params), { + WebClient.instance.protobuf.sendSessionCommand(Command_Register_ext, create(Command_RegisterSchema, params), { onResponseCode: { - [Data.Response_ResponseCode.RespRegistrationAccepted]: () => { + [Response_ResponseCode.RespRegistrationAccepted]: () => { login({ host: options.host, port: options.port, userName: options.userName, - reason: App.WebSocketConnectReason.LOGIN, }, password, passwordSalt); WebClient.instance.response.session.registrationSuccess(); }, - [Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { - updateStatus(App.StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); + [Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: (raw) => { + updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); WebClient.instance.response.session.accountAwaitingActivation({ + ...raw, host: options.host, port: options.port, userName: options.userName, }); disconnect(); }, - [Data.Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + [Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( () => WebClient.instance.response.session.registrationUserNameError('Username is taken') ), - [Data.Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + [Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( () => WebClient.instance.response.session.registrationUserNameError('Invalid username') ), - [Data.Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + [Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( () => WebClient.instance.response.session.registrationPasswordError('Your password was too short') ), - [Data.Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + [Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( () => WebClient.instance.response.session.registrationRequiresEmail() ), - [Data.Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + [Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( () => WebClient.instance.response.session.registrationEmailError('This email provider has been blocked') ), - [Data.Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( + [Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( () => WebClient.instance.response.session.registrationEmailError('Max accounts reached for this email') ), - [Data.Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + [Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( () => WebClient.instance.response.session.registrationFailed('Registration is currently disabled') ), - [Data.Response_ResponseCode.RespUserIsBanned]: (raw) => { - const register = getExtension(raw, Data.Response_Register_ext); + [Response_ResponseCode.RespUserIsBanned]: (raw) => { + const register = getExtension(raw, 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 16e56adf8..82d971b78 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 { Data } from '@app/types'; +import { Command_RemoveFromList_ext, Command_RemoveFromListSchema } from '@app/generated'; 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( - Data.Command_RemoveFromList_ext, - create(Data.Command_RemoveFromListSchema, { list, userName }), + Command_RemoveFromList_ext, + create(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 059efabda..1da4fe7d1 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 { Data } from '@app/types'; +import { Command_ReplayDeleteMatch_ext, Command_ReplayDeleteMatchSchema } from '@app/generated'; export function replayDeleteMatch(gameId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ReplayDeleteMatch_ext, - create(Data.Command_ReplayDeleteMatchSchema, { gameId }), + Command_ReplayDeleteMatch_ext, + create(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 950bf5891..928abfd15 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 { Data } from '@app/types'; +import { Command_ReplayDownload_ext, Command_ReplayDownloadSchema, Response_ReplayDownload_ext } from '@app/generated'; export function replayDownload(replayId: number): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ReplayDownload_ext, - create(Data.Command_ReplayDownloadSchema, { replayId }), + Command_ReplayDownload_ext, + create(Command_ReplayDownloadSchema, { replayId }), { - responseExt: Data.Response_ReplayDownload_ext, + responseExt: 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 f53751f57..272307845 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 { Data } from '@app/types'; +import { Command_ReplayGetCode_ext, Command_ReplayGetCodeSchema, Response_ReplayGetCode_ext } from '@app/generated'; export function replayGetCode(gameId: number, onCodeReceived: (code: string) => void): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_ReplayGetCode_ext, create(Data.Command_ReplayGetCodeSchema, { gameId }), { - responseExt: Data.Response_ReplayGetCode_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_ReplayGetCode_ext, create(Command_ReplayGetCodeSchema, { gameId }), { + responseExt: 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 6618f41f0..60919f534 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 { Data } from '@app/types'; +import { Command_ReplayList_ext, Command_ReplayListSchema, Response_ReplayList_ext } from '@app/generated'; export function replayList(): void { - WebClient.instance.protobuf.sendSessionCommand(Data.Command_ReplayList_ext, create(Data.Command_ReplayListSchema), { - responseExt: Data.Response_ReplayList_ext, + WebClient.instance.protobuf.sendSessionCommand(Command_ReplayList_ext, create(Command_ReplayListSchema), { + responseExt: 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 3803d04bd..3d4e4af53 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 { Data } from '@app/types'; +import { Command_ReplayModifyMatch_ext, Command_ReplayModifyMatchSchema } from '@app/generated'; export function replayModifyMatch(gameId: number, doNotHide: boolean): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ReplayModifyMatch_ext, - create(Data.Command_ReplayModifyMatchSchema, { gameId, doNotHide }), + Command_ReplayModifyMatch_ext, + create(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 dec72113f..aa0e403e0 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 { Data } from '@app/types'; +import { Command_ReplaySubmitCode_ext, Command_ReplaySubmitCodeSchema } from '@app/generated'; export function replaySubmitCode( replayCode: string, @@ -8,8 +8,8 @@ export function replaySubmitCode( onError?: (responseCode: number) => void, ): void { WebClient.instance.protobuf.sendSessionCommand( - Data.Command_ReplaySubmitCode_ext, - create(Data.Command_ReplaySubmitCodeSchema, { replayCode }), + Command_ReplaySubmitCode_ext, + create(Command_ReplaySubmitCodeSchema, { replayCode }), { onSuccess, onError, diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index 8c7843c82..4579edb66 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -1,66 +1,41 @@ -import { App, Enriched, Data } from '@app/types'; - import { create } from '@bufbuild/protobuf'; +import { + Command_RequestPasswordSalt_ext, + Command_RequestPasswordSaltSchema, + Response_PasswordSalt_ext, + Response_ResponseCode, + type RequestPasswordSaltParams, +} from '@app/generated'; + +import { StatusEnum } from '../../StatusEnum'; import { CLIENT_CONFIG } from '../../config'; import { WebClient } from '../../WebClient'; +import type { ConnectTarget } from '../../WebClientConfig'; +import { updateStatus } from './'; -import { - activate, - disconnect, - login, - forgotPasswordReset, - updateStatus -} from './'; - -type PasswordSaltOptions = - | Omit - | Omit - | Omit; - -export function requestPasswordSalt(options: PasswordSaltOptions, password?: string, newPassword?: string): void { +export function requestPasswordSalt( + options: ConnectTarget & RequestPasswordSaltParams, + onSaltReceived: (passwordSalt: string) => void, + onFailure: () => void, +): void { const { userName } = options; - 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, { + WebClient.instance.protobuf.sendSessionCommand(Command_RequestPasswordSalt_ext, create(Command_RequestPasswordSaltSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Data.Response_PasswordSalt_ext, + responseExt: Response_PasswordSalt_ext, onSuccess: (resp) => { - 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); - } + onSaltReceived(resp?.passwordSalt); }, onResponseCode: { - [Data.Response_ResponseCode.RespRegistrationRequired]: () => { - updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: registration required'); + [Response_ResponseCode.RespRegistrationRequired]: () => { + updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); onFailure(); }, }, onError: () => { - updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); + updateStatus(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 09f4876c9..9a3c7c608 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts @@ -19,14 +19,32 @@ vi.mock('./', async () => { import { Mock } from 'vitest'; import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { WebClient } from '../../WebClient'; import * as SessionIndexMocks from './'; -import { App, Enriched, Data } from '@app/types'; +import { App, Enriched } from '@app/types'; +import { StatusEnum } from '../../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 { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; import { create, setExtension } from '@bufbuild/protobuf'; -import { connect } from './connect'; +import { connect, testConnect } from './connect'; import { updateStatus } from './updateStatus'; import { login } from './login'; import { register } from './register'; @@ -36,6 +54,7 @@ import { forgotPasswordRequest } from './forgotPasswordRequest'; import { forgotPasswordReset } from './forgotPasswordReset'; import { requestPasswordSalt } from './requestPasswordSalt'; +useWebClientCleanup(); const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers( WebClient.instance.protobuf.sendSessionCommand as Mock, @@ -88,13 +107,7 @@ 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'); @@ -107,47 +120,17 @@ beforeEach(() => { // ---------------------------------------------------------------- describe('connect', () => { - 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(); + 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 REGISTER reason', () => { - connect(makeRegisterOpts({ userName: 'u', realName: 'U' })); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); - }); +describe('testConnect', () => { - 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')); + it('calls WebClient.instance.testConnect with the target', () => { + testConnect({ host: 'h', port: '1' }); + expect(WebClient.instance.testConnect).toHaveBeenCalledWith({ host: 'h', port: '1' }); }); }); @@ -157,9 +140,9 @@ describe('connect', () => { describe('updateStatus', () => { it('calls WebClient.instance.response.session.updateStatus and WebClient.instance.updateStatus', () => { - updateStatus(App.StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.response.session.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED); + updateStatus(StatusEnum.CONNECTED, 'OK'); + expect(WebClient.instance.response.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'OK'); + expect(WebClient.instance.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED); }); }); @@ -171,27 +154,27 @@ describe('login', () => { it('sends Command_Login with plain password when no salt', () => { login(makeLoginOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Login_ext, + Command_Login_ext, expect.objectContaining({ password: 'pw' }), - expect.objectContaining({ responseExt: Data.Response_Login_ext }) + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); it('sends Command_Login with hashedPassword when salt is given', () => { login(makeLoginOpts(), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Login_ext, + Command_Login_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), - expect.objectContaining({ responseExt: Data.Response_Login_ext }) + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); it('uses options.hashedPassword if provided', () => { login(makeLoginOpts({ hashedPassword: 'pre_hashed' }), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Login_ext, + Command_Login_ext, expect.objectContaining({ hashedPassword: 'pre_hashed' }), - expect.objectContaining({ responseExt: Data.Response_Login_ext }) + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); @@ -205,7 +188,7 @@ describe('login', () => { expect(WebClient.instance.response.session.loginSuccessful).toHaveBeenCalled(); expect(SessionIndexMocks.listUsers).toHaveBeenCalled(); expect(SessionIndexMocks.listRooms).toHaveBeenCalled(); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.LOGGED_IN, 'Logged in.'); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.'); }); it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { @@ -226,56 +209,56 @@ describe('login', () => { it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespClientUpdateRequired); + invokeResponseCode(Response_ResponseCode.RespClientUpdateRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespWrongPassword', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespWrongPassword); + invokeResponseCode(Response_ResponseCode.RespWrongPassword); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUsernameInvalid', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); + invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespWouldOverwriteOldSession', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespWouldOverwriteOldSession); + invokeResponseCode(Response_ResponseCode.RespWouldOverwriteOldSession); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUserIsBanned', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned); + invokeResponseCode(Response_ResponseCode.RespUserIsBanned); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); + invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespClientIdRequired', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespClientIdRequired); + invokeResponseCode(Response_ResponseCode.RespClientIdRequired); expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespContextError', () => { login(makeLoginOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespContextError); + invokeResponseCode(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(Data.Response_ResponseCode.RespAccountNotActivated); + invokeResponseCode(Response_ResponseCode.RespAccountNotActivated); expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -297,7 +280,7 @@ describe('register', () => { it('sends Command_Register with plain password when no salt', () => { register(makeRegisterOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Register_ext, + Command_Register_ext, expect.objectContaining({ password: 'pw' }), expect.any(Object) ); @@ -306,7 +289,7 @@ describe('register', () => { it('uses hashedPassword when salt is provided', () => { register(makeRegisterOpts(), 'pw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Register_ext, + Command_Register_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.any(Object) ); @@ -314,21 +297,21 @@ describe('register', () => { it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAccepted); + invokeResponseCode(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(Data.Response_ResponseCode.RespRegistrationAccepted); + invokeResponseCode(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(Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); + invokeResponseCode(Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -337,53 +320,53 @@ describe('register', () => { it('RespUserAlreadyExists calls registrationUserNameError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespUserAlreadyExists); + invokeResponseCode(Response_ResponseCode.RespUserAlreadyExists); expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); }); it('RespUsernameInvalid calls registrationUserNameError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); + invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); }); it('RespPasswordTooShort calls registrationPasswordError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespPasswordTooShort); + invokeResponseCode(Response_ResponseCode.RespPasswordTooShort); expect(WebClient.instance.response.session.registrationPasswordError).toHaveBeenCalled(); }); it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespEmailRequiredToRegister); + invokeResponseCode(Response_ResponseCode.RespEmailRequiredToRegister); expect(WebClient.instance.response.session.registrationRequiresEmail).toHaveBeenCalled(); }); it('RespEmailBlackListed calls registrationEmailError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespEmailBlackListed); + invokeResponseCode(Response_ResponseCode.RespEmailBlackListed); expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); }); it('RespTooManyRequests calls registrationEmailError', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespTooManyRequests); + invokeResponseCode(Response_ResponseCode.RespTooManyRequests); expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); }); it('RespRegistrationDisabled calls registrationFailed', () => { register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Data.Response_ResponseCode.RespRegistrationDisabled); + invokeResponseCode(Response_ResponseCode.RespRegistrationDisabled); expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalled(); }); it('RespUserIsBanned calls registrationFailed with deniedReasonStr and deniedEndTime', () => { register(makeRegisterOpts(), 'pw'); - const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespUserIsBanned }); - setExtension(raw, Data.Response_Register_ext, create(Data.Response_RegisterSchema, { + const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespUserIsBanned }); + setExtension(raw, Response_Register_ext, create(Response_RegisterSchema, { deniedReasonStr: 'bad user', deniedEndTime: 9999n, })); - invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned, raw); + invokeResponseCode(Response_ResponseCode.RespUserIsBanned, raw); expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalledWith('bad user', 9999); }); @@ -402,12 +385,12 @@ describe('activate', () => { it('sends Command_Activate with userName and token, not password', () => { activate(makeActivateOpts(), 'pw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Activate_ext, + Command_Activate_ext, expect.objectContaining({ userName: 'alice', token: 'tok' }), expect.any(Object) ); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Activate_ext, + Command_Activate_ext, expect.not.objectContaining({ password: expect.anything() }), expect.any(Object) ); @@ -415,7 +398,7 @@ describe('activate', () => { it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { activate(makeActivateOpts(), 'pw', 'salt'); - invokeResponseCode(Data.Response_ResponseCode.RespActivationAccepted); + invokeResponseCode(Response_ResponseCode.RespActivationAccepted); expect(WebClient.instance.response.session.accountActivationSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt'); }); @@ -436,7 +419,7 @@ describe('forgotPasswordChallenge', () => { it('sends Command_ForgotPasswordChallenge', () => { forgotPasswordChallenge(makeForgotChallengeOpts()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) + Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) ); }); @@ -463,9 +446,9 @@ describe('forgotPasswordRequest', () => { it('sends Command_ForgotPasswordRequest', () => { forgotPasswordRequest(makeForgotRequestOpts()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ForgotPasswordRequest_ext, + Command_ForgotPasswordRequest_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_ForgotPasswordRequest_ext }) + expect.objectContaining({ responseExt: Response_ForgotPasswordRequest_ext }) ); }); @@ -501,7 +484,7 @@ describe('forgotPasswordReset', () => { it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { forgotPasswordReset(makeForgotResetOpts(), 'newpw'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ForgotPasswordReset_ext, + Command_ForgotPasswordReset_ext, expect.objectContaining({ newPassword: 'newpw' }), expect.any(Object) ); @@ -510,7 +493,7 @@ describe('forgotPasswordReset', () => { it('sends hashed new password when salt provided', () => { forgotPasswordReset(makeForgotResetOpts(), 'newpw', 'salt'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ForgotPasswordReset_ext, + Command_ForgotPasswordReset_ext, expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), expect.any(Object) ); @@ -537,66 +520,40 @@ describe('forgotPasswordReset', () => { describe('requestPasswordSalt', () => { it('sends Command_RequestPasswordSalt', () => { - requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); + const onSaltReceived = vi.fn(); + const onFailure = vi.fn(); + requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_RequestPasswordSalt_ext, + Command_RequestPasswordSalt_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_PasswordSalt_ext }) + expect.objectContaining({ responseExt: Response_PasswordSalt_ext }) ); }); - it('onSuccess with LOGIN reason forwards password+salt to login', () => { - requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); + it('onSuccess calls onSaltReceived with the salt', () => { + const onSaltReceived = vi.fn(); + const onFailure = vi.fn(); + requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); const resp = { passwordSalt: 'salt123' }; invokeOnSuccess(resp, { responseCode: 0 }); - expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); + expect(onSaltReceived).toHaveBeenCalledWith('salt123'); }); - 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('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 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'); + it('onError calls updateStatus DISCONNECTED and onFailure', () => { + const onSaltReceived = vi.fn(); + const onFailure = vi.fn(); + requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); invokeOnError(); expect(SessionIndexMocks.updateStatus).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(); + expect(onFailure).toHaveBeenCalled(); }); }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index 97b2acdab..689aaa3d9 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -19,6 +19,7 @@ vi.mock('./', async () => { import { Mock } from 'vitest'; import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { WebClient } from '../../WebClient'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; @@ -47,8 +48,44 @@ import { addToList, addToBuddyList, addToIgnoreList } from './addToList'; import { removeFromList, removeFromBuddyList, removeFromIgnoreList } from './removeFromList'; import { replayGetCode } from './replayGetCode'; import { replaySubmitCode } from './replaySubmitCode'; -import { Data } from '@app/types'; +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'; +useWebClientCleanup(); const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( WebClient.instance.protobuf.sendSessionCommand as Mock, @@ -67,7 +104,7 @@ describe('accountEdit', () => { it('sends Command_AccountEdit with correct params', () => { accountEdit('pw', 'Alice', 'a@b.com', 'US'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_AccountEdit_ext, + Command_AccountEdit_ext, expect.objectContaining({ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }), expect.any(Object) ); @@ -85,7 +122,7 @@ describe('accountImage', () => { const img = new Uint8Array([1, 2]); accountImage(img); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) + Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) ); }); @@ -101,7 +138,7 @@ describe('accountPassword', () => { it('sends Command_AccountPassword', () => { accountPassword('old', 'new', 'hashed'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_AccountPassword_ext, + Command_AccountPassword_ext, expect.objectContaining({ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }), expect.any(Object) ); @@ -118,7 +155,7 @@ describe('deckDel', () => { it('sends Command_DeckDel', () => { deckDel(42); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckDel_ext, + Command_DeckDel_ext, expect.objectContaining({ deckId: 42 }), expect.any(Object) ); @@ -135,7 +172,7 @@ describe('deckDelDir', () => { it('sends Command_DeckDelDir', () => { deckDelDir('/path'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) + Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) ); }); @@ -150,9 +187,9 @@ describe('deckList', () => { it('sends Command_DeckList', () => { deckList(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckList_ext, + Command_DeckList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_DeckList_ext }) + expect.objectContaining({ responseExt: Response_DeckList_ext }) ); }); @@ -168,7 +205,7 @@ describe('deckNewDir', () => { it('sends Command_DeckNewDir', () => { deckNewDir('/path', 'dir'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) + Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) ); }); @@ -183,9 +220,9 @@ describe('deckUpload', () => { it('sends Command_DeckUpload', () => { deckUpload('/path', 1, 'content'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckUpload_ext, + Command_DeckUpload_ext, expect.objectContaining({ path: '/path', deckId: 1, deckList: 'content' }), - expect.objectContaining({ responseExt: Data.Response_DeckUpload_ext }) + expect.objectContaining({ responseExt: Response_DeckUpload_ext }) ); }); @@ -208,9 +245,9 @@ describe('getGamesOfUser', () => { it('sends Command_GetGamesOfUser', () => { getGamesOfUser('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_GetGamesOfUser_ext, + Command_GetGamesOfUser_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_GetGamesOfUser_ext }) + expect.objectContaining({ responseExt: Response_GetGamesOfUser_ext }) ); }); @@ -226,9 +263,9 @@ describe('getUserInfo', () => { it('sends Command_GetUserInfo', () => { getUserInfo('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_GetUserInfo_ext, + Command_GetUserInfo_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_GetUserInfo_ext }) + expect.objectContaining({ responseExt: Response_GetUserInfo_ext }) ); }); @@ -244,9 +281,9 @@ describe('joinRoom', () => { it('sends Command_JoinRoom', () => { joinRoom(5); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_JoinRoom_ext, + Command_JoinRoom_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_JoinRoom_ext }) + expect.objectContaining({ responseExt: Response_JoinRoom_ext }) ); }); @@ -261,7 +298,7 @@ describe('joinRoom', () => { describe('listRooms (command)', () => { it('sends Command_ListRooms', () => { listRooms(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith(Data.Command_ListRooms_ext, expect.any(Object)); + expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith(Command_ListRooms_ext, expect.any(Object)); }); }); @@ -269,9 +306,9 @@ describe('listUsers', () => { it('sends Command_ListUsers', () => { listUsers(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ListUsers_ext, + Command_ListUsers_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_ListUsers_ext }) + expect.objectContaining({ responseExt: Response_ListUsers_ext }) ); }); @@ -287,7 +324,7 @@ describe('message', () => { it('sends Command_Message', () => { message('bob', 'hi'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) + Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) ); }); @@ -298,7 +335,7 @@ describe('ping', () => { const pingReceived = vi.fn(); ping(pingReceived); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_Ping_ext, expect.any(Object), expect.any(Object) + Command_Ping_ext, expect.any(Object), expect.any(Object) ); }); @@ -314,7 +351,7 @@ describe('replayDeleteMatch', () => { it('sends Command_ReplayDeleteMatch', () => { replayDeleteMatch(7); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplayDeleteMatch_ext, + Command_ReplayDeleteMatch_ext, expect.objectContaining({ gameId: 7 }), expect.any(Object) ); @@ -331,9 +368,9 @@ describe('replayList', () => { it('sends Command_ReplayList', () => { replayList(); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplayList_ext, + Command_ReplayList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_ReplayList_ext }) + expect.objectContaining({ responseExt: Response_ReplayList_ext }) ); }); @@ -349,7 +386,7 @@ describe('replayModifyMatch', () => { it('sends Command_ReplayModifyMatch', () => { replayModifyMatch(7, true); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) + Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) ); }); @@ -364,7 +401,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToBuddyList sends Command_AddToList with list=buddy', () => { addToBuddyList('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_AddToList_ext, + Command_AddToList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -373,7 +410,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToIgnoreList sends Command_AddToList with list=ignore', () => { addToIgnoreList('bob'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_AddToList_ext, + Command_AddToList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -390,7 +427,7 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => { removeFromBuddyList('alice'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_RemoveFromList_ext, + Command_RemoveFromList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -399,7 +436,7 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => { removeFromIgnoreList('bob'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_RemoveFromList_ext, + Command_RemoveFromList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -416,9 +453,9 @@ describe('replayGetCode', () => { it('sends Command_ReplayGetCode with gameId and responseExt', () => { replayGetCode(42, vi.fn()); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplayGetCode_ext, + Command_ReplayGetCode_ext, expect.any(Object), - expect.objectContaining({ responseExt: Data.Response_ReplayGetCode_ext }) + expect.objectContaining({ responseExt: Response_ReplayGetCode_ext }) ); }); @@ -434,7 +471,7 @@ describe('replaySubmitCode', () => { it('sends Command_ReplaySubmitCode with replayCode', () => { replaySubmitCode('42-abc123'); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) + Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) ); }); @@ -457,9 +494,9 @@ describe('deckDownload', () => { it('sends Command_DeckDownload', () => { deckDownload(42); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_DeckDownload_ext, + Command_DeckDownload_ext, expect.objectContaining({ deckId: 42 }), - expect.objectContaining({ responseExt: Data.Response_DeckDownload_ext }) + expect.objectContaining({ responseExt: Response_DeckDownload_ext }) ); }); @@ -475,9 +512,9 @@ describe('replayDownload', () => { it('sends Command_ReplayDownload', () => { replayDownload(99); expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Data.Command_ReplayDownload_ext, + Command_ReplayDownload_ext, expect.objectContaining({ replayId: 99 }), - expect.objectContaining({ responseExt: Data.Response_ReplayDownload_ext }) + expect.objectContaining({ responseExt: Response_ReplayDownload_ext }) ); }); diff --git a/webclient/src/websocket/commands/session/updateStatus.ts b/webclient/src/websocket/commands/session/updateStatus.ts index 1289c5ed8..d573ff909 100644 --- a/webclient/src/websocket/commands/session/updateStatus.ts +++ b/webclient/src/websocket/commands/session/updateStatus.ts @@ -1,7 +1,7 @@ -import { App } from '@app/types'; +import { StatusEnum } from '../../StatusEnum'; import { WebClient } from '../../WebClient'; -export function updateStatus(status: App.StatusEnum, description: string): void { - WebClient.instance.response.session.updateStatus(status, description); +export function updateStatus(status: 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 9f3d12643..ad025e3da 100644 --- a/webclient/src/websocket/config.ts +++ b/webclient/src/websocket/config.ts @@ -19,8 +19,6 @@ 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 c88082378..dd95bf0c0 100644 --- a/webclient/src/websocket/events/game/attachCard.ts +++ b/webclient/src/websocket/events/game/attachCard.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_AttachCard } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function attachCard(data: Data.Event_AttachCard, meta: Enriched.GameEventMeta): void { +export function attachCard(data: Event_AttachCard, meta: 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 65b062274..7df8ef8f5 100644 --- a/webclient/src/websocket/events/game/changeZoneProperties.ts +++ b/webclient/src/websocket/events/game/changeZoneProperties.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_ChangeZoneProperties } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function changeZoneProperties(data: Data.Event_ChangeZoneProperties, meta: Enriched.GameEventMeta): void { +export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: 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 6ae8946aa..5ef3ea55a 100644 --- a/webclient/src/websocket/events/game/createArrow.ts +++ b/webclient/src/websocket/events/game/createArrow.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_CreateArrow } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function createArrow(data: Data.Event_CreateArrow, meta: Enriched.GameEventMeta): void { +export function createArrow(data: Event_CreateArrow, meta: 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 8c1873339..e70dec92d 100644 --- a/webclient/src/websocket/events/game/createCounter.ts +++ b/webclient/src/websocket/events/game/createCounter.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_CreateCounter } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function createCounter(data: Data.Event_CreateCounter, meta: Enriched.GameEventMeta): void { +export function createCounter(data: Event_CreateCounter, meta: 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 6396f5b82..aa276406f 100644 --- a/webclient/src/websocket/events/game/createToken.ts +++ b/webclient/src/websocket/events/game/createToken.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_CreateToken } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function createToken(data: Data.Event_CreateToken, meta: Enriched.GameEventMeta): void { +export function createToken(data: Event_CreateToken, meta: 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 ac3d40345..8d81f18ee 100644 --- a/webclient/src/websocket/events/game/delCounter.ts +++ b/webclient/src/websocket/events/game/delCounter.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_DelCounter } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function delCounter(data: Data.Event_DelCounter, meta: Enriched.GameEventMeta): void { +export function delCounter(data: Event_DelCounter, meta: 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 52e0d6bf0..6a3274a25 100644 --- a/webclient/src/websocket/events/game/deleteArrow.ts +++ b/webclient/src/websocket/events/game/deleteArrow.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_DeleteArrow } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function deleteArrow(data: Data.Event_DeleteArrow, meta: Enriched.GameEventMeta): void { +export function deleteArrow(data: Event_DeleteArrow, meta: 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 70d59582c..6788e89e1 100644 --- a/webclient/src/websocket/events/game/destroyCard.ts +++ b/webclient/src/websocket/events/game/destroyCard.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_DestroyCard } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function destroyCard(data: Data.Event_DestroyCard, meta: Enriched.GameEventMeta): void { +export function destroyCard(data: Event_DestroyCard, meta: 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 2932ebe3b..fce24b125 100644 --- a/webclient/src/websocket/events/game/drawCards.ts +++ b/webclient/src/websocket/events/game/drawCards.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_DrawCards } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function drawCards(data: Data.Event_DrawCards, meta: Enriched.GameEventMeta): void { +export function drawCards(data: Event_DrawCards, meta: 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 c528aba2e..34c33a6c6 100644 --- a/webclient/src/websocket/events/game/dumpZone.ts +++ b/webclient/src/websocket/events/game/dumpZone.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_DumpZone } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function dumpZone(data: Data.Event_DumpZone, meta: Enriched.GameEventMeta): void { +export function dumpZone(data: Event_DumpZone, meta: 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 9930580a0..1c78e6977 100644 --- a/webclient/src/websocket/events/game/flipCard.ts +++ b/webclient/src/websocket/events/game/flipCard.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_FlipCard } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function flipCard(data: Data.Event_FlipCard, meta: Enriched.GameEventMeta): void { +export function flipCard(data: Event_FlipCard, meta: 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 9a4c0924d..cd54984d9 100644 --- a/webclient/src/websocket/events/game/gameClosed.ts +++ b/webclient/src/websocket/events/game/gameClosed.ts @@ -1,6 +1,6 @@ -import { Enriched } from '@app/types'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function gameClosed(_data: {}, meta: Enriched.GameEventMeta): void { +export function gameClosed(_data: {}, meta: 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 d5537fb23..df0f3bb8b 100644 --- a/webclient/src/websocket/events/game/gameEvents.spec.ts +++ b/webclient/src/websocket/events/game/gameEvents.spec.ts @@ -38,8 +38,34 @@ vi.mock('../../WebClient', () => ({ }, })); +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { create } from '@bufbuild/protobuf'; -import { Data } from '@app/types'; +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 { WebClient } from '../../WebClient'; import { attachCard } from './attachCard'; @@ -72,11 +98,13 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; +useWebClientCleanup(); + const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 }; describe('joinGame event', () => { it('delegates to WebClient.instance.response.game.playerJoined with gameId from meta', () => { - const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); + const playerProperties = create(ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); const data = { playerProperties }; joinGame(data, meta); expect(WebClient.instance.response.game.playerJoined).toHaveBeenCalledWith(5, playerProperties); @@ -114,7 +142,7 @@ describe('kicked event', () => { describe('gameStateChanged event', () => { it('delegates to WebClient.instance.response.game.gameStateChanged with gameId and full data', () => { - const data = create(Data.Event_GameStateChangedSchema, { playerList: [] }); + const data = create(Event_GameStateChangedSchema, { playerList: [] }); gameStateChanged(data, meta); expect(WebClient.instance.response.game.gameStateChanged).toHaveBeenCalledWith(5, data); }); @@ -122,7 +150,7 @@ describe('gameStateChanged event', () => { describe('playerPropertiesChanged event', () => { it('delegates to WebClient.instance.response.game.playerPropertiesChanged with gameId, playerId, properties', () => { - const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); + const playerProperties = create(ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); const data = { playerProperties }; playerPropertiesChanged(data, meta); expect(WebClient.instance.response.game.playerPropertiesChanged).toHaveBeenCalledWith(5, 2, playerProperties); @@ -131,7 +159,7 @@ describe('playerPropertiesChanged event', () => { describe('gameSay event', () => { it('delegates to WebClient.instance.response.game.gameSay with gameId, playerId, message', () => { - const data = create(Data.Event_GameSaySchema, { message: 'gg' }); + const data = create(Event_GameSaySchema, { message: 'gg' }); gameSay(data, meta); expect(WebClient.instance.response.game.gameSay).toHaveBeenCalledWith(5, 2, 'gg'); }); @@ -139,7 +167,7 @@ describe('gameSay event', () => { describe('moveCard event', () => { it('delegates to WebClient.instance.response.game.cardMoved with gameId, playerId and data', () => { - const data = create(Data.Event_MoveCardSchema, { cardId: 3 }); + const data = create(Event_MoveCardSchema, { cardId: 3 }); moveCard(data, meta); expect(WebClient.instance.response.game.cardMoved).toHaveBeenCalledWith(5, 2, data); }); @@ -147,7 +175,7 @@ describe('moveCard event', () => { describe('flipCard event', () => { it('delegates to WebClient.instance.response.game.cardFlipped with gameId, playerId and data', () => { - const data = create(Data.Event_FlipCardSchema, { cardId: 3 }); + const data = create(Event_FlipCardSchema, { cardId: 3 }); flipCard(data, meta); expect(WebClient.instance.response.game.cardFlipped).toHaveBeenCalledWith(5, 2, data); }); @@ -155,7 +183,7 @@ describe('flipCard event', () => { describe('destroyCard event', () => { it('delegates to WebClient.instance.response.game.cardDestroyed with gameId, playerId and data', () => { - const data = create(Data.Event_DestroyCardSchema, { cardId: 3 }); + const data = create(Event_DestroyCardSchema, { cardId: 3 }); destroyCard(data, meta); expect(WebClient.instance.response.game.cardDestroyed).toHaveBeenCalledWith(5, 2, data); }); @@ -163,7 +191,7 @@ describe('destroyCard event', () => { describe('attachCard event', () => { it('delegates to WebClient.instance.response.game.cardAttached with gameId, playerId and data', () => { - const data = create(Data.Event_AttachCardSchema, { cardId: 3 }); + const data = create(Event_AttachCardSchema, { cardId: 3 }); attachCard(data, meta); expect(WebClient.instance.response.game.cardAttached).toHaveBeenCalledWith(5, 2, data); }); @@ -171,7 +199,7 @@ describe('attachCard event', () => { describe('createToken event', () => { it('delegates to WebClient.instance.response.game.tokenCreated with gameId, playerId and data', () => { - const data = create(Data.Event_CreateTokenSchema, { cardId: 3 }); + const data = create(Event_CreateTokenSchema, { cardId: 3 }); createToken(data, meta); expect(WebClient.instance.response.game.tokenCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -179,7 +207,7 @@ describe('createToken event', () => { describe('setCardAttr event', () => { it('delegates to WebClient.instance.response.game.cardAttrChanged with gameId, playerId and data', () => { - const data = create(Data.Event_SetCardAttrSchema, { cardId: 3 }); + const data = create(Event_SetCardAttrSchema, { cardId: 3 }); setCardAttr(data, meta); expect(WebClient.instance.response.game.cardAttrChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -187,7 +215,7 @@ describe('setCardAttr event', () => { describe('setCardCounter event', () => { it('delegates to WebClient.instance.response.game.cardCounterChanged with gameId, playerId and data', () => { - const data = create(Data.Event_SetCardCounterSchema, { cardId: 3 }); + const data = create(Event_SetCardCounterSchema, { cardId: 3 }); setCardCounter(data, meta); expect(WebClient.instance.response.game.cardCounterChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -195,7 +223,7 @@ describe('setCardCounter event', () => { describe('createArrow event', () => { it('delegates to WebClient.instance.response.game.arrowCreated with gameId, playerId and data', () => { - const data = create(Data.Event_CreateArrowSchema, {}); + const data = create(Event_CreateArrowSchema, {}); createArrow(data, meta); expect(WebClient.instance.response.game.arrowCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -203,7 +231,7 @@ describe('createArrow event', () => { describe('deleteArrow event', () => { it('delegates to WebClient.instance.response.game.arrowDeleted with gameId, playerId and data', () => { - const data = create(Data.Event_DeleteArrowSchema, { arrowId: 9 }); + const data = create(Event_DeleteArrowSchema, { arrowId: 9 }); deleteArrow(data, meta); expect(WebClient.instance.response.game.arrowDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -211,7 +239,7 @@ describe('deleteArrow event', () => { describe('createCounter event', () => { it('delegates to WebClient.instance.response.game.counterCreated with gameId, playerId and data', () => { - const data = create(Data.Event_CreateCounterSchema, {}); + const data = create(Event_CreateCounterSchema, {}); createCounter(data, meta); expect(WebClient.instance.response.game.counterCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -219,7 +247,7 @@ describe('createCounter event', () => { describe('setCounter event', () => { it('delegates to WebClient.instance.response.game.counterSet with gameId, playerId and data', () => { - const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 20 }); + const data = create(Event_SetCounterSchema, { counterId: 1, value: 20 }); setCounter(data, meta); expect(WebClient.instance.response.game.counterSet).toHaveBeenCalledWith(5, 2, data); }); @@ -227,7 +255,7 @@ describe('setCounter event', () => { describe('delCounter event', () => { it('delegates to WebClient.instance.response.game.counterDeleted with gameId, playerId and data', () => { - const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); + const data = create(Event_DelCounterSchema, { counterId: 1 }); delCounter(data, meta); expect(WebClient.instance.response.game.counterDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -235,7 +263,7 @@ describe('delCounter event', () => { describe('drawCards event', () => { it('delegates to WebClient.instance.response.game.cardsDrawn with gameId, playerId and data', () => { - const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [] }); + const data = create(Event_DrawCardsSchema, { number: 2, cards: [] }); drawCards(data, meta); expect(WebClient.instance.response.game.cardsDrawn).toHaveBeenCalledWith(5, 2, data); }); @@ -243,7 +271,7 @@ describe('drawCards event', () => { describe('revealCards event', () => { it('delegates to WebClient.instance.response.game.cardsRevealed with gameId, playerId and data', () => { - const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); + const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); revealCards(data, meta); expect(WebClient.instance.response.game.cardsRevealed).toHaveBeenCalledWith(5, 2, data); }); @@ -251,7 +279,7 @@ describe('revealCards event', () => { describe('shuffle event', () => { it('delegates to WebClient.instance.response.game.zoneShuffled with gameId, playerId and data', () => { - const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck' }); + const data = create(Event_ShuffleSchema, { zoneName: 'deck' }); shuffle(data, meta); expect(WebClient.instance.response.game.zoneShuffled).toHaveBeenCalledWith(5, 2, data); }); @@ -259,7 +287,7 @@ describe('shuffle event', () => { describe('rollDie event', () => { it('delegates to WebClient.instance.response.game.dieRolled with gameId, playerId and data', () => { - const data = create(Data.Event_RollDieSchema, { die: 6, result: 4 }); + const data = create(Event_RollDieSchema, { die: 6, result: 4 }); rollDie(data, meta); expect(WebClient.instance.response.game.dieRolled).toHaveBeenCalledWith(5, 2, data); }); @@ -267,7 +295,7 @@ describe('rollDie event', () => { describe('setActivePlayer event', () => { it('delegates to WebClient.instance.response.game.activePlayerSet with gameId and activePlayerId', () => { - const data = create(Data.Event_SetActivePlayerSchema, { activePlayerId: 3 }); + const data = create(Event_SetActivePlayerSchema, { activePlayerId: 3 }); setActivePlayer(data, meta); expect(WebClient.instance.response.game.activePlayerSet).toHaveBeenCalledWith(5, 3); }); @@ -275,7 +303,7 @@ describe('setActivePlayer event', () => { describe('setActivePhase event', () => { it('delegates to WebClient.instance.response.game.activePhaseSet with gameId and phase', () => { - const data = create(Data.Event_SetActivePhaseSchema, { phase: 4 }); + const data = create(Event_SetActivePhaseSchema, { phase: 4 }); setActivePhase(data, meta); expect(WebClient.instance.response.game.activePhaseSet).toHaveBeenCalledWith(5, 4); }); @@ -283,7 +311,7 @@ describe('setActivePhase event', () => { describe('reverseTurn event', () => { it('delegates to WebClient.instance.response.game.turnReversed with gameId and reversed', () => { - const data = create(Data.Event_ReverseTurnSchema, { reversed: true }); + const data = create(Event_ReverseTurnSchema, { reversed: true }); reverseTurn(data, meta); expect(WebClient.instance.response.game.turnReversed).toHaveBeenCalledWith(5, true); }); @@ -291,7 +319,7 @@ describe('reverseTurn event', () => { describe('dumpZone event', () => { it('delegates to WebClient.instance.response.game.zoneDumped with gameId, playerId and data', () => { - const data = create(Data.Event_DumpZoneSchema, { zoneName: 'hand' }); + const data = create(Event_DumpZoneSchema, { zoneName: 'hand' }); dumpZone(data, meta); expect(WebClient.instance.response.game.zoneDumped).toHaveBeenCalledWith(5, 2, data); }); @@ -299,7 +327,7 @@ describe('dumpZone event', () => { describe('changeZoneProperties event', () => { it('delegates to WebClient.instance.response.game.zonePropertiesChanged with gameId, playerId and data', () => { - const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); + const data = create(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 4c24f0373..ce39254c6 100644 --- a/webclient/src/websocket/events/game/gameHostChanged.ts +++ b/webclient/src/websocket/events/game/gameHostChanged.ts @@ -1,10 +1,10 @@ -import { Enriched } from '@app/types'; +import type { GameEventMeta } from '../../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: Enriched.GameEventMeta): void { +export function gameHostChanged(_data: {}, meta: 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 f84c54c0f..63a616cab 100644 --- a/webclient/src/websocket/events/game/gameSay.ts +++ b/webclient/src/websocket/events/game/gameSay.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_GameSay } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function gameSay(data: Data.Event_GameSay, meta: Enriched.GameEventMeta): void { +export function gameSay(data: Event_GameSay, meta: 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 533ffb3c9..0c431c611 100644 --- a/webclient/src/websocket/events/game/gameStateChanged.ts +++ b/webclient/src/websocket/events/game/gameStateChanged.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_GameStateChanged } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function gameStateChanged(data: Data.Event_GameStateChanged, meta: Enriched.GameEventMeta): void { +export function gameStateChanged(data: Event_GameStateChanged, meta: 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 e9931880c..8d2f69821 100644 --- a/webclient/src/websocket/events/game/index.ts +++ b/webclient/src/websocket/events/game/index.ts @@ -1,6 +1,41 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Data, Enriched } from '@app/types'; +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 '../../types'; import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; @@ -32,44 +67,44 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; -type GameRegistryEntry = Data.RegistryEntry; +type GameRegistryEntry = RegistryEntry; export type GameExtensionRegistry = GameRegistryEntry[]; function makeGameEntry( - ext: GenExtension, - handler: (value: V, meta: Enriched.GameEventMeta) => void, + ext: GenExtension, + handler: (value: V, meta: GameEventMeta) => void, ): GameRegistryEntry { - return Data.makeEntry(ext, handler); + return makeEntry(ext, handler); } export const GameEvents: GameExtensionRegistry = [ - 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), + 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), ]; diff --git a/webclient/src/websocket/events/game/joinGame.ts b/webclient/src/websocket/events/game/joinGame.ts index 1f3b627d0..a6275df12 100644 --- a/webclient/src/websocket/events/game/joinGame.ts +++ b/webclient/src/websocket/events/game/joinGame.ts @@ -1,7 +1,8 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_Join } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function joinGame(data: Data.Event_Join, meta: Enriched.GameEventMeta): void { +export function joinGame(data: Event_Join, meta: 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 ca4a64d5e..5cbe7da21 100644 --- a/webclient/src/websocket/events/game/kicked.ts +++ b/webclient/src/websocket/events/game/kicked.ts @@ -1,6 +1,6 @@ -import { Enriched } from '@app/types'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function kicked(_data: {}, meta: Enriched.GameEventMeta): void { +export function kicked(_data: {}, meta: 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 2354e41ec..0fd11812a 100644 --- a/webclient/src/websocket/events/game/leaveGame.ts +++ b/webclient/src/websocket/events/game/leaveGame.ts @@ -1,6 +1,6 @@ -import { Enriched } from '@app/types'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function leaveGame(data: { reason: number }, meta: Enriched.GameEventMeta): void { +export function leaveGame(data: { reason: number }, meta: 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 d2f589d2b..67287eab2 100644 --- a/webclient/src/websocket/events/game/moveCard.ts +++ b/webclient/src/websocket/events/game/moveCard.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_MoveCard } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function moveCard(data: Data.Event_MoveCard, meta: Enriched.GameEventMeta): void { +export function moveCard(data: Event_MoveCard, meta: 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 5181d9ccc..c88dc3977 100644 --- a/webclient/src/websocket/events/game/playerPropertiesChanged.ts +++ b/webclient/src/websocket/events/game/playerPropertiesChanged.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_PlayerPropertiesChanged } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function playerPropertiesChanged(data: Data.Event_PlayerPropertiesChanged, meta: Enriched.GameEventMeta): void { +export function playerPropertiesChanged(data: Event_PlayerPropertiesChanged, meta: 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 8e2f814f5..078dba459 100644 --- a/webclient/src/websocket/events/game/revealCards.ts +++ b/webclient/src/websocket/events/game/revealCards.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_RevealCards } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function revealCards(data: Data.Event_RevealCards, meta: Enriched.GameEventMeta): void { +export function revealCards(data: Event_RevealCards, meta: 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 2b21074fa..94473aa07 100644 --- a/webclient/src/websocket/events/game/reverseTurn.ts +++ b/webclient/src/websocket/events/game/reverseTurn.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_ReverseTurn } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function reverseTurn(data: Data.Event_ReverseTurn, meta: Enriched.GameEventMeta): void { +export function reverseTurn(data: Event_ReverseTurn, meta: 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 ee8bf8098..7bcf5c3f8 100644 --- a/webclient/src/websocket/events/game/rollDie.ts +++ b/webclient/src/websocket/events/game/rollDie.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_RollDie } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function rollDie(data: Data.Event_RollDie, meta: Enriched.GameEventMeta): void { +export function rollDie(data: Event_RollDie, meta: 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 ad7c3ba88..3f433934f 100644 --- a/webclient/src/websocket/events/game/setActivePhase.ts +++ b/webclient/src/websocket/events/game/setActivePhase.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_SetActivePhase } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function setActivePhase(data: Data.Event_SetActivePhase, meta: Enriched.GameEventMeta): void { +export function setActivePhase(data: Event_SetActivePhase, meta: 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 569e1dadb..006964383 100644 --- a/webclient/src/websocket/events/game/setActivePlayer.ts +++ b/webclient/src/websocket/events/game/setActivePlayer.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_SetActivePlayer } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function setActivePlayer(data: Data.Event_SetActivePlayer, meta: Enriched.GameEventMeta): void { +export function setActivePlayer(data: Event_SetActivePlayer, meta: 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 a3229df7f..d2d3c6e73 100644 --- a/webclient/src/websocket/events/game/setCardAttr.ts +++ b/webclient/src/websocket/events/game/setCardAttr.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_SetCardAttr } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function setCardAttr(data: Data.Event_SetCardAttr, meta: Enriched.GameEventMeta): void { +export function setCardAttr(data: Event_SetCardAttr, meta: 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 622e1af54..ae9fa9ccd 100644 --- a/webclient/src/websocket/events/game/setCardCounter.ts +++ b/webclient/src/websocket/events/game/setCardCounter.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_SetCardCounter } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function setCardCounter(data: Data.Event_SetCardCounter, meta: Enriched.GameEventMeta): void { +export function setCardCounter(data: Event_SetCardCounter, meta: 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 e62f695ba..26c35c08e 100644 --- a/webclient/src/websocket/events/game/setCounter.ts +++ b/webclient/src/websocket/events/game/setCounter.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_SetCounter } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function setCounter(data: Data.Event_SetCounter, meta: Enriched.GameEventMeta): void { +export function setCounter(data: Event_SetCounter, meta: 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 b784bb634..1b0bb17ad 100644 --- a/webclient/src/websocket/events/game/shuffle.ts +++ b/webclient/src/websocket/events/game/shuffle.ts @@ -1,6 +1,7 @@ -import type { Data, Enriched } from '@app/types'; +import type { Event_Shuffle } from '@app/generated'; +import type { GameEventMeta } from '../../types'; import { WebClient } from '../../WebClient'; -export function shuffle(data: Data.Event_Shuffle, meta: Enriched.GameEventMeta): void { +export function shuffle(data: Event_Shuffle, meta: 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 c02805659..2b27954d2 100644 --- a/webclient/src/websocket/events/room/index.ts +++ b/webclient/src/websocket/events/room/index.ts @@ -1,6 +1,15 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Data } from '@app/types'; +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 { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -8,20 +17,20 @@ import { listGames } from './listGames'; import { roomSay } from './roomSay'; import { removeMessages } from './removeMessages'; -type RoomRegistryEntry = Data.RegistryEntry; +type RoomRegistryEntry = RegistryEntry; export type RoomExtensionRegistry = RoomRegistryEntry[]; function makeRoomEntry( - ext: GenExtension, - handler: (value: V, roomEvent: Data.RoomEvent) => void, + ext: GenExtension, + handler: (value: V, roomEvent: RoomEvent) => void, ): RoomRegistryEntry { - return Data.makeEntry(ext, handler); + return makeEntry(ext, handler); } export const RoomEvents: RoomExtensionRegistry = [ - 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), + 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), ]; diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts index f4d31943c..203dcebe5 100644 --- a/webclient/src/websocket/events/room/joinRoom.ts +++ b/webclient/src/websocket/events/room/joinRoom.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_JoinRoom, RoomEvent } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function joinRoom({ userInfo }: Data.Event_JoinRoom, { roomId }: Data.RoomEvent): void { +export function joinRoom({ userInfo }: Event_JoinRoom, { roomId }: 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 b03bda6e2..819e8f438 100644 --- a/webclient/src/websocket/events/room/leaveRoom.ts +++ b/webclient/src/websocket/events/room/leaveRoom.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_LeaveRoom, RoomEvent } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function leaveRoom({ name }: Data.Event_LeaveRoom, { roomId }: Data.RoomEvent): void { +export function leaveRoom({ name }: Event_LeaveRoom, { roomId }: 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 b96596223..a06d4742f 100644 --- a/webclient/src/websocket/events/room/listGames.ts +++ b/webclient/src/websocket/events/room/listGames.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_ListGames, RoomEvent } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function listGames({ gameList }: Data.Event_ListGames, { roomId }: Data.RoomEvent): void { +export function listGames({ gameList }: Event_ListGames, { roomId }: 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 343635259..0c196438e 100644 --- a/webclient/src/websocket/events/room/removeMessages.ts +++ b/webclient/src/websocket/events/room/removeMessages.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_RemoveMessages, RoomEvent } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function removeMessages({ name, amount }: Data.Event_RemoveMessages, { roomId }: Data.RoomEvent): void { +export function removeMessages({ name, amount }: Event_RemoveMessages, { roomId }: 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 142de91d1..3bf1e01f4 100644 --- a/webclient/src/websocket/events/room/roomEvents.spec.ts +++ b/webclient/src/websocket/events/room/roomEvents.spec.ts @@ -14,8 +14,16 @@ vi.mock('../../WebClient', () => ({ }, })); +import { useWebClientCleanup } from '../../__mocks__/helpers'; import { create } from '@bufbuild/protobuf'; -import { Data } from '@app/types'; +import { + Event_JoinRoomSchema, + Event_LeaveRoomSchema, + Event_ListGamesSchema, + Event_RemoveMessagesSchema, + Event_RoomSaySchema, + RoomEventSchema, +} from '@app/generated'; import { WebClient } from '../../WebClient'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -23,12 +31,14 @@ import { listGames } from './listGames'; import { removeMessages } from './removeMessages'; import { roomSay } from './roomSay'; -const makeRoomEvent = (roomId: number) => create(Data.RoomEventSchema, { roomId }); +useWebClientCleanup(); + +const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId }); describe('joinRoom room event', () => { it('calls response.room.userJoined with roomId and userInfo', () => { - const data = create(Data.Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); + const data = create(Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); joinRoom(data, makeRoomEvent(3)); expect(WebClient.instance.response.room.userJoined).toHaveBeenCalledWith(3, data.userInfo); }); @@ -37,7 +47,7 @@ describe('joinRoom room event', () => { describe('leaveRoom room event', () => { it('calls response.room.userLeft with roomId and name', () => { - leaveRoom(create(Data.Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); + leaveRoom(create(Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); expect(WebClient.instance.response.room.userLeft).toHaveBeenCalledWith(4, 'alice'); }); }); @@ -45,7 +55,7 @@ describe('leaveRoom room event', () => { describe('listGames room event', () => { it('calls response.room.updateGames with roomId and gameList', () => { - const data = create(Data.Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); + const data = create(Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); listGames(data, makeRoomEvent(5)); expect(WebClient.instance.response.room.updateGames).toHaveBeenCalledWith(5, data.gameList); }); @@ -54,7 +64,7 @@ describe('listGames room event', () => { describe('removeMessages room event', () => { it('calls response.room.removeMessages with roomId, name, amount', () => { - removeMessages(create(Data.Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); + removeMessages(create(Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); expect(WebClient.instance.response.room.removeMessages).toHaveBeenCalledWith(6, 'bob', 10); }); }); @@ -66,7 +76,7 @@ describe('roomSay room event', () => { afterEach(() => vi.useRealTimers()); it('calls response.room.addMessage with roomId and message', () => { - const data = create(Data.Event_RoomSaySchema, { message: 'hello' }); + const data = create(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 c712bd89c..919189093 100644 --- a/webclient/src/websocket/events/room/roomSay.ts +++ b/webclient/src/websocket/events/room/roomSay.ts @@ -1,9 +1,7 @@ -import type { Data } from '@app/types'; -import { Enriched } from '@app/types'; +import type { Event_RoomSay, RoomEvent } from '@app/generated'; import { WebClient } from '../../WebClient'; - -export function roomSay(data: Data.Event_RoomSay, { roomId }: Data.RoomEvent): void { - const message: Enriched.Message = { ...data, timeReceived: Date.now() }; +export function roomSay(data: Event_RoomSay, { roomId }: RoomEvent): void { + const 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 819234e94..a8e27a654 100644 --- a/webclient/src/websocket/events/session/addToList.ts +++ b/webclient/src/websocket/events/session/addToList.ts @@ -1,7 +1,7 @@ -import type { Data } from '@app/types'; +import type { Event_AddToList } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function addToList({ listName, userInfo }: Data.Event_AddToList): void { +export function addToList({ listName, userInfo }: 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 fcde2fceb..f3fbd7343 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -1,7 +1,8 @@ -import { App, Data } from '@app/types'; +import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated'; +import { StatusEnum } from '../../StatusEnum'; import { updateStatus } from '../../commands/session'; -export function connectionClosed({ reason, reasonStr, endTime }: Data.Event_ConnectionClosed): void { +export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void { let message: string; // @TODO (5) @@ -9,35 +10,35 @@ export function connectionClosed({ reason, reasonStr, endTime }: Data.Event_Conn message = reasonStr; } else { switch (reason) { - case Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: + case Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: message = 'The server has reached its maximum user capacity'; break; - case Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: + case Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: message = 'There are too many concurrent connections from your address'; break; - case Data.Event_ConnectionClosed_CloseReason.BANNED: + case 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 Data.Event_ConnectionClosed_CloseReason.DEMOTED: + case Event_ConnectionClosed_CloseReason.DEMOTED: message = 'You were demoted'; break; - case Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: + case Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: message = 'Scheduled server shutdown'; break; - case Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID: + case Event_ConnectionClosed_CloseReason.USERNAMEINVALID: message = 'Invalid username'; break; - case Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: + case Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: message = 'You have been logged out due to logging in at another location'; break; - case Data.Event_ConnectionClosed_CloseReason.OTHER: + case Event_ConnectionClosed_CloseReason.OTHER: default: message = 'Unknown reason'; break; } } - updateStatus(App.StatusEnum.DISCONNECTED, message); + updateStatus(StatusEnum.DISCONNECTED, message); } diff --git a/webclient/src/websocket/events/session/gameJoined.ts b/webclient/src/websocket/events/session/gameJoined.ts index ed38a4ba1..83a555949 100644 --- a/webclient/src/websocket/events/session/gameJoined.ts +++ b/webclient/src/websocket/events/session/gameJoined.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_GameJoined } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function gameJoined(gameJoined: Data.Event_GameJoined): void { +export function gameJoined(gameJoined: 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 3d5935c9f..847ea3246 100644 --- a/webclient/src/websocket/events/session/index.ts +++ b/webclient/src/websocket/events/session/index.ts @@ -1,6 +1,24 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Data } from '@app/types'; +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 { addToList } from './addToList'; import { connectionClosed } from './connectionClosed'; @@ -17,29 +35,29 @@ import { userLeft } from './userLeft'; import { userMessage } from './userMessage'; import { gameJoined } from './gameJoined'; -type SessionRegistryEntry = Data.RegistryEntry; +type SessionRegistryEntry = RegistryEntry; export type SessionExtensionRegistry = SessionRegistryEntry[]; function makeSessionEntry( - ext: GenExtension, + ext: GenExtension, handler: (value: V) => void, ): SessionRegistryEntry { - return Data.makeEntry(ext, handler); + return makeEntry(ext, handler); } export const SessionEvents: SessionExtensionRegistry = [ - 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), + 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), ]; diff --git a/webclient/src/websocket/events/session/listRooms.ts b/webclient/src/websocket/events/session/listRooms.ts index 30f3285e6..8dac345a0 100644 --- a/webclient/src/websocket/events/session/listRooms.ts +++ b/webclient/src/websocket/events/session/listRooms.ts @@ -1,9 +1,9 @@ -import type { Data } from '@app/types'; +import type { Event_ListRooms } from '@app/generated'; import { CLIENT_OPTIONS } from '../../config'; import { joinRoom } from '../../commands/session'; import { WebClient } from '../../WebClient'; -export function listRooms({ roomList }: Data.Event_ListRooms): void { +export function listRooms({ roomList }: 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 8a6184edf..df8840781 100644 --- a/webclient/src/websocket/events/session/notifyUser.ts +++ b/webclient/src/websocket/events/session/notifyUser.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_NotifyUser } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function notifyUser(payload: Data.Event_NotifyUser): void { +export function notifyUser(payload: 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 3e5feae83..8b4db5b21 100644 --- a/webclient/src/websocket/events/session/removeFromList.ts +++ b/webclient/src/websocket/events/session/removeFromList.ts @@ -1,7 +1,7 @@ -import type { Data } from '@app/types'; +import type { Event_RemoveFromList } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function removeFromList({ listName, userName }: Data.Event_RemoveFromList): void { +export function removeFromList({ listName, userName }: 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 9acc7135b..8fe1e2b89 100644 --- a/webclient/src/websocket/events/session/replayAdded.ts +++ b/webclient/src/websocket/events/session/replayAdded.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_ReplayAdded } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function replayAdded({ matchInfo }: Data.Event_ReplayAdded): void { +export function replayAdded({ matchInfo }: 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 e8751bb32..df2b0c05a 100644 --- a/webclient/src/websocket/events/session/serverCompleteList.ts +++ b/webclient/src/websocket/events/session/serverCompleteList.ts @@ -1,7 +1,7 @@ -import type { Data } from '@app/types'; +import type { Event_ServerCompleteList } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function serverCompleteList({ userList, roomList }: Data.Event_ServerCompleteList): void { +export function serverCompleteList({ userList, roomList }: 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 79ac7a101..347558b89 100644 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -1,88 +1,6 @@ -import { App, Data, Enriched } from '@app/types'; - +import type { Event_ServerIdentification } from '@app/generated'; import { WebClient } from '../../WebClient'; -import { PROTOCOL_VERSION } from '../../config'; -import { - activate, - disconnect, - login, - register, - requestPasswordSalt, - forgotPasswordChallenge, - forgotPasswordRequest, - forgotPasswordReset, - updateStatus, -} from '../../commands/session'; -import { generateSalt, passwordSaltSupported } from '../../utils'; -export function serverIdentification(info: Data.Event_ServerIdentification): void { - const { serverName, serverVersion, protocolVersion, serverOptions } = info; - if (protocolVersion !== PROTOCOL_VERSION) { - updateStatus(App.StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); - disconnect(); - return; - } - const getPasswordSalt = passwordSaltSupported(serverOptions); - const options = WebClient.instance.options; - - if (!options) { - 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 App.WebSocketConnectReason.LOGIN: { - const { password, ...rest } = options; - updateStatus(App.StatusEnum.LOGGING_IN, 'Logging In...'); - if (getPasswordSalt) { - requestPasswordSalt(rest, password); - } else { - login(rest, password); - } - break; - } - case App.WebSocketConnectReason.REGISTER: { - const { password, ...rest } = options; - const passwordSalt = getPasswordSalt ? generateSalt() : null; - register(rest, password, passwordSalt); - break; - } - case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: { - const { password, ...rest } = options; - if (getPasswordSalt) { - requestPasswordSalt(rest, password); - } else { - activate(rest, password); - } - break; - } - case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: - forgotPasswordRequest(options); - break; - case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - forgotPasswordChallenge(options); - break; - case App.WebSocketConnectReason.PASSWORD_RESET: { - const { newPassword, ...rest } = options; - if (getPasswordSalt) { - requestPasswordSalt(rest, undefined, newPassword); - } else { - forgotPasswordReset(rest, newPassword); - } - break; - } - default: { - const { reason } = options as Enriched.WebSocketConnectOptions; - updateStatus(App.StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${reason}`); - disconnect(); - break; - } - } - - WebClient.instance.options = null; - WebClient.instance.response.session.updateInfo(serverName, serverVersion); +export function serverIdentification(info: Event_ServerIdentification): void { + WebClient.instance.config.onServerIdentified(info); } diff --git a/webclient/src/websocket/events/session/serverMessage.ts b/webclient/src/websocket/events/session/serverMessage.ts index 42bb14f82..c7f3b7b9d 100644 --- a/webclient/src/websocket/events/session/serverMessage.ts +++ b/webclient/src/websocket/events/session/serverMessage.ts @@ -1,7 +1,7 @@ -import type { Data } from '@app/types'; +import type { Event_ServerMessage } from '@app/generated'; import { WebClient } from '../../WebClient'; import { sanitizeHtml } from '../../utils'; -export function serverMessage({ message }: Data.Event_ServerMessage): void { +export function serverMessage({ message }: 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 a0fce78c1..cdd404739 100644 --- a/webclient/src/websocket/events/session/serverShutdown.ts +++ b/webclient/src/websocket/events/session/serverShutdown.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_ServerShutdown } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function serverShutdown(payload: Data.Event_ServerShutdown): void { +export function serverShutdown(payload: 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 4ac961a5d..0cf649f7f 100644 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ b/webclient/src/websocket/events/session/sessionEvents.spec.ts @@ -4,7 +4,7 @@ vi.mock('../../WebClient', () => ({ WebClient: { instance: { - options: null, + config: { onServerIdentified: vi.fn() }, response: { session: { gameJoined: vi.fn(), @@ -33,35 +33,44 @@ vi.mock('../../WebClient', () => ({ vi.mock('../../config', () => ({ CLIENT_OPTIONS: { autojoinrooms: false }, - PROTOCOL_VERSION: 14, })); vi.mock('../../commands/session', () => ({ joinRoom: vi.fn(), updateStatus: vi.fn(), disconnect: vi.fn(), - login: vi.fn(), - register: vi.fn(), - activate: vi.fn(), - requestPasswordSalt: vi.fn(), - forgotPasswordRequest: vi.fn(), - forgotPasswordChallenge: vi.fn(), - forgotPasswordReset: vi.fn(), })); vi.mock('../../utils', () => ({ - generateSalt: vi.fn().mockReturnValue('newSalt'), - passwordSaltSupported: vi.fn().mockReturnValue(0), sanitizeHtml: vi.fn((msg: string) => msg), })); -import { App, Data, Enriched } from '@app/types'; +import { useWebClientCleanup } from '../../__mocks__/helpers'; +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 { create } from '@bufbuild/protobuf'; import { WebClient } from '../../WebClient'; import * as Config from '../../config'; import * as SessionCmds from '../../commands/session'; -import * as Utils from '../../utils'; import { gameJoined } from './gameJoined'; import { notifyUser } from './notifyUser'; import { replayAdded } from './replayAdded'; @@ -76,23 +85,18 @@ import { removeFromList } from './removeFromList'; import { listRooms } from './listRooms'; import { connectionClosed } from './connectionClosed'; import { serverIdentification } from './serverIdentification'; -import { Mock } from 'vitest'; +useWebClientCleanup(); 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(Data.Event_GameJoinedSchema, { playerId: 1 }); + const data = create(Event_GameJoinedSchema, { playerId: 1 }); gameJoined(data); expect(WebClient.instance.response.session.gameJoined).toHaveBeenCalledWith(data); }); @@ -104,7 +108,7 @@ describe('gameJoined', () => { describe('notifyUser', () => { it('calls WebClient.instance.response.session.notifyUser', () => { - const data = create(Data.Event_NotifyUserSchema, { warningReason: 'yo' }); + const data = create(Event_NotifyUserSchema, { warningReason: 'yo' }); notifyUser(data); expect(WebClient.instance.response.session.notifyUser).toHaveBeenCalledWith(data); }); @@ -116,8 +120,8 @@ describe('notifyUser', () => { describe('replayAdded', () => { it('calls WebClient.instance.response.session.replayAdded with matchInfo', () => { - const data = create(Data.Event_ReplayAddedSchema, { - matchInfo: create(Data.ServerInfo_ReplayMatchSchema, { gameId: 42 }), + const data = create(Event_ReplayAddedSchema, { + matchInfo: create(ServerInfo_ReplayMatchSchema, { gameId: 42 }), }); replayAdded(data); expect(WebClient.instance.response.session.replayAdded).toHaveBeenCalledWith(data.matchInfo); @@ -130,7 +134,7 @@ describe('replayAdded', () => { describe('serverCompleteList', () => { it('calls WebClient.instance.response.session.updateUsers and WebClient.instance.response.room.updateRooms', () => { - const data = create(Data.Event_ServerCompleteListSchema, { userList: [], roomList: [] }); + const data = create(Event_ServerCompleteListSchema, { userList: [], roomList: [] }); serverCompleteList(data); expect(WebClient.instance.response.session.updateUsers).toHaveBeenCalledWith(data.userList); expect(WebClient.instance.response.room.updateRooms).toHaveBeenCalledWith(data.roomList); @@ -143,7 +147,7 @@ describe('serverCompleteList', () => { describe('serverMessage', () => { it('calls WebClient.instance.response.session.serverMessage with message', () => { - serverMessage(create(Data.Event_ServerMessageSchema, { message: 'hello server' })); + serverMessage(create(Event_ServerMessageSchema, { message: 'hello server' })); expect(WebClient.instance.response.session.serverMessage).toHaveBeenCalledWith('hello server'); }); }); @@ -154,7 +158,7 @@ describe('serverMessage', () => { describe('serverShutdown', () => { it('calls WebClient.instance.response.session.serverShutdown', () => { - const payload = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance' }); + const payload = create(Event_ServerShutdownSchema, { reason: 'maintenance' }); serverShutdown(payload); expect(WebClient.instance.response.session.serverShutdown).toHaveBeenCalledWith(payload); }); @@ -166,8 +170,8 @@ describe('serverShutdown', () => { describe('userJoined', () => { it('calls WebClient.instance.response.session.userJoined with userInfo', () => { - const data = create(Data.Event_UserJoinedSchema, { - userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), + const data = create(Event_UserJoinedSchema, { + userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), }); userJoined(data); expect(WebClient.instance.response.session.userJoined).toHaveBeenCalledWith(data.userInfo); @@ -180,7 +184,7 @@ describe('userJoined', () => { describe('userLeft', () => { it('calls WebClient.instance.response.session.userLeft with name', () => { - userLeft(create(Data.Event_UserLeftSchema, { name: 'bob' })); + userLeft(create(Event_UserLeftSchema, { name: 'bob' })); expect(WebClient.instance.response.session.userLeft).toHaveBeenCalledWith('bob'); }); }); @@ -191,7 +195,7 @@ describe('userLeft', () => { describe('userMessage', () => { it('calls WebClient.instance.response.session.userMessage', () => { - const payload = create(Data.Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); + const payload = create(Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); userMessage(payload); expect(WebClient.instance.response.session.userMessage).toHaveBeenCalledWith(payload); }); @@ -210,25 +214,25 @@ describe('addToList', () => { }); it('buddy list → addToBuddyList', () => { - const data = create(Data.Event_AddToListSchema, { + const data = create(Event_AddToListSchema, { listName: 'buddy', - userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), + userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), }); addToList(data); expect(WebClient.instance.response.session.addToBuddyList).toHaveBeenCalledWith(data.userInfo); }); it('ignore list → addToIgnoreList', () => { - const data = create(Data.Event_AddToListSchema, { + const data = create(Event_AddToListSchema, { listName: 'ignore', - userInfo: create(Data.ServerInfo_UserSchema, { name: 'bob' }), + userInfo: create(ServerInfo_UserSchema, { name: 'bob' }), }); addToList(data); expect(WebClient.instance.response.session.addToIgnoreList).toHaveBeenCalledWith(data.userInfo); }); it('unknown list → console.log', () => { - addToList(create(Data.Event_AddToListSchema, { listName: 'unknown' })); + addToList(create(Event_AddToListSchema, { listName: 'unknown' })); expect(logSpy).toHaveBeenCalled(); }); }); @@ -239,18 +243,18 @@ describe('addToList', () => { describe('removeFromList', () => { it('buddy list → removeFromBuddyList', () => { - removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); + removeFromList(create(Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); expect(WebClient.instance.response.session.removeFromBuddyList).toHaveBeenCalledWith('alice'); }); it('ignore list → removeFromIgnoreList', () => { - removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); + removeFromList(create(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(Data.Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); + removeFromList(create(Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); expect(logSpy).toHaveBeenCalled(); logSpy.mockRestore(); }); @@ -262,24 +266,24 @@ describe('removeFromList', () => { describe('listRooms', () => { it('calls WebClient.instance.response.room.updateRooms', () => { - listRooms(create(Data.Event_ListRoomsSchema, { roomList: [] })); + listRooms(create(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(Data.Event_ListRoomsSchema, { - roomList: [create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 1 })] + listRooms(create(Event_ListRoomsSchema, { + roomList: [create(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(Data.Event_ListRoomsSchema, { + listRooms(create(Event_ListRoomsSchema, { roomList: [ - create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), - create(Data.ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) + create(ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), + create(ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) ] })); expect(SessionCmds.joinRoom).toHaveBeenCalledTimes(1); @@ -293,12 +297,12 @@ describe('listRooms', () => { describe('connectionClosed', () => { it('uses reasonStr when provided', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom'); }); it('USER_LIMIT_REACHED → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('maximum user capacity') @@ -306,43 +310,43 @@ describe('connectionClosed', () => { }); it('TOO_MANY_CONNECTIONS → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('too many concurrent')); }); it('BANNED → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('DEMOTED → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.DEMOTED })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.DEMOTED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted')); }); it('SERVER_SHUTDOWN → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('shutdown')); }); it('USERNAMEINVALID → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username')); }); it('LOGGEDINELSEWERE → specific message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out')); }); it('OTHER → "Unknown reason"', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.OTHER })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.OTHER })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'Unknown reason'); }); it('BANNED with valid positive endTime → shows formatted date', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { - reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, + connectionClosed(create(Event_ConnectionClosedSchema, { + reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), @@ -351,30 +355,30 @@ describe('connectionClosed', () => { }); it('BANNED with endTime = 0 → shows generic banned message', () => { - connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: 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(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: 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(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN })); + connectionClosed(create(Event_ConnectionClosedSchema, { reason: 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(Data.Event_ConnectionClosedSchema, { - reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity, + connectionClosed(create(Event_ConnectionClosedSchema, { + reason: 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(Data.Event_ConnectionClosedSchema, - { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); + connectionClosed(create(Event_ConnectionClosedSchema, + { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason'); }); }); @@ -384,156 +388,10 @@ describe('connectionClosed', () => { // ---------------------------------------------------------------- describe('serverIdentification', () => { - beforeEach(() => { - ConfigMock.PROTOCOL_VERSION = 14; - WebClient.instance.options = null; - }); - - 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(); - }); - - 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 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.not.objectContaining({ password: expect.anything() }), - 'secret' - ); - }); - - 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', - }; - (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Data.Event_ServerIdentificationSchema, - { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); - expect(SessionCmds.register).toHaveBeenCalledWith( - expect.not.objectContaining({ password: expect.anything() }), - 'secret', - null - ); - }); - - 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', - }; - (Utils.passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(create(Data.Event_ServerIdentificationSchema, - { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); - expect(SessionCmds.register).toHaveBeenCalledWith( - expect.not.objectContaining({ password: expect.anything() }), - 'secret', - 'newSalt' - ); - }); - - 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', - }; - (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Data.Event_ServerIdentificationSchema, - { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); - expect(SessionCmds.activate).toHaveBeenCalledWith( - expect.not.objectContaining({ password: expect.anything() }), - 'secret' - ); - }); - - 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', - }; - (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({ password: expect.anything() }), - 'secret' - ); - }); - - 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(); + it('calls config.onServerIdentified with the event info', () => { + const info = create(Event_ServerIdentificationSchema, + { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 }); + serverIdentification(info); + expect(WebClient.instance.config.onServerIdentified).toHaveBeenCalledWith(info); }); }); diff --git a/webclient/src/websocket/events/session/userJoined.ts b/webclient/src/websocket/events/session/userJoined.ts index e474bf6e0..5f0e6014d 100644 --- a/webclient/src/websocket/events/session/userJoined.ts +++ b/webclient/src/websocket/events/session/userJoined.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_UserJoined } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function userJoined({ userInfo }: Data.Event_UserJoined): void { +export function userJoined({ userInfo }: 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 fe20593da..f53bff2d4 100644 --- a/webclient/src/websocket/events/session/userLeft.ts +++ b/webclient/src/websocket/events/session/userLeft.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_UserLeft } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function userLeft({ name }: Data.Event_UserLeft): void { +export function userLeft({ name }: 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 aecbd01b0..5b3ca5905 100644 --- a/webclient/src/websocket/events/session/userMessage.ts +++ b/webclient/src/websocket/events/session/userMessage.ts @@ -1,6 +1,6 @@ -import type { Data } from '@app/types'; +import type { Event_UserMessage } from '@app/generated'; import { WebClient } from '../../WebClient'; -export function userMessage(payload: Data.Event_UserMessage): void { +export function userMessage(payload: Event_UserMessage): void { WebClient.instance.response.session.userMessage(payload); } diff --git a/webclient/src/websocket/index.ts b/webclient/src/websocket/index.ts index 61a67c4db..47325e509 100644 --- a/webclient/src/websocket/index.ts +++ b/webclient/src/websocket/index.ts @@ -2,3 +2,17 @@ export * from './commands'; export * from './interfaces'; export { WebClient } from './WebClient'; +export { StatusEnum } from './StatusEnum'; +export type { WebClientConfig, ConnectTarget } from './WebClientConfig'; +export type { + KeyOf, + GameEventMeta, + WebSocketSessionResponseOverrides, + WebSocketRoomResponseOverrides, +} from './types'; + +export { SessionEvents } from './events/session'; +export { RoomEvents } from './events/room'; +export { GameEvents } from './events/game'; + +export { generateSalt, passwordSaltSupported, hashPassword } from './utils'; diff --git a/webclient/src/websocket/interfaces/WebClientRequest.ts b/webclient/src/websocket/interfaces/WebClientRequest.ts index ba44e9841..b6a8965a8 100644 --- a/webclient/src/websocket/interfaces/WebClientRequest.ts +++ b/webclient/src/websocket/interfaces/WebClientRequest.ts @@ -1,13 +1,68 @@ -import { Data, Enriched } from '@app/types'; +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'; -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; +import type { ConnectTarget } from '../WebClientConfig'; +import type { KeyOf } from '../types'; + +// ── 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; disconnect(): void; } @@ -52,49 +107,51 @@ export interface IModeratorRequest { getBanHistory(userName: string): void; getWarnHistory(userName: string): void; getWarnList(modName: string, userName: string, userClientid: string): void; - viewLogHistory(filters: Data.ViewLogHistoryParams): void; + viewLogHistory(filters: ViewLogHistoryParams): void; warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void; } export interface IGameRequest { leaveGame(gameId: number): void; - kickFromGame(gameId: number, params: Data.KickFromGameParams): void; - gameSay(gameId: number, params: Data.GameSayParams): void; - readyStart(gameId: number, params: Data.ReadyStartParams): void; + kickFromGame(gameId: number, params: KickFromGameParams): void; + gameSay(gameId: number, params: GameSayParams): void; + readyStart(gameId: number, params: ReadyStartParams): void; concede(gameId: number): void; unconcede(gameId: number): void; - judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void; + judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void; nextTurn(gameId: number): void; - setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void; + setActivePhase(gameId: number, params: SetActivePhaseParams): void; reverseTurn(gameId: number): 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; + 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; undoDraw(gameId: number): 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; + 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; } -export interface IWebClientRequest { - authentication: IAuthenticationRequest; +export interface IWebClientRequest< + A extends AuthRequestMap = AuthRequestMap, +> { + 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 8a60fc4e9..898557c67 100644 --- a/webclient/src/websocket/interfaces/WebClientResponse.ts +++ b/webclient/src/websocket/interfaces/WebClientResponse.ts @@ -1,28 +1,79 @@ -import { App, Data, Enriched } from '@app/types'; +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'; -export interface ISessionResponse { +import type { StatusEnum } from '../StatusEnum'; +import type { + KeyOf, + WebSocketSessionResponseOverrides, + WebSocketRoomResponseOverrides, +} from '../types'; + +export interface ISessionResponse { initialized(): void; connectionAttempted(): void; clearStore(): void; - loginSuccessful(options: Enriched.LoginSuccessContext): void; + loginSuccessful(result: T[KeyOf]): void; loginFailed(): void; connectionFailed(): void; testConnectionSuccessful(): void; testConnectionFailed(): void; - updateBuddyList(buddyList: Data.ServerInfo_User[]): void; - addToBuddyList(user: Data.ServerInfo_User): void; + updateBuddyList(buddyList: ServerInfo_User[]): void; + addToBuddyList(user: ServerInfo_User): void; removeFromBuddyList(userName: string): void; - updateIgnoreList(ignoreList: Data.ServerInfo_User[]): void; - addToIgnoreList(user: Data.ServerInfo_User): void; + updateIgnoreList(ignoreList: ServerInfo_User[]): void; + addToIgnoreList(user: ServerInfo_User): void; removeFromIgnoreList(userName: string): void; updateInfo(name: string, version: string): 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; + updateStatus(state: StatusEnum, description: string): void; + updateUser(user: ServerInfo_User): void; + updateUsers(users: ServerInfo_User[]): void; + userJoined(user: ServerInfo_User): void; userLeft(userName: string): void; serverMessage(message: string): void; - accountAwaitingActivation(options: Enriched.PendingActivationContext): void; + accountAwaitingActivation(result: T[KeyOf]): void; accountActivationSuccess(): void; accountActivationFailed(): void; registrationRequiresEmail(): void; @@ -38,36 +89,36 @@ export interface ISessionResponse { accountPasswordChange(): void; accountEditChanged(realName?: string, email?: string, country?: string): void; accountImageChanged(avatarBmp: Uint8Array): void; - getUserInfo(userInfo: Data.ServerInfo_User): void; - getGamesOfUser(userName: string, response: Data.Response_GetGamesOfUser): void; - gameJoined(gameJoinedData: Data.Event_GameJoined): void; - notifyUser(notification: Data.Event_NotifyUser): void; - playerPropertiesChanged(gameId: number, playerId: number, payload: Data.Event_PlayerPropertiesChanged): void; - serverShutdown(data: Data.Event_ServerShutdown): void; - userMessage(messageData: Data.Event_UserMessage): void; + getUserInfo(userInfo: ServerInfo_User): void; + getGamesOfUser(userName: string, response: Response_GetGamesOfUser): void; + gameJoined(gameJoinedData: Event_GameJoined): void; + notifyUser(notification: Event_NotifyUser): void; + playerPropertiesChanged(gameId: number, playerId: number, payload: Event_PlayerPropertiesChanged): void; + serverShutdown(data: Event_ServerShutdown): void; + userMessage(messageData: Event_UserMessage): void; addToList(list: string, userName: string): void; removeFromList(list: string, userName: string): void; deleteServerDeck(deckId: number): void; - updateServerDecks(deckList: Data.Response_DeckList): void; - uploadServerDeck(path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem): void; - downloadServerDeck(deckId: number, response: Data.Response_DeckDownload): void; + updateServerDecks(deckList: Response_DeckList): void; + uploadServerDeck(path: string, treeItem: ServerInfo_DeckStorage_TreeItem): void; + downloadServerDeck(deckId: number, response: Response_DeckDownload): void; createServerDeckDir(path: string, dirName: string): void; deleteServerDeckDir(path: string): void; - replayList(matchList: Data.ServerInfo_ReplayMatch[]): void; - replayAdded(matchInfo: Data.ServerInfo_ReplayMatch): void; + replayList(matchList: ServerInfo_ReplayMatch[]): void; + replayAdded(matchInfo: ServerInfo_ReplayMatch): void; replayModifyMatch(gameId: number, doNotHide: boolean): void; replayDeleteMatch(gameId: number): void; - replayDownloaded(replayId: number, response: Data.Response_ReplayDownload): void; + replayDownloaded(replayId: number, response: Response_ReplayDownload): void; } -export interface IRoomResponse { +export interface IRoomResponse { clearStore(): void; - joinRoom(roomInfo: Data.ServerInfo_Room): void; + joinRoom(roomInfo: ServerInfo_Room): void; leaveRoom(roomId: number): 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; + 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; userLeft(roomId: number, name: string): void; removeMessages(roomId: number, name: string, amount: number): void; gameCreated(roomId: number): void; @@ -76,35 +127,35 @@ export interface IRoomResponse { export interface IGameResponse { clearStore(): void; - gameStateChanged(gameId: number, data: Data.Event_GameStateChanged): void; - playerJoined(gameId: number, playerProperties: Data.ServerInfo_PlayerProperties): void; + gameStateChanged(gameId: number, data: Event_GameStateChanged): void; + playerJoined(gameId: number, playerProperties: ServerInfo_PlayerProperties): void; playerLeft(gameId: number, playerId: number, reason: number): void; - playerPropertiesChanged(gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties): void; + playerPropertiesChanged(gameId: number, playerId: number, properties: ServerInfo_PlayerProperties): void; gameClosed(gameId: number): void; gameHostChanged(gameId: number, hostId: number): void; kicked(gameId: number): void; gameSay(gameId: number, playerId: number, message: string): void; - cardMoved(gameId: number, playerId: number, data: Data.Event_MoveCard): void; - cardFlipped(gameId: number, playerId: number, data: Data.Event_FlipCard): void; - cardDestroyed(gameId: number, playerId: number, data: Data.Event_DestroyCard): void; - cardAttached(gameId: number, playerId: number, data: Data.Event_AttachCard): void; - tokenCreated(gameId: number, playerId: number, data: Data.Event_CreateToken): void; - cardAttrChanged(gameId: number, playerId: number, data: Data.Event_SetCardAttr): void; - cardCounterChanged(gameId: number, playerId: number, data: Data.Event_SetCardCounter): void; - arrowCreated(gameId: number, playerId: number, data: Data.Event_CreateArrow): void; - arrowDeleted(gameId: number, playerId: number, data: Data.Event_DeleteArrow): void; - counterCreated(gameId: number, playerId: number, data: Data.Event_CreateCounter): void; - counterSet(gameId: number, playerId: number, data: Data.Event_SetCounter): void; - counterDeleted(gameId: number, playerId: number, data: Data.Event_DelCounter): void; - cardsDrawn(gameId: number, playerId: number, data: Data.Event_DrawCards): void; - cardsRevealed(gameId: number, playerId: number, data: Data.Event_RevealCards): void; - zoneShuffled(gameId: number, playerId: number, data: Data.Event_Shuffle): void; - dieRolled(gameId: number, playerId: number, data: Data.Event_RollDie): void; + cardMoved(gameId: number, playerId: number, data: Event_MoveCard): void; + cardFlipped(gameId: number, playerId: number, data: Event_FlipCard): void; + cardDestroyed(gameId: number, playerId: number, data: Event_DestroyCard): void; + cardAttached(gameId: number, playerId: number, data: Event_AttachCard): void; + tokenCreated(gameId: number, playerId: number, data: Event_CreateToken): void; + cardAttrChanged(gameId: number, playerId: number, data: Event_SetCardAttr): void; + cardCounterChanged(gameId: number, playerId: number, data: Event_SetCardCounter): void; + arrowCreated(gameId: number, playerId: number, data: Event_CreateArrow): void; + arrowDeleted(gameId: number, playerId: number, data: Event_DeleteArrow): void; + counterCreated(gameId: number, playerId: number, data: Event_CreateCounter): void; + counterSet(gameId: number, playerId: number, data: Event_SetCounter): void; + counterDeleted(gameId: number, playerId: number, data: Event_DelCounter): void; + cardsDrawn(gameId: number, playerId: number, data: Event_DrawCards): void; + cardsRevealed(gameId: number, playerId: number, data: Event_RevealCards): void; + zoneShuffled(gameId: number, playerId: number, data: Event_Shuffle): void; + dieRolled(gameId: number, playerId: number, data: Event_RollDie): void; activePlayerSet(gameId: number, activePlayerId: number): void; activePhaseSet(gameId: number, phase: number): void; turnReversed(gameId: number, reversed: boolean): void; - zoneDumped(gameId: number, playerId: number, data: Data.Event_DumpZone): void; - zonePropertiesChanged(gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties): void; + zoneDumped(gameId: number, playerId: number, data: Event_DumpZone): void; + zonePropertiesChanged(gameId: number, playerId: number, data: Event_ChangeZoneProperties): void; } export interface IAdminResponse { @@ -116,10 +167,10 @@ export interface IAdminResponse { export interface IModeratorResponse { banFromServer(userName: string): void; - banHistory(userName: string, banHistory: Data.ServerInfo_Ban[]): void; - viewLogs(logs: Data.ServerInfo_ChatMessage[]): void; - warnHistory(userName: string, warnHistory: Data.ServerInfo_Warning[]): void; - warnListOptions(warnList: Data.Response_WarnList[]): void; + banHistory(userName: string, banHistory: ServerInfo_Ban[]): void; + viewLogs(logs: ServerInfo_ChatMessage[]): void; + warnHistory(userName: string, warnHistory: ServerInfo_Warning[]): void; + warnListOptions(warnList: Response_WarnList[]): void; warnUser(userName: string): void; grantReplayAccess(replayId: number, moderatorName: string): void; forceActivateUser(usernameToActivate: string, moderatorName: string): void; @@ -127,9 +178,12 @@ export interface IModeratorResponse { updateAdminNotes(userName: string, notes: string): void; } -export interface IWebClientResponse { - session: ISessionResponse; - room: IRoomResponse; +export interface IWebClientResponse< + S extends ResponseMap = WebSocketSessionResponseOverrides, + R extends RoomEventMap = WebSocketRoomResponseOverrides, +> { + session: ISessionResponse; + room: IRoomResponse; game: IGameResponse; admin: IAdminResponse; moderator: IModeratorResponse; diff --git a/webclient/src/websocket/interfaces/index.ts b/webclient/src/websocket/interfaces/index.ts index abe347cd5..9478bac59 100644 --- a/webclient/src/websocket/interfaces/index.ts +++ b/webclient/src/websocket/interfaces/index.ts @@ -8,6 +8,7 @@ export type { } from './WebClientResponse'; export type { + AuthRequestMap, IAuthenticationRequest, ISessionRequest, IRoomsRequest, diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts index 098869e82..f7701bebe 100644 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ b/webclient/src/websocket/services/ProtobufService.spec.ts @@ -7,66 +7,74 @@ vi.mock('@bufbuild/protobuf', async (importOriginal) => ({ setExtension: vi.fn(), })); -vi.mock('../events', () => ({ - GameEvents: [], - RoomEvents: [], - SessionEvents: [], -})); - vi.mock('../WebClient', () => ({ __esModule: true, default: {}, + WebClient: { _instance: null }, })); +import { useWebClientCleanup } from '../__mocks__/helpers'; import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; import { ProtobufService } from './ProtobufService'; -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 { EventRegistries } from './ProtobufService'; -import { Data } from '@app/types'; +import type { + AdminCommand, + GameCommand, + GameEvent, + ModeratorCommand, + Response, + RoomCommand, + RoomEvent, + SessionCommand, + SessionEvent, +} from '@app/generated'; +import { + CommandContainerSchema, + ResponseSchema, + ServerMessageSchema, + ServerMessage_MessageType, +} from '@app/generated'; 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; processServerResponse(response: unknown): void; }; +useWebClientCleanup(); + let mockSocket: { isOpen: ReturnType; send: ReturnType }; -let registryTeardowns: Array<() => void>; +let mockEvents: EventRegistries; beforeEach(() => { mockSocket = { isOpen: vi.fn().mockReturnValue(true), send: vi.fn(), }; - registryTeardowns = []; -}); - -afterEach(() => { - while (registryTeardowns.length > 0) { - registryTeardowns.pop()!(); - } + mockEvents = { + sessionEvents: [], + roomEvents: [], + gameEvents: [], + }; }); 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', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendSessionCommand(sessionExt, vi.fn()); expect((service as ProtobufInternal).cmdId).toBe(1); service.resetCommands(); @@ -77,25 +85,25 @@ describe('ProtobufService', () => { describe('sendCommand', () => { it('increments cmdId and stores callback', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); - service.sendCommand(create(Data.CommandContainerSchema), cb); + service.sendCommand(create(CommandContainerSchema), cb); expect((service as ProtobufInternal).cmdId).toBe(1); expect((service as ProtobufInternal).pendingCommands.get(1)).toBe(cb); }); it('sends encoded data when socket is OPEN', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); mockSocket.isOpen.mockReturnValue(true); - service.sendCommand(create(Data.CommandContainerSchema), vi.fn()); + service.sendCommand(create(CommandContainerSchema), vi.fn()); expect(mockSocket.send).toHaveBeenCalled(); }); it('does not register callback or increment cmdId when transport is closed', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); mockSocket.isOpen.mockReturnValue(false); const cb = vi.fn(); - service.sendCommand(create(Data.CommandContainerSchema), cb); + service.sendCommand(create(CommandContainerSchema), cb); expect(mockSocket.send).not.toHaveBeenCalled(); expect((service as ProtobufInternal).cmdId).toBe(0); expect((service as ProtobufInternal).pendingCommands.size).toBe(0); @@ -104,151 +112,151 @@ describe('ProtobufService', () => { describe('sendSessionCommand', () => { it('stores callback and increments cmdId', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendSessionCommand(sessionExt, {}); expect((service as ProtobufInternal).cmdId).toBe(1); expect((service as ProtobufInternal).pendingCommands.get(1)).toBeTypeOf('function'); }); it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); service.sendSessionCommand(sessionExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(Data.ResponseSchema)); + storedCb(create(ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendSessionCommand(sessionExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(ResponseSchema))).not.toThrow(); }); }); describe('sendRoomCommand', () => { it('stores callback and increments cmdId', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendRoomCommand(42, roomExt, {}); expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); service.sendRoomCommand(42, roomExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(Data.ResponseSchema)); + storedCb(create(ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendRoomCommand(42, roomExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(ResponseSchema))).not.toThrow(); }); }); describe('sendGameCommand', () => { it('stores callback and increments cmdId', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendGameCommand(7, gameExt, {}); expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); service.sendGameCommand(7, gameExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(Data.ResponseSchema)); + storedCb(create(ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendGameCommand(7, gameExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(ResponseSchema))).not.toThrow(); }); }); describe('sendModeratorCommand', () => { it('stores callback and increments cmdId', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendModeratorCommand(moderatorExt, {}); expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); service.sendModeratorCommand(moderatorExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(Data.ResponseSchema)); + storedCb(create(ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendModeratorCommand(moderatorExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(ResponseSchema))).not.toThrow(); }); }); describe('sendAdminCommand', () => { it('stores callback and increments cmdId', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendAdminCommand(adminExt, {}); expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); service.sendAdminCommand(adminExt, {}, { onResponse: cb }); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(Data.ResponseSchema)); + storedCb(create(ResponseSchema)); - expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); + expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); service.sendAdminCommand(adminExt, {}); const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); + expect(() => storedCb(create(ResponseSchema))).not.toThrow(); }); }); describe('handleMessageEvent', () => { it('routes RESPONSE message to processServerResponse', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const cb = vi.fn(); (service as ProtobufInternal).cmdId = 1; (service as ProtobufInternal).pendingCommands.set(1, cb); vi.mocked(fromBinary).mockReturnValue( - create(Data.ServerMessageSchema, { - messageType: Data.ServerMessage_MessageType.RESPONSE, - response: create(Data.ResponseSchema, { cmdId: BigInt(1) }), + create(ServerMessageSchema, { + messageType: ServerMessage_MessageType.RESPONSE, + response: create(ResponseSchema, { cmdId: BigInt(1) }), }) ); @@ -258,12 +266,12 @@ describe('ProtobufService', () => { }); it('routes ROOM_EVENT message', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const processRoomEvent = vi.spyOn(service as ProtobufInternal, 'processRoomEvent'); vi.mocked(fromBinary).mockReturnValue( - create(Data.ServerMessageSchema, { - messageType: Data.ServerMessage_MessageType.ROOM_EVENT, + create(ServerMessageSchema, { + messageType: ServerMessage_MessageType.ROOM_EVENT, }) ); @@ -272,12 +280,12 @@ describe('ProtobufService', () => { }); it('routes SESSION_EVENT message', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const processSessionEvent = vi.spyOn(service as ProtobufInternal, 'processSessionEvent'); vi.mocked(fromBinary).mockReturnValue( - create(Data.ServerMessageSchema, { - messageType: Data.ServerMessage_MessageType.SESSION_EVENT, + create(ServerMessageSchema, { + messageType: ServerMessage_MessageType.SESSION_EVENT, }) ); @@ -286,12 +294,12 @@ describe('ProtobufService', () => { }); it('routes GAME_EVENT_CONTAINER message', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const processGameEvent = vi.spyOn(service as ProtobufInternal, 'processGameEvent'); vi.mocked(fromBinary).mockReturnValue( - create(Data.ServerMessageSchema, { - messageType: Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER, + create(ServerMessageSchema, { + messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER, }) ); @@ -300,11 +308,11 @@ describe('ProtobufService', () => { }); it('logs unknown message types (default case)', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); vi.mocked(fromBinary).mockReturnValue( - create(Data.ServerMessageSchema, { + create(ServerMessageSchema, { messageType: 999, }) ); @@ -315,13 +323,13 @@ describe('ProtobufService', () => { }); it('does nothing when decoded message is null', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(fromBinary).mockReturnValue(null!); expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); }); it('catches and logs decode errors', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.mocked(fromBinary).mockImplementation(() => { throw new Error('decode error'); @@ -334,19 +342,19 @@ describe('ProtobufService', () => { describe('processGameEvent', () => { it('returns early when container has no eventList', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(false); (service as ProtobufInternal).processGameEvent(null, {}); expect(hasExtension).not.toHaveBeenCalled(); }); 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 }; - registryTeardowns.push(withEventRegistry(GameEvents as GameExtensionRegistry, [mockExt, handler])); + mockEvents.gameEvents.push([mockExt, handler] as any); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -359,12 +367,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 }; - registryTeardowns.push(withEventRegistry(GameEvents as GameExtensionRegistry, [mockExt, handler])); + mockEvents.gameEvents.push([mockExt, handler] as any); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -379,7 +387,7 @@ describe('ProtobufService', () => { describe('processServerResponse', () => { it('returns early when response is undefined', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); (service as ProtobufInternal).pendingCommands.set(1, vi.fn()); (service as ProtobufInternal).processServerResponse(undefined); expect((service as ProtobufInternal).pendingCommands.size).toBe(1); @@ -388,19 +396,19 @@ describe('ProtobufService', () => { describe('processRoomEvent', () => { it('returns early when event is undefined', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(false); (service as ProtobufInternal).processRoomEvent(undefined); expect(hasExtension).not.toHaveBeenCalled(); }); 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 }; - registryTeardowns.push(withEventRegistry(RoomEvents as RoomExtensionRegistry, [mockExt, handler])); + mockEvents.roomEvents.push([mockExt, handler] as any); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); @@ -413,19 +421,19 @@ describe('ProtobufService', () => { describe('processSessionEvent', () => { it('returns early when event is undefined', () => { - const service = new ProtobufService(mockSocket); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(false); (service as ProtobufInternal).processSessionEvent(undefined); expect(hasExtension).not.toHaveBeenCalled(); }); 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 }; - registryTeardowns.push(withEventRegistry(SessionEvents as SessionExtensionRegistry, [mockExt, handler])); + mockEvents.sessionEvents.push([mockExt, handler] as any); + const service = new ProtobufService(mockSocket, mockEvents); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index c4b6cd3ce..45cf3eecf 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,11 +1,31 @@ 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, + type RegistryEntry, + type GameEvent, +} from '@app/generated'; -import { GameEvents, RoomEvents, SessionEvents } from '../events'; -import { Data, Enriched } from '@app/types'; - - +import type { GameEventMeta } from '../types'; import { type CommandOptions, handleResponse } from './command-options'; export interface SocketTransport { @@ -13,14 +33,22 @@ export interface SocketTransport { isOpen(): boolean; } +export interface EventRegistries { + sessionEvents: RegistryEntry[]; + roomEvents: RegistryEntry[]; + gameEvents: RegistryEntry[]; +} + export class ProtobufService { private cmdId = 0; - private pendingCommands = new Map void>(); + private pendingCommands = new Map void>(); private transport: SocketTransport; + private events: EventRegistries; - constructor(transport: SocketTransport) { + constructor(transport: SocketTransport, events: EventRegistries) { this.transport = transport; + this.events = events; } public resetCommands() { @@ -30,13 +58,13 @@ export class ProtobufService { public sendGameCommand( gameId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const gameCmd = create(Data.GameCommandSchema); + const gameCmd = create(GameCommandSchema); setExtension(gameCmd, ext, value); - const cmd = create(Data.CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); + const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -46,13 +74,13 @@ export class ProtobufService { public sendRoomCommand( roomId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const roomCmd = create(Data.RoomCommandSchema); + const roomCmd = create(RoomCommandSchema); setExtension(roomCmd, ext, value); - const cmd = create(Data.CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); + const cmd = create(CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -61,13 +89,13 @@ export class ProtobufService { } public sendSessionCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const sesCmd = create(Data.SessionCommandSchema); + const sesCmd = create(SessionCommandSchema); setExtension(sesCmd, ext, value); - const cmd = create(Data.CommandContainerSchema, { sessionCommand: [sesCmd] }); + const cmd = create(CommandContainerSchema, { sessionCommand: [sesCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -76,13 +104,13 @@ export class ProtobufService { } public sendModeratorCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const modCmd = create(Data.ModeratorCommandSchema); + const modCmd = create(ModeratorCommandSchema); setExtension(modCmd, ext, value); - const cmd = create(Data.CommandContainerSchema, { moderatorCommand: [modCmd] }); + const cmd = create(CommandContainerSchema, { moderatorCommand: [modCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -91,13 +119,13 @@ export class ProtobufService { } public sendAdminCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const adminCmd = create(Data.AdminCommandSchema); + const adminCmd = create(AdminCommandSchema); setExtension(adminCmd, ext, value); - const cmd = create(Data.CommandContainerSchema, { adminCommand: [adminCmd] }); + const cmd = create(CommandContainerSchema, { adminCommand: [adminCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -105,7 +133,7 @@ export class ProtobufService { }); } - public sendCommand(cmd: Data.CommandContainer, callback: (raw: Data.Response) => void) { + public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void) { if (!this.transport.isOpen()) { return; } @@ -113,26 +141,26 @@ export class ProtobufService { this.cmdId++; cmd.cmdId = BigInt(this.cmdId); this.pendingCommands.set(this.cmdId, callback); - this.transport.send(toBinary(Data.CommandContainerSchema, cmd)); + this.transport.send(toBinary(CommandContainerSchema, cmd)); } public handleMessageEvent({ data }: MessageEvent): void { try { const uint8msg = new Uint8Array(data); - const msg: Data.ServerMessage = fromBinary(Data.ServerMessageSchema, uint8msg); + const msg: ServerMessage = fromBinary(ServerMessageSchema, uint8msg); if (msg) { switch (msg.messageType) { - case Data.ServerMessage_MessageType.RESPONSE: + case ServerMessage_MessageType.RESPONSE: this.processServerResponse(msg.response); break; - case Data.ServerMessage_MessageType.ROOM_EVENT: + case ServerMessage_MessageType.ROOM_EVENT: this.processRoomEvent(msg.roomEvent); break; - case Data.ServerMessage_MessageType.SESSION_EVENT: + case ServerMessage_MessageType.SESSION_EVENT: this.processSessionEvent(msg.sessionEvent); break; - case Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER: + case ServerMessage_MessageType.GAME_EVENT_CONTAINER: this.processGameEvent(msg.gameEventContainer); break; default: @@ -145,7 +173,7 @@ export class ProtobufService { } } - private processServerResponse(response: Data.Response | undefined) { + private processServerResponse(response: Response | undefined) { if (!response) { return; } @@ -157,11 +185,11 @@ export class ProtobufService { } } - private processRoomEvent(event: Data.RoomEvent | undefined) { + private processRoomEvent(event: RoomEvent | undefined) { if (!event) { return; } - for (const [ext, handler] of RoomEvents) { + for (const [ext, handler] of this.events.roomEvents) { if (hasExtension(event, ext)) { handler(getExtension(event, ext), event); return; @@ -169,11 +197,11 @@ export class ProtobufService { } } - private processSessionEvent(event: Data.SessionEvent | undefined) { + private processSessionEvent(event: SessionEvent | undefined) { if (!event) { return; } - for (const [ext, handler] of SessionEvents) { + for (const [ext, handler] of this.events.sessionEvents) { if (hasExtension(event, ext)) { handler(getExtension(event, ext), undefined); return; @@ -181,7 +209,7 @@ export class ProtobufService { } } - private processGameEvent(container: Data.GameEventContainer | undefined): void { + private processGameEvent(container: GameEventContainer | undefined): void { if (!container?.eventList?.length) { return; } @@ -189,7 +217,7 @@ export class ProtobufService { const { gameId, context, secondsElapsed, forcedByJudge } = container; for (const event of container.eventList) { - const meta: Enriched.GameEventMeta = { + const meta: GameEventMeta = { gameId: gameId ?? -1, playerId: event.playerId ?? -1, context, @@ -197,7 +225,7 @@ export class ProtobufService { forcedByJudge: forcedByJudge ?? 0, }; - for (const [ext, handler] of GameEvents) { + for (const [ext, handler] of this.events.gameEvents) { if (hasExtension(event, ext)) { handler(getExtension(event, ext), meta); break; @@ -207,4 +235,3 @@ export class ProtobufService { } } - diff --git a/webclient/src/websocket/services/WebSocketService.spec.ts b/webclient/src/websocket/services/WebSocketService.spec.ts index e8a25802c..e105cd678 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 { App } from '@app/types'; +import { StatusEnum } from '../StatusEnum'; type WebSocketInternal = WebSocketService & { keepAliveService: KeepAliveService; @@ -19,11 +19,7 @@ let MockWS: Mock; let mockInstance: ReturnType['mockInstance']; let restoreWebSocket: ReturnType['restore']; let mockConfig: WebSocketServiceConfig; -let mockResponse: { - session: { - connectionFailed: Mock; - }; -}; +let mockOnConnectionFailed: Mock; let mockOnStatusChange: Mock; let locationRestores: Array<() => void>; @@ -35,16 +31,12 @@ beforeEach(() => { mockInstance = installed.mockInstance; restoreWebSocket = installed.restore; - mockResponse = { - session: { - connectionFailed: vi.fn(), - }, - }; + mockOnConnectionFailed = vi.fn(); mockOnStatusChange = vi.fn(); mockConfig = { keepAliveFn: vi.fn(), - response: mockResponse as unknown as WebSocketServiceConfig['response'], + onConnectionFailed: mockOnConnectionFailed, onStatusChange: mockOnStatusChange, }; @@ -78,7 +70,7 @@ describe('WebSocketService', () => { // trigger keepAliveService.disconnected$ (service as WebSocketInternal).keepAliveService.disconnected$.next(); expect(mockInstance.close).toHaveBeenCalled(); - expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection timeout'); + expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection timeout'); }); }); @@ -120,7 +112,7 @@ describe('WebSocketService', () => { it('calls onStatusChange CONNECTED on open', () => { createConnectedService(); mockInstance.onopen(); - expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'Connected'); + expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); }); it('starts the ping loop with the keepalive interval', () => { @@ -147,14 +139,14 @@ describe('WebSocketService', () => { it('calls onStatusChange DISCONNECTED on close when not already DISCONNECTED', () => { createConnectedService(); mockInstance.onclose(); - expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('does not overwrite status if already DISCONNECTED', () => { createConnectedService(); mockInstance.onerror(); mockInstance.onclose(); - expect(mockOnStatusChange).not.toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(mockOnStatusChange).not.toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('ends the ping loop on close', () => { @@ -170,13 +162,13 @@ describe('WebSocketService', () => { it('calls onStatusChange DISCONNECTED on error', () => { createConnectedService(); mockInstance.onerror(); - expect(mockOnStatusChange).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Failed'); + expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Failed'); }); - it('calls response.session.connectionFailed on error', () => { + it('calls onConnectionFailed on error', () => { createConnectedService(); mockInstance.onerror(); - expect(mockResponse.session.connectionFailed).toHaveBeenCalled(); + expect(mockOnConnectionFailed).toHaveBeenCalled(); }); }); diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts index 3e93ad19e..7134cbe71 100644 --- a/webclient/src/websocket/services/WebSocketService.ts +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -1,22 +1,20 @@ import { Subject } from 'rxjs'; -import { App, Enriched } from '@app/types'; - +import { StatusEnum } from '../StatusEnum'; import { KeepAliveService } from './KeepAliveService'; import { CLIENT_OPTIONS } from '../config'; -import { IWebClientResponse } from '../interfaces'; +import type { ConnectTarget } from '../WebClientConfig'; export interface WebSocketServiceConfig { keepAliveFn: (pingReceived: () => void) => void; - response: IWebClientResponse; - onStatusChange: (status: App.StatusEnum, description: string) => void; + onStatusChange: (status: StatusEnum, description: string) => void; + onConnectionFailed: () => void; } export class WebSocketService { private socket: WebSocket; private config: WebSocketServiceConfig; - private response: IWebClientResponse; private keepAliveService: KeepAliveService; private errorFired = false; @@ -26,21 +24,20 @@ 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(App.StatusEnum.DISCONNECTED, 'Connection timeout'); + this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection timeout'); }); } - public connect(options: Enriched.WebSocketConnectOptions, protocol: string = 'wss'): void { + public connect(target: ConnectTarget, protocol: string = 'wss'): void { if (window.location.hostname === 'localhost') { protocol = 'ws'; } - const { host, port } = options; + const { host, port } = target; this.keepalive = CLIENT_OPTIONS.keepalive; this.socket = this.createWebSocket(`${protocol}://${host}:${port}`); @@ -69,7 +66,7 @@ export class WebSocketService { socket.onopen = () => { clearTimeout(connectionTimer); this.errorFired = false; - this.config.onStatusChange(App.StatusEnum.CONNECTED, 'Connected'); + this.config.onStatusChange(StatusEnum.CONNECTED, 'Connected'); this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: () => void) => { this.config.keepAliveFn(pingReceived); @@ -79,7 +76,7 @@ export class WebSocketService { socket.onclose = () => { // dont overwrite failure messages if (!this.errorFired) { - this.config.onStatusChange(App.StatusEnum.DISCONNECTED, 'Connection Closed'); + this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Closed'); } this.errorFired = false; this.keepAliveService.endPingLoop(); @@ -87,8 +84,8 @@ export class WebSocketService { socket.onerror = () => { this.errorFired = true; - this.config.onStatusChange(App.StatusEnum.DISCONNECTED, 'Connection Failed'); - this.response.session.connectionFailed(); + this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Failed'); + this.config.onConnectionFailed(); }; 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 23065d8e6..19dd644be 100644 --- a/webclient/src/websocket/services/command-options.spec.ts +++ b/webclient/src/websocket/services/command-options.spec.ts @@ -1,5 +1,6 @@ import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Data } from '@app/types'; +import type { Response } from '@app/generated'; +import { Response_ResponseCode, ResponseSchema } from '@app/generated'; vi.mock('@bufbuild/protobuf', async () => { const actual = await vi.importActual('@bufbuild/protobuf'); return { ...actual, getExtension: vi.fn() }; @@ -19,14 +20,14 @@ describe('handleResponse', () => { it('calls onResponse and returns early when provided', () => { const onResponse = vi.fn(); const onSuccess = vi.fn(); - handleResponse('test', create(Data.ResponseSchema, { responseCode: 99 }), { onResponse, onSuccess }); + handleResponse('test', create(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(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); + const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespOk }); handleResponse('test', raw, { onSuccess }); expect(onSuccess).toHaveBeenCalledWith(); }); @@ -34,28 +35,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(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); + const fakeExt = {} as unknown as GenExtension; + const raw = create(ResponseSchema, { responseCode: 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(Data.ResponseSchema, { responseCode: 5 }), { onResponseCode: { 5: specificHandler } }); + handleResponse('test', create(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(Data.ResponseSchema, { responseCode: 99 }); + const raw = create(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(Data.ResponseSchema, { responseCode: 42 }), {}); + handleResponse('test.Type', create(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 671d0a02b..6d1138886 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 { Data } from '@app/types'; +import { Response_ResponseCode, type Response } from '@app/generated'; interface CommandOptionsBase { - onError?: (responseCode: number, raw: Data.Response) => void; - onResponseCode?: { [code: number]: (raw: Data.Response) => void }; - onResponse?: (raw: Data.Response) => void; + onError?: (responseCode: number, raw: Response) => void; + onResponseCode?: { [code: number]: (raw: Response) => void }; + onResponse?: (raw: Response) => void; } export interface CommandOptionsWithResponse extends CommandOptionsBase { - responseExt: GenExtension; - onSuccess?: (response: R, raw: Data.Response) => void; + responseExt: GenExtension; + onSuccess?: (response: R, raw: 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: Data.Response, options: CommandOptions): void { +export function handleResponse(typeName: string, raw: Response, options: CommandOptions): void { if (options.onResponse) { options.onResponse(raw); return; @@ -32,7 +32,7 @@ export function handleResponse(typeName: string, raw: Data.Response, options: const { responseCode } = raw; - if (responseCode === Data.Response_ResponseCode.RespOk) { + if (responseCode === Response_ResponseCode.RespOk) { if (hasResponseExt(options)) { options.onSuccess?.(getExtension(raw, options.responseExt), raw); } else { diff --git a/webclient/src/websocket/types.ts b/webclient/src/websocket/types.ts new file mode 100644 index 000000000..cc1dface5 --- /dev/null +++ b/webclient/src/websocket/types.ts @@ -0,0 +1,44 @@ +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/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts index 22951ce49..6a230d9ea 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 { Data } from '@app/types'; +import { Event_ServerIdentification_ServerOptions } from '@app/generated'; 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 & Data.Event_ServerIdentification_ServerOptions.SupportsPasswordHash; + return serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash; } diff --git a/webclient/vite.config.ts b/webclient/vite.config.ts index 86c243058..1c1157f0a 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: false, + isolate: true, coverage: { provider: 'v8', reporter: ['text', 'html'], diff --git a/webclient/vitest.integration.config.ts b/webclient/vitest.integration.config.ts index dfba71824..0b88f927a 100644 --- a/webclient/vitest.integration.config.ts +++ b/webclient/vitest.integration.config.ts @@ -16,12 +16,15 @@ 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/**/*.{ts,tsx}'], + include: [ + 'src/websocket/**/*.{ts,tsx}', + 'src/store/**/*.{ts,tsx}', + 'src/api/**/*.{ts,tsx}', + ], exclude: [ 'src/generated/**', 'src/**/*.spec.{ts,tsx}',