From e2319c84dbaaad0e4813e1d227f647049da2ef32 Mon Sep 17 00:00:00 2001 From: seavor Date: Thu, 7 May 2026 15:54:59 -0500 Subject: [PATCH] replace websocket with sockatrice --- webclient/.gitignore | 2 - webclient/buf.gen.plugin.mjs | 165 ----- webclient/buf.gen.yaml | 12 - webclient/eslint.boundaries.mjs | 22 +- webclient/eslint.config.mjs | 2 +- webclient/integration/src/helpers/setup.ts | 17 +- .../src/websocket/connection.spec.ts | 3 +- webclient/package-lock.json | 242 +------- webclient/package.json | 12 +- webclient/public/pb/.gitignore | 4 - .../src/__test-utils__/makeHookWrapper.tsx | 2 +- webclient/src/__test-utils__/mockWebClient.ts | 2 +- .../__test-utils__/renderWithProviders.tsx | 2 +- .../config.ts => api/clientConfig.ts} | 18 +- webclient/src/api/index.ts | 2 +- webclient/src/api/request/AdminRequestImpl.ts | 20 - .../api/request/AuthenticationRequestImpl.ts | 61 -- webclient/src/api/request/GameRequestImpl.ts | 142 ----- .../src/api/request/ModeratorRequestImpl.ts | 53 -- webclient/src/api/request/RoomsRequestImpl.ts | 25 - .../src/api/request/SessionRequestImpl.ts | 52 -- webclient/src/api/request/index.ts | 21 - webclient/src/containers/Player/usePlayer.ts | 2 +- webclient/src/containers/Server/Rooms.tsx | 2 +- webclient/src/hooks/useWebClient.spec.tsx | 4 +- webclient/src/hooks/useWebClient.tsx | 14 +- webclient/src/websocket/WebClient.spec.ts | 312 ---------- webclient/src/websocket/WebClient.ts | 166 ----- .../src/websocket/__mocks__/WebClient.ts | 169 ------ .../websocket/__mocks__/callbackHelpers.ts | 41 -- webclient/src/websocket/__mocks__/helpers.ts | 40 -- .../__mocks__/sessionCommandMocks.ts | 111 ---- .../src/websocket/commands/admin/adjustMod.ts | 14 - .../commands/admin/adminCommands.spec.ts | 93 --- .../src/websocket/commands/admin/index.ts | 4 - .../websocket/commands/admin/reloadConfig.ts | 10 - .../commands/admin/shutdownServer.ts | 14 - .../commands/admin/updateServerMessage.ts | 10 - .../commands/game/attachCard.presence.spec.ts | 73 --- .../src/websocket/commands/game/attachCard.ts | 7 - .../commands/game/changeZoneProperties.ts | 11 - .../src/websocket/commands/game/concede.ts | 7 - .../websocket/commands/game/createArrow.ts | 7 - .../websocket/commands/game/createCounter.ts | 7 - .../websocket/commands/game/createToken.ts | 7 - .../src/websocket/commands/game/deckSelect.ts | 7 - .../src/websocket/commands/game/delCounter.ts | 7 - .../websocket/commands/game/deleteArrow.ts | 7 - .../src/websocket/commands/game/drawCards.ts | 7 - .../src/websocket/commands/game/dumpZone.ts | 7 - .../src/websocket/commands/game/flipCard.ts | 7 - .../commands/game/gameCommands.spec.ts | 314 ---------- .../src/websocket/commands/game/gameSay.ts | 7 - .../websocket/commands/game/incCardCounter.ts | 7 - .../src/websocket/commands/game/incCounter.ts | 7 - .../src/websocket/commands/game/index.ts | 34 -- .../src/websocket/commands/game/judge.ts | 10 - .../websocket/commands/game/kickFromGame.ts | 7 - .../src/websocket/commands/game/leaveGame.ts | 7 - .../src/websocket/commands/game/moveCard.ts | 7 - .../src/websocket/commands/game/mulligan.ts | 7 - .../src/websocket/commands/game/nextTurn.ts | 7 - .../src/websocket/commands/game/readyStart.ts | 7 - .../websocket/commands/game/revealCards.ts | 7 - .../websocket/commands/game/reverseTurn.ts | 7 - .../src/websocket/commands/game/rollDie.ts | 7 - .../websocket/commands/game/setActivePhase.ts | 7 - .../websocket/commands/game/setCardAttr.ts | 7 - .../websocket/commands/game/setCardCounter.ts | 7 - .../src/websocket/commands/game/setCounter.ts | 7 - .../commands/game/setSideboardLock.ts | 11 - .../commands/game/setSideboardPlan.ts | 11 - .../src/websocket/commands/game/shuffle.ts | 7 - .../src/websocket/commands/game/unconcede.ts | 7 - .../src/websocket/commands/game/undoDraw.ts | 7 - webclient/src/websocket/commands/index.ts | 5 - .../commands/moderator/banFromServer.ts | 15 - .../commands/moderator/forceActivateUser.ts | 13 - .../commands/moderator/getAdminNotes.ts | 13 - .../commands/moderator/getBanHistory.ts | 13 - .../commands/moderator/getWarnHistory.ts | 17 - .../commands/moderator/getWarnList.ts | 17 - .../commands/moderator/grantReplayAccess.ts | 16 - .../src/websocket/commands/moderator/index.ts | 10 - .../moderator/moderatorCommands.spec.ts | 219 ------- .../commands/moderator/updateAdminNotes.ts | 16 - .../commands/moderator/viewLogHistory.ts | 14 - .../websocket/commands/moderator/warnUser.ts | 13 - .../src/websocket/commands/room/createGame.ts | 13 - .../src/websocket/commands/room/index.ts | 4 - .../src/websocket/commands/room/joinGame.ts | 47 -- .../src/websocket/commands/room/leaveRoom.ts | 12 - .../commands/room/roomCommands.spec.ts | 144 ----- .../src/websocket/commands/room/roomSay.ts | 13 - .../websocket/commands/session/accountEdit.ts | 13 - .../commands/session/accountImage.ts | 12 - .../commands/session/accountPassword.ts | 13 - .../websocket/commands/session/activate.ts | 39 -- .../websocket/commands/session/addToList.ts | 20 - .../src/websocket/commands/session/connect.ts | 10 - .../src/websocket/commands/session/deckDel.ts | 12 - .../websocket/commands/session/deckDelDir.ts | 12 - .../commands/session/deckDownload.ts | 17 - .../websocket/commands/session/deckList.ts | 15 - .../websocket/commands/session/deckNewDir.ts | 12 - .../websocket/commands/session/deckUpload.ts | 19 - .../websocket/commands/session/disconnect.ts | 5 - .../session/forgotPasswordChallenge.ts | 37 -- .../commands/session/forgotPasswordRequest.ts | 39 -- .../commands/session/forgotPasswordReset.ts | 48 -- .../commands/session/getGamesOfUser.ts | 13 - .../websocket/commands/session/getUserInfo.ts | 13 - .../src/websocket/commands/session/index.ts | 34 -- .../websocket/commands/session/joinRoom.ts | 15 - .../websocket/commands/session/listRooms.ts | 8 - .../websocket/commands/session/listUsers.ts | 13 - .../src/websocket/commands/session/login.ts | 88 --- .../src/websocket/commands/session/message.ts | 8 - .../src/websocket/commands/session/ping.ts | 12 - .../websocket/commands/session/register.ts | 89 --- .../commands/session/removeFromList.ts | 24 - .../commands/session/replayDeleteMatch.ts | 16 - .../commands/session/replayDownload.ts | 17 - .../commands/session/replayGetCode.ts | 12 - .../websocket/commands/session/replayList.ts | 13 - .../commands/session/replayModifyMatch.ts | 16 - .../commands/session/replaySubmitCode.ts | 18 - .../commands/session/requestPasswordSalt.ts | 42 -- .../session/sessionCommands-complex.spec.ts | 534 ---------------- .../session/sessionCommands-simple.spec.ts | 520 ---------------- .../commands/session/updateStatus.ts | 7 - .../src/websocket/events/game/attachCard.ts | 7 - .../events/game/changeZoneProperties.ts | 7 - .../src/websocket/events/game/createArrow.ts | 7 - .../websocket/events/game/createCounter.ts | 7 - .../src/websocket/events/game/createToken.ts | 7 - .../src/websocket/events/game/delCounter.ts | 7 - .../src/websocket/events/game/deleteArrow.ts | 7 - .../src/websocket/events/game/destroyCard.ts | 7 - .../src/websocket/events/game/drawCards.ts | 7 - .../src/websocket/events/game/dumpZone.ts | 7 - .../src/websocket/events/game/flipCard.ts | 7 - .../src/websocket/events/game/gameClosed.ts | 8 - .../websocket/events/game/gameEvents.spec.ts | 291 --------- .../websocket/events/game/gameHostChanged.ts | 12 - .../src/websocket/events/game/gameSay.ts | 7 - .../websocket/events/game/gameStateChanged.ts | 7 - webclient/src/websocket/events/game/index.ts | 110 ---- .../src/websocket/events/game/joinGame.ts | 8 - webclient/src/websocket/events/game/kicked.ts | 8 - .../src/websocket/events/game/leaveGame.ts | 8 - .../src/websocket/events/game/moveCard.ts | 7 - .../events/game/playerPropertiesChanged.ts | 7 - .../src/websocket/events/game/revealCards.ts | 7 - .../src/websocket/events/game/reverseTurn.ts | 7 - .../src/websocket/events/game/rollDie.ts | 7 - .../websocket/events/game/setActivePhase.ts | 7 - .../websocket/events/game/setActivePlayer.ts | 7 - .../src/websocket/events/game/setCardAttr.ts | 7 - .../websocket/events/game/setCardCounter.ts | 7 - .../src/websocket/events/game/setCounter.ts | 7 - .../src/websocket/events/game/shuffle.ts | 7 - webclient/src/websocket/events/index.ts | 4 - webclient/src/websocket/events/room/index.ts | 36 -- .../src/websocket/events/room/joinRoom.ts | 6 - .../src/websocket/events/room/leaveRoom.ts | 6 - .../src/websocket/events/room/listGames.ts | 6 - .../websocket/events/room/removeMessages.ts | 6 - .../websocket/events/room/roomEvents.spec.ts | 66 -- .../src/websocket/events/room/roomSay.ts | 7 - .../src/websocket/events/session/addToList.ts | 18 - .../events/session/connectionClosed.ts | 43 -- .../websocket/events/session/gameJoined.ts | 6 - .../src/websocket/events/session/index.ts | 63 -- .../src/websocket/events/session/listRooms.ts | 16 - .../websocket/events/session/notifyUser.ts | 6 - .../events/session/removeFromList.ts | 18 - .../websocket/events/session/replayAdded.ts | 6 - .../events/session/serverCompleteList.ts | 7 - .../events/session/serverIdentification.ts | 98 --- .../websocket/events/session/serverMessage.ts | 7 - .../events/session/serverShutdown.ts | 6 - .../session-login-flow.integration.spec.ts | 143 ----- .../events/session/sessionEvents.spec.ts | 481 --------------- .../websocket/events/session/userJoined.ts | 6 - .../src/websocket/events/session/userLeft.ts | 6 - .../websocket/events/session/userMessage.ts | 6 - webclient/src/websocket/index.ts | 10 - .../services/KeepAliveService.spec.ts | 77 --- .../websocket/services/KeepAliveService.ts | 39 -- .../services/ProtobufService.spec.ts | 573 ------------------ .../src/websocket/services/ProtobufService.ts | 227 ------- .../services/WebSocketService.spec.ts | 327 ---------- .../websocket/services/WebSocketService.ts | 228 ------- .../services/command-options.spec.ts | 57 -- .../src/websocket/services/command-options.ts | 54 -- .../src/websocket/types/ConnectOptions.ts | 76 --- .../src/websocket/types/SignalContexts.ts | 17 - webclient/src/websocket/types/StatusEnum.ts | 11 - .../src/websocket/types/WebClientConfig.ts | 24 - .../src/websocket/types/WebClientRequest.ts | 167 ----- .../src/websocket/types/WebClientResponse.ts | 188 ------ .../src/websocket/types/WebSocketConfig.ts | 20 - webclient/src/websocket/types/index.ts | 1 - webclient/src/websocket/types/namespace.ts | 25 - .../websocket/utils/buildWebSocketUrl.spec.ts | 33 - .../src/websocket/utils/buildWebSocketUrl.ts | 21 - .../websocket/utils/connectionState.spec.ts | 42 -- .../src/websocket/utils/connectionState.ts | 13 - .../src/websocket/utils/guid.util.spec.ts | 24 - webclient/src/websocket/utils/guid.util.ts | 15 - webclient/src/websocket/utils/index.ts | 4 - .../websocket/utils/passwordHasher.spec.ts | 58 -- .../src/websocket/utils/passwordHasher.ts | 34 -- .../websocket/utils/sanitizeHtml.util.spec.ts | 82 --- .../src/websocket/utils/sanitizeHtml.util.ts | 25 - webclient/tsconfig.json | 6 +- webclient/vite.config.ts | 15 +- 218 files changed, 89 insertions(+), 9100 deletions(-) delete mode 100644 webclient/buf.gen.plugin.mjs delete mode 100644 webclient/buf.gen.yaml delete mode 100644 webclient/public/pb/.gitignore rename webclient/src/{websocket/config.ts => api/clientConfig.ts} (61%) delete mode 100644 webclient/src/api/request/AdminRequestImpl.ts delete mode 100644 webclient/src/api/request/AuthenticationRequestImpl.ts delete mode 100644 webclient/src/api/request/GameRequestImpl.ts delete mode 100644 webclient/src/api/request/ModeratorRequestImpl.ts delete mode 100644 webclient/src/api/request/RoomsRequestImpl.ts delete mode 100644 webclient/src/api/request/SessionRequestImpl.ts delete mode 100644 webclient/src/api/request/index.ts delete mode 100644 webclient/src/websocket/WebClient.spec.ts delete mode 100644 webclient/src/websocket/WebClient.ts delete mode 100644 webclient/src/websocket/__mocks__/WebClient.ts delete mode 100644 webclient/src/websocket/__mocks__/callbackHelpers.ts delete mode 100644 webclient/src/websocket/__mocks__/helpers.ts delete mode 100644 webclient/src/websocket/__mocks__/sessionCommandMocks.ts delete mode 100644 webclient/src/websocket/commands/admin/adjustMod.ts delete mode 100644 webclient/src/websocket/commands/admin/adminCommands.spec.ts delete mode 100644 webclient/src/websocket/commands/admin/index.ts delete mode 100644 webclient/src/websocket/commands/admin/reloadConfig.ts delete mode 100644 webclient/src/websocket/commands/admin/shutdownServer.ts delete mode 100644 webclient/src/websocket/commands/admin/updateServerMessage.ts delete mode 100644 webclient/src/websocket/commands/game/attachCard.presence.spec.ts delete mode 100644 webclient/src/websocket/commands/game/attachCard.ts delete mode 100644 webclient/src/websocket/commands/game/changeZoneProperties.ts delete mode 100644 webclient/src/websocket/commands/game/concede.ts delete mode 100644 webclient/src/websocket/commands/game/createArrow.ts delete mode 100644 webclient/src/websocket/commands/game/createCounter.ts delete mode 100644 webclient/src/websocket/commands/game/createToken.ts delete mode 100644 webclient/src/websocket/commands/game/deckSelect.ts delete mode 100644 webclient/src/websocket/commands/game/delCounter.ts delete mode 100644 webclient/src/websocket/commands/game/deleteArrow.ts delete mode 100644 webclient/src/websocket/commands/game/drawCards.ts delete mode 100644 webclient/src/websocket/commands/game/dumpZone.ts delete mode 100644 webclient/src/websocket/commands/game/flipCard.ts delete mode 100644 webclient/src/websocket/commands/game/gameCommands.spec.ts delete mode 100644 webclient/src/websocket/commands/game/gameSay.ts delete mode 100644 webclient/src/websocket/commands/game/incCardCounter.ts delete mode 100644 webclient/src/websocket/commands/game/incCounter.ts delete mode 100644 webclient/src/websocket/commands/game/index.ts delete mode 100644 webclient/src/websocket/commands/game/judge.ts delete mode 100644 webclient/src/websocket/commands/game/kickFromGame.ts delete mode 100644 webclient/src/websocket/commands/game/leaveGame.ts delete mode 100644 webclient/src/websocket/commands/game/moveCard.ts delete mode 100644 webclient/src/websocket/commands/game/mulligan.ts delete mode 100644 webclient/src/websocket/commands/game/nextTurn.ts delete mode 100644 webclient/src/websocket/commands/game/readyStart.ts delete mode 100644 webclient/src/websocket/commands/game/revealCards.ts delete mode 100644 webclient/src/websocket/commands/game/reverseTurn.ts delete mode 100644 webclient/src/websocket/commands/game/rollDie.ts delete mode 100644 webclient/src/websocket/commands/game/setActivePhase.ts delete mode 100644 webclient/src/websocket/commands/game/setCardAttr.ts delete mode 100644 webclient/src/websocket/commands/game/setCardCounter.ts delete mode 100644 webclient/src/websocket/commands/game/setCounter.ts delete mode 100644 webclient/src/websocket/commands/game/setSideboardLock.ts delete mode 100644 webclient/src/websocket/commands/game/setSideboardPlan.ts delete mode 100644 webclient/src/websocket/commands/game/shuffle.ts delete mode 100644 webclient/src/websocket/commands/game/unconcede.ts delete mode 100644 webclient/src/websocket/commands/game/undoDraw.ts delete mode 100644 webclient/src/websocket/commands/index.ts delete mode 100644 webclient/src/websocket/commands/moderator/banFromServer.ts delete mode 100644 webclient/src/websocket/commands/moderator/forceActivateUser.ts delete mode 100644 webclient/src/websocket/commands/moderator/getAdminNotes.ts delete mode 100644 webclient/src/websocket/commands/moderator/getBanHistory.ts delete mode 100644 webclient/src/websocket/commands/moderator/getWarnHistory.ts delete mode 100644 webclient/src/websocket/commands/moderator/getWarnList.ts delete mode 100644 webclient/src/websocket/commands/moderator/grantReplayAccess.ts delete mode 100644 webclient/src/websocket/commands/moderator/index.ts delete mode 100644 webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts delete mode 100644 webclient/src/websocket/commands/moderator/updateAdminNotes.ts delete mode 100644 webclient/src/websocket/commands/moderator/viewLogHistory.ts delete mode 100644 webclient/src/websocket/commands/moderator/warnUser.ts delete mode 100644 webclient/src/websocket/commands/room/createGame.ts delete mode 100644 webclient/src/websocket/commands/room/index.ts delete mode 100644 webclient/src/websocket/commands/room/joinGame.ts delete mode 100644 webclient/src/websocket/commands/room/leaveRoom.ts delete mode 100644 webclient/src/websocket/commands/room/roomCommands.spec.ts delete mode 100644 webclient/src/websocket/commands/room/roomSay.ts delete mode 100644 webclient/src/websocket/commands/session/accountEdit.ts delete mode 100644 webclient/src/websocket/commands/session/accountImage.ts delete mode 100644 webclient/src/websocket/commands/session/accountPassword.ts delete mode 100644 webclient/src/websocket/commands/session/activate.ts delete mode 100644 webclient/src/websocket/commands/session/addToList.ts delete mode 100644 webclient/src/websocket/commands/session/connect.ts delete mode 100644 webclient/src/websocket/commands/session/deckDel.ts delete mode 100644 webclient/src/websocket/commands/session/deckDelDir.ts delete mode 100644 webclient/src/websocket/commands/session/deckDownload.ts delete mode 100644 webclient/src/websocket/commands/session/deckList.ts delete mode 100644 webclient/src/websocket/commands/session/deckNewDir.ts delete mode 100644 webclient/src/websocket/commands/session/deckUpload.ts delete mode 100644 webclient/src/websocket/commands/session/disconnect.ts delete mode 100644 webclient/src/websocket/commands/session/forgotPasswordChallenge.ts delete mode 100644 webclient/src/websocket/commands/session/forgotPasswordRequest.ts delete mode 100644 webclient/src/websocket/commands/session/forgotPasswordReset.ts delete mode 100644 webclient/src/websocket/commands/session/getGamesOfUser.ts delete mode 100644 webclient/src/websocket/commands/session/getUserInfo.ts delete mode 100644 webclient/src/websocket/commands/session/index.ts delete mode 100644 webclient/src/websocket/commands/session/joinRoom.ts delete mode 100644 webclient/src/websocket/commands/session/listRooms.ts delete mode 100644 webclient/src/websocket/commands/session/listUsers.ts delete mode 100644 webclient/src/websocket/commands/session/login.ts delete mode 100644 webclient/src/websocket/commands/session/message.ts delete mode 100644 webclient/src/websocket/commands/session/ping.ts delete mode 100644 webclient/src/websocket/commands/session/register.ts delete mode 100644 webclient/src/websocket/commands/session/removeFromList.ts delete mode 100644 webclient/src/websocket/commands/session/replayDeleteMatch.ts delete mode 100644 webclient/src/websocket/commands/session/replayDownload.ts delete mode 100644 webclient/src/websocket/commands/session/replayGetCode.ts delete mode 100644 webclient/src/websocket/commands/session/replayList.ts delete mode 100644 webclient/src/websocket/commands/session/replayModifyMatch.ts delete mode 100644 webclient/src/websocket/commands/session/replaySubmitCode.ts delete mode 100644 webclient/src/websocket/commands/session/requestPasswordSalt.ts delete mode 100644 webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts delete mode 100644 webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts delete mode 100644 webclient/src/websocket/commands/session/updateStatus.ts delete mode 100644 webclient/src/websocket/events/game/attachCard.ts delete mode 100644 webclient/src/websocket/events/game/changeZoneProperties.ts delete mode 100644 webclient/src/websocket/events/game/createArrow.ts delete mode 100644 webclient/src/websocket/events/game/createCounter.ts delete mode 100644 webclient/src/websocket/events/game/createToken.ts delete mode 100644 webclient/src/websocket/events/game/delCounter.ts delete mode 100644 webclient/src/websocket/events/game/deleteArrow.ts delete mode 100644 webclient/src/websocket/events/game/destroyCard.ts delete mode 100644 webclient/src/websocket/events/game/drawCards.ts delete mode 100644 webclient/src/websocket/events/game/dumpZone.ts delete mode 100644 webclient/src/websocket/events/game/flipCard.ts delete mode 100644 webclient/src/websocket/events/game/gameClosed.ts delete mode 100644 webclient/src/websocket/events/game/gameEvents.spec.ts delete mode 100644 webclient/src/websocket/events/game/gameHostChanged.ts delete mode 100644 webclient/src/websocket/events/game/gameSay.ts delete mode 100644 webclient/src/websocket/events/game/gameStateChanged.ts delete mode 100644 webclient/src/websocket/events/game/index.ts delete mode 100644 webclient/src/websocket/events/game/joinGame.ts delete mode 100644 webclient/src/websocket/events/game/kicked.ts delete mode 100644 webclient/src/websocket/events/game/leaveGame.ts delete mode 100644 webclient/src/websocket/events/game/moveCard.ts delete mode 100644 webclient/src/websocket/events/game/playerPropertiesChanged.ts delete mode 100644 webclient/src/websocket/events/game/revealCards.ts delete mode 100644 webclient/src/websocket/events/game/reverseTurn.ts delete mode 100644 webclient/src/websocket/events/game/rollDie.ts delete mode 100644 webclient/src/websocket/events/game/setActivePhase.ts delete mode 100644 webclient/src/websocket/events/game/setActivePlayer.ts delete mode 100644 webclient/src/websocket/events/game/setCardAttr.ts delete mode 100644 webclient/src/websocket/events/game/setCardCounter.ts delete mode 100644 webclient/src/websocket/events/game/setCounter.ts delete mode 100644 webclient/src/websocket/events/game/shuffle.ts delete mode 100644 webclient/src/websocket/events/index.ts delete mode 100644 webclient/src/websocket/events/room/index.ts delete mode 100644 webclient/src/websocket/events/room/joinRoom.ts delete mode 100644 webclient/src/websocket/events/room/leaveRoom.ts delete mode 100644 webclient/src/websocket/events/room/listGames.ts delete mode 100644 webclient/src/websocket/events/room/removeMessages.ts delete mode 100644 webclient/src/websocket/events/room/roomEvents.spec.ts delete mode 100644 webclient/src/websocket/events/room/roomSay.ts delete mode 100644 webclient/src/websocket/events/session/addToList.ts delete mode 100644 webclient/src/websocket/events/session/connectionClosed.ts delete mode 100644 webclient/src/websocket/events/session/gameJoined.ts delete mode 100644 webclient/src/websocket/events/session/index.ts delete mode 100644 webclient/src/websocket/events/session/listRooms.ts delete mode 100644 webclient/src/websocket/events/session/notifyUser.ts delete mode 100644 webclient/src/websocket/events/session/removeFromList.ts delete mode 100644 webclient/src/websocket/events/session/replayAdded.ts delete mode 100644 webclient/src/websocket/events/session/serverCompleteList.ts delete mode 100644 webclient/src/websocket/events/session/serverIdentification.ts delete mode 100644 webclient/src/websocket/events/session/serverMessage.ts delete mode 100644 webclient/src/websocket/events/session/serverShutdown.ts delete mode 100644 webclient/src/websocket/events/session/session-login-flow.integration.spec.ts delete mode 100644 webclient/src/websocket/events/session/sessionEvents.spec.ts delete mode 100644 webclient/src/websocket/events/session/userJoined.ts delete mode 100644 webclient/src/websocket/events/session/userLeft.ts delete mode 100644 webclient/src/websocket/events/session/userMessage.ts delete mode 100644 webclient/src/websocket/index.ts delete mode 100644 webclient/src/websocket/services/KeepAliveService.spec.ts delete mode 100644 webclient/src/websocket/services/KeepAliveService.ts delete mode 100644 webclient/src/websocket/services/ProtobufService.spec.ts delete mode 100644 webclient/src/websocket/services/ProtobufService.ts delete mode 100644 webclient/src/websocket/services/WebSocketService.spec.ts delete mode 100644 webclient/src/websocket/services/WebSocketService.ts delete mode 100644 webclient/src/websocket/services/command-options.spec.ts delete mode 100644 webclient/src/websocket/services/command-options.ts delete mode 100644 webclient/src/websocket/types/ConnectOptions.ts delete mode 100644 webclient/src/websocket/types/SignalContexts.ts delete mode 100644 webclient/src/websocket/types/StatusEnum.ts delete mode 100644 webclient/src/websocket/types/WebClientConfig.ts delete mode 100644 webclient/src/websocket/types/WebClientRequest.ts delete mode 100644 webclient/src/websocket/types/WebClientResponse.ts delete mode 100644 webclient/src/websocket/types/WebSocketConfig.ts delete mode 100644 webclient/src/websocket/types/index.ts delete mode 100644 webclient/src/websocket/types/namespace.ts delete mode 100644 webclient/src/websocket/utils/buildWebSocketUrl.spec.ts delete mode 100644 webclient/src/websocket/utils/buildWebSocketUrl.ts delete mode 100644 webclient/src/websocket/utils/connectionState.spec.ts delete mode 100644 webclient/src/websocket/utils/connectionState.ts delete mode 100644 webclient/src/websocket/utils/guid.util.spec.ts delete mode 100644 webclient/src/websocket/utils/guid.util.ts delete mode 100644 webclient/src/websocket/utils/index.ts delete mode 100644 webclient/src/websocket/utils/passwordHasher.spec.ts delete mode 100644 webclient/src/websocket/utils/passwordHasher.ts delete mode 100644 webclient/src/websocket/utils/sanitizeHtml.util.spec.ts delete mode 100644 webclient/src/websocket/utils/sanitizeHtml.util.ts diff --git a/webclient/.gitignore b/webclient/.gitignore index fc4e6b035..b89915586 100644 --- a/webclient/.gitignore +++ b/webclient/.gitignore @@ -9,9 +9,7 @@ /plans # generated ./src files -/src/proto-files.json /src/server-props.json -/src/generated/ # testing /coverage diff --git a/webclient/buf.gen.plugin.mjs b/webclient/buf.gen.plugin.mjs deleted file mode 100644 index 35354ce4c..000000000 --- a/webclient/buf.gen.plugin.mjs +++ /dev/null @@ -1,165 +0,0 @@ -// @ts-check -/** - * Custom protoc-gen-es sibling plugin. Emits `src/generated/index.ts`, a - * single rollup that re-exports every generated `_pb` module and adds - * `MessageInitShape` param aliases for every `Command_*` message. - * - * Wired into `buf.gen.yaml` as a second local plugin. Runs with the same - * descriptor set protoc-gen-es consumes, so output always tracks the protos. - */ -import { createEcmaScriptPlugin, runNodeJs } from '@bufbuild/protoplugin'; - -const HEADER = [ - '// @generated by protoc-gen-data. DO NOT EDIT.', - '// Rollup of all proto modules + MessageInitShape param aliases for every Command_*,', - '// plus type maps for Response/Event extensions grouped by scope.', - '/* eslint-disable */', - '', - '', -].join('\n'); - -const inner = createEcmaScriptPlugin({ - name: 'protoc-gen-data', - version: 'v0.1.0', - generateTs(schema) { - const f = schema.generateFile('index.ts'); - - const MessageInitShape = f.import('MessageInitShape', '@bufbuild/protobuf', true); - const MessageType = f.import('Message', '@bufbuild/protobuf', true); - const GenExtensionType = f.import('GenExtension', '@bufbuild/protobuf/codegenv2', true); - - const sortedFiles = [...schema.files].sort((a, b) => a.name.localeCompare(b.name)); - - for (const file of sortedFiles) { - f.print('export * from ', f.string(`./proto/${file.name}_pb`), ';'); - } - f.print(); - - const commandMessages = []; - for (const file of sortedFiles) { - for (const msg of file.messages) { - if (msg.name.startsWith('Command_')) { - commandMessages.push(msg); - } - } - } - commandMessages.sort((a, b) => a.name.localeCompare(b.name)); - - // importSchema() resolves paths relative to this plugin's `out` dir, which - // yields `./_pb` — but the _pb files live under ./proto/ (protoc-gen-es's - // out). Build the import path explicitly so it points inside the proto subdir. - for (const msg of commandMessages) { - const alias = msg.name.slice('Command_'.length) + 'Params'; - const schemaName = `${msg.name}Schema`; - const schemaSym = f.import(schemaName, `./proto/${msg.file.name}_pb`, true); - f.print('export type ', alias, ' = ', MessageInitShape, ';'); - } - 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. - // Specialised aliases (Session/Room/Game) still live in protobuf-types.ts - // because GameExtensionRegistry needs GameEventMeta — a hand-written - // domain type whose import would create a generated/ ↔ types/ cycle. - f.print('export type RegistryEntry = ['); - f.print(' ', GenExtensionType, ','); - f.print(' (value: V, meta: M) => void,'); - f.print('];'); - f.print(); - // Return type widens V to `unknown` so the heterogeneous entries that - // callers build can be stored in a homogeneous `RegistryEntry[]` - // array. This is the actual value-add over a bare tuple literal. - f.print('export function makeEntry('); - f.print(' ext: ', GenExtensionType, ','); - f.print(' handler: (value: V, meta: M) => void,'); - f.print('): RegistryEntry {'); - f.print(' return [ext, handler] as unknown as RegistryEntry;'); - f.print('}'); - }, -}); - -// Skip f.preamble() above and inject a custom rollup-aware header here instead — -// preamble() would write "@generated from file X.proto" which is misleading for -// a rollup file built from every input proto. -/** @type {import('@bufbuild/protoplugin').Plugin} */ -const plugin = { - name: inner.name, - version: inner.version, - run(request) { - const response = inner.run(request); - for (const file of response.file) { - if (file.name === 'index.ts' && typeof file.content === 'string') { - file.content = HEADER + file.content; - } - } - return response; - }, -}; - -runNodeJs(plugin); diff --git a/webclient/buf.gen.yaml b/webclient/buf.gen.yaml deleted file mode 100644 index 641ffe58a..000000000 --- a/webclient/buf.gen.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: v2 -inputs: - - directory: ../libcockatrice_protocol/libcockatrice/protocol/pb -plugins: - - local: protoc-gen-es - out: src/generated/proto - opt: - - target=ts - - local: [node, buf.gen.plugin.mjs] - out: src/generated - opt: - - target=ts diff --git a/webclient/eslint.boundaries.mjs b/webclient/eslint.boundaries.mjs index c5012b44b..f5870c6e8 100644 --- a/webclient/eslint.boundaries.mjs +++ b/webclient/eslint.boundaries.mjs @@ -6,43 +6,37 @@ const elements = [ { type: 'containers', pattern: ['src/containers/**'] }, { type: 'dialogs', pattern: ['src/dialogs/**'] }, { type: 'forms', pattern: ['src/forms/**'] }, - { type: 'generated', pattern: ['src/generated/**'] }, { type: 'hooks', pattern: ['src/hooks/**'] }, { type: 'images', pattern: ['src/images/**'] }, { type: 'services', pattern: ['src/services/**'] }, { type: 'store', pattern: ['src/store/**'] }, { type: 'types', pattern: ['src/types/**'] }, { type: 'utils', pattern: ['src/utils/**'] }, - { type: 'websocket-types', pattern: ['src/websocket/types/**'] }, - { type: 'websocket', pattern: ['src/websocket/**'] }, ]; const types = (...types) => types.map((type) => ({ to: { type } })); const rules = [ - { from: { type: 'generated' }, allow: [] }, - { from: { type: 'websocket-types' }, allow: types('generated') }, - { from: { type: 'websocket' }, allow: types('generated', 'websocket-types') }, - { from: { type: 'types' }, allow: types('generated') }, + { from: { type: 'types' }, allow: [] }, { from: { type: 'utils' }, allow: types('types') }, - { from: { type: 'store' }, allow: types('types', 'utils', 'websocket-types') }, - { from: { type: 'api' }, allow: types('store', 'types', 'utils', 'websocket', 'websocket-types') }, + { from: { type: 'store' }, allow: types('types', 'utils') }, + { from: { type: 'api' }, allow: types('store', 'types', 'utils') }, { from: { type: 'images' }, allow: types('types') }, { from: { type: 'services' }, allow: types('api', 'store', 'types', 'utils') }, - { from: { type: 'hooks' }, allow: types('api', 'services', 'store', 'types', 'utils', 'websocket', 'websocket-types') }, + { from: { type: 'hooks' }, allow: types('api', 'services', 'store', 'types', 'utils') }, { from: { type: 'components' }, - allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils', 'websocket-types') + allow: types('api', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils') }, { from: { type: 'containers' }, - allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils', 'websocket-types') + allow: types('api', 'components', 'dialogs', 'forms', 'hooks', 'images', 'services', 'store', 'types', 'utils') }, - { from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'store', 'types', 'utils', 'websocket-types') }, - { from: { type: 'forms' }, allow: types('components', 'hooks', 'services', 'store', 'types', 'utils', 'websocket-types') }, + { from: { type: 'dialogs' }, allow: types('components', 'forms', 'hooks', 'services', 'store', 'types', 'utils') }, + { from: { type: 'forms' }, allow: types('components', 'hooks', 'services', 'store', 'types', 'utils') }, ]; export const boundariesConfig = [ diff --git a/webclient/eslint.config.mjs b/webclient/eslint.config.mjs index f8885d58e..8bfbcef3f 100644 --- a/webclient/eslint.config.mjs +++ b/webclient/eslint.config.mjs @@ -5,7 +5,7 @@ import { boundariesConfig } from './eslint.boundaries.mjs'; export default tseslint.config( // Global ignores - { ignores: ['node_modules/**', 'build/**', 'public/pb/**', 'src/generated/**'] }, + { ignores: ['node_modules/**', 'build/**'] }, // Base JS recommended js.configs.recommended, diff --git a/webclient/integration/src/helpers/setup.ts b/webclient/integration/src/helpers/setup.ts index e9bfc56de..715ed6731 100644 --- a/webclient/integration/src/helpers/setup.ts +++ b/webclient/integration/src/helpers/setup.ts @@ -21,8 +21,14 @@ import { ServerDispatch, RoomsDispatch, GameDispatch } from '@app/store'; import { Data } from '@app/types'; import { WebClient, setPendingOptions } from '@app/websocket'; import { WebsocketTypes } from '@app/websocket/types'; -import { PROTOCOL_VERSION } from '../../../src/websocket/config'; -import { createWebClientRequest, createWebClientResponse } from '@app/api'; +import { + CLIENT_CONFIG, + CLIENT_OPTIONS, + PROTOCOL_VERSION, + createWebClientResponse, +} from '@app/api'; + +export { PROTOCOL_VERSION }; import { buildResponse, @@ -195,7 +201,12 @@ installMockWebSocket(); beforeEach(() => { vi.useFakeTimers(); - new WebClient(createWebClientRequest(), createWebClientResponse()); + new WebClient( + createWebClientResponse(), + CLIENT_CONFIG, + CLIENT_OPTIONS, + PROTOCOL_VERSION, + ); }); afterEach(() => { diff --git a/webclient/integration/src/websocket/connection.spec.ts b/webclient/integration/src/websocket/connection.spec.ts index 78a366f71..7219d8df9 100644 --- a/webclient/integration/src/websocket/connection.spec.ts +++ b/webclient/integration/src/websocket/connection.spec.ts @@ -9,14 +9,13 @@ import { Data } from '@app/types'; import { store } from '@app/store'; import { WebsocketTypes } from '@app/websocket/types'; -import { PROTOCOL_VERSION } from '../../../src/websocket/config'; - import { getMockWebSocket, getWebClient, openMockWebSocket, setPendingOptions, connectAndHandshake, + PROTOCOL_VERSION, } from '../helpers/setup'; import { buildSessionEventMessage, diff --git a/webclient/package-lock.json b/webclient/package-lock.json index 49ce37c9e..f8bd07940 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -18,7 +18,6 @@ "@reduxjs/toolkit": "^2.11.2", "crypto-js": "^4.2.0", "dexie": "^4.4.2", - "dompurify": "^3.4.0", "final-form": "^5.0.0", "final-form-set-field-touched": "^1.0.1", "i18next": "^26.0.5", @@ -35,18 +34,16 @@ "react-router-dom": "^7.14.1", "react-virtualized-auto-sizer": "^2.0.3", "react-window": "^2.2.7", - "rxjs": "^7.5.4" + "rxjs": "^7.5.4", + "sockatrice": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz" }, "devDependencies": { - "@bufbuild/buf": "^1.68.1", - "@bufbuild/protoc-gen-es": "^2.11.0", "@eslint/js": "^10.0.1", "@mui/types": "^9.0.0", "@playwright/test": "^1.51.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.3.2", - "@types/dompurify": "^3.0.5", "@types/node": "^22.19.17", "@types/prop-types": "^15.7.4", "@types/react": "^19.0.0", @@ -299,207 +296,12 @@ "specificity": "bin/cli.js" } }, - "node_modules/@bufbuild/buf": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.68.1.tgz", - "integrity": "sha512-QDJ3oy4qZ5EVS2JYtmpE1n9FuaoABthxIddXB050huGddatr1sjHJSSAXXpLotOI18pW3KQ4zzU1x5Ms+pEEOw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "bin": { - "buf": "bin/buf", - "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", - "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@bufbuild/buf-darwin-arm64": "1.68.1", - "@bufbuild/buf-darwin-x64": "1.68.1", - "@bufbuild/buf-linux-aarch64": "1.68.1", - "@bufbuild/buf-linux-armv7": "1.68.1", - "@bufbuild/buf-linux-x64": "1.68.1", - "@bufbuild/buf-win32-arm64": "1.68.1", - "@bufbuild/buf-win32-x64": "1.68.1" - } - }, - "node_modules/@bufbuild/buf-darwin-arm64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.68.1.tgz", - "integrity": "sha512-+Cu/2Kr6Add3s+Zk/edcF9QdpnrsukQkdR/z4fk4+qr6YZqfWfiV8f+s14I3h7qPrPnGeCeynvmZ9NmJ1BMYuA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-darwin-x64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.68.1.tgz", - "integrity": "sha512-hvAs452aJ6io9hZKSfr3TvC+//16zW5y5u3ucsIXVkl5mkmKWSCkPbZwGpjNCfRGGUsyRJGL6rixxTgLKw2k5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-linux-aarch64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.68.1.tgz", - "integrity": "sha512-GLCakHzZVKUPlAiJEPGMBLW+yBk8tuz6NNcoeQU5lB5AO7ks8V8x9cy4CQjne4YSl3niF1JtvAQckLKhEWPueQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-linux-armv7": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.68.1.tgz", - "integrity": "sha512-lUMCULl3MOYQe0oAPWnqNVYy8pL+F3Jeq6C4sSY+0E9udaACMc2mZ32gYingaMop9O1qS58HjJbezFxxL+CJqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-linux-x64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.68.1.tgz", - "integrity": "sha512-eRU3UWiZQthAgx+qFTG3EeJ/VeOcZzAkKYGt5ansOnOIJHBm+3RG2KqA+Jm8q3EFqB1XpVcGxPXnIu/qmFJXaQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-win32-arm64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.68.1.tgz", - "integrity": "sha512-v3xlKzs3l2C+mYv+T0sYol05DTmsFKYmM5Vz8+AyrXdjxRwq2QH7m0arVWwxHX2MwyhQxKA+qqjoF8bCUM7xxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/buf-win32-x64": { - "version": "1.68.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.68.1.tgz", - "integrity": "sha512-b62pwu+G7n5tF8n1QIoT85K7xgKJZS8SzdN020weOa7IVvMNHCDqMq7nrkz46fXCkK7MtD1YJ6sUp86sWSZPsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@bufbuild/protobuf": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, - "node_modules/@bufbuild/protoc-gen-es": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.11.0.tgz", - "integrity": "sha512-VzQuwEQDXipbZ1soWUuAWm1Z0C3B/IDWGeysnbX6ogJ6As91C2mdvAND/ekQ4YIWgen4d5nqLfIBOWLqCCjYUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@bufbuild/protobuf": "2.11.0", - "@bufbuild/protoplugin": "2.11.0" - }, - "bin": { - "protoc-gen-es": "bin/protoc-gen-es" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@bufbuild/protobuf": "2.11.0" - }, - "peerDependenciesMeta": { - "@bufbuild/protobuf": { - "optional": true - } - } - }, - "node_modules/@bufbuild/protoplugin": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz", - "integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@bufbuild/protobuf": "2.11.0", - "@typescript/vfs": "^1.6.2", - "typescript": "5.4.5" - } - }, - "node_modules/@bufbuild/protoplugin/node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -2063,16 +1865,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dompurify": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "*" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -2169,8 +1961,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", @@ -2421,19 +2213,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript/vfs": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz", - "integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.3" - }, - "peerDependencies": { - "typescript": "*" - } - }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", @@ -5593,6 +5372,19 @@ "dev": true, "license": "ISC" }, + "node_modules/sockatrice": { + "version": "2.1.3", + "resolved": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz", + "integrity": "sha512-xywXolumrums59NteojtdmVKfjF8Ah71ewseczecZBqSmNAQ6zhM9enhmukWLvgQJr0rh7o1RDX5m+54kct+vA==", + "license": "GPL-2.0-or-later", + "dependencies": { + "@bufbuild/protobuf": "^2.11.0", + "dompurify": "^3.4.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/webclient/package.json b/webclient/package.json index c66612ed8..95bf1cd81 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -25,11 +25,10 @@ "test:e2e:down": "docker compose -f e2e/docker/docker-compose.e2e.yml down -v", "test:e2e:install-browsers": "npx playwright install --with-deps chromium", "pretest:e2e": "npm run test:e2e:install-browsers", - "prebuild": "npm run proto:generate && node prebuild.js", + "prebuild": "node prebuild.js", "prepare": "cd .. && husky", - "prestart": "npm run proto:generate && node prebuild.js", + "prestart": "node prebuild.js", "preview": "vite preview", - "proto:generate": "npx buf generate", "start": "vite", "translate": "node prebuild.js -i18nOnly" }, @@ -44,7 +43,6 @@ "@reduxjs/toolkit": "^2.11.2", "crypto-js": "^4.2.0", "dexie": "^4.4.2", - "dompurify": "^3.4.0", "final-form": "^5.0.0", "final-form-set-field-touched": "^1.0.1", "i18next": "^26.0.5", @@ -61,11 +59,10 @@ "react-router-dom": "^7.14.1", "react-virtualized-auto-sizer": "^2.0.3", "react-window": "^2.2.7", - "rxjs": "^7.5.4" + "rxjs": "^7.5.4", + "sockatrice": "https://github.com/seavor/Sockatrice/releases/download/v2.1.3/sockatrice-2.1.3.tgz" }, "devDependencies": { - "@bufbuild/buf": "^1.68.1", - "@bufbuild/protoc-gen-es": "^2.11.0", "@eslint/js": "^10.0.1", "@playwright/test": "^1.51.0", "@types/ws": "^8.5.12", @@ -73,7 +70,6 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^16.3.2", - "@types/dompurify": "^3.0.5", "@types/node": "^22.19.17", "@types/prop-types": "^15.7.4", "@types/react": "^19.0.0", diff --git a/webclient/public/pb/.gitignore b/webclient/public/pb/.gitignore deleted file mode 100644 index 571442132..000000000 --- a/webclient/public/pb/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore all files -* -# Except gitignore -!.gitignore \ No newline at end of file diff --git a/webclient/src/__test-utils__/makeHookWrapper.tsx b/webclient/src/__test-utils__/makeHookWrapper.tsx index 6a2514d84..348823fa7 100644 --- a/webclient/src/__test-utils__/makeHookWrapper.tsx +++ b/webclient/src/__test-utils__/makeHookWrapper.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { configureStore, Reducer } from '@reduxjs/toolkit'; import { WebClientContext } from '../hooks/useWebClient'; -import type { WebClient } from '../websocket'; +import type { WebClient } from '@app/websocket'; import { createMockWebClient } from './mockWebClient'; // Minimal Provider wrapper for hook-only tests. Use this instead of diff --git a/webclient/src/__test-utils__/mockWebClient.ts b/webclient/src/__test-utils__/mockWebClient.ts index 3460199ef..c1bcfb8d1 100644 --- a/webclient/src/__test-utils__/mockWebClient.ts +++ b/webclient/src/__test-utils__/mockWebClient.ts @@ -27,7 +27,7 @@ export function createMockWebClient() { accountEdit: vi.fn(), accountPassword: vi.fn(), accountImage: vi.fn(), - listUsers: vi.fn(), + message: vi.fn(), }, rooms: { joinRoom: vi.fn(), diff --git a/webclient/src/__test-utils__/renderWithProviders.tsx b/webclient/src/__test-utils__/renderWithProviders.tsx index 6cef77e4d..2557d6210 100644 --- a/webclient/src/__test-utils__/renderWithProviders.tsx +++ b/webclient/src/__test-utils__/renderWithProviders.tsx @@ -37,7 +37,7 @@ const testTheme = createTheme({ }); import { WebClientContext } from '../hooks/useWebClient'; -import type { WebClient } from '../websocket'; +import type { WebClient } from '@app/websocket'; import rootReducer from '../store/rootReducer'; import { ToastProvider } from '../components/Toast/ToastContext'; import { storeMiddlewareOptions } from '../store/store'; diff --git a/webclient/src/websocket/config.ts b/webclient/src/api/clientConfig.ts similarity index 61% rename from webclient/src/websocket/config.ts rename to webclient/src/api/clientConfig.ts index f27988561..74bc57f0a 100644 --- a/webclient/src/websocket/config.ts +++ b/webclient/src/api/clientConfig.ts @@ -1,6 +1,8 @@ +import type { WebsocketTypes } from '@app/websocket/types'; + export const PROTOCOL_VERSION = 14; -export const CLIENT_CONFIG = { +export const CLIENT_CONFIG: WebsocketTypes.ClientConfig = { clientid: 'webatrice', clientver: 'webclient-1.0 (2019-10-31)', clientfeatures: [ @@ -9,19 +11,17 @@ export const CLIENT_CONFIG = { 'feature_set', 'room_chat_history', 'client_warnings', - /* unimplemented features */ 'forgot_password', 'idle_client', 'mod_log_lookup', 'user_ban_history', - // satisfy server reqs for POC 'websocket', '2.7.0_min_version', - '2.8.0_min_version' - ] -} as const; + '2.8.0_min_version', + ], +}; -export const CLIENT_OPTIONS = { +export const CLIENT_OPTIONS: WebsocketTypes.ClientOptions = { autojoinrooms: true, - keepalive: 5000 -} as const; + keepalive: 5000, +}; diff --git a/webclient/src/api/index.ts b/webclient/src/api/index.ts index fb201a02a..18bea8404 100644 --- a/webclient/src/api/index.ts +++ b/webclient/src/api/index.ts @@ -1,2 +1,2 @@ export { createWebClientResponse } from './response'; -export { createWebClientRequest } from './request'; +export { CLIENT_CONFIG, CLIENT_OPTIONS, PROTOCOL_VERSION } from './clientConfig'; diff --git a/webclient/src/api/request/AdminRequestImpl.ts b/webclient/src/api/request/AdminRequestImpl.ts deleted file mode 100644 index 1c22d7be6..000000000 --- a/webclient/src/api/request/AdminRequestImpl.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AdminCommands } from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; - -export class AdminRequestImpl implements WebsocketTypes.IAdminRequest { - adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { - AdminCommands.adjustMod(userName, shouldBeMod, shouldBeJudge); - } - - reloadConfig(): void { - AdminCommands.reloadConfig(); - } - - shutdownServer(reason: string, minutes: number): void { - AdminCommands.shutdownServer(reason, minutes); - } - - updateServerMessage(): void { - AdminCommands.updateServerMessage(); - } -} diff --git a/webclient/src/api/request/AuthenticationRequestImpl.ts b/webclient/src/api/request/AuthenticationRequestImpl.ts deleted file mode 100644 index e94f9d2bf..000000000 --- a/webclient/src/api/request/AuthenticationRequestImpl.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - WebClient, - SessionCommands, - setPendingOptions, -} from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; - -interface AppAuthRequestOverrides extends WebsocketTypes.AuthRequestMap { - LoginParams: Omit; - ConnectTarget: Omit; - RegisterParams: Omit; - ActivateParams: Omit; - ForgotPasswordRequestParams: Omit; - ForgotPasswordChallengeParams: Omit; - ForgotPasswordResetParams: Omit; -} - -const CONNECTING_STATUS_LABEL = 'Connecting...'; - -function beginConnect( - options: { host: string; port: string | number }, - reason: WebsocketTypes.WebSocketConnectReason, -): void { - setPendingOptions({ ...options, reason }); - SessionCommands.updateStatus(WebsocketTypes.StatusEnum.CONNECTING, CONNECTING_STATUS_LABEL); - WebClient.instance.connect({ host: options.host, port: options.port }); -} - -export class AuthenticationRequestImpl implements WebsocketTypes.IAuthenticationRequest { - login(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.LOGIN); - } - - testConnection(options: Omit): void { - WebClient.instance.testConnect({ host: options.host, port: options.port }); - } - - register(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.REGISTER); - } - - activateAccount(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.ACTIVATE_ACCOUNT); - } - - resetPasswordRequest(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET_REQUEST); - } - - resetPasswordChallenge(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); - } - - resetPassword(options: Omit): void { - beginConnect(options, WebsocketTypes.WebSocketConnectReason.PASSWORD_RESET); - } - - disconnect(): void { - SessionCommands.disconnect(); - } -} diff --git a/webclient/src/api/request/GameRequestImpl.ts b/webclient/src/api/request/GameRequestImpl.ts deleted file mode 100644 index 78a9dc829..000000000 --- a/webclient/src/api/request/GameRequestImpl.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { GameCommands } from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; - -import { Data } from '@app/types'; - -export class GameRequestImpl implements WebsocketTypes.IGameRequest { - leaveGame(gameId: number): void { - GameCommands.leaveGame(gameId); - } - - kickFromGame(gameId: number, params: Data.KickFromGameParams): void { - GameCommands.kickFromGame(gameId, params); - } - - gameSay(gameId: number, params: Data.GameSayParams): void { - GameCommands.gameSay(gameId, params); - } - - readyStart(gameId: number, params: Data.ReadyStartParams): void { - GameCommands.readyStart(gameId, params); - } - - concede(gameId: number): void { - GameCommands.concede(gameId); - } - - unconcede(gameId: number): void { - GameCommands.unconcede(gameId); - } - - judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void { - GameCommands.judge(gameId, targetId, innerGameCommand); - } - - nextTurn(gameId: number): void { - GameCommands.nextTurn(gameId); - } - - setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void { - GameCommands.setActivePhase(gameId, params); - } - - reverseTurn(gameId: number): void { - GameCommands.reverseTurn(gameId); - } - - moveCard(gameId: number, params: Data.MoveCardParams): void { - GameCommands.moveCard(gameId, params); - } - - flipCard(gameId: number, params: Data.FlipCardParams): void { - GameCommands.flipCard(gameId, params); - } - - attachCard(gameId: number, params: Data.AttachCardParams): void { - GameCommands.attachCard(gameId, params); - } - - createToken(gameId: number, params: Data.CreateTokenParams): void { - GameCommands.createToken(gameId, params); - } - - setCardAttr(gameId: number, params: Data.SetCardAttrParams): void { - GameCommands.setCardAttr(gameId, params); - } - - setCardCounter(gameId: number, params: Data.SetCardCounterParams): void { - GameCommands.setCardCounter(gameId, params); - } - - incCardCounter(gameId: number, params: Data.IncCardCounterParams): void { - GameCommands.incCardCounter(gameId, params); - } - - drawCards(gameId: number, params: Data.DrawCardsParams): void { - GameCommands.drawCards(gameId, params); - } - - undoDraw(gameId: number): void { - GameCommands.undoDraw(gameId); - } - - createArrow(gameId: number, params: Data.CreateArrowParams): void { - GameCommands.createArrow(gameId, params); - } - - deleteArrow(gameId: number, params: Data.DeleteArrowParams): void { - GameCommands.deleteArrow(gameId, params); - } - - createCounter(gameId: number, params: Data.CreateCounterParams): void { - GameCommands.createCounter(gameId, params); - } - - setCounter(gameId: number, params: Data.SetCounterParams): void { - GameCommands.setCounter(gameId, params); - } - - incCounter(gameId: number, params: Data.IncCounterParams): void { - GameCommands.incCounter(gameId, params); - } - - delCounter(gameId: number, params: Data.DelCounterParams): void { - GameCommands.delCounter(gameId, params); - } - - shuffle(gameId: number, params: Data.ShuffleParams): void { - GameCommands.shuffle(gameId, params); - } - - dumpZone(gameId: number, params: Data.DumpZoneParams): void { - GameCommands.dumpZone(gameId, params); - } - - revealCards(gameId: number, params: Data.RevealCardsParams): void { - GameCommands.revealCards(gameId, params); - } - - changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void { - GameCommands.changeZoneProperties(gameId, params); - } - - deckSelect(gameId: number, params: Data.DeckSelectParams): void { - GameCommands.deckSelect(gameId, params); - } - - setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void { - GameCommands.setSideboardPlan(gameId, params); - } - - setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void { - GameCommands.setSideboardLock(gameId, params); - } - - mulligan(gameId: number, params: Data.MulliganParams): void { - GameCommands.mulligan(gameId, params); - } - - rollDie(gameId: number, params: Data.RollDieParams): void { - GameCommands.rollDie(gameId, params); - } -} diff --git a/webclient/src/api/request/ModeratorRequestImpl.ts b/webclient/src/api/request/ModeratorRequestImpl.ts deleted file mode 100644 index d8b7dd40e..000000000 --- a/webclient/src/api/request/ModeratorRequestImpl.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Data } from '@app/types'; -import { ModeratorCommands } from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; - -export class ModeratorRequestImpl implements WebsocketTypes.IModeratorRequest { - banFromServer( - minutes: number, - userName?: string, - address?: string, - reason?: string, - visibleReason?: string, - clientid?: string, - removeMessages?: number - ): void { - ModeratorCommands.banFromServer(minutes, userName, address, reason, visibleReason, clientid, removeMessages); - } - - forceActivateUser(usernameToActivate: string, moderatorName: string): void { - ModeratorCommands.forceActivateUser(usernameToActivate, moderatorName); - } - - getAdminNotes(userName: string): void { - ModeratorCommands.getAdminNotes(userName); - } - - getBanHistory(userName: string): void { - ModeratorCommands.getBanHistory(userName); - } - - getWarnHistory(userName: string): void { - ModeratorCommands.getWarnHistory(userName); - } - - getWarnList(modName: string, userName: string, userClientid: string): void { - ModeratorCommands.getWarnList(modName, userName, userClientid); - } - - grantReplayAccess(replayId: number, moderatorName: string): void { - ModeratorCommands.grantReplayAccess(replayId, moderatorName); - } - - updateAdminNotes(userName: string, notes: string): void { - ModeratorCommands.updateAdminNotes(userName, notes); - } - - viewLogHistory(filters: Data.ViewLogHistoryParams): void { - ModeratorCommands.viewLogHistory(filters); - } - - warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - ModeratorCommands.warnUser(userName, reason, clientid, removeMessages); - } -} diff --git a/webclient/src/api/request/RoomsRequestImpl.ts b/webclient/src/api/request/RoomsRequestImpl.ts deleted file mode 100644 index 27ff2eebb..000000000 --- a/webclient/src/api/request/RoomsRequestImpl.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RoomCommands, SessionCommands } from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; -import type { App } from '@app/types'; - -export class RoomsRequestImpl implements WebsocketTypes.IRoomsRequest { - joinRoom(roomId: number): void { - SessionCommands.joinRoom(roomId); - } - - leaveRoom(roomId: number): void { - RoomCommands.leaveRoom(roomId); - } - - roomSay(roomId: number, message: string): void { - RoomCommands.roomSay(roomId, message); - } - - createGame(roomId: number, params: App.CreateGameParams): void { - RoomCommands.createGame(roomId, params); - } - - joinGame(roomId: number, params: App.JoinGameParams): void { - RoomCommands.joinGame(roomId, params); - } -} diff --git a/webclient/src/api/request/SessionRequestImpl.ts b/webclient/src/api/request/SessionRequestImpl.ts deleted file mode 100644 index d0ddf472f..000000000 --- a/webclient/src/api/request/SessionRequestImpl.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SessionCommands } from '@app/websocket'; -import { WebsocketTypes } from '@app/websocket/types'; - -export class SessionRequestImpl implements WebsocketTypes.ISessionRequest { - addToBuddyList(userName: string): void { - SessionCommands.addToBuddyList(userName); - } - - removeFromBuddyList(userName: string): void { - SessionCommands.removeFromBuddyList(userName); - } - - addToIgnoreList(userName: string): void { - SessionCommands.addToIgnoreList(userName); - } - - removeFromIgnoreList(userName: string): void { - SessionCommands.removeFromIgnoreList(userName); - } - - changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void { - SessionCommands.accountPassword(oldPassword, newPassword, hashedNewPassword); - } - - changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void { - SessionCommands.accountEdit(passwordCheck, realName, email, country); - } - - changeAccountImage(image: Uint8Array): void { - SessionCommands.accountImage(image); - } - - sendDirectMessage(userName: string, message: string): void { - SessionCommands.message(userName, message); - } - - getUserInfo(userName: string): void { - SessionCommands.getUserInfo(userName); - } - - getUserGames(userName: string): void { - SessionCommands.getGamesOfUser(userName); - } - - deckDownload(deckId: number): void { - SessionCommands.deckDownload(deckId); - } - - replayDownload(replayId: number): void { - SessionCommands.replayDownload(replayId); - } -} diff --git a/webclient/src/api/request/index.ts b/webclient/src/api/request/index.ts deleted file mode 100644 index 77093ff38..000000000 --- a/webclient/src/api/request/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { WebsocketTypes } from '@app/websocket/types'; - -import { AuthenticationRequestImpl } from './AuthenticationRequestImpl'; -import { SessionRequestImpl } from './SessionRequestImpl'; -import { RoomsRequestImpl } from './RoomsRequestImpl'; -import { GameRequestImpl } from './GameRequestImpl'; -import { AdminRequestImpl } from './AdminRequestImpl'; -import { ModeratorRequestImpl } from './ModeratorRequestImpl'; - -export { AuthenticationRequestImpl, SessionRequestImpl, RoomsRequestImpl, GameRequestImpl, AdminRequestImpl, ModeratorRequestImpl }; - -export function createWebClientRequest(): WebsocketTypes.IWebClientRequest { - return { - authentication: new AuthenticationRequestImpl(), - session: new SessionRequestImpl(), - rooms: new RoomsRequestImpl(), - game: new GameRequestImpl(), - admin: new AdminRequestImpl(), - moderator: new ModeratorRequestImpl(), - }; -} diff --git a/webclient/src/containers/Player/usePlayer.ts b/webclient/src/containers/Player/usePlayer.ts index 6a74467b8..8120c98e2 100644 --- a/webclient/src/containers/Player/usePlayer.ts +++ b/webclient/src/containers/Player/usePlayer.ts @@ -60,7 +60,7 @@ export function usePlayer(): PlayerViewModel { const onRemoveBuddy = () => name && webClient.request.session.removeFromBuddyList(name); const onAddIgnore = () => name && webClient.request.session.addToIgnoreList(name); const onRemoveIgnore = () => name && webClient.request.session.removeFromIgnoreList(name); - const onSendMessage = (message: string) => name && webClient.request.session.sendDirectMessage(name, message); + const onSendMessage = (message: string) => name && webClient.request.session.message(name, message); const onWarnUser = (reason: string) => name && webClient.request.moderator.warnUser(name, reason); const onBanFromServer = (minutes: number, reason: string, visibleReason?: string) => name && webClient.request.moderator.banFromServer(minutes, name, undefined, reason, visibleReason); diff --git a/webclient/src/containers/Server/Rooms.tsx b/webclient/src/containers/Server/Rooms.tsx index 98915599c..64cd25afb 100644 --- a/webclient/src/containers/Server/Rooms.tsx +++ b/webclient/src/containers/Server/Rooms.tsx @@ -31,7 +31,7 @@ const Rooms = ({ rooms, joinedRooms }: RoomsProps) => { if (joinedRoomIds.has(roomId)) { navigate(generatePath(App.RouteEnum.ROOM, { roomId: String(roomId) })); } else { - webClient.request.rooms.joinRoom(roomId); + webClient.request.session.joinRoom(roomId); } }; diff --git a/webclient/src/hooks/useWebClient.spec.tsx b/webclient/src/hooks/useWebClient.spec.tsx index 629de8714..87901f7ab 100644 --- a/webclient/src/hooks/useWebClient.spec.tsx +++ b/webclient/src/hooks/useWebClient.spec.tsx @@ -7,8 +7,10 @@ vi.mock('@app/websocket', () => ({ })); vi.mock('@app/api', () => ({ - createWebClientRequest: vi.fn(() => 'request'), createWebClientResponse: vi.fn(() => 'response'), + CLIENT_CONFIG: { clientid: 'test', clientver: 'test', clientfeatures: [] }, + CLIENT_OPTIONS: { autojoinrooms: false, keepalive: 5000 }, + PROTOCOL_VERSION: 14, })); function Wrapper({ children }: { children: ReactNode }) { diff --git a/webclient/src/hooks/useWebClient.tsx b/webclient/src/hooks/useWebClient.tsx index 0d3706d67..c69776b40 100644 --- a/webclient/src/hooks/useWebClient.tsx +++ b/webclient/src/hooks/useWebClient.tsx @@ -1,12 +1,22 @@ import { createContext, useContext, useState, ReactNode } from 'react'; import { WebClient } from '@app/websocket'; -import { createWebClientRequest, createWebClientResponse } from '@app/api'; +import { + CLIENT_CONFIG, + CLIENT_OPTIONS, + PROTOCOL_VERSION, + createWebClientResponse, +} from '@app/api'; export const WebClientContext = createContext(null); WebClientContext.displayName = 'WebClientContext'; export function WebClientProvider({ children }: { children: ReactNode }) { - const [client] = useState(() => new WebClient(createWebClientRequest(), createWebClientResponse())); + const [client] = useState(() => new WebClient( + createWebClientResponse(), + CLIENT_CONFIG, + CLIENT_OPTIONS, + PROTOCOL_VERSION, + )); return {children}; } diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts deleted file mode 100644 index 3edc330f0..000000000 --- a/webclient/src/websocket/WebClient.spec.ts +++ /dev/null @@ -1,312 +0,0 @@ -const captured = vi.hoisted(() => ({ - wsOptions: null as WebSocketServiceConfig | null, - pbOptions: null as SocketTransport | null, -})); - -vi.mock('./services/WebSocketService', () => ({ - WebSocketService: vi.fn().mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) { - captured.wsOptions = options; - return { - message$: { subscribe: vi.fn() }, - connect: vi.fn(), - disconnect: vi.fn(), - send: vi.fn(), - checkReadyState: vi.fn().mockReturnValue(true), - }; - }), -})); - -vi.mock('./services/ProtobufService', () => ({ - ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) { - captured.pbOptions = transport; - return { - handleMessageEvent: vi.fn(), - resetCommands: vi.fn(), - }; - }), -})); - -import { WebClient } from './WebClient'; -import { WebSocketService } from './services/WebSocketService'; -import { ProtobufService } from './services/ProtobufService'; -import { StatusEnum } from './types/StatusEnum'; -import { Subject } from 'rxjs'; -import { Mock } from 'vitest'; -import { SocketTransport } from './services/ProtobufService'; -import { WebSocketServiceConfig } from './services/WebSocketService'; -import type { IWebClientResponse } from './types/WebClientResponse'; -import type { IWebClientRequest } from './types/WebClientRequest'; -import type { ConnectTarget } from './types/WebClientConfig'; -import { installMockWebSocket } from './__mocks__/helpers'; -import { create, setExtension, toBinary } from '@bufbuild/protobuf'; -import { - Event_ServerIdentification_ext, - Event_ServerIdentification_ServerOptions, - Event_ServerIdentificationSchema, - ServerMessageSchema, - ServerMessage_MessageType, - SessionEventSchema, -} from '@app/generated'; -import { PROTOCOL_VERSION } from './config'; - -function buildServerIdentificationMessage({ - protocolVersion = PROTOCOL_VERSION, - serverOptions = 0, -}: { protocolVersion?: number; serverOptions?: number } = {}): Uint8Array { - const ident = create(Event_ServerIdentificationSchema, { - serverName: 'TestServer', - serverVersion: '2.8.0', - protocolVersion, - serverOptions, - }); - const sessionEvent = create(SessionEventSchema); - setExtension(sessionEvent, Event_ServerIdentification_ext, ident); - const server = create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.SESSION_EVENT, - sessionEvent, - }); - return toBinary(ServerMessageSchema, server); -} - -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() }, - admin: {}, - moderator: {}, - } as unknown as IWebClientResponse; -} - -function makeMockRequest(): IWebClientRequest { - return {} as IWebClientRequest; -} - -describe('WebClient', () => { - let client: WebClient; - let mockResponse: IWebClientResponse; - let mockRequest: IWebClientRequest; - let messageSubject: Subject; - - beforeEach(() => { - (WebClient as unknown as { _instance: WebClient | null })._instance = null; - - (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(transport: SocketTransport) { - captured.pbOptions = transport; - return { - handleMessageEvent: vi.fn(), - resetCommands: vi.fn(), - }; - }); - messageSubject = new Subject(); - (WebSocketService as Mock).mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) { - captured.wsOptions = options; - return { - message$: messageSubject, - connect: vi.fn(), - disconnect: vi.fn(), - send: vi.fn(), - checkReadyState: vi.fn().mockReturnValue(true), - }; - }); - vi.spyOn(console, 'log').mockImplementation(() => {}); - - mockResponse = makeMockResponse(); - mockRequest = makeMockRequest(); - client = new WebClient(mockRequest, mockResponse); - }); - - afterEach(() => { - vi.restoreAllMocks(); - (WebClient as unknown as { _instance: WebClient | null })._instance = null; - }); - - describe('constructor', () => { - it('stores the request and response on the instance', () => { - expect(client.request).toBe(mockRequest); - expect(client.response).toBe(mockResponse); - }); - - it('subscribes socket.message$ to protobuf.handleMessageEvent', () => { - const event = { data: new ArrayBuffer(0) } as MessageEvent; - messageSubject.next(event); - expect(client.protobuf.handleMessageEvent).toHaveBeenCalledWith(event); - }); - - it('calls response.session.initialized', () => { - expect(mockResponse.session.initialized).toHaveBeenCalled(); - }); - - it('sets WebClient.instance to the constructed instance', () => { - expect(WebClient.instance).toBe(client); - }); - - it('throws when instantiated more than once', () => { - expect(() => new WebClient(makeMockRequest(), makeMockResponse())).toThrow(/singleton/); - }); - }); - - describe('static instance accessor', () => { - it('throws when accessed before construction', () => { - (WebClient as unknown as { _instance: WebClient | null })._instance = null; - expect(() => WebClient.instance).toThrow(/not been initialized/); - }); - }); - - describe('connect', () => { - it('calls response.session.connectionAttempted', () => { - const target: ConnectTarget = { host: 'h', port: '1' }; - client.connect(target); - expect(mockResponse.session.connectionAttempted).toHaveBeenCalled(); - }); - - it('calls socket.connect with target', () => { - const target: ConnectTarget = { host: 'h', port: '1' }; - client.connect(target); - expect(client.socket.connect).toHaveBeenCalledWith(target); - }); - }); - - describe('testConnect', () => { - let MockWS: ReturnType['MockWS']; - let wsMockInstance: ReturnType['mockInstance']; - let restoreWS: ReturnType['restore']; - - beforeEach(() => { - vi.useFakeTimers(); - const installed = installMockWebSocket(); - MockWS = installed.MockWS; - wsMockInstance = installed.mockInstance; - restoreWS = installed.restore; - }); - - afterEach(() => { - restoreWS(); - vi.useRealTimers(); - }); - - const target: ConnectTarget = { host: 'h', port: '1' }; - - it('creates a WebSocket with the correct URL', () => { - client.testConnect(target); - expect(MockWS).toHaveBeenCalledWith(expect.stringContaining('://h:1')); - }); - - it('routes path-bearing hosts through the default TLS port (nginx proxy)', () => { - client.testConnect({ host: 'server.example.com/servatrice', port: '4748' }); - expect(MockWS).toHaveBeenCalledWith(expect.stringMatching(/:\/\/server\.example\.com\/servatrice$/)); - }); - - it('dispatches testConnectionSuccessful with supportsHashedPassword=true when the bit is set', () => { - client.testConnect(target); - const data = buildServerIdentificationMessage({ - serverOptions: Event_ServerIdentification_ServerOptions.SupportsPasswordHash, - }); - wsMockInstance.onmessage({ data: data.buffer }); - expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(true); - expect(wsMockInstance.close).toHaveBeenCalled(); - }); - - it('dispatches testConnectionSuccessful with supportsHashedPassword=false for naked-password servers', () => { - client.testConnect(target); - const data = buildServerIdentificationMessage({ serverOptions: 0 }); - wsMockInstance.onmessage({ data: data.buffer }); - expect(mockResponse.session.testConnectionSuccessful).toHaveBeenCalledWith(false); - }); - - it('fails on protocol-version mismatch instead of reporting success', () => { - client.testConnect(target); - const data = buildServerIdentificationMessage({ protocolVersion: PROTOCOL_VERSION + 1 }); - wsMockInstance.onmessage({ data: data.buffer }); - expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled(); - expect(mockResponse.session.testConnectionSuccessful).not.toHaveBeenCalled(); - }); - - it('calls testConnectionFailed on error', () => { - client.testConnect(target); - wsMockInstance.onerror(); - expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled(); - }); - - it('fires testConnectionFailed when ServerIdentification never arrives before the keepalive timeout', () => { - client.testConnect(target); - vi.advanceTimersByTime(5000); - expect(wsMockInstance.close).toHaveBeenCalled(); - expect(mockResponse.session.testConnectionFailed).toHaveBeenCalled(); - }); - - it('closes the prior in-flight socket on rapid re-click', () => { - const { instances } = installMockWebSocket(); - // The fresh installMockWebSocket replaces the stub from beforeEach so - // we observe the next two constructions in isolation. - client.testConnect(target); - const first = instances[instances.length - 1]; - expect(first.close).not.toHaveBeenCalled(); - - client.testConnect(target); - expect(first.close).toHaveBeenCalled(); - }); - }); - - describe('disconnect', () => { - it('delegates to socket.disconnect', () => { - client.disconnect(); - expect(client.socket.disconnect).toHaveBeenCalled(); - }); - }); - - describe('updateStatus', () => { - it('sets the status', () => { - client.updateStatus(StatusEnum.CONNECTED); - expect(client.status).toBe(StatusEnum.CONNECTED); - }); - - it('calls protobuf.resetCommands on DISCONNECTED', () => { - client.updateStatus(StatusEnum.DISCONNECTED); - expect(client.protobuf.resetCommands).toHaveBeenCalled(); - }); - - it('does not reset protobuf when status is not DISCONNECTED', () => { - client.updateStatus(StatusEnum.CONNECTED); - expect(client.protobuf.resetCommands).not.toHaveBeenCalled(); - }); - }); - - describe('constructor closures', () => { - it('keepAliveFn is set to ping function in WebSocketService', () => { - expect(captured.wsOptions!.keepAliveFn).toBeDefined(); - expect(typeof captured.wsOptions!.keepAliveFn).toBe('function'); - }); - - it('onStatusChange routes to response.session.updateStatus and updates own status', () => { - captured.wsOptions!.onStatusChange(StatusEnum.CONNECTED, 'Connected'); - expect(mockResponse.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); - expect(client.status).toBe(StatusEnum.CONNECTED); - }); - - it('onConnectionFailed routes to response.session.connectionFailed', () => { - captured.wsOptions!.onConnectionFailed(); - expect(mockResponse.session.connectionFailed).toHaveBeenCalled(); - }); - - it('send closure delegates to socket.send', () => { - const data = new Uint8Array([1, 2, 3]); - captured.pbOptions!.send(data); - expect(client.socket.send).toHaveBeenCalledWith(data); - }); - - it('isOpen closure delegates to socket.checkReadyState', () => { - const result = captured.pbOptions!.isOpen(); - expect(client.socket.checkReadyState).toHaveBeenCalledWith(WebSocket.OPEN); - expect(result).toBe(true); - }); - }); -}); diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts deleted file mode 100644 index 9e95b587e..000000000 --- a/webclient/src/websocket/WebClient.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { fromBinary, getExtension, hasExtension } from '@bufbuild/protobuf'; - -import { - Event_ServerIdentification_ext, - ServerMessageSchema, - ServerMessage_MessageType, -} from '@app/generated'; - -import { ping } from './commands/session'; -import { CLIENT_OPTIONS, PROTOCOL_VERSION } from './config'; -import { GameEvents } from './events/game'; -import { RoomEvents } from './events/room'; -import { SessionEvents } from './events/session'; -import type { ConnectTarget } from './types/WebClientConfig'; -import type { IWebClientRequest } from './types/WebClientRequest'; -import type { IWebClientResponse } from './types/WebClientResponse'; -import { StatusEnum } from './types/StatusEnum'; -import { ProtobufService } from './services/ProtobufService'; -import { WebSocketService } from './services/WebSocketService'; -import { buildWebSocketUrl } from './utils/buildWebSocketUrl'; -import { passwordSaltSupported } from './utils/passwordHasher'; - -export class WebClient { - private static _instance: WebClient | null = null; - - static get instance(): WebClient { - if (!WebClient._instance) { - throw new Error( - 'WebClient has not been initialized. Instantiate it via `new WebClient()` before accessing `WebClient.instance`.' - ); - } - return WebClient._instance; - } - - protobuf: ProtobufService; - socket: WebSocketService; - status: StatusEnum; - private testSocket: WebSocket | null = null; - - constructor( - public request: IWebClientRequest, - public response: IWebClientResponse - ) { - if (WebClient._instance) { - throw new Error('WebClient is a singleton and has already been initialized.'); - } - - this.socket = new WebSocketService({ - keepAliveFn: ping, - onStatusChange: (status, description) => { - this.response.session.updateStatus(status, description); - this.updateStatus(status); - }, - onConnectionFailed: () => { - this.response.session.connectionFailed(); - }, - reconnect: { - maxAttempts: 5, - baseDelayMs: 1000, - maxDelayMs: 30000, - }, - }); - - this.protobuf = new ProtobufService( - { - send: (data) => this.socket.send(data), - isOpen: () => this.socket.checkReadyState(WebSocket.OPEN), - }, - { game: GameEvents, room: RoomEvents, session: SessionEvents }, - ); - - this.socket.message$.subscribe((message: MessageEvent) => { - this.protobuf.handleMessageEvent(message); - }); - - WebClient._instance = this; - - this.response.session.initialized(); - } - - public connect(target: ConnectTarget): void { - this.response.session.connectionAttempted(); - this.socket.connect(target); - } - - public testConnect(target: ConnectTarget): void { - // A prior test connection still in flight when the user re-clicks would - // otherwise leak the socket until its keepalive timeout. Close eagerly. - if (this.testSocket) { - this.testSocket.close(); - this.testSocket = null; - } - - const protocol = window.location.hostname === 'localhost' ? 'ws' : 'wss'; - const socket = new WebSocket(buildWebSocketUrl(protocol, target.host, target.port)); - socket.binaryType = 'arraybuffer'; - this.testSocket = socket; - - // "Green" means reachable AND speaking a compatible Cockatrice protocol. - // Waiting for Event_ServerIdentification lets us read the hashed-password - // capability before the user ever logs in. The bitmask is resolved here - // (the websocket layer owns protocol details) so downstream consumers - // receive a domain-level boolean instead of a raw integer. - let resolved = false; - const resolve = (ok: boolean, supportsHashedPassword = false): void => { - if (resolved) { - return; - } - resolved = true; - clearTimeout(timeout); - // Suppress dispatches from a superseded socket — a newer test has - // already taken over and we'd race a stale result into its pending-ref. - if (this.testSocket === socket) { - if (ok) { - this.response.session.testConnectionSuccessful(supportsHashedPassword); - } else { - this.response.session.testConnectionFailed(); - } - this.testSocket = null; - } - socket.close(); - }; - - const timeout = setTimeout(() => resolve(false), CLIENT_OPTIONS.keepalive); - - socket.onmessage = (event: MessageEvent) => { - try { - const msg = fromBinary(ServerMessageSchema, new Uint8Array(event.data)); - if (msg.messageType !== ServerMessage_MessageType.SESSION_EVENT) { - return; - } - const sessionEvent = msg.sessionEvent; - if (!sessionEvent || !hasExtension(sessionEvent, Event_ServerIdentification_ext)) { - return; - } - const ident = getExtension(sessionEvent, Event_ServerIdentification_ext); - if (ident.protocolVersion !== PROTOCOL_VERSION) { - resolve(false); - return; - } - resolve(true, passwordSaltSupported(ident.serverOptions)); - } catch { - resolve(false); - } - }; - - socket.onerror = () => resolve(false); - socket.onclose = () => resolve(false); - } - - public disconnect(): void { - this.socket.disconnect(); - } - - public updateStatus(status: StatusEnum): void { - this.status = status; - - if (status === StatusEnum.DISCONNECTED) { - this.protobuf.resetCommands(); - } - } - - public get isReconnecting(): boolean { - return this.status === StatusEnum.RECONNECTING; - } -} diff --git a/webclient/src/websocket/__mocks__/WebClient.ts b/webclient/src/websocket/__mocks__/WebClient.ts deleted file mode 100644 index 8bf6e6a71..000000000 --- a/webclient/src/websocket/__mocks__/WebClient.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Shared WebClient mock — the single source of truth for all websocket - * layer unit tests. - * - * Vitest resolves this file whenever a spec calls `vi.mock('...WebClient')` - * without providing a factory. Each spec file gets its own module graph - * (isolate: true), so there are no factory-conflict issues. - * - * Usage in spec files: - * - * vi.mock('../../WebClient'); - * import { WebClient } from '../../WebClient'; - * // WebClient.instance.response.game.cardMoved ← vi.fn() - * // WebClient.instance.protobuf.sendGameCommand ← vi.fn() - * - * `useWebClientCleanup()` is NOT required — `instance` is a plain - * property, not a getter that throws. - */ - -const session = { - initialized: vi.fn(), - connectionAttempted: vi.fn(), - clearStore: vi.fn(), - loginSuccessful: vi.fn(), - loginFailed: vi.fn(), - connectionFailed: vi.fn(), - testConnectionSuccessful: vi.fn(), - testConnectionFailed: vi.fn(), - updateBuddyList: vi.fn(), - addToBuddyList: vi.fn(), - removeFromBuddyList: vi.fn(), - updateIgnoreList: vi.fn(), - addToIgnoreList: vi.fn(), - removeFromIgnoreList: vi.fn(), - updateInfo: vi.fn(), - updateStatus: vi.fn(), - updateUser: vi.fn(), - updateUsers: vi.fn(), - userJoined: vi.fn(), - userLeft: vi.fn(), - serverMessage: vi.fn(), - accountAwaitingActivation: vi.fn(), - accountActivationSuccess: vi.fn(), - accountActivationFailed: vi.fn(), - registrationRequiresEmail: vi.fn(), - registrationSuccess: vi.fn(), - registrationFailed: vi.fn(), - registrationEmailError: vi.fn(), - registrationPasswordError: vi.fn(), - registrationUserNameError: vi.fn(), - resetPasswordChallenge: vi.fn(), - resetPassword: vi.fn(), - resetPasswordSuccess: vi.fn(), - resetPasswordFailed: vi.fn(), - accountPasswordChange: vi.fn(), - accountEditChanged: vi.fn(), - accountImageChanged: vi.fn(), - getUserInfo: vi.fn(), - getGamesOfUser: vi.fn(), - gameJoined: vi.fn(), - notifyUser: vi.fn(), - playerPropertiesChanged: vi.fn(), - serverShutdown: vi.fn(), - userMessage: vi.fn(), - addToList: vi.fn(), - removeFromList: vi.fn(), - deleteServerDeck: vi.fn(), - updateServerDecks: vi.fn(), - uploadServerDeck: vi.fn(), - downloadServerDeck: vi.fn(), - createServerDeckDir: vi.fn(), - deleteServerDeckDir: vi.fn(), - replayList: vi.fn(), - replayAdded: vi.fn(), - replayModifyMatch: vi.fn(), - replayDeleteMatch: vi.fn(), - replayDownloaded: vi.fn(), -}; - -const room = { - clearStore: vi.fn(), - joinRoom: vi.fn(), - leaveRoom: vi.fn(), - updateRooms: vi.fn(), - updateGames: vi.fn(), - addMessage: vi.fn(), - userJoined: vi.fn(), - userLeft: vi.fn(), - removeMessages: vi.fn(), - gameCreated: vi.fn(), - joinedGame: vi.fn(), - setJoinGamePending: vi.fn(), - setJoinGameError: vi.fn(), -}; - -const game = { - clearStore: vi.fn(), - gameStateChanged: vi.fn(), - playerJoined: vi.fn(), - playerLeft: vi.fn(), - playerPropertiesChanged: vi.fn(), - gameClosed: vi.fn(), - gameHostChanged: vi.fn(), - kicked: vi.fn(), - gameSay: vi.fn(), - cardMoved: vi.fn(), - cardFlipped: vi.fn(), - cardDestroyed: vi.fn(), - cardAttached: vi.fn(), - tokenCreated: vi.fn(), - cardAttrChanged: vi.fn(), - cardCounterChanged: vi.fn(), - arrowCreated: vi.fn(), - arrowDeleted: vi.fn(), - counterCreated: vi.fn(), - counterSet: vi.fn(), - counterDeleted: vi.fn(), - cardsDrawn: vi.fn(), - cardsRevealed: vi.fn(), - zoneShuffled: vi.fn(), - dieRolled: vi.fn(), - activePlayerSet: vi.fn(), - activePhaseSet: vi.fn(), - turnReversed: vi.fn(), - zoneDumped: vi.fn(), - zonePropertiesChanged: vi.fn(), -}; - -const admin = { - adjustMod: vi.fn(), - reloadConfig: vi.fn(), - shutdownServer: vi.fn(), - updateServerMessage: vi.fn(), -}; - -const moderator = { - banFromServer: vi.fn(), - banHistory: vi.fn(), - viewLogs: vi.fn(), - warnHistory: vi.fn(), - warnListOptions: vi.fn(), - warnUser: vi.fn(), - grantReplayAccess: vi.fn(), - forceActivateUser: vi.fn(), - getAdminNotes: vi.fn(), - updateAdminNotes: vi.fn(), -}; - -export const WebClient = { - _instance: null as any, - instance: { - connect: vi.fn(), - testConnect: vi.fn(), - disconnect: vi.fn(), - updateStatus: vi.fn(), - status: 0 as number, - config: {}, - protobuf: { - sendSessionCommand: vi.fn(), - sendRoomCommand: vi.fn(), - sendGameCommand: vi.fn(), - sendAdminCommand: vi.fn(), - sendModeratorCommand: vi.fn(), - resetCommands: vi.fn(), - }, - response: { session, room, game, admin, moderator }, - }, -}; - diff --git a/webclient/src/websocket/__mocks__/callbackHelpers.ts b/webclient/src/websocket/__mocks__/callbackHelpers.ts deleted file mode 100644 index cee6ffb87..000000000 --- a/webclient/src/websocket/__mocks__/callbackHelpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Factory for invoking BackendService command callbacks in unit tests. - * - * @param mockFn - The vi.Mock for the BackendService send method - * (e.g. BackendService.sendSessionCommand as vi.Mock). - * @param optsArgIndex - Index of the options argument in the mock call. - * Defaults to 2 (ext, value, options). - * Use 3 for sendRoomCommand (roomId, ext, value, options). - */ -import { Mock } from 'vitest'; - -export function makeCallbackHelpers(mockFn: Mock, optsArgIndex = 2) { - function getLastSendOpts() { - const calls = mockFn.mock.calls; - return calls[calls.length - 1]?.[optsArgIndex]; - } - - function invokeOnSuccess(response: any = {}, raw?: any) { - getLastSendOpts()?.onSuccess?.(response, raw ?? response); - } - - function invokeResponseCode(code: number, raw: any = { responseCode: code }) { - const opts = getLastSendOpts(); - if (opts?.onResponseCode?.[code]) { - opts.onResponseCode[code](raw); - } - } - - function invokeOnError(code: number = 99, raw: any = {}) { - getLastSendOpts()?.onError?.(code, raw); - } - - function invokeCallback(callbackName: string, ...args: any[]) { - const opts = getLastSendOpts(); - if (opts?.[callbackName]) { - opts[callbackName](...args); - } - } - - return { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError, invokeCallback }; -} diff --git a/webclient/src/websocket/__mocks__/helpers.ts b/webclient/src/websocket/__mocks__/helpers.ts deleted file mode 100644 index 0173c9b43..000000000 --- a/webclient/src/websocket/__mocks__/helpers.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Shared mock factories for websocket layer unit tests. - */ - -/** Builds a mock WebSocket instance */ -export function makeMockWebSocketInstance() { - return { - send: vi.fn(), - close: vi.fn(), - readyState: WebSocket.OPEN as number, - binaryType: '' as BinaryType, - onopen: null as any, - onclose: null as any, - onerror: null as any, - onmessage: null as any, - }; -} - -/** Installs a mock WebSocket constructor on global. Returns the mock instance and a cleanup function. */ -export function installMockWebSocket() { - const originalWebSocket = (globalThis as any).WebSocket; - const mockInstance = makeMockWebSocketInstance(); - const instances: ReturnType[] = [mockInstance]; - let firstCall = true; - const MockWS = vi.fn(function MockWebSocket() { - if (firstCall) { - firstCall = false; - return mockInstance; - } - const next = makeMockWebSocketInstance(); - instances.push(next); - return next; - }) as any; - MockWS.OPEN = 1; - MockWS.CLOSED = 3; - (globalThis as any).WebSocket = MockWS; - return { MockWS, mockInstance, instances, restore: () => { - (globalThis as any).WebSocket = originalWebSocket; - } }; -} diff --git a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts deleted file mode 100644 index d82cbed0f..000000000 --- a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Shared mock shape factories for session command specs. - * - * Usage inside vi.mock() factory callbacks: - * - * vi.mock('../../WebClient', async () => { - * const { makeWebClientMock } = await import('../../__mocks__/sessionCommandMocks'); - * return { WebClient: { instance: makeWebClientMock() } }; - * }); - */ - -/** Superset WebClient instance mock — covers all properties used across both session spec files. */ -export function makeWebClientMock() { - return { - connect: vi.fn(), - testConnect: vi.fn(), - disconnect: vi.fn(), - updateStatus: vi.fn(), - config: {}, - status: 0, - protobuf: { - sendSessionCommand: vi.fn(), - sendRoomCommand: vi.fn(), - sendGameCommand: vi.fn(), - sendAdminCommand: vi.fn(), - sendModeratorCommand: vi.fn(), - }, - response: { - session: makeSessionPersistenceMock(), - room: { joinRoom: vi.fn() }, - game: {}, - admin: {}, - moderator: {}, - }, - }; -} - -/** Utils mock with unified return values. */ -export function makeUtilsMock() { - return { - hashPassword: vi.fn().mockReturnValue('hashed_pw'), - generateSalt: vi.fn().mockReturnValue('randSalt'), - passwordSaltSupported: vi.fn().mockReturnValue(0), - }; -} - -/** Superset SessionPersistence mock — covers all methods used across both session spec files. */ -export function makeSessionPersistenceMock() { - return { - loginSuccessful: vi.fn(), - loginFailed: vi.fn(), - updateBuddyList: vi.fn(), - updateIgnoreList: vi.fn(), - updateUser: vi.fn(), - updateUsers: vi.fn(), - accountAwaitingActivation: vi.fn(), - accountActivationSuccess: vi.fn(), - accountActivationFailed: vi.fn(), - updateStatus: vi.fn(), - addToList: vi.fn(), - removeFromList: vi.fn(), - deleteServerDeck: vi.fn(), - deleteServerDeckDir: vi.fn(), - updateServerDecks: vi.fn(), - uploadServerDeck: vi.fn(), - createServerDeckDir: vi.fn(), - getGamesOfUser: vi.fn(), - getUserInfo: vi.fn(), - accountPasswordChange: vi.fn(), - accountEditChanged: vi.fn(), - accountImageChanged: vi.fn(), - replayList: vi.fn(), - replayAdded: vi.fn(), - replayModifyMatch: vi.fn(), - replayDeleteMatch: vi.fn(), - downloadServerDeck: vi.fn(), - replayDownloaded: vi.fn(), - resetPasswordChallenge: vi.fn(), - resetPassword: vi.fn(), - resetPasswordFailed: vi.fn(), - resetPasswordSuccess: vi.fn(), - registrationFailed: vi.fn(), - registrationSuccess: vi.fn(), - registrationUserNameError: vi.fn(), - registrationPasswordError: vi.fn(), - registrationEmailError: vi.fn(), - registrationRequiresEmail: vi.fn(), - }; -} - -/** - * Session barrel mock — pure vi.fn() map for all cross-command calls. - * Used as-is by sessionCommands-complex.spec.ts, or spread over jest.requireActual - * by sessionCommands-simple.spec.ts to preserve real implementations for - * the commands under test. - */ -export function makeSessionBarrelMock() { - return { - login: vi.fn(), - register: vi.fn(), - activate: vi.fn(), - forgotPasswordReset: vi.fn(), - forgotPasswordRequest: vi.fn(), - forgotPasswordChallenge: vi.fn(), - requestPasswordSalt: vi.fn(), - listUsers: vi.fn(), - listRooms: vi.fn(), - updateStatus: vi.fn(), - disconnect: vi.fn(), - }; -} diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts deleted file mode 100644 index 7d9562506..000000000 --- a/webclient/src/websocket/commands/admin/adjustMod.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -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( - 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 deleted file mode 100644 index 08db9e409..000000000 --- a/webclient/src/websocket/commands/admin/adminCommands.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -vi.mock('../../WebClient'); - -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { WebClient } from '../../WebClient'; -import { adjustMod } from './adjustMod'; -import { reloadConfig } from './reloadConfig'; -import { shutdownServer } from './shutdownServer'; -import { updateServerMessage } from './updateServerMessage'; -import { - Command_AdjustMod_ext, - Command_ReloadConfig_ext, - Command_ShutdownServer_ext, - Command_UpdateServerMessage_ext, -} from '@app/generated'; - -import { Mock } from 'vitest'; - -const { invokeOnSuccess } = makeCallbackHelpers( - WebClient.instance.protobuf.sendAdminCommand as Mock, - 2 -); - -describe('adjustMod', () => { - - it('calls sendAdminCommand with Command_AdjustMod extension and fields', () => { - adjustMod('alice', true, false); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_AdjustMod_ext, - expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }), - expect.any(Object) - ); - }); - - it('onSuccess calls response.admin.adjustMod', () => { - adjustMod('alice', true, false); - invokeOnSuccess(); - expect(WebClient.instance.response.admin.adjustMod).toHaveBeenCalledWith('alice', true, false); - }); -}); - -describe('reloadConfig', () => { - - it('calls sendAdminCommand with Command_ReloadConfig extension', () => { - reloadConfig(); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_ReloadConfig_ext, - expect.any(Object), - expect.any(Object) - ); - }); - - it('onSuccess calls response.admin.reloadConfig', () => { - reloadConfig(); - invokeOnSuccess(); - expect(WebClient.instance.response.admin.reloadConfig).toHaveBeenCalled(); - }); -}); - -describe('shutdownServer', () => { - - it('calls sendAdminCommand with Command_ShutdownServer extension and fields', () => { - shutdownServer('maintenance', 10); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_ShutdownServer_ext, - expect.objectContaining({ reason: 'maintenance', minutes: 10 }), - expect.any(Object) - ); - }); - - it('onSuccess calls response.admin.shutdownServer', () => { - shutdownServer('maintenance', 10); - invokeOnSuccess(); - expect(WebClient.instance.response.admin.shutdownServer).toHaveBeenCalled(); - }); -}); - -describe('updateServerMessage', () => { - - it('calls sendAdminCommand with Command_UpdateServerMessage extension', () => { - updateServerMessage(); - expect(WebClient.instance.protobuf.sendAdminCommand).toHaveBeenCalledWith( - Command_UpdateServerMessage_ext, - expect.any(Object), - expect.any(Object) - ); - }); - - it('onSuccess calls response.admin.updateServerMessage', () => { - updateServerMessage(); - invokeOnSuccess(); - expect(WebClient.instance.response.admin.updateServerMessage).toHaveBeenCalled(); - }); -}); diff --git a/webclient/src/websocket/commands/admin/index.ts b/webclient/src/websocket/commands/admin/index.ts deleted file mode 100644 index 5d4ae21bf..000000000 --- a/webclient/src/websocket/commands/admin/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './adjustMod'; -export * from './reloadConfig'; -export * from './shutdownServer'; -export * from './updateServerMessage'; diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts deleted file mode 100644 index 0856db673..000000000 --- a/webclient/src/websocket/commands/admin/reloadConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; -export function reloadConfig(): void { - 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 deleted file mode 100644 index a341ffa81..000000000 --- a/webclient/src/websocket/commands/admin/shutdownServer.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -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( - 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 deleted file mode 100644 index 7c99ddd5b..000000000 --- a/webclient/src/websocket/commands/admin/updateServerMessage.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; -export function updateServerMessage(): void { - 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.presence.spec.ts b/webclient/src/websocket/commands/game/attachCard.presence.spec.ts deleted file mode 100644 index 8feedffeb..000000000 --- a/webclient/src/websocket/commands/game/attachCard.presence.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Byte-level verification that Command_AttachCard's proto2 optional target -// fields stay UNSET when we build an "Unattach" — i.e. the client omits -// them entirely rather than sending sentinel values. Desktop's server-side -// `has_target_player_id()` / `has_target_zone()` / `has_target_card_id()` -// drives the unattach branch; if any target field gets serialized, the -// server treats it as a re-attach instead of an unattach. -// -// See the M6 Unattach deferrable in webclient/plans/gameboard-deferrables.md. - -import { create, isFieldSet, toBinary, fromBinary } from '@bufbuild/protobuf'; -import { Command_AttachCardSchema } from '@app/generated'; - -describe('Command_AttachCard proto2 presence (Unattach invariant)', () => { - it('omitting target fields leaves them unset on the in-memory message', () => { - const msg = create(Command_AttachCardSchema, { - startZone: 'table', - cardId: 10, - }); - - expect(isFieldSet(msg, Command_AttachCardSchema.field.startZone)).toBe(true); - expect(isFieldSet(msg, Command_AttachCardSchema.field.cardId)).toBe(true); - expect(isFieldSet(msg, Command_AttachCardSchema.field.targetPlayerId)).toBe(false); - expect(isFieldSet(msg, Command_AttachCardSchema.field.targetZone)).toBe(false); - expect(isFieldSet(msg, Command_AttachCardSchema.field.targetCardId)).toBe(false); - }); - - it('omitting target fields omits them from the serialized bytes', () => { - const msg = create(Command_AttachCardSchema, { - startZone: 'table', - cardId: 10, - }); - - const bytes = toBinary(Command_AttachCardSchema, msg); - // Round-trip to confirm the wire format also reports the fields unset. - const parsed = fromBinary(Command_AttachCardSchema, bytes); - - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetPlayerId)).toBe(false); - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetZone)).toBe(false); - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetCardId)).toBe(false); - }); - - it('passing an explicit targetPlayerId DOES set presence (so "attach" paths still work)', () => { - const msg = create(Command_AttachCardSchema, { - startZone: 'table', - cardId: 10, - targetPlayerId: 5, - targetZone: 'table', - targetCardId: 99, - }); - - const bytes = toBinary(Command_AttachCardSchema, msg); - const parsed = fromBinary(Command_AttachCardSchema, bytes); - - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetPlayerId)).toBe(true); - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetZone)).toBe(true); - expect(isFieldSet(parsed, Command_AttachCardSchema.field.targetCardId)).toBe(true); - expect(parsed.targetPlayerId).toBe(5); - }); - - it('setting a target field to its default -1 still records presence (presence != default)', () => { - // This is the trap the deferrable warned about: proto2 `optional` with a - // default of -1 treats an explicit `-1` as "field set with value -1", - // which the server interprets as "attach to slot -1" rather than unattach. - // Clients must OMIT the field entirely to unattach. - const msg = create(Command_AttachCardSchema, { - startZone: 'table', - cardId: 10, - targetPlayerId: -1, - }); - - expect(isFieldSet(msg, Command_AttachCardSchema.field.targetPlayerId)).toBe(true); - }); -}); diff --git a/webclient/src/websocket/commands/game/attachCard.ts b/webclient/src/websocket/commands/game/attachCard.ts deleted file mode 100644 index 5864a331a..000000000 --- a/webclient/src/websocket/commands/game/attachCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_AttachCard_ext, Command_AttachCardSchema, type AttachCardParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function attachCard(gameId: number, params: AttachCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/changeZoneProperties.ts b/webclient/src/websocket/commands/game/changeZoneProperties.ts deleted file mode 100644 index e186bba91..000000000 --- a/webclient/src/websocket/commands/game/changeZoneProperties.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_ChangeZoneProperties_ext, Command_ChangeZonePropertiesSchema, type ChangeZonePropertiesParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void { - WebClient.instance.protobuf.sendGameCommand( - gameId, - 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 deleted file mode 100644 index 9007e1223..000000000 --- a/webclient/src/websocket/commands/game/concede.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_Concede_ext, Command_ConcedeSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function concede(gameId: number): void { - 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 deleted file mode 100644 index 87aaf094c..000000000 --- a/webclient/src/websocket/commands/game/createArrow.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_CreateArrow_ext, Command_CreateArrowSchema, type CreateArrowParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function createArrow(gameId: number, params: CreateArrowParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/createCounter.ts b/webclient/src/websocket/commands/game/createCounter.ts deleted file mode 100644 index 886c63b3b..000000000 --- a/webclient/src/websocket/commands/game/createCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_CreateCounter_ext, Command_CreateCounterSchema, type CreateCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function createCounter(gameId: number, params: CreateCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/createToken.ts b/webclient/src/websocket/commands/game/createToken.ts deleted file mode 100644 index b4c837417..000000000 --- a/webclient/src/websocket/commands/game/createToken.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_CreateToken_ext, Command_CreateTokenSchema, type CreateTokenParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function createToken(gameId: number, params: CreateTokenParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/deckSelect.ts b/webclient/src/websocket/commands/game/deckSelect.ts deleted file mode 100644 index d2f28a7c0..000000000 --- a/webclient/src/websocket/commands/game/deckSelect.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_DeckSelect_ext, Command_DeckSelectSchema, type DeckSelectParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function deckSelect(gameId: number, params: DeckSelectParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/delCounter.ts b/webclient/src/websocket/commands/game/delCounter.ts deleted file mode 100644 index 6b215d123..000000000 --- a/webclient/src/websocket/commands/game/delCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_DelCounter_ext, Command_DelCounterSchema, type DelCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function delCounter(gameId: number, params: DelCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/deleteArrow.ts b/webclient/src/websocket/commands/game/deleteArrow.ts deleted file mode 100644 index 8391277f7..000000000 --- a/webclient/src/websocket/commands/game/deleteArrow.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_DeleteArrow_ext, Command_DeleteArrowSchema, type DeleteArrowParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function deleteArrow(gameId: number, params: DeleteArrowParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/drawCards.ts b/webclient/src/websocket/commands/game/drawCards.ts deleted file mode 100644 index 5c7ed8c0a..000000000 --- a/webclient/src/websocket/commands/game/drawCards.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_DrawCards_ext, Command_DrawCardsSchema, type DrawCardsParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function drawCards(gameId: number, params: DrawCardsParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/dumpZone.ts b/webclient/src/websocket/commands/game/dumpZone.ts deleted file mode 100644 index 62d792d2e..000000000 --- a/webclient/src/websocket/commands/game/dumpZone.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_DumpZone_ext, Command_DumpZoneSchema, type DumpZoneParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function dumpZone(gameId: number, params: DumpZoneParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/flipCard.ts b/webclient/src/websocket/commands/game/flipCard.ts deleted file mode 100644 index b4b894962..000000000 --- a/webclient/src/websocket/commands/game/flipCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_FlipCard_ext, Command_FlipCardSchema, type FlipCardParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function flipCard(gameId: number, params: FlipCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts deleted file mode 100644 index 8706a9e3d..000000000 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ /dev/null @@ -1,314 +0,0 @@ -vi.mock('../../WebClient'); - -import { WebClient } from '../../WebClient'; -import { create, setExtension } from '@bufbuild/protobuf'; -import { - Command_AttachCard_ext, - Command_ChangeZoneProperties_ext, - Command_Concede_ext, - Command_CreateArrow_ext, - Command_CreateCounter_ext, - Command_CreateToken_ext, - Command_DeckSelect_ext, - Command_DelCounter_ext, - Command_DeleteArrow_ext, - Command_DrawCards_ext, - Command_DrawCardsSchema, - Command_DumpZone_ext, - Command_FlipCard_ext, - Command_GameSay_ext, - Command_IncCardCounter_ext, - Command_IncCounter_ext, - Command_Judge_ext, - Command_KickFromGame_ext, - Command_LeaveGame_ext, - Command_MoveCard_ext, - Command_Mulligan_ext, - Command_NextTurn_ext, - Command_ReadyStart_ext, - Command_RevealCards_ext, - Command_ReverseTurn_ext, - Command_RollDie_ext, - Command_SetActivePhase_ext, - Command_SetCardAttr_ext, - Command_SetCardCounter_ext, - Command_SetCounter_ext, - Command_SetSideboardLock_ext, - Command_SetSideboardPlan_ext, - Command_Shuffle_ext, - Command_UndoDraw_ext, - Command_Unconcede_ext, - GameCommandSchema, -} from '@app/generated'; - -import { attachCard } from './attachCard'; -import { changeZoneProperties } from './changeZoneProperties'; -import { concede } from './concede'; -import { createArrow } from './createArrow'; -import { createCounter } from './createCounter'; -import { createToken } from './createToken'; -import { deckSelect } from './deckSelect'; -import { delCounter } from './delCounter'; -import { deleteArrow } from './deleteArrow'; -import { drawCards } from './drawCards'; -import { dumpZone } from './dumpZone'; -import { flipCard } from './flipCard'; -import { gameSay } from './gameSay'; -import { incCardCounter } from './incCardCounter'; -import { incCounter } from './incCounter'; -import { kickFromGame } from './kickFromGame'; -import { leaveGame } from './leaveGame'; -import { moveCard } from './moveCard'; -import { mulligan } from './mulligan'; -import { nextTurn } from './nextTurn'; -import { readyStart } from './readyStart'; -import { revealCards } from './revealCards'; -import { reverseTurn } from './reverseTurn'; -import { rollDie } from './rollDie'; -import { setActivePhase } from './setActivePhase'; -import { setCardAttr } from './setCardAttr'; -import { setCardCounter } from './setCardCounter'; -import { setCounter } from './setCounter'; -import { setSideboardLock } from './setSideboardLock'; -import { setSideboardPlan } from './setSideboardPlan'; -import { shuffle } from './shuffle'; -import { undoDraw } from './undoDraw'; -import { unconcede } from './unconcede'; -import { judge } from './judge'; - -const gameId = 1; - -describe('Game commands — delegate to WebClient.instance.protobuf.sendGameCommand', () => { - it('attachCard sends Command_AttachCard', () => { - attachCard(gameId, { cardId: 10, startZone: 'hand' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) - ); - }); - - it('changeZoneProperties sends Command_ChangeZoneProperties', () => { - changeZoneProperties(gameId, { zoneName: 'side' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_Concede_ext, expect.any(Object)); - }); - - it('createArrow sends Command_CreateArrow', () => { - createArrow(gameId, { startPlayerId: 1, startZone: 'hand' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) - ); - }); - - it('createCounter sends Command_CreateCounter', () => { - createCounter(gameId, { counterName: 'life' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) - ); - }); - - it('deckSelect sends Command_DeckSelect', () => { - deckSelect(gameId, { deckId: 5 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) - ); - }); - - it('delCounter sends Command_DelCounter', () => { - delCounter(gameId, { counterId: 3 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) - ); - }); - - it('deleteArrow sends Command_DeleteArrow', () => { - deleteArrow(gameId, { arrowId: 2 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) - ); - }); - - it('drawCards sends Command_DrawCards', () => { - drawCards(gameId, { number: 3 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) - ); - }); - - it('flipCard sends Command_FlipCard', () => { - flipCard(gameId, { cardId: 7, faceDown: false }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) - ); - }); - - it('gameSay sends Command_GameSay', () => { - gameSay(gameId, { message: 'hello' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) - ); - }); - - it('incCounter sends Command_IncCounter', () => { - incCounter(gameId, { counterId: 1, delta: 5 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) - ); - }); - - it('kickFromGame sends Command_KickFromGame', () => { - kickFromGame(gameId, { playerId: 2 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_LeaveGame_ext, expect.any(Object)); - }); - - it('moveCard sends Command_MoveCard', () => { - moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_MoveCard_ext, - expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' }) - ); - }); - - it('mulligan sends Command_Mulligan', () => { - mulligan(gameId, { number: 7 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_NextTurn_ext, expect.any(Object)); - }); - - it('readyStart sends Command_ReadyStart', () => { - readyStart(gameId, { ready: true }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_ReadyStart_ext, expect.objectContaining({ ready: true }) - ); - }); - - it('revealCards sends Command_RevealCards', () => { - revealCards(gameId, { zoneName: 'hand', cardId: [1, 2] }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) - ); - }); - - it('reverseTurn sends Command_ReverseTurn with empty object', () => { - reverseTurn(gameId); - 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, Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) - ); - }); - - it('setCardAttr sends Command_SetCardAttr', () => { - setCardAttr(gameId, { zone: 'play', cardId: 5, attrValue: '2' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardAttr_ext, - expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' }) - ); - }); - - it('setCardCounter sends Command_SetCardCounter', () => { - setCardCounter(gameId, { cardId: 5, counterId: 1 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) - ); - }); - - it('setCounter sends Command_SetCounter', () => { - setCounter(gameId, { counterId: 1, value: 10 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) - ); - }); - - it('setSideboardLock sends Command_SetSideboardLock', () => { - setSideboardLock(gameId, { locked: true }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) - ); - }); - - it('setSideboardPlan sends Command_SetSideboardPlan', () => { - setSideboardPlan(gameId, { moveList: [] }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) - ); - }); - - it('shuffle sends Command_Shuffle', () => { - shuffle(gameId, { zoneName: 'hand' }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - 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, Command_UndoDraw_ext, expect.any(Object)); - }); - - it('unconcede sends Command_Unconcede with empty object', () => { - unconcede(gameId); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object)); - }); - - it('rollDie sends Command_RollDie', () => { - rollDie(gameId, { sides: 6, count: 2 }); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_RollDie_ext, expect.objectContaining({ sides: 6, count: 2 }) - ); - }); - - it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => { - const targetId = 3; - const innerCmd = create(GameCommandSchema); - setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 })); - judge(gameId, targetId, innerCmd); - expect(WebClient.instance.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, - 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 deleted file mode 100644 index 7ae7263e9..000000000 --- a/webclient/src/websocket/commands/game/gameSay.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_GameSay_ext, Command_GameSaySchema, type GameSayParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function gameSay(gameId: number, params: GameSayParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params)); -} diff --git a/webclient/src/websocket/commands/game/incCardCounter.ts b/webclient/src/websocket/commands/game/incCardCounter.ts deleted file mode 100644 index 54349d920..000000000 --- a/webclient/src/websocket/commands/game/incCardCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_IncCardCounter_ext, Command_IncCardCounterSchema, type IncCardCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function incCardCounter(gameId: number, params: IncCardCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/incCounter.ts b/webclient/src/websocket/commands/game/incCounter.ts deleted file mode 100644 index 4ca0b8558..000000000 --- a/webclient/src/websocket/commands/game/incCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_IncCounter_ext, Command_IncCounterSchema, type IncCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function incCounter(gameId: number, params: IncCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/index.ts b/webclient/src/websocket/commands/game/index.ts deleted file mode 100644 index bed2a9776..000000000 --- a/webclient/src/websocket/commands/game/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -export { leaveGame } from './leaveGame'; -export { kickFromGame } from './kickFromGame'; -export { gameSay } from './gameSay'; -export { readyStart } from './readyStart'; -export { concede } from './concede'; -export { unconcede } from './unconcede'; -export { judge } from './judge'; -export { nextTurn } from './nextTurn'; -export { setActivePhase } from './setActivePhase'; -export { reverseTurn } from './reverseTurn'; -export { moveCard } from './moveCard'; -export { flipCard } from './flipCard'; -export { attachCard } from './attachCard'; -export { createToken } from './createToken'; -export { setCardAttr } from './setCardAttr'; -export { setCardCounter } from './setCardCounter'; -export { incCardCounter } from './incCardCounter'; -export { drawCards } from './drawCards'; -export { undoDraw } from './undoDraw'; -export { createArrow } from './createArrow'; -export { deleteArrow } from './deleteArrow'; -export { createCounter } from './createCounter'; -export { setCounter } from './setCounter'; -export { incCounter } from './incCounter'; -export { delCounter } from './delCounter'; -export { shuffle } from './shuffle'; -export { dumpZone } from './dumpZone'; -export { revealCards } from './revealCards'; -export { changeZoneProperties } from './changeZoneProperties'; -export { deckSelect } from './deckSelect'; -export { setSideboardPlan } from './setSideboardPlan'; -export { setSideboardLock } from './setSideboardLock'; -export { mulligan } from './mulligan'; -export { rollDie } from './rollDie'; diff --git a/webclient/src/websocket/commands/game/judge.ts b/webclient/src/websocket/commands/game/judge.ts deleted file mode 100644 index c61a1092d..000000000 --- a/webclient/src/websocket/commands/game/judge.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_Judge_ext, Command_JudgeSchema, type GameCommand } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index c7b724b88..000000000 --- a/webclient/src/websocket/commands/game/kickFromGame.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_KickFromGame_ext, Command_KickFromGameSchema, type KickFromGameParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function kickFromGame(gameId: number, params: KickFromGameParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/leaveGame.ts b/webclient/src/websocket/commands/game/leaveGame.ts deleted file mode 100644 index 05d160059..000000000 --- a/webclient/src/websocket/commands/game/leaveGame.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_LeaveGame_ext, Command_LeaveGameSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function leaveGame(gameId: number): void { - 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 deleted file mode 100644 index 9b5dc52b5..000000000 --- a/webclient/src/websocket/commands/game/moveCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_MoveCard_ext, Command_MoveCardSchema, type MoveCardParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function moveCard(gameId: number, params: MoveCardParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/mulligan.ts b/webclient/src/websocket/commands/game/mulligan.ts deleted file mode 100644 index b6ac92cb9..000000000 --- a/webclient/src/websocket/commands/game/mulligan.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_Mulligan_ext, Command_MulliganSchema, type MulliganParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function mulligan(gameId: number, params: MulliganParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/nextTurn.ts b/webclient/src/websocket/commands/game/nextTurn.ts deleted file mode 100644 index 4633e9233..000000000 --- a/webclient/src/websocket/commands/game/nextTurn.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_NextTurn_ext, Command_NextTurnSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function nextTurn(gameId: number): void { - 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 deleted file mode 100644 index 29c65c2a8..000000000 --- a/webclient/src/websocket/commands/game/readyStart.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_ReadyStart_ext, Command_ReadyStartSchema, type ReadyStartParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function readyStart(gameId: number, params: ReadyStartParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/revealCards.ts b/webclient/src/websocket/commands/game/revealCards.ts deleted file mode 100644 index e4a1cf715..000000000 --- a/webclient/src/websocket/commands/game/revealCards.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_RevealCards_ext, Command_RevealCardsSchema, type RevealCardsParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function revealCards(gameId: number, params: RevealCardsParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/reverseTurn.ts b/webclient/src/websocket/commands/game/reverseTurn.ts deleted file mode 100644 index 7ce90a669..000000000 --- a/webclient/src/websocket/commands/game/reverseTurn.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_ReverseTurn_ext, Command_ReverseTurnSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function reverseTurn(gameId: number): void { - 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 deleted file mode 100644 index e76bc2445..000000000 --- a/webclient/src/websocket/commands/game/rollDie.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_RollDie_ext, Command_RollDieSchema, type RollDieParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function rollDie(gameId: number, params: RollDieParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_RollDie_ext, create(Command_RollDieSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/setActivePhase.ts b/webclient/src/websocket/commands/game/setActivePhase.ts deleted file mode 100644 index c56d42d37..000000000 --- a/webclient/src/websocket/commands/game/setActivePhase.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetActivePhase_ext, Command_SetActivePhaseSchema, type SetActivePhaseParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setActivePhase(gameId: number, params: SetActivePhaseParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/setCardAttr.ts b/webclient/src/websocket/commands/game/setCardAttr.ts deleted file mode 100644 index 3f62f82a6..000000000 --- a/webclient/src/websocket/commands/game/setCardAttr.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetCardAttr_ext, Command_SetCardAttrSchema, type SetCardAttrParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setCardAttr(gameId: number, params: SetCardAttrParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/setCardCounter.ts b/webclient/src/websocket/commands/game/setCardCounter.ts deleted file mode 100644 index e70631ed3..000000000 --- a/webclient/src/websocket/commands/game/setCardCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetCardCounter_ext, Command_SetCardCounterSchema, type SetCardCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setCardCounter(gameId: number, params: SetCardCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/setCounter.ts b/webclient/src/websocket/commands/game/setCounter.ts deleted file mode 100644 index 290c1b515..000000000 --- a/webclient/src/websocket/commands/game/setCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetCounter_ext, Command_SetCounterSchema, type SetCounterParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setCounter(gameId: number, params: SetCounterParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/setSideboardLock.ts b/webclient/src/websocket/commands/game/setSideboardLock.ts deleted file mode 100644 index cc7844d36..000000000 --- a/webclient/src/websocket/commands/game/setSideboardLock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetSideboardLock_ext, Command_SetSideboardLockSchema, type SetSideboardLockParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void { - WebClient.instance.protobuf.sendGameCommand( - gameId, - 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 deleted file mode 100644 index 8caa495c2..000000000 --- a/webclient/src/websocket/commands/game/setSideboardPlan.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_SetSideboardPlan_ext, Command_SetSideboardPlanSchema, type SetSideboardPlanParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void { - WebClient.instance.protobuf.sendGameCommand( - gameId, - 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 deleted file mode 100644 index 9e9a6772c..000000000 --- a/webclient/src/websocket/commands/game/shuffle.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_Shuffle_ext, Command_ShuffleSchema, type ShuffleParams } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function shuffle(gameId: number, params: ShuffleParams): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params)); -} diff --git a/webclient/src/websocket/commands/game/unconcede.ts b/webclient/src/websocket/commands/game/unconcede.ts deleted file mode 100644 index 505b5991c..000000000 --- a/webclient/src/websocket/commands/game/unconcede.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_Unconcede_ext, Command_UnconcedeSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function unconcede(gameId: number): void { - 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 deleted file mode 100644 index b11e76d29..000000000 --- a/webclient/src/websocket/commands/game/undoDraw.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { Command_UndoDraw_ext, Command_UndoDrawSchema } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function undoDraw(gameId: number): void { - WebClient.instance.protobuf.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema)); -} diff --git a/webclient/src/websocket/commands/index.ts b/webclient/src/websocket/commands/index.ts deleted file mode 100644 index 5bed30ffe..000000000 --- a/webclient/src/websocket/commands/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * as AdminCommands from './admin'; -export * as GameCommands from './game'; -export * as ModeratorCommands from './moderator'; -export * as RoomCommands from './room'; -export * as SessionCommands from './session'; diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts deleted file mode 100644 index d2c2bae64..000000000 --- a/webclient/src/websocket/commands/moderator/banFromServer.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -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(Command_BanFromServer_ext, create(Command_BanFromServerSchema, { - minutes, userName, address, reason, visibleReason, clientid, removeMessages - }), { - onSuccess: () => { - WebClient.instance.response.moderator.banFromServer(userName); - }, - }); -} diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts deleted file mode 100644 index 6b7625809..000000000 --- a/webclient/src/websocket/commands/moderator/forceActivateUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ForceActivateUser_ext, Command_ForceActivateUserSchema } from '@app/generated'; - -export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { - 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 deleted file mode 100644 index 6339b8b77..000000000 --- a/webclient/src/websocket/commands/moderator/getAdminNotes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema, Response_GetAdminNotes_ext } from '@app/generated'; - -export function getAdminNotes(userName: string): void { - 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 deleted file mode 100644 index ecc48fb26..000000000 --- a/webclient/src/websocket/commands/moderator/getBanHistory.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GetBanHistory_ext, Command_GetBanHistorySchema, Response_BanHistory_ext } from '@app/generated'; - -export function getBanHistory(userName: string): void { - 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 deleted file mode 100644 index 9f27cf2c6..000000000 --- a/webclient/src/websocket/commands/moderator/getWarnHistory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema, Response_WarnHistory_ext } from '@app/generated'; - -export function getWarnHistory(userName: string): void { - WebClient.instance.protobuf.sendModeratorCommand( - Command_GetWarnHistory_ext, - create(Command_GetWarnHistorySchema, { userName }), - { - 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 deleted file mode 100644 index 262cba2df..000000000 --- a/webclient/src/websocket/commands/moderator/getWarnList.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -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( - Command_GetWarnList_ext, - create(Command_GetWarnListSchema, { modName, userName, userClientid }), - { - 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 deleted file mode 100644 index 0b288efc7..000000000 --- a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema } from '@app/generated'; - -export function grantReplayAccess(replayId: number, moderatorName: string): void { - WebClient.instance.protobuf.sendModeratorCommand( - Command_GrantReplayAccess_ext, - create(Command_GrantReplayAccessSchema, { replayId, moderatorName }), - { - onSuccess: () => { - WebClient.instance.response.moderator.grantReplayAccess(replayId, moderatorName); - }, - }, - ); -} diff --git a/webclient/src/websocket/commands/moderator/index.ts b/webclient/src/websocket/commands/moderator/index.ts deleted file mode 100644 index 10bb0e1c6..000000000 --- a/webclient/src/websocket/commands/moderator/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './banFromServer'; -export * from './forceActivateUser'; -export * from './getAdminNotes'; -export * from './getBanHistory'; -export * from './getWarnHistory'; -export * from './getWarnList'; -export * from './grantReplayAccess'; -export * from './updateAdminNotes'; -export * from './viewLogHistory'; -export * from './warnUser'; diff --git a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts deleted file mode 100644 index 4dd7fcf76..000000000 --- a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -vi.mock('../../WebClient'); - -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { WebClient } from '../../WebClient'; -import { - Command_BanFromServer_ext, - Command_ForceActivateUser_ext, - Command_GetAdminNotes_ext, - Command_GetBanHistory_ext, - Command_GetWarnHistory_ext, - Command_GetWarnList_ext, - Command_GrantReplayAccess_ext, - Command_UpdateAdminNotes_ext, - Command_ViewLogHistory_ext, - Command_ViewLogHistorySchema, - Command_WarnUser_ext, - Response_BanHistory_ext, - Response_GetAdminNotes_ext, - Response_ViewLogHistory_ext, - Response_WarnHistory_ext, - Response_WarnList_ext, -} from '@app/generated'; - -import { banFromServer } from './banFromServer'; -import { forceActivateUser } from './forceActivateUser'; -import { getAdminNotes } from './getAdminNotes'; -import { getBanHistory } from './getBanHistory'; -import { getWarnHistory } from './getWarnHistory'; -import { getWarnList } from './getWarnList'; -import { grantReplayAccess } from './grantReplayAccess'; -import { updateAdminNotes } from './updateAdminNotes'; -import { viewLogHistory } from './viewLogHistory'; -import { warnUser } from './warnUser'; -import { create } from '@bufbuild/protobuf'; -import { Mock } from 'vitest'; - -const { invokeOnSuccess } = makeCallbackHelpers( - WebClient.instance.protobuf.sendModeratorCommand as Mock, - 2 -); - -describe('banFromServer', () => { - - it('calls sendModeratorCommand with Command_BanFromServer', () => { - banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_BanFromServer_ext, - expect.objectContaining({ minutes: 30, userName: 'alice' }), - expect.any(Object) - ); - }); - - it('onSuccess calls response.moderator.banFromServer', () => { - banFromServer(30, 'alice'); - invokeOnSuccess(); - expect(WebClient.instance.response.moderator.banFromServer).toHaveBeenCalledWith('alice'); - }); -}); - -describe('forceActivateUser', () => { - - it('calls sendModeratorCommand with Command_ForceActivateUser', () => { - forceActivateUser('alice', 'mod1'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls response.moderator.forceActivateUser', () => { - forceActivateUser('alice', 'mod1'); - invokeOnSuccess(); - expect(WebClient.instance.response.moderator.forceActivateUser).toHaveBeenCalledWith('alice', 'mod1'); - }); -}); - -describe('getAdminNotes', () => { - - it('calls sendModeratorCommand with Command_GetAdminNotes', () => { - getAdminNotes('alice'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetAdminNotes_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_GetAdminNotes_ext }) - ); - }); - - it('onSuccess calls response.moderator.getAdminNotes with notes', () => { - getAdminNotes('alice'); - const resp = { notes: 'some notes' }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.moderator.getAdminNotes).toHaveBeenCalledWith('alice', 'some notes'); - }); -}); - -describe('getBanHistory', () => { - - it('calls sendModeratorCommand with Command_GetBanHistory', () => { - getBanHistory('alice'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetBanHistory_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_BanHistory_ext }) - ); - }); - - it('onSuccess calls response.moderator.banHistory with banList', () => { - getBanHistory('alice'); - const resp = { banList: [{ id: 1 }] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.moderator.banHistory).toHaveBeenCalledWith('alice', [{ id: 1 }]); - }); -}); - -describe('getWarnHistory', () => { - - it('calls sendModeratorCommand with Command_GetWarnHistory', () => { - getWarnHistory('alice'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnHistory_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnHistory_ext }) - ); - }); - - it('onSuccess calls response.moderator.warnHistory with warnList', () => { - getWarnHistory('alice'); - const resp = { warnList: [{ id: 2 }] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.moderator.warnHistory).toHaveBeenCalledWith('alice', [{ id: 2 }]); - }); -}); - -describe('getWarnList', () => { - - it('calls sendModeratorCommand with Command_GetWarnList', () => { - getWarnList('mod1', 'alice', 'US'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnList_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnList_ext }) - ); - }); - - it('onSuccess calls response.moderator.warnListOptions with the response', () => { - getWarnList('mod1', 'alice', 'US'); - const resp = { warning: ['w1', 'w2'], userName: 'alice', userClientid: 'US' }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.moderator.warnListOptions).toHaveBeenCalledWith([resp]); - }); -}); - -describe('grantReplayAccess', () => { - - it('calls sendModeratorCommand with Command_GrantReplayAccess', () => { - grantReplayAccess(10, 'mod1'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls response.moderator.grantReplayAccess', () => { - grantReplayAccess(10, 'mod1'); - invokeOnSuccess(); - expect(WebClient.instance.response.moderator.grantReplayAccess).toHaveBeenCalledWith(10, 'mod1'); - }); -}); - -describe('updateAdminNotes', () => { - - it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => { - updateAdminNotes('alice', 'new notes'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls response.moderator.updateAdminNotes', () => { - updateAdminNotes('alice', 'new notes'); - invokeOnSuccess(); - expect(WebClient.instance.response.moderator.updateAdminNotes).toHaveBeenCalledWith('alice', 'new notes'); - }); -}); - -describe('viewLogHistory', () => { - - it('calls sendModeratorCommand with Command_ViewLogHistory', () => { - const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); - viewLogHistory(filters); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ViewLogHistory_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_ViewLogHistory_ext }) - ); - }); - - it('onSuccess calls response.moderator.viewLogs with logMessage', () => { - const filters = create(Command_ViewLogHistorySchema, { dateRange: 7 }); - viewLogHistory(filters); - const resp = { logMessage: ['log1'] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.moderator.viewLogs).toHaveBeenCalledWith(['log1']); - }); -}); - -describe('warnUser', () => { - - it('calls sendModeratorCommand with Command_WarnUser', () => { - warnUser('alice', 'bad behavior', 'cid'); - expect(WebClient.instance.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_WarnUser_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls response.moderator.warnUser', () => { - warnUser('alice', 'bad behavior', 'cid'); - invokeOnSuccess(); - expect(WebClient.instance.response.moderator.warnUser).toHaveBeenCalledWith('alice'); - }); -}); diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts deleted file mode 100644 index 7d760938d..000000000 --- a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema } from '@app/generated'; - -export function updateAdminNotes(userName: string, notes: string): void { - WebClient.instance.protobuf.sendModeratorCommand( - 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 deleted file mode 100644 index e222899f0..000000000 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema, Response_ViewLogHistory_ext } from '@app/generated'; -import type { ViewLogHistoryParams } from '@app/generated'; - -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 deleted file mode 100644 index 22d25ac75..000000000 --- a/webclient/src/websocket/commands/moderator/warnUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_WarnUser_ext, Command_WarnUserSchema } from '@app/generated'; - -export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); - WebClient.instance.protobuf.sendModeratorCommand(Command_WarnUser_ext, cmd, { - 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 deleted file mode 100644 index 133e8a396..000000000 --- a/webclient/src/websocket/commands/room/createGame.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_CreateGame_ext, Command_CreateGameSchema } from '@app/generated'; -import type { CreateGameParams } from '@app/generated'; - -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/index.ts b/webclient/src/websocket/commands/room/index.ts deleted file mode 100644 index 18235618c..000000000 --- a/webclient/src/websocket/commands/room/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './createGame'; -export * from './joinGame'; -export * from './leaveRoom'; -export * from './roomSay'; diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts deleted file mode 100644 index c9d25811d..000000000 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_JoinGame_ext, Command_JoinGameSchema, Response_ResponseCode } from '@app/generated'; -import type { JoinGameParams } from '@app/generated'; - -// Desktop message strings from cockatrice/src/interface/widgets/server/game_selector.cpp:234-260 -// (GameSelector::checkResponse). Codes not listed here (e.g. RespContextError) intentionally -// fall through to the desktop's `default:;` — silent to the user. -const ERROR_MESSAGES: Record = { - [Response_ResponseCode.RespNotInRoom]: 'Please join the appropriate room first.', - [Response_ResponseCode.RespNameNotFound]: 'The game does not exist any more.', - [Response_ResponseCode.RespGameFull]: 'The game is already full.', - [Response_ResponseCode.RespWrongPassword]: 'Wrong password.', - [Response_ResponseCode.RespSpectatorsNotAllowed]: 'Spectators are not allowed in this game.', - [Response_ResponseCode.RespOnlyBuddies]: 'This game is only open to its creator\'s buddies.', - [Response_ResponseCode.RespUserLevelTooLow]: 'This game is only open to registered users.', - [Response_ResponseCode.RespInIgnoreList]: 'You are being ignored by the creator of this game.', -}; - -export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { - const response = WebClient.instance.response.room; - response.setJoinGamePending(true); - - const onResponseCode: { [code: number]: () => void } = { - // Match desktop default:; — acknowledge silently, no user dialog. - [Response_ResponseCode.RespContextError]: () => response.setJoinGamePending(false), - }; - for (const codeStr of Object.keys(ERROR_MESSAGES)) { - const code = Number(codeStr); - onResponseCode[code] = () => response.setJoinGameError(code, ERROR_MESSAGES[code]); - } - - WebClient.instance.protobuf.sendRoomCommand( - roomId, - Command_JoinGame_ext, - create(Command_JoinGameSchema, joinGameParams), - { - onSuccess: () => { - response.setJoinGamePending(false); - response.joinedGame(roomId, joinGameParams.gameId); - }, - onResponseCode, - onError: () => response.setJoinGamePending(false), - }, - ); -} diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts deleted file mode 100644 index 004e704c0..000000000 --- a/webclient/src/websocket/commands/room/leaveRoom.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from '@app/generated'; - -export function leaveRoom(roomId: number): void { - 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 deleted file mode 100644 index e0c37a31a..000000000 --- a/webclient/src/websocket/commands/room/roomCommands.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -vi.mock('../../WebClient'); - -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { WebClient } from '../../WebClient'; -import { - Command_CreateGame_ext, - Command_CreateGameSchema, - Command_JoinGame_ext, - Command_JoinGameSchema, - Command_LeaveRoom_ext, - Command_RoomSay_ext, - Response_ResponseCode, -} from '@app/generated'; - -import { createGame } from './createGame'; -import { joinGame } from './joinGame'; -import { leaveRoom } from './leaveRoom'; -import { roomSay } from './roomSay'; -import { create } from '@bufbuild/protobuf'; -import { Mock } from 'vitest'; - -const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers( - WebClient.instance.protobuf.sendRoomCommand as Mock, - // sendRoomCommand(roomId, ext, value, options) — options at index 3 - 3 -); - -describe('createGame', () => { - - it('calls sendRoomCommand with Command_CreateGame', () => { - createGame(5, create(Command_CreateGameSchema, { maxPlayers: 4 })); - expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) - ); - }); - - it('onSuccess calls response.room.gameCreated with roomId', () => { - createGame(5, create(Command_CreateGameSchema, {})); - invokeOnSuccess(); - expect(WebClient.instance.response.room.gameCreated).toHaveBeenCalledWith(5); - }); -}); - -describe('joinGame', () => { - beforeEach(() => { - (WebClient.instance.response.room.joinedGame as Mock).mockClear(); - (WebClient.instance.response.room.setJoinGamePending as Mock).mockClear(); - (WebClient.instance.response.room.setJoinGameError as Mock).mockClear(); - }); - - it('calls sendRoomCommand with Command_JoinGame', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42, password: '' })); - expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) - ); - }); - - it('dispatches setJoinGamePending(true) before sending', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); - expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenCalledWith(true); - }); - - it('onSuccess clears pending and calls response.room.joinedGame with roomId and gameId', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); - invokeOnSuccess(); - expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false); - expect(WebClient.instance.response.room.joinedGame).toHaveBeenCalledWith(7, 42); - }); - - // Desktop GameSelector::checkResponse — matching message strings from - // cockatrice/src/interface/widgets/server/game_selector.cpp:234-260. - const errorCases: Array<[number, string]> = [ - [Response_ResponseCode.RespNotInRoom, 'Please join the appropriate room first.'], - [Response_ResponseCode.RespNameNotFound, 'The game does not exist any more.'], - [Response_ResponseCode.RespGameFull, 'The game is already full.'], - [Response_ResponseCode.RespWrongPassword, 'Wrong password.'], - [Response_ResponseCode.RespSpectatorsNotAllowed, 'Spectators are not allowed in this game.'], - [Response_ResponseCode.RespOnlyBuddies, 'This game is only open to its creator\'s buddies.'], - [Response_ResponseCode.RespUserLevelTooLow, 'This game is only open to registered users.'], - [Response_ResponseCode.RespInIgnoreList, 'You are being ignored by the creator of this game.'], - ]; - - it.each(errorCases)('code %i dispatches setJoinGameError with desktop-matching message', (code, message) => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); - invokeResponseCode(code); - expect(WebClient.instance.response.room.setJoinGameError).toHaveBeenCalledWith(code, message); - expect(WebClient.instance.response.room.joinedGame).not.toHaveBeenCalled(); - }); - - it('code 11 (RespContextError) is silent — clears pending, no setJoinGameError, no console.error', () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); - invokeResponseCode(Response_ResponseCode.RespContextError); - expect(WebClient.instance.response.room.setJoinGameError).not.toHaveBeenCalled(); - expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false); - expect(consoleError).not.toHaveBeenCalled(); - consoleError.mockRestore(); - }); - - it('unknown response code goes to onError — clears pending, no setJoinGameError', () => { - joinGame(7, create(Command_JoinGameSchema, { gameId: 42 })); - invokeOnError(99); - expect(WebClient.instance.response.room.setJoinGameError).not.toHaveBeenCalled(); - expect(WebClient.instance.response.room.setJoinGamePending).toHaveBeenLastCalledWith(false); - }); -}); - -describe('leaveRoom', () => { - - it('calls sendRoomCommand with Command_LeaveRoom', () => { - leaveRoom(3); - expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls response.room.leaveRoom with roomId', () => { - leaveRoom(3); - invokeOnSuccess(); - expect(WebClient.instance.response.room.leaveRoom).toHaveBeenCalledWith(3); - }); -}); - -describe('roomSay', () => { - - it('calls sendRoomCommand with trimmed message', () => { - roomSay(2, ' hello '); - expect(WebClient.instance.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 2, - Command_RoomSay_ext, - expect.objectContaining({ message: 'hello' }) - ); - }); - - it('does not call sendRoomCommand when message is blank', () => { - roomSay(2, ' '); - expect(WebClient.instance.protobuf.sendRoomCommand).not.toHaveBeenCalled(); - }); - - it('does not call sendRoomCommand when message is empty string', () => { - roomSay(2, ''); - expect(WebClient.instance.protobuf.sendRoomCommand).not.toHaveBeenCalled(); - }); -}); diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts deleted file mode 100644 index 2b3618420..000000000 --- a/webclient/src/websocket/commands/room/roomSay.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; -import { Command_RoomSay_ext, Command_RoomSaySchema } from '@app/generated'; - -export function roomSay(roomId: number, message: string): void { - const trimmed = message.trim(); - - if (!trimmed) { - return; - } - - 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 deleted file mode 100644 index be44b19e8..000000000 --- a/webclient/src/websocket/commands/session/accountEdit.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_AccountEdit_ext, Command_AccountEditSchema } from '@app/generated'; - -export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country }); - WebClient.instance.protobuf.sendSessionCommand(Command_AccountEdit_ext, cmd, { - 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 deleted file mode 100644 index 47df13c92..000000000 --- a/webclient/src/websocket/commands/session/accountImage.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_AccountImage_ext, Command_AccountImageSchema } from '@app/generated'; - -export function accountImage(image: Uint8Array): void { - 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 deleted file mode 100644 index 236c2e162..000000000 --- a/webclient/src/websocket/commands/session/accountPassword.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from '@app/generated'; - -export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); - WebClient.instance.protobuf.sendSessionCommand(Command_AccountPassword_ext, cmd, { - onSuccess: () => { - WebClient.instance.response.session.accountPasswordChange(); - }, - }); -} diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts deleted file mode 100644 index 368e676f7..000000000 --- a/webclient/src/websocket/commands/session/activate.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_Activate_ext, - Command_ActivateSchema, - Response_ResponseCode, - type ActivateParams, -} from '@app/generated'; - -import { StatusEnum } from '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { disconnect, login, updateStatus } from './'; - -export function activate(options: ConnectTarget & ActivateParams, password?: string, passwordSalt?: string): void { - const { userName, token } = options; - - WebClient.instance.protobuf.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, { - ...CLIENT_CONFIG, - userName, - token, - }), { - onResponseCode: { - [Response_ResponseCode.RespActivationAccepted]: () => { - WebClient.instance.response.session.accountActivationSuccess(); - login({ - host: options.host, - port: options.port, - userName: options.userName, - }, password, passwordSalt); - }, - }, - onError: () => { - 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 deleted file mode 100644 index 76836cb38..000000000 --- a/webclient/src/websocket/commands/session/addToList.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_AddToList_ext, Command_AddToListSchema } from '@app/generated'; - -export function addToBuddyList(userName: string): void { - addToList('buddy', userName); -} - -export function addToIgnoreList(userName: string): void { - addToList('ignore', userName); -} - -export function addToList(list: string, userName: string): void { - 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 deleted file mode 100644 index d260ca8c2..000000000 --- a/webclient/src/websocket/commands/session/connect.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; - -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 deleted file mode 100644 index e5cc88483..000000000 --- a/webclient/src/websocket/commands/session/deckDel.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_DeckDel_ext, Command_DeckDelSchema } from '@app/generated'; - -export function deckDel(deckId: number): void { - 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 deleted file mode 100644 index 740bbd02b..000000000 --- a/webclient/src/websocket/commands/session/deckDelDir.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_DeckDelDir_ext, Command_DeckDelDirSchema } from '@app/generated'; - -export function deckDelDir(path: string): void { - 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 deleted file mode 100644 index 566e56272..000000000 --- a/webclient/src/websocket/commands/session/deckDownload.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_DeckDownload_ext, Command_DeckDownloadSchema, Response_DeckDownload_ext } from '@app/generated'; - -export function deckDownload(deckId: number): void { - WebClient.instance.protobuf.sendSessionCommand( - Command_DeckDownload_ext, - create(Command_DeckDownloadSchema, { deckId }), - { - 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 deleted file mode 100644 index e0940010b..000000000 --- a/webclient/src/websocket/commands/session/deckList.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_DeckList_ext, Command_DeckListSchema, Response_DeckList_ext } from '@app/generated'; - -export function deckList(): void { - 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 deleted file mode 100644 index dc27fb0ca..000000000 --- a/webclient/src/websocket/commands/session/deckNewDir.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_DeckNewDir_ext, Command_DeckNewDirSchema } from '@app/generated'; - -export function deckNewDir(path: string, dirName: string): void { - 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 deleted file mode 100644 index 8b4c647ba..000000000 --- a/webclient/src/websocket/commands/session/deckUpload.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -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( - Command_DeckUpload_ext, - create(Command_DeckUploadSchema, { path, deckId, deckList }), - { - 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/disconnect.ts b/webclient/src/websocket/commands/session/disconnect.ts deleted file mode 100644 index 05044c89e..000000000 --- a/webclient/src/websocket/commands/session/disconnect.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WebClient } from '../../WebClient'; - -export function disconnect(): void { - WebClient.instance.disconnect(); -} diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts deleted file mode 100644 index 88b9da8e3..000000000 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_ForgotPasswordChallenge_ext, - Command_ForgotPasswordChallengeSchema, - type ForgotPasswordChallengeParams, -} from '@app/generated'; - -import { StatusEnum } from '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { disconnect, updateStatus } from './'; - -export function forgotPasswordChallenge(options: ConnectTarget & ForgotPasswordChallengeParams): void { - const { userName, email } = options; - - WebClient.instance.protobuf.sendSessionCommand( - Command_ForgotPasswordChallenge_ext, - create(Command_ForgotPasswordChallengeSchema, { - ...CLIENT_CONFIG, - userName, - email, - }), - { - onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); - WebClient.instance.response.session.resetPassword(); - disconnect(); - }, - onError: () => { - 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 deleted file mode 100644 index 8099072dc..000000000 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_ForgotPasswordRequest_ext, - Command_ForgotPasswordRequestSchema, - Response_ForgotPasswordRequest_ext, - type ForgotPasswordRequestParams, -} from '@app/generated'; - -import { StatusEnum } from '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { disconnect, updateStatus } from './'; - -export function forgotPasswordRequest(options: ConnectTarget & ForgotPasswordRequestParams): void { - const { userName } = options; - - WebClient.instance.protobuf.sendSessionCommand(Command_ForgotPasswordRequest_ext, create(Command_ForgotPasswordRequestSchema, { - ...CLIENT_CONFIG, - userName, - }), { - responseExt: Response_ForgotPasswordRequest_ext, - onSuccess: (resp) => { - if (resp?.challengeEmail) { - updateStatus(StatusEnum.DISCONNECTED, null); - WebClient.instance.response.session.resetPasswordChallenge(); - } else { - updateStatus(StatusEnum.DISCONNECTED, null); - WebClient.instance.response.session.resetPassword(); - } - disconnect(); - }, - onError: () => { - 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 deleted file mode 100644 index 74792c292..000000000 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { hashPassword } from '../../utils'; -import { disconnect, updateStatus } from '.'; - -export function forgotPasswordReset( - options: ConnectTarget & ForgotPasswordResetParams, - newPassword?: string, - passwordSalt?: string -): void { - const { userName, token } = options; - - const params: MessageInitShape = { - ...CLIENT_CONFIG, - userName, - token, - ...(passwordSalt - ? { hashedNewPassword: hashPassword(passwordSalt, newPassword) } - : { newPassword }), - }; - - WebClient.instance.protobuf.sendSessionCommand( - Command_ForgotPasswordReset_ext, - create(Command_ForgotPasswordResetSchema, params), - { - onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); - WebClient.instance.response.session.resetPasswordSuccess(); - disconnect(); - }, - onError: () => { - 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 deleted file mode 100644 index 91cb2c3ec..000000000 --- a/webclient/src/websocket/commands/session/getGamesOfUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GetGamesOfUser_ext, Command_GetGamesOfUserSchema, Response_GetGamesOfUser_ext } from '@app/generated'; - -export function getGamesOfUser(userName: string): void { - 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 deleted file mode 100644 index ff266670f..000000000 --- a/webclient/src/websocket/commands/session/getUserInfo.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_GetUserInfo_ext, Command_GetUserInfoSchema, Response_GetUserInfo_ext } from '@app/generated'; - -export function getUserInfo(userName: string): void { - 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/index.ts b/webclient/src/websocket/commands/session/index.ts deleted file mode 100644 index db2efc4c9..000000000 --- a/webclient/src/websocket/commands/session/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -export * from './accountEdit'; -export * from './accountImage'; -export * from './accountPassword'; -export * from './activate'; -export * from './addToList'; -export * from './connect'; -export * from './deckDel'; -export * from './deckDelDir'; -export * from './deckDownload'; -export * from './deckList'; -export * from './deckNewDir'; -export * from './deckUpload'; -export * from './disconnect'; -export * from './forgotPasswordChallenge'; -export * from './forgotPasswordRequest'; -export * from './forgotPasswordReset'; -export * from './getGamesOfUser'; -export * from './getUserInfo'; -export * from './joinRoom'; -export * from './listRooms'; -export * from './listUsers'; -export * from './login'; -export * from './message'; -export * from './ping'; -export * from './register'; -export * from './removeFromList'; -export * from './replayDeleteMatch'; -export * from './replayDownload'; -export * from './replayGetCode'; -export * from './replayList'; -export * from './replayModifyMatch'; -export * from './replaySubmitCode'; -export * from './requestPasswordSalt'; -export * from './updateStatus'; diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts deleted file mode 100644 index 494819208..000000000 --- a/webclient/src/websocket/commands/session/joinRoom.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_JoinRoom_ext, Command_JoinRoomSchema, Response_JoinRoom_ext } from '@app/generated'; - -export function joinRoom(roomId: number): void { - 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 deleted file mode 100644 index cfbcc7cb7..000000000 --- a/webclient/src/websocket/commands/session/listRooms.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ListRooms_ext, Command_ListRoomsSchema } from '@app/generated'; - -export function listRooms(): void { - 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 deleted file mode 100644 index 9978cca48..000000000 --- a/webclient/src/websocket/commands/session/listUsers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ListUsers_ext, Command_ListUsersSchema, Response_ListUsers_ext } from '@app/generated'; - -export function listUsers(): void { - 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 deleted file mode 100644 index 8f194d650..000000000 --- a/webclient/src/websocket/commands/session/login.ts +++ /dev/null @@ -1,88 +0,0 @@ -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 '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { hashPassword } from '../../utils'; -import { - disconnect, - listUsers, - listRooms, - updateStatus, -} from './'; - -export function login(options: ConnectTarget & LoginParams, password?: string, passwordSalt?: string): void { - const { userName, hashedPassword } = options; - - const loginConfig = { - ...CLIENT_CONFIG, - clientid: 'webatrice', - userName, - ...(passwordSalt - ? { hashedPassword: hashedPassword || hashPassword(passwordSalt, password) } - : { password }), - } satisfies MessageInitShape; - - const onLoginError = (message: string, extra?: () => void) => { - updateStatus(StatusEnum.DISCONNECTED, message); - extra?.(); - WebClient.instance.response.session.loginFailed(); - disconnect(); - }; - - WebClient.instance.protobuf.sendSessionCommand(Command_Login_ext, create(Command_LoginSchema, loginConfig), { - responseExt: Response_Login_ext, - 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 }); - - listUsers(); - listRooms(); - - updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); - }, - onResponseCode: { - [Response_ResponseCode.RespClientUpdateRequired]: () => - onLoginError('Login failed: missing features'), - [Response_ResponseCode.RespWrongPassword]: () => - onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespUsernameInvalid]: () => - onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespWouldOverwriteOldSession]: () => - onLoginError('Login failed: duplicated user session'), - [Response_ResponseCode.RespUserIsBanned]: () => - onLoginError('Login failed: banned user'), - [Response_ResponseCode.RespRegistrationRequired]: () => - onLoginError('Login failed: registration required'), - [Response_ResponseCode.RespClientIdRequired]: () => - onLoginError('Login failed: missing client ID'), - [Response_ResponseCode.RespContextError]: () => - onLoginError('Login failed: server error'), - [Response_ResponseCode.RespAccountNotActivated]: () => - onLoginError('Login failed: account not activated', - () => { - WebClient.instance.response.session.accountAwaitingActivation({ - host: options.host, - port: options.port, - userName: options.userName, - }); - } - ), - }, - onError: (responseCode) => - onLoginError(`Login failed: unknown error: ${responseCode}`), - }); -} diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts deleted file mode 100644 index bdb4ac759..000000000 --- a/webclient/src/websocket/commands/session/message.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_Message_ext, Command_MessageSchema } from '@app/generated'; - -export function message(userName: string, message: string): void { - 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 deleted file mode 100644 index 7ad16482f..000000000 --- a/webclient/src/websocket/commands/session/ping.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_Ping_ext, Command_PingSchema } from '@app/generated'; - -export function ping(pingReceived: () => void): void { - // Uses `onResponse` (not `onSuccess`) so KeepAliveService treats any server - // reply as proof of life, independent of responseCode. - 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 deleted file mode 100644 index c1dde363a..000000000 --- a/webclient/src/websocket/commands/session/register.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { hashPassword } from '../../utils'; -import { login, disconnect, updateStatus } from './'; - -export function register(options: ConnectTarget & RegisterParams, password?: string, passwordSalt?: string): void { - const { userName, email, country, realName } = options; - - const params: MessageInitShape = { - ...CLIENT_CONFIG, - userName, - email, - country, - realName, - ...(passwordSalt - ? { hashedPassword: hashPassword(passwordSalt, password) } - : { password }), - }; - - const onRegistrationError = (action: () => void) => { - action(); - updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); - disconnect(); - }; - - WebClient.instance.protobuf.sendSessionCommand(Command_Register_ext, create(Command_RegisterSchema, params), { - onResponseCode: { - [Response_ResponseCode.RespRegistrationAccepted]: () => { - login({ - host: options.host, - port: options.port, - userName: options.userName, - }, password, passwordSalt); - WebClient.instance.response.session.registrationSuccess(); - }, - [Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); - WebClient.instance.response.session.accountAwaitingActivation({ - host: options.host, - port: options.port, - userName: options.userName, - }); - disconnect(); - }, - [Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationUserNameError('Username is taken') - ), - [Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationUserNameError('Invalid username') - ), - [Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationPasswordError('Your password was too short') - ), - [Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationRequiresEmail() - ), - [Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationEmailError('This email provider has been blocked') - ), - [Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationEmailError('Max accounts reached for this email') - ), - [Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( - () => WebClient.instance.response.session.registrationFailed('Registration is currently disabled') - ), - [Response_ResponseCode.RespUserIsBanned]: (raw) => { - const register = getExtension(raw, Response_Register_ext); - onRegistrationError( - () => WebClient.instance.response.session.registrationFailed(register.deniedReasonStr, Number(register.deniedEndTime)) - ); - }, - }, - onError: () => onRegistrationError( - () => WebClient.instance.response.session.registrationFailed('Registration failed due to a server issue') - ), - }); -} diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts deleted file mode 100644 index 82d971b78..000000000 --- a/webclient/src/websocket/commands/session/removeFromList.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_RemoveFromList_ext, Command_RemoveFromListSchema } from '@app/generated'; - -export function removeFromBuddyList(userName: string): void { - removeFromList('buddy', userName); -} - -export function removeFromIgnoreList(userName: string): void { - removeFromList('ignore', userName); -} - -export function removeFromList(list: string, userName: string): void { - WebClient.instance.protobuf.sendSessionCommand( - 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 deleted file mode 100644 index 1da4fe7d1..000000000 --- a/webclient/src/websocket/commands/session/replayDeleteMatch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ReplayDeleteMatch_ext, Command_ReplayDeleteMatchSchema } from '@app/generated'; - -export function replayDeleteMatch(gameId: number): void { - WebClient.instance.protobuf.sendSessionCommand( - 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 deleted file mode 100644 index 928abfd15..000000000 --- a/webclient/src/websocket/commands/session/replayDownload.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ReplayDownload_ext, Command_ReplayDownloadSchema, Response_ReplayDownload_ext } from '@app/generated'; - -export function replayDownload(replayId: number): void { - WebClient.instance.protobuf.sendSessionCommand( - Command_ReplayDownload_ext, - create(Command_ReplayDownloadSchema, { replayId }), - { - 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 deleted file mode 100644 index 272307845..000000000 --- a/webclient/src/websocket/commands/session/replayGetCode.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; -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(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 deleted file mode 100644 index 60919f534..000000000 --- a/webclient/src/websocket/commands/session/replayList.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ReplayList_ext, Command_ReplayListSchema, Response_ReplayList_ext } from '@app/generated'; - -export function replayList(): void { - 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 deleted file mode 100644 index 3d4e4af53..000000000 --- a/webclient/src/websocket/commands/session/replayModifyMatch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; - -import { Command_ReplayModifyMatch_ext, Command_ReplayModifyMatchSchema } from '@app/generated'; - -export function replayModifyMatch(gameId: number, doNotHide: boolean): void { - WebClient.instance.protobuf.sendSessionCommand( - 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 deleted file mode 100644 index 22ef418bc..000000000 --- a/webclient/src/websocket/commands/session/replaySubmitCode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { WebClient } from '../../WebClient'; -import { Command_ReplaySubmitCode_ext, Command_ReplaySubmitCodeSchema } from '@app/generated'; - -export function replaySubmitCode( - replayCode: string, - onSubmitted?: () => void, - onFailure?: (responseCode: number) => void, -): void { - WebClient.instance.protobuf.sendSessionCommand( - Command_ReplaySubmitCode_ext, - create(Command_ReplaySubmitCodeSchema, { replayCode }), - { - onSuccess: onSubmitted, - onError: onFailure, - } - ); -} diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts deleted file mode 100644 index 3c4e4ff35..000000000 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { create } from '@bufbuild/protobuf'; -import { - Command_RequestPasswordSalt_ext, - Command_RequestPasswordSaltSchema, - Response_PasswordSalt_ext, - Response_ResponseCode, - type RequestPasswordSaltParams, -} from '@app/generated'; - -import { StatusEnum } from '../../types/StatusEnum'; -import { CLIENT_CONFIG } from '../../config'; -import { WebClient } from '../../WebClient'; -import type { ConnectTarget } from '../../types/WebClientConfig'; -import { updateStatus } from './'; - -export function requestPasswordSalt( - options: ConnectTarget & RequestPasswordSaltParams, - onSaltReceived: (passwordSalt: string) => void, - onFailure: () => void, -): void { - const { userName } = options; - - WebClient.instance.protobuf.sendSessionCommand(Command_RequestPasswordSalt_ext, create(Command_RequestPasswordSaltSchema, { - ...CLIENT_CONFIG, - userName, - }), { - responseExt: Response_PasswordSalt_ext, - onSuccess: (resp) => { - onSaltReceived(resp?.passwordSalt); - }, - onResponseCode: { - [Response_ResponseCode.RespRegistrationRequired]: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); - onFailure(); - }, - }, - onError: () => { - 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 deleted file mode 100644 index dda7b8c05..000000000 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ /dev/null @@ -1,534 +0,0 @@ -// Tests for complex session commands that call WebClient directly -// or have multiple branching callbacks. - -vi.mock('../../WebClient'); - -vi.mock('../../utils', async () => { - const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); - return makeUtilsMock(); -}); - -// Intercept all re-exported commands to avoid recursive real invocations -vi.mock('./', async () => { - const { makeSessionBarrelMock } = await import('../../__mocks__/sessionCommandMocks'); - return makeSessionBarrelMock(); -}); - -import { Mock } from 'vitest'; -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { WebClient } from '../../WebClient'; -import * as SessionIndexMocks from './'; -import { - WebSocketConnectReason, - type LoginConnectOptions, - type RegisterConnectOptions, - type ActivateConnectOptions, - type PasswordResetRequestConnectOptions, - type PasswordResetChallengeConnectOptions, - type PasswordResetConnectOptions, -} from '../../types/ConnectOptions'; -import { StatusEnum } from '../../types/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, testConnect } from './connect'; -import { updateStatus } from './updateStatus'; -import { login } from './login'; -import { register } from './register'; -import { activate } from './activate'; -import { forgotPasswordChallenge } from './forgotPasswordChallenge'; -import { forgotPasswordRequest } from './forgotPasswordRequest'; -import { forgotPasswordReset } from './forgotPasswordReset'; -import { requestPasswordSalt } from './requestPasswordSalt'; - -const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers( - WebClient.instance.protobuf.sendSessionCommand as Mock, - 2 -); - -const baseTransport = { host: 'h', port: '1' }; -const makeLoginOpts = (overrides: Partial = {}): LoginConnectOptions => ({ - ...baseTransport, - userName: 'alice', - reason: WebSocketConnectReason.LOGIN, - ...overrides, -}); -const makeRegisterOpts = ( - overrides: Partial = {} -): RegisterConnectOptions => ({ - ...baseTransport, - userName: 'alice', - password: 'pw', - email: 'a@b.com', - country: 'US', - realName: 'Al', - reason: WebSocketConnectReason.REGISTER, - ...overrides, -}); -const makeActivateOpts = ( - overrides: Partial = {} -): ActivateConnectOptions => ({ - ...baseTransport, - userName: 'alice', - token: 'tok', - reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, - ...overrides, -}); -const makeForgotRequestOpts = (): PasswordResetRequestConnectOptions => ({ - ...baseTransport, - userName: 'alice', - reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST, -}); -const makeForgotChallengeOpts = (): PasswordResetChallengeConnectOptions => ({ - ...baseTransport, - userName: 'alice', - email: 'a@b.com', - reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE, -}); -const makeForgotResetOpts = (): PasswordResetConnectOptions => ({ - ...baseTransport, - userName: 'alice', - token: 'tok', - newPassword: 'newpw', - reason: WebSocketConnectReason.PASSWORD_RESET, -}); - - -beforeEach(() => { - (hashPassword as Mock).mockReturnValue('hashed_pw'); - (generateSalt as Mock).mockReturnValue('randSalt'); - (passwordSaltSupported as Mock).mockReturnValue(0); -}); - -describe('connect', () => { - - it('calls WebClient.instance.connect with the target', () => { - connect({ host: 'h', port: '1' }); - expect(WebClient.instance.connect).toHaveBeenCalledWith({ host: 'h', port: '1' }); - }); -}); - -describe('testConnect', () => { - - it('calls WebClient.instance.testConnect with the target', () => { - testConnect({ host: 'h', port: '1' }); - expect(WebClient.instance.testConnect).toHaveBeenCalledWith({ host: 'h', port: '1' }); - }); -}); - -describe('updateStatus', () => { - - it('calls WebClient.instance.response.session.updateStatus and WebClient.instance.updateStatus', () => { - updateStatus(StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.response.session.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'OK'); - expect(WebClient.instance.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED); - }); -}); - -describe('login', () => { - - it('sends Command_Login with plain password when no salt', () => { - login(makeLoginOpts(), 'pw'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, - expect.objectContaining({ password: 'pw' }), - 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( - Command_Login_ext, - expect.objectContaining({ hashedPassword: 'hashed_pw' }), - 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( - Command_Login_ext, - expect.objectContaining({ hashedPassword: 'pre_hashed' }), - expect.objectContaining({ responseExt: Response_Login_ext }) - ); - }); - - it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => { - login(makeLoginOpts(), 'pw'); - const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0 }); - expect(WebClient.instance.response.session.updateBuddyList).toHaveBeenCalledWith([]); - expect(WebClient.instance.response.session.updateIgnoreList).toHaveBeenCalledWith([]); - expect(WebClient.instance.response.session.updateUser).toHaveBeenCalledWith({ name: 'alice' }); - expect(WebClient.instance.response.session.loginSuccessful).toHaveBeenCalled(); - expect(SessionIndexMocks.listUsers).toHaveBeenCalled(); - expect(SessionIndexMocks.listRooms).toHaveBeenCalled(); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.'); - }); - - it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { - login(makeLoginOpts(), 'secret'); - const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0 }); - const calledWith = (WebClient.instance.response.session.loginSuccessful as Mock).mock.calls[0][0]; - expect(calledWith).not.toHaveProperty('password'); - }); - - it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => { - login({ host: 'h', port: '1', userName: 'alice', reason: WebSocketConnectReason.LOGIN }, 'pw', 'salt'); - const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0 }); - const calledWith = (WebClient.instance.response.session.loginSuccessful as Mock).mock.calls[0][0]; - expect(calledWith).toHaveProperty('hashedPassword', 'hashed_pw'); - }); - - it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientUpdateRequired); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('onResponseCode RespWrongPassword', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespWrongPassword); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespUsernameInvalid', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespWouldOverwriteOldSession', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespWouldOverwriteOldSession); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespUserIsBanned', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespRegistrationRequired', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespClientIdRequired', () => { - login(makeLoginOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientIdRequired); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onResponseCode RespContextError', () => { - login(makeLoginOpts(), 'pw'); - 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(Response_ResponseCode.RespAccountNotActivated); - expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( - expect.not.objectContaining({ password: expect.anything() }) - ); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); - - it('onError calls onLoginError with unknown error message', () => { - login(makeLoginOpts(), 'pw'); - invokeOnError(999); - expect(WebClient.instance.response.session.loginFailed).toHaveBeenCalled(); - }); -}); - -describe('register', () => { - - it('sends Command_Register with plain password when no salt', () => { - register(makeRegisterOpts(), 'pw'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, - expect.objectContaining({ password: 'pw' }), - expect.any(Object) - ); - }); - - it('uses hashedPassword when salt is provided', () => { - register(makeRegisterOpts(), 'pw', 'salt'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, - expect.objectContaining({ hashedPassword: 'hashed_pw' }), - expect.any(Object) - ); - }); - - it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { - register(makeRegisterOpts(), 'pw'); - 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(Response_ResponseCode.RespRegistrationAccepted); - expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt'); - expect(WebClient.instance.response.session.registrationSuccess).toHaveBeenCalled(); - }); - - it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => { - register(makeRegisterOpts({ password: 'leaked' }), 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); - expect(WebClient.instance.response.session.accountAwaitingActivation).toHaveBeenCalledWith( - expect.not.objectContaining({ password: expect.anything() }) - ); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('RespUserAlreadyExists calls registrationUserNameError', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserAlreadyExists); - expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); - }); - - it('RespUsernameInvalid calls registrationUserNameError', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); - expect(WebClient.instance.response.session.registrationUserNameError).toHaveBeenCalled(); - }); - - it('RespPasswordTooShort calls registrationPasswordError', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespPasswordTooShort); - expect(WebClient.instance.response.session.registrationPasswordError).toHaveBeenCalled(); - }); - - it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailRequiredToRegister); - expect(WebClient.instance.response.session.registrationRequiresEmail).toHaveBeenCalled(); - }); - - it('RespEmailBlackListed calls registrationEmailError', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailBlackListed); - expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); - }); - - it('RespTooManyRequests calls registrationEmailError', () => { - register(makeRegisterOpts(), 'pw'); - invokeResponseCode(Response_ResponseCode.RespTooManyRequests); - expect(WebClient.instance.response.session.registrationEmailError).toHaveBeenCalled(); - }); - - it('RespRegistrationDisabled calls registrationFailed', () => { - register(makeRegisterOpts(), 'pw'); - 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(ResponseSchema, { responseCode: Response_ResponseCode.RespUserIsBanned }); - setExtension(raw, Response_Register_ext, create(Response_RegisterSchema, { - deniedReasonStr: 'bad user', deniedEndTime: 9999n, - })); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned, raw); - expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalledWith('bad user', 9999); - }); - - it('onError calls registrationFailed', () => { - register(makeRegisterOpts(), 'pw'); - invokeOnError(); - expect(WebClient.instance.response.session.registrationFailed).toHaveBeenCalled(); - }); -}); - -describe('activate', () => { - - it('sends Command_Activate with userName and token, not password', () => { - activate(makeActivateOpts(), 'pw'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, - expect.objectContaining({ userName: 'alice', token: 'tok' }), - expect.any(Object) - ); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, - expect.not.objectContaining({ password: expect.anything() }), - expect.any(Object) - ); - }); - - it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { - activate(makeActivateOpts(), 'pw', 'salt'); - invokeResponseCode(Response_ResponseCode.RespActivationAccepted); - expect(WebClient.instance.response.session.accountActivationSuccess).toHaveBeenCalled(); - expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt'); - }); - - it('onError calls accountActivationFailed and disconnect', () => { - activate(makeActivateOpts()); - invokeOnError(); - expect(WebClient.instance.response.session.accountActivationFailed).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); -}); - -describe('forgotPasswordChallenge', () => { - - it('sends Command_ForgotPasswordChallenge', () => { - forgotPasswordChallenge(makeForgotChallengeOpts()); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('onSuccess calls resetPassword and disconnect', () => { - forgotPasswordChallenge(makeForgotChallengeOpts()); - invokeOnSuccess(); - expect(WebClient.instance.response.session.resetPassword).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordChallenge(makeForgotChallengeOpts()); - invokeOnError(); - expect(WebClient.instance.response.session.resetPasswordFailed).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); -}); - -describe('forgotPasswordRequest', () => { - - it('sends Command_ForgotPasswordRequest', () => { - forgotPasswordRequest(makeForgotRequestOpts()); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordRequest_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_ForgotPasswordRequest_ext }) - ); - }); - - it('onSuccess with challengeEmail calls resetPasswordChallenge', () => { - forgotPasswordRequest(makeForgotRequestOpts()); - const resp = { challengeEmail: true }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.resetPasswordChallenge).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('onSuccess without challengeEmail calls resetPassword', () => { - forgotPasswordRequest(makeForgotRequestOpts()); - const resp = { challengeEmail: false }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.resetPassword).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordRequest(makeForgotRequestOpts()); - invokeOnError(); - expect(WebClient.instance.response.session.resetPasswordFailed).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); -}); - -describe('forgotPasswordReset', () => { - - it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { - forgotPasswordReset(makeForgotResetOpts(), 'newpw'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, - expect.objectContaining({ newPassword: 'newpw' }), - expect.any(Object) - ); - }); - - it('sends hashed new password when salt provided', () => { - forgotPasswordReset(makeForgotResetOpts(), 'newpw', 'salt'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, - expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), - expect.any(Object) - ); - }); - - it('onSuccess calls resetPasswordSuccess and disconnect', () => { - forgotPasswordReset(makeForgotResetOpts(), 'newpw'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.resetPasswordSuccess).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); - - it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordReset(makeForgotResetOpts(), 'newpw'); - invokeOnError(); - expect(WebClient.instance.response.session.resetPasswordFailed).toHaveBeenCalled(); - expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); - }); -}); - -describe('requestPasswordSalt', () => { - - it('sends Command_RequestPasswordSalt', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RequestPasswordSalt_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_PasswordSalt_ext }) - ); - }); - - it('onSuccess calls onSaltReceived with the salt', () => { - const onSaltReceived = vi.fn(); - const onFailure = vi.fn(); - requestPasswordSalt({ host: 'h', port: '1', userName: 'alice' }, onSaltReceived, onFailure); - const resp = { passwordSalt: 'salt123' }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(onSaltReceived).toHaveBeenCalledWith('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('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(onFailure).toHaveBeenCalled(); - }); -}); diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts deleted file mode 100644 index 486526fbd..000000000 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ /dev/null @@ -1,520 +0,0 @@ -// Shared mock setup for session command tests - -vi.mock('../../WebClient'); - -vi.mock('../../utils', async () => { - const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); - return makeUtilsMock(); -}); - -// Mock session commands barrel to allow cross-command calls while keeping real implementations -vi.mock('./', async () => { - const actual = await vi.importActual('./'); - const { makeSessionBarrelMock } = await import('../../__mocks__/sessionCommandMocks'); - return { ...(actual as Record), ...makeSessionBarrelMock() }; -}); - -import { Mock } from 'vitest'; -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { WebClient } from '../../WebClient'; -import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; - -import { accountEdit } from './accountEdit'; -import { accountImage } from './accountImage'; -import { accountPassword } from './accountPassword'; -import { deckDel } from './deckDel'; -import { deckDelDir } from './deckDelDir'; -import { deckDownload } from './deckDownload'; -import { deckList } from './deckList'; -import { deckNewDir } from './deckNewDir'; -import { deckUpload } from './deckUpload'; -import { disconnect } from './disconnect'; -import { getGamesOfUser } from './getGamesOfUser'; -import { getUserInfo } from './getUserInfo'; -import { joinRoom } from './joinRoom'; -import { listRooms } from './listRooms'; -import { listUsers } from './listUsers'; -import { message } from './message'; -import { ping } from './ping'; -import { replayDeleteMatch } from './replayDeleteMatch'; -import { replayDownload } from './replayDownload'; -import { replayList } from './replayList'; -import { replayModifyMatch } from './replayModifyMatch'; -import { addToList, addToBuddyList, addToIgnoreList } from './addToList'; -import { removeFromList, removeFromBuddyList, removeFromIgnoreList } from './removeFromList'; -import { replayGetCode } from './replayGetCode'; -import { replaySubmitCode } from './replaySubmitCode'; -import { - Command_AccountEdit_ext, - Command_AccountImage_ext, - Command_AccountPassword_ext, - Command_AddToList_ext, - Command_DeckDel_ext, - Command_DeckDelDir_ext, - Command_DeckDownload_ext, - Command_DeckList_ext, - Command_DeckNewDir_ext, - Command_DeckUpload_ext, - Command_GetGamesOfUser_ext, - Command_GetUserInfo_ext, - Command_JoinRoom_ext, - Command_ListRooms_ext, - Command_ListUsers_ext, - Command_Message_ext, - Command_Ping_ext, - Command_RemoveFromList_ext, - Command_ReplayDeleteMatch_ext, - Command_ReplayDownload_ext, - Command_ReplayGetCode_ext, - Command_ReplayList_ext, - Command_ReplayModifyMatch_ext, - Command_ReplaySubmitCode_ext, - Response_DeckDownload_ext, - Response_DeckList_ext, - Response_DeckUpload_ext, - Response_GetGamesOfUser_ext, - Response_GetUserInfo_ext, - Response_JoinRoom_ext, - Response_ListUsers_ext, - Response_ReplayDownload_ext, - Response_ReplayGetCode_ext, - Response_ReplayList_ext, -} from '@app/generated'; - -const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( - WebClient.instance.protobuf.sendSessionCommand as Mock, - 2 -); - -beforeEach(() => { - (hashPassword as Mock).mockReturnValue('hashed_pw'); - (generateSalt as Mock).mockReturnValue('randSalt'); - (passwordSaltSupported as Mock).mockReturnValue(0); -}); - - -describe('accountEdit', () => { - it('sends Command_AccountEdit with correct params', () => { - accountEdit('pw', 'Alice', 'a@b.com', 'US'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountEdit_ext, - expect.objectContaining({ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }), - expect.any(Object) - ); - }); - - it('calls WebClient.instance.response.session.accountEditChanged on success', () => { - accountEdit('pw', 'Alice', 'a@b.com', 'US'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.accountEditChanged).toHaveBeenCalledWith('Alice', 'a@b.com', 'US'); - }); -}); - -describe('accountImage', () => { - it('sends Command_AccountImage', () => { - const img = new Uint8Array([1, 2]); - accountImage(img); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) - ); - }); - - it('calls WebClient.instance.response.session.accountImageChanged on success', () => { - const img = new Uint8Array([1, 2]); - accountImage(img); - invokeOnSuccess(); - expect(WebClient.instance.response.session.accountImageChanged).toHaveBeenCalledWith(img); - }); -}); - -describe('accountPassword', () => { - it('sends Command_AccountPassword', () => { - accountPassword('old', 'new', 'hashed'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountPassword_ext, - expect.objectContaining({ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }), - expect.any(Object) - ); - }); - - it('calls WebClient.instance.response.session.accountPasswordChange on success', () => { - accountPassword('old', 'new', 'hashed'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.accountPasswordChange).toHaveBeenCalled(); - }); -}); - -describe('deckDel', () => { - it('sends Command_DeckDel', () => { - deckDel(42); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDel_ext, - expect.objectContaining({ deckId: 42 }), - expect.any(Object) - ); - }); - - it('calls deleteServerDeck on success', () => { - deckDel(42); - invokeOnSuccess(); - expect(WebClient.instance.response.session.deleteServerDeck).toHaveBeenCalledWith(42); - }); -}); - -describe('deckDelDir', () => { - it('sends Command_DeckDelDir', () => { - deckDelDir('/path'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) - ); - }); - - it('calls deleteServerDeckDir on success', () => { - deckDelDir('/path'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.deleteServerDeckDir).toHaveBeenCalledWith('/path'); - }); -}); - -describe('deckList', () => { - it('sends Command_DeckList', () => { - deckList(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckList_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_DeckList_ext }) - ); - }); - - it('calls updateServerDecks on success', () => { - deckList(); - const root = { items: [] }; - invokeOnSuccess({ root }, { responseCode: 0 }); - expect(WebClient.instance.response.session.updateServerDecks).toHaveBeenCalledWith({ root }); - }); -}); - -describe('deckNewDir', () => { - it('sends Command_DeckNewDir', () => { - deckNewDir('/path', 'dir'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) - ); - }); - - it('calls createServerDeckDir on success', () => { - deckNewDir('/path', 'dir'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.createServerDeckDir).toHaveBeenCalledWith('/path', 'dir'); - }); -}); - -describe('deckUpload', () => { - it('sends Command_DeckUpload', () => { - deckUpload('/path', 1, 'content'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckUpload_ext, - expect.objectContaining({ path: '/path', deckId: 1, deckList: 'content' }), - expect.objectContaining({ responseExt: Response_DeckUpload_ext }) - ); - }); - - it('calls uploadServerDeck on success', () => { - deckUpload('/path', 1, 'content'); - const resp = { newFile: { id: 1 } }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.uploadServerDeck).toHaveBeenCalledWith('/path', resp.newFile); - }); -}); - -describe('disconnect', () => { - it('calls WebClient.instance.disconnect', () => { - disconnect(); - expect(WebClient.instance.disconnect).toHaveBeenCalled(); - }); -}); - -describe('getGamesOfUser', () => { - it('sends Command_GetGamesOfUser', () => { - getGamesOfUser('alice'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetGamesOfUser_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_GetGamesOfUser_ext }) - ); - }); - - it('calls getGamesOfUser on success', () => { - getGamesOfUser('alice'); - const resp = { gameList: [] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.getGamesOfUser).toHaveBeenCalledWith('alice', resp); - }); -}); - -describe('getUserInfo', () => { - it('sends Command_GetUserInfo', () => { - getUserInfo('alice'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetUserInfo_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_GetUserInfo_ext }) - ); - }); - - it('calls getUserInfo on success', () => { - getUserInfo('alice'); - const resp = { userInfo: { name: 'alice' } }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.getUserInfo).toHaveBeenCalledWith(resp.userInfo); - }); -}); - -describe('joinRoom', () => { - it('sends Command_JoinRoom', () => { - joinRoom(5); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_JoinRoom_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_JoinRoom_ext }) - ); - }); - - it('calls WebClient.instance.response.room.joinRoom on success', () => { - joinRoom(5); - const resp = { roomInfo: { roomId: 5 } }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.room.joinRoom).toHaveBeenCalledWith(resp.roomInfo); - }); -}); - -describe('listRooms (command)', () => { - it('sends Command_ListRooms', () => { - listRooms(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith(Command_ListRooms_ext, expect.any(Object)); - }); -}); - -describe('listUsers', () => { - it('sends Command_ListUsers', () => { - listUsers(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ListUsers_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_ListUsers_ext }) - ); - }); - - it('calls WebClient.instance.response.session.updateUsers with the user list on success', () => { - listUsers(); - const resp = { userList: [{ name: 'Alice' }] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.updateUsers).toHaveBeenCalledWith([{ name: 'Alice' }]); - }); -}); - -describe('message', () => { - it('sends Command_Message', () => { - message('bob', 'hi'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) - ); - }); - -}); - -describe('ping', () => { - it('sends Command_Ping', () => { - const pingReceived = vi.fn(); - ping(pingReceived); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Ping_ext, expect.any(Object), expect.any(Object) - ); - }); - - it('calls pingReceived via onResponse', () => { - const pingReceived = vi.fn(); - ping(pingReceived); - invokeCallback('onResponse', {}); - expect(pingReceived).toHaveBeenCalled(); - }); -}); - -describe('replayDeleteMatch', () => { - it('sends Command_ReplayDeleteMatch', () => { - replayDeleteMatch(7); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayDeleteMatch_ext, - expect.objectContaining({ gameId: 7 }), - expect.any(Object) - ); - }); - - it('calls replayDeleteMatch on success', () => { - replayDeleteMatch(7); - invokeOnSuccess(); - expect(WebClient.instance.response.session.replayDeleteMatch).toHaveBeenCalledWith(7); - }); -}); - -describe('replayList', () => { - it('sends Command_ReplayList', () => { - replayList(); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayList_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayList_ext }) - ); - }); - - it('calls replayList on success', () => { - replayList(); - const resp = { matchList: [] }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.replayList).toHaveBeenCalledWith([]); - }); -}); - -describe('replayModifyMatch', () => { - it('sends Command_ReplayModifyMatch', () => { - replayModifyMatch(7, true); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) - ); - }); - - it('calls replayModifyMatch on success', () => { - replayModifyMatch(7, true); - invokeOnSuccess(); - expect(WebClient.instance.response.session.replayModifyMatch).toHaveBeenCalledWith(7, true); - }); -}); - -describe('addToList / addToBuddyList / addToIgnoreList', () => { - it('addToBuddyList sends Command_AddToList with list=buddy', () => { - addToBuddyList('alice'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, - expect.objectContaining({ list: 'buddy' }), - expect.any(Object) - ); - }); - - it('addToIgnoreList sends Command_AddToList with list=ignore', () => { - addToIgnoreList('bob'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, - expect.objectContaining({ list: 'ignore' }), - expect.any(Object) - ); - }); - - it('onSuccess calls WebClient.instance.response.session.addToList', () => { - addToList('buddy', 'alice'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.addToList).toHaveBeenCalledWith('buddy', 'alice'); - }); -}); - -describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { - it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => { - removeFromBuddyList('alice'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, - expect.objectContaining({ list: 'buddy' }), - expect.any(Object) - ); - }); - - it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => { - removeFromIgnoreList('bob'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, - expect.objectContaining({ list: 'ignore' }), - expect.any(Object) - ); - }); - - it('onSuccess calls WebClient.instance.response.session.removeFromList', () => { - removeFromList('buddy', 'alice'); - invokeOnSuccess(); - expect(WebClient.instance.response.session.removeFromList).toHaveBeenCalledWith('buddy', 'alice'); - }); -}); - -describe('replayGetCode', () => { - it('sends Command_ReplayGetCode with gameId and responseExt', () => { - replayGetCode(42, vi.fn()); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayGetCode_ext, - expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayGetCode_ext }) - ); - }); - - it('calls onCodeReceived with replayCode on success', () => { - const onCodeReceived = vi.fn(); - replayGetCode(42, onCodeReceived); - invokeOnSuccess({ replayCode: 'abc123-xyz' }); - expect(onCodeReceived).toHaveBeenCalledWith('abc123-xyz'); - }); -}); - -describe('replaySubmitCode', () => { - it('sends Command_ReplaySubmitCode with replayCode', () => { - replaySubmitCode('42-abc123'); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) - ); - }); - - it('forwards onSubmitted callback', () => { - const onSubmitted = vi.fn(); - replaySubmitCode('42-abc123', onSubmitted); - invokeOnSuccess(); - expect(onSubmitted).toHaveBeenCalled(); - }); - - it('forwards onFailure callback', () => { - const onFailure = vi.fn(); - replaySubmitCode('42-abc123', undefined, onFailure); - invokeCallback('onError', 404); - expect(onFailure).toHaveBeenCalledWith(404); - }); -}); - -describe('deckDownload', () => { - it('sends Command_DeckDownload', () => { - deckDownload(42); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDownload_ext, - expect.objectContaining({ deckId: 42 }), - expect.objectContaining({ responseExt: Response_DeckDownload_ext }) - ); - }); - - it('calls downloadServerDeck on success', () => { - deckDownload(42); - const resp = { deck: 'deck-content' }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.downloadServerDeck).toHaveBeenCalledWith(42, resp); - }); -}); - -describe('replayDownload', () => { - it('sends Command_ReplayDownload', () => { - replayDownload(99); - expect(WebClient.instance.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayDownload_ext, - expect.objectContaining({ replayId: 99 }), - expect.objectContaining({ responseExt: Response_ReplayDownload_ext }) - ); - }); - - it('calls replayDownloaded on success', () => { - replayDownload(99); - const resp = { replayData: new Uint8Array([1, 2, 3]) }; - invokeOnSuccess(resp, { responseCode: 0 }); - expect(WebClient.instance.response.session.replayDownloaded).toHaveBeenCalledWith(99, resp); - }); -}); diff --git a/webclient/src/websocket/commands/session/updateStatus.ts b/webclient/src/websocket/commands/session/updateStatus.ts deleted file mode 100644 index 596b72f21..000000000 --- a/webclient/src/websocket/commands/session/updateStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { StatusEnum } from '../../types/StatusEnum'; -import { WebClient } from '../../WebClient'; - -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/events/game/attachCard.ts b/webclient/src/websocket/events/game/attachCard.ts deleted file mode 100644 index 9de5c6114..000000000 --- a/webclient/src/websocket/events/game/attachCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_AttachCard } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 8afff45bf..000000000 --- a/webclient/src/websocket/events/game/changeZoneProperties.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_ChangeZoneProperties } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index ef2443365..000000000 --- a/webclient/src/websocket/events/game/createArrow.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_CreateArrow } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 54abcd0cf..000000000 --- a/webclient/src/websocket/events/game/createCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_CreateCounter } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 9c406da16..000000000 --- a/webclient/src/websocket/events/game/createToken.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_CreateToken } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index bf4231a93..000000000 --- a/webclient/src/websocket/events/game/delCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_DelCounter } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index d3653313b..000000000 --- a/webclient/src/websocket/events/game/deleteArrow.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_DeleteArrow } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index de010fbfb..000000000 --- a/webclient/src/websocket/events/game/destroyCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_DestroyCard } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index c9ab5e26e..000000000 --- a/webclient/src/websocket/events/game/drawCards.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_DrawCards } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 24f2f3e72..000000000 --- a/webclient/src/websocket/events/game/dumpZone.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_DumpZone } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 4b57e89c3..000000000 --- a/webclient/src/websocket/events/game/flipCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_FlipCard } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index ad9d6cf5c..000000000 --- a/webclient/src/websocket/events/game/gameClosed.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event_GameClosed } from '@app/generated'; - -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -export function gameClosed(_data: Event_GameClosed, 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 deleted file mode 100644 index a5756bb34..000000000 --- a/webclient/src/websocket/events/game/gameEvents.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -vi.mock('../../WebClient'); -import { create } from '@bufbuild/protobuf'; -import { - Event_AttachCardSchema, - Event_ChangeZonePropertiesSchema, - Event_CreateArrowSchema, - Event_CreateCounterSchema, - Event_CreateTokenSchema, - Event_DelCounterSchema, - Event_DeleteArrowSchema, - Event_DestroyCardSchema, - Event_DrawCardsSchema, - Event_DumpZoneSchema, - Event_FlipCardSchema, - Event_GameSaySchema, - Event_GameStateChangedSchema, - Event_MoveCardSchema, - Event_RevealCardsSchema, - Event_ReverseTurnSchema, - Event_RollDieSchema, - Event_SetActivePhaseSchema, - Event_SetActivePlayerSchema, - Event_SetCardAttrSchema, - Event_SetCardCounterSchema, - Event_SetCounterSchema, - Event_ShuffleSchema, - ServerInfo_PlayerPropertiesSchema, -} from '@app/generated'; -import { WebClient } from '../../WebClient'; -import { attachCard } from './attachCard'; -import { changeZoneProperties } from './changeZoneProperties'; -import { createArrow } from './createArrow'; -import { createCounter } from './createCounter'; -import { createToken } from './createToken'; -import { delCounter } from './delCounter'; -import { deleteArrow } from './deleteArrow'; -import { destroyCard } from './destroyCard'; -import { drawCards } from './drawCards'; -import { dumpZone } from './dumpZone'; -import { flipCard } from './flipCard'; -import { gameClosed } from './gameClosed'; -import { gameHostChanged } from './gameHostChanged'; -import { gameSay } from './gameSay'; -import { gameStateChanged } from './gameStateChanged'; -import { joinGame } from './joinGame'; -import { kicked } from './kicked'; -import { leaveGame } from './leaveGame'; -import { moveCard } from './moveCard'; -import { playerPropertiesChanged } from './playerPropertiesChanged'; -import { revealCards } from './revealCards'; -import { reverseTurn } from './reverseTurn'; -import { rollDie } from './rollDie'; -import { setActivePhase } from './setActivePhase'; -import { setActivePlayer } from './setActivePlayer'; -import { setCardAttr } from './setCardAttr'; -import { setCardCounter } from './setCardCounter'; -import { setCounter } from './setCounter'; -import { shuffle } from './shuffle'; - -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(ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); - const data = { playerProperties }; - joinGame(data, meta); - expect(WebClient.instance.response.game.playerJoined).toHaveBeenCalledWith(5, playerProperties); - }); -}); - -describe('leaveGame event', () => { - it('delegates to WebClient.instance.response.game.playerLeft with gameId/playerId from meta', () => { - const data = { reason: 3 }; - leaveGame(data, meta); - expect(WebClient.instance.response.game.playerLeft).toHaveBeenCalledWith(5, 2, 3); - }); -}); - -describe('gameClosed event', () => { - it('delegates to WebClient.instance.response.game.gameClosed with gameId', () => { - gameClosed({}, meta); - expect(WebClient.instance.response.game.gameClosed).toHaveBeenCalledWith(5); - }); -}); - -describe('gameHostChanged event', () => { - it('delegates to WebClient.instance.response.game.gameHostChanged using meta.playerId as hostId', () => { - gameHostChanged({}, meta); - expect(WebClient.instance.response.game.gameHostChanged).toHaveBeenCalledWith(5, 2); - }); -}); - -describe('kicked event', () => { - it('delegates to WebClient.instance.response.game.kicked with gameId', () => { - kicked({}, meta); - expect(WebClient.instance.response.game.kicked).toHaveBeenCalledWith(5); - }); -}); - -describe('gameStateChanged event', () => { - it('delegates to WebClient.instance.response.game.gameStateChanged with gameId and full data', () => { - const data = create(Event_GameStateChangedSchema, { playerList: [] }); - gameStateChanged(data, meta); - expect(WebClient.instance.response.game.gameStateChanged).toHaveBeenCalledWith(5, data); - }); -}); - -describe('playerPropertiesChanged event', () => { - it('delegates to WebClient.instance.response.game.playerPropertiesChanged with gameId, playerId, properties', () => { - const playerProperties = create(ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); - const data = { playerProperties }; - playerPropertiesChanged(data, meta); - expect(WebClient.instance.response.game.playerPropertiesChanged).toHaveBeenCalledWith(5, 2, playerProperties); - }); -}); - -describe('gameSay event', () => { - it('delegates to WebClient.instance.response.game.gameSay with gameId, playerId, message, timeReceived', () => { - const data = create(Event_GameSaySchema, { message: 'gg' }); - gameSay(data, meta); - expect(WebClient.instance.response.game.gameSay).toHaveBeenCalledWith(5, 2, 'gg', expect.any(Number)); - }); -}); - -describe('moveCard event', () => { - it('delegates to WebClient.instance.response.game.cardMoved with gameId, playerId and data', () => { - const data = create(Event_MoveCardSchema, { cardId: 3 }); - moveCard(data, meta); - expect(WebClient.instance.response.game.cardMoved).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('flipCard event', () => { - it('delegates to WebClient.instance.response.game.cardFlipped with gameId, playerId and data', () => { - const data = create(Event_FlipCardSchema, { cardId: 3 }); - flipCard(data, meta); - expect(WebClient.instance.response.game.cardFlipped).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('destroyCard event', () => { - it('delegates to WebClient.instance.response.game.cardDestroyed with gameId, playerId and data', () => { - const data = create(Event_DestroyCardSchema, { cardId: 3 }); - destroyCard(data, meta); - expect(WebClient.instance.response.game.cardDestroyed).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('attachCard event', () => { - it('delegates to WebClient.instance.response.game.cardAttached with gameId, playerId and data', () => { - const data = create(Event_AttachCardSchema, { cardId: 3 }); - attachCard(data, meta); - expect(WebClient.instance.response.game.cardAttached).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('createToken event', () => { - it('delegates to WebClient.instance.response.game.tokenCreated with gameId, playerId and data', () => { - const data = create(Event_CreateTokenSchema, { cardId: 3 }); - createToken(data, meta); - expect(WebClient.instance.response.game.tokenCreated).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('setCardAttr event', () => { - it('delegates to WebClient.instance.response.game.cardAttrChanged with gameId, playerId and data', () => { - const data = create(Event_SetCardAttrSchema, { cardId: 3 }); - setCardAttr(data, meta); - expect(WebClient.instance.response.game.cardAttrChanged).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('setCardCounter event', () => { - it('delegates to WebClient.instance.response.game.cardCounterChanged with gameId, playerId and data', () => { - const data = create(Event_SetCardCounterSchema, { cardId: 3 }); - setCardCounter(data, meta); - expect(WebClient.instance.response.game.cardCounterChanged).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('createArrow event', () => { - it('delegates to WebClient.instance.response.game.arrowCreated with gameId, playerId and data', () => { - const data = create(Event_CreateArrowSchema, {}); - createArrow(data, meta); - expect(WebClient.instance.response.game.arrowCreated).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('deleteArrow event', () => { - it('delegates to WebClient.instance.response.game.arrowDeleted with gameId, playerId and data', () => { - const data = create(Event_DeleteArrowSchema, { arrowId: 9 }); - deleteArrow(data, meta); - expect(WebClient.instance.response.game.arrowDeleted).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('createCounter event', () => { - it('delegates to WebClient.instance.response.game.counterCreated with gameId, playerId and data', () => { - const data = create(Event_CreateCounterSchema, {}); - createCounter(data, meta); - expect(WebClient.instance.response.game.counterCreated).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('setCounter event', () => { - it('delegates to WebClient.instance.response.game.counterSet with gameId, playerId and data', () => { - const data = create(Event_SetCounterSchema, { counterId: 1, value: 20 }); - setCounter(data, meta); - expect(WebClient.instance.response.game.counterSet).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('delCounter event', () => { - it('delegates to WebClient.instance.response.game.counterDeleted with gameId, playerId and data', () => { - const data = create(Event_DelCounterSchema, { counterId: 1 }); - delCounter(data, meta); - expect(WebClient.instance.response.game.counterDeleted).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('drawCards event', () => { - it('delegates to WebClient.instance.response.game.cardsDrawn with gameId, playerId and data', () => { - const data = create(Event_DrawCardsSchema, { number: 2, cards: [] }); - drawCards(data, meta); - expect(WebClient.instance.response.game.cardsDrawn).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('revealCards event', () => { - it('delegates to WebClient.instance.response.game.cardsRevealed with gameId, playerId and data', () => { - const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); - revealCards(data, meta); - expect(WebClient.instance.response.game.cardsRevealed).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('shuffle event', () => { - it('delegates to WebClient.instance.response.game.zoneShuffled with gameId, playerId and data', () => { - const data = create(Event_ShuffleSchema, { zoneName: 'deck' }); - shuffle(data, meta); - expect(WebClient.instance.response.game.zoneShuffled).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('rollDie event', () => { - it('delegates to WebClient.instance.response.game.dieRolled with gameId, playerId and data', () => { - const data = create(Event_RollDieSchema, { die: 6, result: 4 }); - rollDie(data, meta); - expect(WebClient.instance.response.game.dieRolled).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('setActivePlayer event', () => { - it('delegates to WebClient.instance.response.game.activePlayerSet with gameId and activePlayerId', () => { - const data = create(Event_SetActivePlayerSchema, { activePlayerId: 3 }); - setActivePlayer(data, meta); - expect(WebClient.instance.response.game.activePlayerSet).toHaveBeenCalledWith(5, 3); - }); -}); - -describe('setActivePhase event', () => { - it('delegates to WebClient.instance.response.game.activePhaseSet with gameId and phase', () => { - const data = create(Event_SetActivePhaseSchema, { phase: 4 }); - setActivePhase(data, meta); - expect(WebClient.instance.response.game.activePhaseSet).toHaveBeenCalledWith(5, 4); - }); -}); - -describe('reverseTurn event', () => { - it('delegates to WebClient.instance.response.game.turnReversed with gameId and reversed', () => { - const data = create(Event_ReverseTurnSchema, { reversed: true }); - reverseTurn(data, meta); - expect(WebClient.instance.response.game.turnReversed).toHaveBeenCalledWith(5, true); - }); -}); - -describe('dumpZone event', () => { - it('delegates to WebClient.instance.response.game.zoneDumped with gameId, playerId and data', () => { - const data = create(Event_DumpZoneSchema, { zoneName: 'hand' }); - dumpZone(data, meta); - expect(WebClient.instance.response.game.zoneDumped).toHaveBeenCalledWith(5, 2, data); - }); -}); - -describe('changeZoneProperties event', () => { - it('delegates to WebClient.instance.response.game.zonePropertiesChanged with gameId, playerId and data', () => { - const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); - 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 deleted file mode 100644 index b1321cedd..000000000 --- a/webclient/src/websocket/events/game/gameHostChanged.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Event_GameHostChanged } from '@app/generated'; - -import type { GameEventMeta } from '../../types/WebSocketConfig'; -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: Event_GameHostChanged, 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 deleted file mode 100644 index 18a643e0f..000000000 --- a/webclient/src/websocket/events/game/gameSay.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_GameSay } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -export function gameSay(data: Event_GameSay, meta: GameEventMeta): void { - WebClient.instance.response.game.gameSay(meta.gameId, meta.playerId, data.message, Date.now()); -} diff --git a/webclient/src/websocket/events/game/gameStateChanged.ts b/webclient/src/websocket/events/game/gameStateChanged.ts deleted file mode 100644 index cc5b95d7a..000000000 --- a/webclient/src/websocket/events/game/gameStateChanged.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_GameStateChanged } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 5798a1aaa..000000000 --- a/webclient/src/websocket/events/game/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; - -import { - type RegistryEntry, - type GameEvent, - makeEntry, - Event_Join_ext, - Event_Leave_ext, - Event_GameClosed_ext, - Event_GameHostChanged_ext, - Event_Kicked_ext, - Event_GameStateChanged_ext, - Event_PlayerPropertiesChanged_ext, - Event_GameSay_ext, - Event_CreateArrow_ext, - Event_DeleteArrow_ext, - Event_CreateCounter_ext, - Event_SetCounter_ext, - Event_DelCounter_ext, - Event_DrawCards_ext, - Event_RevealCards_ext, - Event_Shuffle_ext, - Event_RollDie_ext, - Event_MoveCard_ext, - Event_FlipCard_ext, - Event_DestroyCard_ext, - Event_AttachCard_ext, - Event_CreateToken_ext, - Event_SetCardAttr_ext, - Event_SetCardCounter_ext, - Event_SetActivePlayer_ext, - Event_SetActivePhase_ext, - Event_DumpZone_ext, - Event_ChangeZoneProperties_ext, - Event_ReverseTurn_ext, -} from '@app/generated'; - -import type { GameEventMeta } from '../../types/WebSocketConfig'; - -import { attachCard } from './attachCard'; -import { changeZoneProperties } from './changeZoneProperties'; -import { createArrow } from './createArrow'; -import { createCounter } from './createCounter'; -import { createToken } from './createToken'; -import { delCounter } from './delCounter'; -import { deleteArrow } from './deleteArrow'; -import { destroyCard } from './destroyCard'; -import { drawCards } from './drawCards'; -import { dumpZone } from './dumpZone'; -import { flipCard } from './flipCard'; -import { gameClosed } from './gameClosed'; -import { gameHostChanged } from './gameHostChanged'; -import { gameSay } from './gameSay'; -import { gameStateChanged } from './gameStateChanged'; -import { joinGame } from './joinGame'; -import { kicked } from './kicked'; -import { leaveGame } from './leaveGame'; -import { moveCard } from './moveCard'; -import { playerPropertiesChanged } from './playerPropertiesChanged'; -import { revealCards } from './revealCards'; -import { reverseTurn } from './reverseTurn'; -import { rollDie } from './rollDie'; -import { setActivePhase } from './setActivePhase'; -import { setActivePlayer } from './setActivePlayer'; -import { setCardAttr } from './setCardAttr'; -import { setCardCounter } from './setCardCounter'; -import { setCounter } from './setCounter'; -import { shuffle } from './shuffle'; - -type GameRegistryEntry = RegistryEntry; -export type GameExtensionRegistry = GameRegistryEntry[]; - -function makeGameEntry( - ext: GenExtension, - handler: (value: V, meta: GameEventMeta) => void, -): GameRegistryEntry { - return makeEntry(ext, handler); -} - -export const GameEvents: GameExtensionRegistry = [ - makeGameEntry(Event_Join_ext, joinGame), - makeGameEntry(Event_Leave_ext, leaveGame), - makeGameEntry(Event_GameClosed_ext, gameClosed), - makeGameEntry(Event_GameHostChanged_ext, gameHostChanged), - makeGameEntry(Event_Kicked_ext, kicked), - makeGameEntry(Event_GameStateChanged_ext, gameStateChanged), - makeGameEntry(Event_PlayerPropertiesChanged_ext, playerPropertiesChanged), - makeGameEntry(Event_GameSay_ext, gameSay), - makeGameEntry(Event_CreateArrow_ext, createArrow), - makeGameEntry(Event_DeleteArrow_ext, deleteArrow), - makeGameEntry(Event_CreateCounter_ext, createCounter), - makeGameEntry(Event_SetCounter_ext, setCounter), - makeGameEntry(Event_DelCounter_ext, delCounter), - makeGameEntry(Event_DrawCards_ext, drawCards), - makeGameEntry(Event_RevealCards_ext, revealCards), - makeGameEntry(Event_Shuffle_ext, shuffle), - makeGameEntry(Event_RollDie_ext, rollDie), - makeGameEntry(Event_MoveCard_ext, moveCard), - makeGameEntry(Event_FlipCard_ext, flipCard), - makeGameEntry(Event_DestroyCard_ext, destroyCard), - makeGameEntry(Event_AttachCard_ext, attachCard), - makeGameEntry(Event_CreateToken_ext, createToken), - makeGameEntry(Event_SetCardAttr_ext, setCardAttr), - makeGameEntry(Event_SetCardCounter_ext, setCardCounter), - makeGameEntry(Event_SetActivePlayer_ext, setActivePlayer), - makeGameEntry(Event_SetActivePhase_ext, setActivePhase), - makeGameEntry(Event_DumpZone_ext, dumpZone), - makeGameEntry(Event_ChangeZoneProperties_ext, changeZoneProperties), - makeGameEntry(Event_ReverseTurn_ext, reverseTurn), -]; diff --git a/webclient/src/websocket/events/game/joinGame.ts b/webclient/src/websocket/events/game/joinGame.ts deleted file mode 100644 index b2ede9a4d..000000000 --- a/webclient/src/websocket/events/game/joinGame.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event_Join } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - - -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 deleted file mode 100644 index 4a6a8c455..000000000 --- a/webclient/src/websocket/events/game/kicked.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event_Kicked } from '@app/generated'; - -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -export function kicked(_data: Event_Kicked, 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 deleted file mode 100644 index 94a84e843..000000000 --- a/webclient/src/websocket/events/game/leaveGame.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Event_Leave } from '@app/generated'; - -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -export function leaveGame(data: Event_Leave, 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 deleted file mode 100644 index 47910ad4e..000000000 --- a/webclient/src/websocket/events/game/moveCard.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_MoveCard } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index d01a1578d..000000000 --- a/webclient/src/websocket/events/game/playerPropertiesChanged.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_PlayerPropertiesChanged } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index cce29c619..000000000 --- a/webclient/src/websocket/events/game/revealCards.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_RevealCards } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 5340e5ae3..000000000 --- a/webclient/src/websocket/events/game/reverseTurn.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_ReverseTurn } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index da411fad4..000000000 --- a/webclient/src/websocket/events/game/rollDie.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_RollDie } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index c7f92961c..000000000 --- a/webclient/src/websocket/events/game/setActivePhase.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_SetActivePhase } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 730f9614e..000000000 --- a/webclient/src/websocket/events/game/setActivePlayer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_SetActivePlayer } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 1ce1d63bc..000000000 --- a/webclient/src/websocket/events/game/setCardAttr.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_SetCardAttr } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index b78786f89..000000000 --- a/webclient/src/websocket/events/game/setCardCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_SetCardCounter } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index c4cf794c2..000000000 --- a/webclient/src/websocket/events/game/setCounter.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_SetCounter } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 2c404390d..000000000 --- a/webclient/src/websocket/events/game/shuffle.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_Shuffle } from '@app/generated'; -import type { GameEventMeta } from '../../types/WebSocketConfig'; -import { WebClient } from '../../WebClient'; - -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/index.ts b/webclient/src/websocket/events/index.ts deleted file mode 100644 index ed74f9b93..000000000 --- a/webclient/src/websocket/events/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './common'; -export * from './room'; -export * from './session'; -export * from './game'; diff --git a/webclient/src/websocket/events/room/index.ts b/webclient/src/websocket/events/room/index.ts deleted file mode 100644 index 0456ea783..000000000 --- a/webclient/src/websocket/events/room/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; - -import { - type RegistryEntry, - type RoomEvent, - makeEntry, - Event_JoinRoom_ext, - Event_LeaveRoom_ext, - Event_ListGames_ext, - Event_RemoveMessages_ext, - Event_RoomSay_ext, -} from '@app/generated'; - -import { joinRoom } from './joinRoom'; -import { leaveRoom } from './leaveRoom'; -import { listGames } from './listGames'; -import { removeMessages } from './removeMessages'; -import { roomSay } from './roomSay'; - -type RoomRegistryEntry = RegistryEntry; -export type RoomExtensionRegistry = RoomRegistryEntry[]; - -function makeRoomEntry( - ext: GenExtension, - handler: (value: V, roomEvent: RoomEvent) => void, -): RoomRegistryEntry { - return makeEntry(ext, handler); -} - -export const RoomEvents: RoomExtensionRegistry = [ - makeRoomEntry(Event_JoinRoom_ext, joinRoom), - makeRoomEntry(Event_LeaveRoom_ext, leaveRoom), - makeRoomEntry(Event_ListGames_ext, listGames), - makeRoomEntry(Event_RemoveMessages_ext, removeMessages), - makeRoomEntry(Event_RoomSay_ext, roomSay), -]; diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts deleted file mode 100644 index 203dcebe5..000000000 --- a/webclient/src/websocket/events/room/joinRoom.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_JoinRoom, RoomEvent } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 819e8f438..000000000 --- a/webclient/src/websocket/events/room/leaveRoom.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_LeaveRoom, RoomEvent } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index a06d4742f..000000000 --- a/webclient/src/websocket/events/room/listGames.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_ListGames, RoomEvent } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 0c196438e..000000000 --- a/webclient/src/websocket/events/room/removeMessages.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_RemoveMessages, RoomEvent } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 3955b524d..000000000 --- a/webclient/src/websocket/events/room/roomEvents.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -vi.mock('../../WebClient'); - -import { create } from '@bufbuild/protobuf'; -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'; -import { listGames } from './listGames'; -import { removeMessages } from './removeMessages'; -import { roomSay } from './roomSay'; - -const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId }); - -describe('joinRoom room event', () => { - - it('calls response.room.userJoined with roomId and userInfo', () => { - const data = create(Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); - joinRoom(data, makeRoomEvent(3)); - expect(WebClient.instance.response.room.userJoined).toHaveBeenCalledWith(3, data.userInfo); - }); -}); - -describe('leaveRoom room event', () => { - - it('calls response.room.userLeft with roomId and name', () => { - leaveRoom(create(Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); - expect(WebClient.instance.response.room.userLeft).toHaveBeenCalledWith(4, 'alice'); - }); -}); - -describe('listGames room event', () => { - - it('calls response.room.updateGames with roomId and gameList', () => { - const data = create(Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); - listGames(data, makeRoomEvent(5)); - expect(WebClient.instance.response.room.updateGames).toHaveBeenCalledWith(5, data.gameList); - }); -}); - -describe('removeMessages room event', () => { - - it('calls response.room.removeMessages with roomId, name, amount', () => { - removeMessages(create(Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); - expect(WebClient.instance.response.room.removeMessages).toHaveBeenCalledWith(6, 'bob', 10); - }); -}); - -describe('roomSay room event', () => { - beforeEach(() => { - vi.useFakeTimers(); vi.setSystemTime(0); - }); - afterEach(() => vi.useRealTimers()); - - it('calls response.room.addMessage with roomId and message', () => { - 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 deleted file mode 100644 index 919189093..000000000 --- a/webclient/src/websocket/events/room/roomSay.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_RoomSay, RoomEvent } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index a8e27a654..000000000 --- a/webclient/src/websocket/events/session/addToList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Event_AddToList } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function addToList({ listName, userInfo }: Event_AddToList): void { - switch (listName) { - case 'buddy': { - WebClient.instance.response.session.addToBuddyList(userInfo); - break; - } - case 'ignore': { - WebClient.instance.response.session.addToIgnoreList(userInfo); - break; - } - default: { - console.log(`Attempted to add to unknown list: ${listName}`); - } - } -} diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts deleted file mode 100644 index d292d5687..000000000 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Event_ConnectionClosed_CloseReason, type Event_ConnectionClosed } from '@app/generated'; -import { StatusEnum } from '../../types/StatusEnum'; -import { updateStatus } from '../../commands/session'; - -export function connectionClosed({ reason, reasonStr, endTime }: Event_ConnectionClosed): void { - let message: string; - - if (reasonStr) { - message = reasonStr; - } else { - switch (reason) { - case Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: - message = 'The server has reached its maximum user capacity'; - break; - case Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: - message = 'There are too many concurrent connections from your address'; - break; - 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 Event_ConnectionClosed_CloseReason.DEMOTED: - message = 'You were demoted'; - break; - case Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: - message = 'Scheduled server shutdown'; - break; - case Event_ConnectionClosed_CloseReason.USERNAMEINVALID: - message = 'Invalid username'; - break; - case Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: - message = 'You have been logged out due to logging in at another location'; - break; - case Event_ConnectionClosed_CloseReason.OTHER: - default: - message = 'Unknown reason'; - break; - } - } - - updateStatus(StatusEnum.DISCONNECTED, message); -} diff --git a/webclient/src/websocket/events/session/gameJoined.ts b/webclient/src/websocket/events/session/gameJoined.ts deleted file mode 100644 index 83a555949..000000000 --- a/webclient/src/websocket/events/session/gameJoined.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_GameJoined } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 7df1c46df..000000000 --- a/webclient/src/websocket/events/session/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; - -import { - type RegistryEntry, - type SessionEvent, - makeEntry, - Event_AddToList_ext, - Event_ConnectionClosed_ext, - Event_GameJoined_ext, - Event_ListRooms_ext, - Event_NotifyUser_ext, - Event_RemoveFromList_ext, - Event_ReplayAdded_ext, - Event_ServerCompleteList_ext, - Event_ServerIdentification_ext, - Event_ServerMessage_ext, - Event_ServerShutdown_ext, - Event_UserJoined_ext, - Event_UserLeft_ext, - Event_UserMessage_ext, -} from '@app/generated'; - -import { addToList } from './addToList'; -import { connectionClosed } from './connectionClosed'; -import { gameJoined } from './gameJoined'; -import { listRooms } from './listRooms'; -import { notifyUser } from './notifyUser'; -import { removeFromList } from './removeFromList'; -import { replayAdded } from './replayAdded'; -import { serverCompleteList } from './serverCompleteList'; -import { serverIdentification } from './serverIdentification'; -import { serverMessage } from './serverMessage'; -import { serverShutdown } from './serverShutdown'; -import { userJoined } from './userJoined'; -import { userLeft } from './userLeft'; -import { userMessage } from './userMessage'; - -type SessionRegistryEntry = RegistryEntry; -export type SessionExtensionRegistry = SessionRegistryEntry[]; - -function makeSessionEntry( - ext: GenExtension, - handler: (value: V) => void, -): SessionRegistryEntry { - return makeEntry(ext, handler); -} - -export const SessionEvents: SessionExtensionRegistry = [ - makeSessionEntry(Event_AddToList_ext, addToList), - makeSessionEntry(Event_ConnectionClosed_ext, connectionClosed), - makeSessionEntry(Event_GameJoined_ext, gameJoined), - makeSessionEntry(Event_ListRooms_ext, listRooms), - makeSessionEntry(Event_NotifyUser_ext, notifyUser), - makeSessionEntry(Event_RemoveFromList_ext, removeFromList), - makeSessionEntry(Event_ReplayAdded_ext, replayAdded), - makeSessionEntry(Event_ServerCompleteList_ext, serverCompleteList), - makeSessionEntry(Event_ServerIdentification_ext, serverIdentification), - makeSessionEntry(Event_ServerMessage_ext, serverMessage), - makeSessionEntry(Event_ServerShutdown_ext, serverShutdown), - makeSessionEntry(Event_UserJoined_ext, userJoined), - makeSessionEntry(Event_UserLeft_ext, userLeft), - makeSessionEntry(Event_UserMessage_ext, userMessage), -]; diff --git a/webclient/src/websocket/events/session/listRooms.ts b/webclient/src/websocket/events/session/listRooms.ts deleted file mode 100644 index 8dac345a0..000000000 --- a/webclient/src/websocket/events/session/listRooms.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 }: Event_ListRooms): void { - WebClient.instance.response.room.updateRooms(roomList); - - if (CLIENT_OPTIONS.autojoinrooms) { - roomList.forEach(({ autoJoin, roomId }) => { - if (autoJoin) { - joinRoom(roomId); - } - }); - } -} diff --git a/webclient/src/websocket/events/session/notifyUser.ts b/webclient/src/websocket/events/session/notifyUser.ts deleted file mode 100644 index df8840781..000000000 --- a/webclient/src/websocket/events/session/notifyUser.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_NotifyUser } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 8b4db5b21..000000000 --- a/webclient/src/websocket/events/session/removeFromList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Event_RemoveFromList } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function removeFromList({ listName, userName }: Event_RemoveFromList): void { - switch (listName) { - case 'buddy': { - WebClient.instance.response.session.removeFromBuddyList(userName); - break; - } - case 'ignore': { - WebClient.instance.response.session.removeFromIgnoreList(userName); - break; - } - default: { - console.log(`Attempted to remove from unknown list: ${listName}`); - } - } -} diff --git a/webclient/src/websocket/events/session/replayAdded.ts b/webclient/src/websocket/events/session/replayAdded.ts deleted file mode 100644 index 8fe1e2b89..000000000 --- a/webclient/src/websocket/events/session/replayAdded.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_ReplayAdded } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index df2b0c05a..000000000 --- a/webclient/src/websocket/events/session/serverCompleteList.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_ServerCompleteList } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 84a21522a..000000000 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Event_ServerIdentification } from '@app/generated'; -import { WebClient } from '../../WebClient'; -import { StatusEnum } from '../../types/StatusEnum'; -import { PROTOCOL_VERSION } from '../../config'; -import { consumePendingOptions } from '../../utils/connectionState'; -import { WebSocketConnectReason } from '../../types/ConnectOptions'; -import { generateSalt, passwordSaltSupported } from '../../utils'; -import * as SessionCommands from '../../commands/session'; - -export function serverIdentification(info: Event_ServerIdentification): void { - const { serverName, serverVersion, protocolVersion, serverOptions } = info; - const response = WebClient.instance.response; - - if (protocolVersion !== PROTOCOL_VERSION) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); - SessionCommands.disconnect(); - return; - } - - const getPasswordSalt = passwordSaltSupported(serverOptions); - const options = consumePendingOptions(); - - if (!options) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Missing connection options'); - SessionCommands.disconnect(); - return; - } - - switch (options.reason) { - case WebSocketConnectReason.LOGIN: { - const { password, ...rest } = options; - SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); - if (getPasswordSalt) { - SessionCommands.requestPasswordSalt(rest, - // Empty salt means the server advertised SupportsPasswordHash but - // can't actually produce one. Treat it as effectively unsupported — - // fall through to a plain-password login rather than failing. - (salt) => salt - ? SessionCommands.login(rest, password, salt) - : SessionCommands.login(rest, password), - () => { - response.session.loginFailed(); SessionCommands.disconnect(); - }, - ); - } else { - SessionCommands.login(rest, password); - } - break; - } - case WebSocketConnectReason.REGISTER: { - const { password, ...rest } = options; - const passwordSalt = getPasswordSalt ? generateSalt() : null; - SessionCommands.register(rest, password, passwordSalt); - break; - } - case WebSocketConnectReason.ACTIVATE_ACCOUNT: { - const { password, ...rest } = options; - if (getPasswordSalt) { - SessionCommands.requestPasswordSalt(rest, - (salt) => SessionCommands.activate(rest, password, salt), - () => { - response.session.accountActivationFailed(); SessionCommands.disconnect(); - }, - ); - } else { - SessionCommands.activate(rest, password); - } - break; - } - case WebSocketConnectReason.PASSWORD_RESET_REQUEST: - SessionCommands.forgotPasswordRequest(options); - break; - case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - SessionCommands.forgotPasswordChallenge(options); - break; - case WebSocketConnectReason.PASSWORD_RESET: { - const { newPassword, ...rest } = options; - if (getPasswordSalt) { - SessionCommands.requestPasswordSalt(rest, - (salt) => SessionCommands.forgotPasswordReset(rest, newPassword, salt), - () => { - response.session.resetPasswordFailed(); SessionCommands.disconnect(); - }, - ); - } else { - SessionCommands.forgotPasswordReset(rest, newPassword); - } - break; - } - default: { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${(options as { reason: number }).reason}`); - SessionCommands.disconnect(); - break; - } - } - - response.session.updateInfo(serverName, serverVersion); -} diff --git a/webclient/src/websocket/events/session/serverMessage.ts b/webclient/src/websocket/events/session/serverMessage.ts deleted file mode 100644 index c7f3b7b9d..000000000 --- a/webclient/src/websocket/events/session/serverMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Event_ServerMessage } from '@app/generated'; -import { WebClient } from '../../WebClient'; -import { sanitizeHtml } from '../../utils'; - -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 deleted file mode 100644 index cdd404739..000000000 --- a/webclient/src/websocket/events/session/serverShutdown.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_ServerShutdown } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -export function serverShutdown(payload: Event_ServerShutdown): void { - WebClient.instance.response.session.serverShutdown(payload); -} diff --git a/webclient/src/websocket/events/session/session-login-flow.integration.spec.ts b/webclient/src/websocket/events/session/session-login-flow.integration.spec.ts deleted file mode 100644 index ead48704a..000000000 --- a/webclient/src/websocket/events/session/session-login-flow.integration.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Integration coverage of the full login chain through the real session -// event handler + real session commands. Exercises: -// serverIdentification(info) -// → requestPasswordSalt (real) -// → login (real) -// → WebClient.instance.response.session.loginSuccessful(...) -// -// Only the transport (sendSessionCommand), the connection-state shim, and the -// password-hash bitmask helper are mocked — everything in between is the -// production code path. This triangulates the "loginSuccessful payload is -// empty" symptom reported in plans/when-the-login-quirky-crane.md. - -vi.mock('../../WebClient'); - -vi.mock('../../config', () => ({ - CLIENT_CONFIG: { clientver: 'test-client', clientfeatures: [] }, - CLIENT_OPTIONS: { autojoinrooms: false, keepalive: 5000 }, - PROTOCOL_VERSION: 14, -})); - -vi.mock('../../utils/connectionState', () => ({ - consumePendingOptions: vi.fn().mockReturnValue(null), -})); - -// Only the bitmask check is mocked; hashPassword stays real so the test -// asserts against a genuinely computed hash (not a sentinel string). -vi.mock('../../utils', async (importOriginal) => ({ - ...(await importOriginal()), - passwordSaltSupported: vi.fn().mockReturnValue(0), -})); - -import { create } from '@bufbuild/protobuf'; -import { Mock } from 'vitest'; -import { Event_ServerIdentificationSchema } from '@app/generated'; - -import { WebClient } from '../../WebClient'; -import { consumePendingOptions } from '../../utils/connectionState'; -import { passwordSaltSupported, hashPassword } from '../../utils'; -import { WebSocketConnectReason } from '../../types/ConnectOptions'; -import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; -import { serverIdentification } from './serverIdentification'; - -const { invokeOnSuccess } = makeCallbackHelpers( - WebClient.instance.protobuf.sendSessionCommand as Mock, - 2, -); - -const makeLoginOptions = () => ({ - host: 'h', - port: '1', - userName: 'alice', - password: 'mypass', - reason: WebSocketConnectReason.LOGIN as const, -}); - -const makeInfo = (overrides: Record = {}) => - create(Event_ServerIdentificationSchema, { - serverName: 'TestServer', - serverVersion: '1.0', - protocolVersion: 14, - serverOptions: 1, - ...overrides, - }); - -beforeEach(() => { - (WebClient.instance.protobuf.sendSessionCommand as Mock).mockClear(); - (WebClient.instance.response.session.loginSuccessful as Mock).mockClear(); - (WebClient.instance.response.session.loginFailed as Mock).mockClear(); - (WebClient.instance.response.session.updateStatus as Mock).mockClear(); - (consumePendingOptions as Mock).mockReset(); - (passwordSaltSupported as Mock).mockReset(); -}); - -describe('integration: fresh login on a hashed-password server', () => { - it('loginSuccessful carries a non-empty hashedPassword matching hashPassword(salt, password)', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - (passwordSaltSupported as Mock).mockReturnValue(1); - - // 1. Server identifies with the hashed-password bit set. - serverIdentification(makeInfo({ serverOptions: 1 })); - - // 2. RequestPasswordSalt command was sent; resolve it with a salt. - invokeOnSuccess({ passwordSalt: 'xyz' }); - - // 3. Login command was sent; resolve it with a minimal success response. - invokeOnSuccess({ buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }); - - // 4. The response layer must receive the SAME hash the client computed. - const expected = hashPassword('xyz', 'mypass'); - expect(WebClient.instance.response.session.loginSuccessful).toHaveBeenCalledTimes(1); - expect(WebClient.instance.response.session.loginSuccessful).toHaveBeenCalledWith({ - hashedPassword: expected, - }); - expect(expected.length).toBeGreaterThan(16); - }); - - it('Command_Login carries hashedPassword and no plaintext password value', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - (passwordSaltSupported as Mock).mockReturnValue(1); - - serverIdentification(makeInfo({ serverOptions: 1 })); - invokeOnSuccess({ passwordSalt: 'xyz' }); - - // Second sendSessionCommand call is the Command_Login. Protobuf always - // materializes the `password` field (proto-default ""), so we assert on - // the VALUE, not the presence: hashedPassword holds the real hash and - // plaintext password is empty. - const calls = (WebClient.instance.protobuf.sendSessionCommand as Mock).mock.calls; - const loginPayload = calls[1]?.[1]; - expect(loginPayload.hashedPassword).toBe(hashPassword('xyz', 'mypass')); - expect(loginPayload.password).toBe(''); - }); -}); - -describe('integration: server returns empty passwordSalt (effectively unsupported)', () => { - it('falls back to a plain-password Command_Login', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - (passwordSaltSupported as Mock).mockReturnValue(1); - - serverIdentification(makeInfo({ serverOptions: 1 })); - // Server's Response_PasswordSalt has the proto default "" — simulate that. - invokeOnSuccess({ passwordSalt: '' }); - - const calls = (WebClient.instance.protobuf.sendSessionCommand as Mock).mock.calls; - // Two commands: RequestPasswordSalt, then the fallback plain Command_Login. - expect(calls.length).toBeGreaterThanOrEqual(2); - const loginPayload = calls[1][1]; - expect(loginPayload.password).toBe('mypass'); - expect(loginPayload.hashedPassword).toBe(''); - }); - - it('does not surface as a login failure', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - (passwordSaltSupported as Mock).mockReturnValue(1); - - serverIdentification(makeInfo({ serverOptions: 1 })); - invokeOnSuccess({ passwordSalt: '' }); - - // Missing salt is treated as "unsupported", not a failure — the client - // should let the plain login proceed without dispatching loginFailed. - expect(WebClient.instance.response.session.loginFailed).not.toHaveBeenCalled(); - }); -}); diff --git a/webclient/src/websocket/events/session/sessionEvents.spec.ts b/webclient/src/websocket/events/session/sessionEvents.spec.ts deleted file mode 100644 index 7383a13d5..000000000 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ /dev/null @@ -1,481 +0,0 @@ -// Tests for simple session events that delegate 1:1 to SessionPersistence -// or RoomPersistence with minimal logic. - -vi.mock('../../WebClient'); - -vi.mock('../../config', () => ({ - CLIENT_OPTIONS: { autojoinrooms: false }, - PROTOCOL_VERSION: 14, -})); - -vi.mock('../../commands/session', () => ({ - joinRoom: vi.fn(), - updateStatus: vi.fn(), - disconnect: vi.fn(), - login: vi.fn(), - register: vi.fn(), - activate: vi.fn(), - forgotPasswordRequest: vi.fn(), - forgotPasswordChallenge: vi.fn(), - forgotPasswordReset: vi.fn(), - requestPasswordSalt: vi.fn(), -})); - -vi.mock('../../utils', () => ({ - sanitizeHtml: vi.fn((msg: string) => msg), - generateSalt: vi.fn().mockReturnValue('randSalt'), - passwordSaltSupported: vi.fn().mockReturnValue(0), -})); - -vi.mock('../../utils/connectionState', () => ({ - consumePendingOptions: vi.fn().mockReturnValue(null), -})); - -import { - 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 { consumePendingOptions } from '../../utils/connectionState'; -import { passwordSaltSupported } from '../../utils'; -import { WebSocketConnectReason } from '../../types/ConnectOptions'; -import { StatusEnum } from '../../types/StatusEnum'; -import { Mock } from 'vitest'; -import { gameJoined } from './gameJoined'; -import { notifyUser } from './notifyUser'; -import { replayAdded } from './replayAdded'; -import { serverCompleteList } from './serverCompleteList'; -import { serverMessage } from './serverMessage'; -import { serverShutdown } from './serverShutdown'; -import { userJoined } from './userJoined'; -import { userLeft } from './userLeft'; -import { userMessage } from './userMessage'; -import { addToList } from './addToList'; -import { removeFromList } from './removeFromList'; -import { listRooms } from './listRooms'; -import { connectionClosed } from './connectionClosed'; -import { serverIdentification } from './serverIdentification'; - -const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] }; - -describe('gameJoined', () => { - - it('calls WebClient.instance.response.session.gameJoined', () => { - const data = create(Event_GameJoinedSchema, { playerId: 1 }); - gameJoined(data); - expect(WebClient.instance.response.session.gameJoined).toHaveBeenCalledWith(data); - }); -}); - -describe('notifyUser', () => { - - it('calls WebClient.instance.response.session.notifyUser', () => { - const data = create(Event_NotifyUserSchema, { warningReason: 'yo' }); - notifyUser(data); - expect(WebClient.instance.response.session.notifyUser).toHaveBeenCalledWith(data); - }); -}); - -describe('replayAdded', () => { - - it('calls WebClient.instance.response.session.replayAdded with matchInfo', () => { - const data = create(Event_ReplayAddedSchema, { - matchInfo: create(ServerInfo_ReplayMatchSchema, { gameId: 42 }), - }); - replayAdded(data); - expect(WebClient.instance.response.session.replayAdded).toHaveBeenCalledWith(data.matchInfo); - }); -}); - -describe('serverCompleteList', () => { - - it('calls WebClient.instance.response.session.updateUsers and WebClient.instance.response.room.updateRooms', () => { - 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); - }); -}); - -describe('serverMessage', () => { - - it('calls WebClient.instance.response.session.serverMessage with message', () => { - serverMessage(create(Event_ServerMessageSchema, { message: 'hello server' })); - expect(WebClient.instance.response.session.serverMessage).toHaveBeenCalledWith('hello server'); - }); -}); - -describe('serverShutdown', () => { - - it('calls WebClient.instance.response.session.serverShutdown', () => { - const payload = create(Event_ServerShutdownSchema, { reason: 'maintenance' }); - serverShutdown(payload); - expect(WebClient.instance.response.session.serverShutdown).toHaveBeenCalledWith(payload); - }); -}); - -describe('userJoined', () => { - - it('calls WebClient.instance.response.session.userJoined with userInfo', () => { - const data = create(Event_UserJoinedSchema, { - userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), - }); - userJoined(data); - expect(WebClient.instance.response.session.userJoined).toHaveBeenCalledWith(data.userInfo); - }); -}); - -describe('userLeft', () => { - - it('calls WebClient.instance.response.session.userLeft with name', () => { - userLeft(create(Event_UserLeftSchema, { name: 'bob' })); - expect(WebClient.instance.response.session.userLeft).toHaveBeenCalledWith('bob'); - }); -}); - -describe('userMessage', () => { - - it('calls WebClient.instance.response.session.userMessage', () => { - const payload = create(Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); - userMessage(payload); - expect(WebClient.instance.response.session.userMessage).toHaveBeenCalledWith(payload); - }); -}); - -describe('addToList', () => { - let logSpy: ReturnType; - beforeEach(() => { - logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - afterEach(() => { - logSpy.mockRestore(); - }); - - it('buddy list → addToBuddyList', () => { - const data = create(Event_AddToListSchema, { - listName: 'buddy', - userInfo: create(ServerInfo_UserSchema, { name: 'alice' }), - }); - addToList(data); - expect(WebClient.instance.response.session.addToBuddyList).toHaveBeenCalledWith(data.userInfo); - }); - - it('ignore list → addToIgnoreList', () => { - const data = create(Event_AddToListSchema, { - listName: 'ignore', - userInfo: create(ServerInfo_UserSchema, { name: 'bob' }), - }); - addToList(data); - expect(WebClient.instance.response.session.addToIgnoreList).toHaveBeenCalledWith(data.userInfo); - }); - - it('unknown list → console.log', () => { - addToList(create(Event_AddToListSchema, { listName: 'unknown' })); - expect(logSpy).toHaveBeenCalled(); - }); -}); - -describe('removeFromList', () => { - - it('buddy list → removeFromBuddyList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); - expect(WebClient.instance.response.session.removeFromBuddyList).toHaveBeenCalledWith('alice'); - }); - - it('ignore list → removeFromIgnoreList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); - expect(WebClient.instance.response.session.removeFromIgnoreList).toHaveBeenCalledWith('bob'); - }); - - it('unknown list → console.log', () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - removeFromList(create(Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); - expect(logSpy).toHaveBeenCalled(); - logSpy.mockRestore(); - }); -}); - -describe('listRooms', () => { - - it('calls WebClient.instance.response.room.updateRooms', () => { - 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(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(Event_ListRoomsSchema, { - roomList: [ - create(ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), - create(ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) - ] - })); - expect(SessionCmds.joinRoom).toHaveBeenCalledTimes(1); - expect(SessionCmds.joinRoom).toHaveBeenCalledWith(2); - }); -}); - -describe('connectionClosed', () => { - - it('uses reasonStr when provided', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom'); - }); - - it('USER_LIMIT_REACHED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith( - expect.anything(), - expect.stringContaining('maximum user capacity') - ); - }); - - it('TOO_MANY_CONNECTIONS → specific message', () => { - 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(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); - }); - - it('DEMOTED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.DEMOTED })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted')); - }); - - it('SERVER_SHUTDOWN → specific message', () => { - 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(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username')); - }); - - it('LOGGEDINELSEWERE → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out')); - }); - - it('OTHER → "Unknown reason"', () => { - 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(Event_ConnectionClosedSchema, { - reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, - })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith( - expect.anything(), - expect.stringContaining('You are banned until') - ); - }); - - it('BANNED with endTime = 0 → shows generic banned message', () => { - 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(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(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(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(Event_ConnectionClosedSchema, - { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason'); - }); -}); - -describe('serverIdentification', () => { - const makeInfo = (overrides: Record = {}) => - create(Event_ServerIdentificationSchema, { - serverName: 'TestServer', - serverVersion: '1.0', - protocolVersion: 14, - serverOptions: 0, - ...overrides, - }); - - const makeLoginOptions = () => ({ - host: 'h', port: '1', userName: 'alice', password: 'pw', - reason: WebSocketConnectReason.LOGIN as const, - }); - - beforeEach(() => { - (consumePendingOptions as Mock).mockReturnValue(null); - (passwordSaltSupported as Mock).mockReturnValue(0); - }); - - it('disconnects on protocol version mismatch', () => { - serverIdentification(makeInfo({ protocolVersion: 99 })); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.stringContaining('Protocol version mismatch')); - expect(SessionCmds.disconnect).toHaveBeenCalled(); - expect(WebClient.instance.response.session.updateInfo).not.toHaveBeenCalled(); - }); - - it('disconnects when pending options are missing', () => { - serverIdentification(makeInfo()); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Missing connection options'); - expect(SessionCmds.disconnect).toHaveBeenCalled(); - expect(WebClient.instance.response.session.updateInfo).not.toHaveBeenCalled(); - }); - - it('LOGIN → calls login without salt when server does not support it', () => { - const opts = makeLoginOptions(); - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGING_IN, 'Logging In...'); - expect(SessionCmds.login).toHaveBeenCalledWith(expect.objectContaining({ userName: 'alice' }), 'pw'); - expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('TestServer', '1.0'); - }); - - it('LOGIN → calls requestPasswordSalt when server supports it', () => { - const opts = makeLoginOptions(); - (consumePendingOptions as Mock).mockReturnValue(opts); - (passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(makeInfo({ serverOptions: 1 })); - expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), - expect.any(Function), - expect.any(Function), - ); - }); - - it('REGISTER → calls register', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', password: 'pw', - email: 'a@b.com', country: 'US', realName: 'Al', - reason: WebSocketConnectReason.REGISTER as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.register).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), 'pw', null, - ); - }); - - it('REGISTER with password salt → passes generated salt', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', password: 'pw', - email: 'a@b.com', country: 'US', realName: 'Al', - reason: WebSocketConnectReason.REGISTER as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - (passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(makeInfo({ serverOptions: 1 })); - expect(SessionCmds.register).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), 'pw', 'randSalt', - ); - }); - - it('ACTIVATE_ACCOUNT → calls activate', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', token: 'tok', password: 'pw', - reason: WebSocketConnectReason.ACTIVATE_ACCOUNT as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.activate).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice', token: 'tok' }), 'pw', - ); - }); - - it('PASSWORD_RESET_REQUEST → calls forgotPasswordRequest', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', - reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.forgotPasswordRequest).toHaveBeenCalledWith(opts); - }); - - it('PASSWORD_RESET_CHALLENGE → calls forgotPasswordChallenge', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', email: 'a@b.com', - reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalledWith(opts); - }); - - it('PASSWORD_RESET → calls forgotPasswordReset without salt', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', token: 'tok', newPassword: 'newpw', - reason: WebSocketConnectReason.PASSWORD_RESET as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - serverIdentification(makeInfo()); - expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice', token: 'tok' }), 'newpw', - ); - }); - - it('PASSWORD_RESET with salt → calls requestPasswordSalt', () => { - const opts = { - host: 'h', port: '1', userName: 'alice', token: 'tok', newPassword: 'newpw', - reason: WebSocketConnectReason.PASSWORD_RESET as const, - }; - (consumePendingOptions as Mock).mockReturnValue(opts); - (passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(makeInfo({ serverOptions: 1 })); - expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( - expect.objectContaining({ userName: 'alice' }), - expect.any(Function), - expect.any(Function), - ); - }); - - it('always calls updateInfo after successful routing', () => { - (consumePendingOptions as Mock).mockReturnValue(makeLoginOptions()); - serverIdentification(makeInfo()); - expect(WebClient.instance.response.session.updateInfo).toHaveBeenCalledWith('TestServer', '1.0'); - }); -}); diff --git a/webclient/src/websocket/events/session/userJoined.ts b/webclient/src/websocket/events/session/userJoined.ts deleted file mode 100644 index 5f0e6014d..000000000 --- a/webclient/src/websocket/events/session/userJoined.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_UserJoined } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index f53bff2d4..000000000 --- a/webclient/src/websocket/events/session/userLeft.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_UserLeft } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 5b3ca5905..000000000 --- a/webclient/src/websocket/events/session/userMessage.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Event_UserMessage } from '@app/generated'; -import { WebClient } from '../../WebClient'; - -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 deleted file mode 100644 index 4afca0522..000000000 --- a/webclient/src/websocket/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './commands'; - -export { WebClient } from './WebClient'; - -export { SessionEvents } from './events/session'; -export { RoomEvents } from './events/room'; -export { GameEvents } from './events/game'; - -export { generateSalt, passwordSaltSupported, hashPassword } from './utils'; -export { setPendingOptions, consumePendingOptions } from './utils/connectionState'; diff --git a/webclient/src/websocket/services/KeepAliveService.spec.ts b/webclient/src/websocket/services/KeepAliveService.spec.ts deleted file mode 100644 index 81e023658..000000000 --- a/webclient/src/websocket/services/KeepAliveService.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { KeepAliveService } from './KeepAliveService'; - -type KeepAliveInternal = KeepAliveService & { - keepalivecb: NodeJS.Timeout; - lastPingPending: boolean; -}; - -describe('KeepAliveService', () => { - let service: KeepAliveService; - let mockIsOpen: ReturnType; - - beforeEach(() => { - vi.useFakeTimers(); - - mockIsOpen = vi.fn().mockReturnValue(true); - service = new KeepAliveService(mockIsOpen); - }); - - it('should create', () => { - expect(service).toBeDefined(); - }); - - describe('startPingLoop', () => { - let resolvePing; - let interval; - let promise; - let ping; - - beforeEach(() => { - interval = 100; - promise = new Promise(resolve => resolvePing = resolve); - ping = (done) => promise.then(done); - - service.startPingLoop(interval, ping); - vi.advanceTimersByTime(interval); - }); - - it('should start ping loop', () => { - expect((service as KeepAliveInternal).keepalivecb).toBeDefined(); - expect((service as KeepAliveInternal).lastPingPending).toBeTruthy(); - }); - - it('should call ping callback when done', () => { - resolvePing(); - - return promise.then(() => { - expect((service as KeepAliveInternal).lastPingPending).toBeFalsy(); - }); - }); - - it('should fire disconnected$ if lastPingPending is still true', () => { - vi.spyOn(service.disconnected$, 'next').mockImplementation(() => {}); - vi.advanceTimersByTime(interval); - - expect(service.disconnected$.next).toHaveBeenCalled(); - }); - - it('should endPingLoop if socket is not open', () => { - vi.spyOn(service, 'endPingLoop').mockImplementation(() => {}); - mockIsOpen.mockReturnValue(false); - - resolvePing(); - vi.advanceTimersByTime(interval); - - expect(service.endPingLoop).toHaveBeenCalled(); - }); - - it('should clear previous interval when startPingLoop is called again', () => { - const clearSpy = vi.spyOn(globalThis, 'clearInterval'); - const previousCb = (service as KeepAliveInternal).keepalivecb; - - service.startPingLoop(interval, ping); - - expect(clearSpy).toHaveBeenCalledWith(previousCb); - }); - }); -}); diff --git a/webclient/src/websocket/services/KeepAliveService.ts b/webclient/src/websocket/services/KeepAliveService.ts deleted file mode 100644 index 7eb38ea18..000000000 --- a/webclient/src/websocket/services/KeepAliveService.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Subject } from 'rxjs'; - -export class KeepAliveService { - private isOpen: () => boolean; - - private keepalivecb: NodeJS.Timeout; - private lastPingPending: boolean; - - public disconnected$ = new Subject(); - - constructor(isOpen: () => boolean) { - this.isOpen = isOpen; - } - - public startPingLoop(interval: number, ping: (onPong: () => void) => void): void { - this.endPingLoop(); - this.keepalivecb = setInterval(() => { - // check if the previous ping got no reply - if (this.lastPingPending) { - this.disconnected$.next(); - } - - // stop the ping loop if we're disconnected - if (!this.isOpen()) { - this.endPingLoop(); - return; - } - - this.lastPingPending = true; - ping(() => this.lastPingPending = false); - }, interval); - } - - public endPingLoop() { - clearInterval(this.keepalivecb); - this.keepalivecb = null; - this.lastPingPending = false; - } -} diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts deleted file mode 100644 index 1f5cf8d7e..000000000 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ /dev/null @@ -1,573 +0,0 @@ -vi.mock('@bufbuild/protobuf', async (importOriginal) => ({ - ...(await importOriginal()), - fromBinary: vi.fn(), - toBinary: vi.fn().mockReturnValue(new Uint8Array()), - hasExtension: vi.fn().mockReturnValue(false), - getExtension: vi.fn(), - setExtension: vi.fn(), -})); - -import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; - -import { ProtobufService, type EventRegistries } from './ProtobufService'; -import type { GameExtensionRegistry } from '../events/game'; -import type { RoomExtensionRegistry } from '../events/room'; -import type { SessionExtensionRegistry } from '../events/session'; - -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>; - processGameEvent(container: unknown, extra?: unknown): void; - processRoomEvent(event: unknown): void; - processSessionEvent(event: unknown): void; - processServerResponse(response: unknown): void; -}; - -let mockSocket: { isOpen: ReturnType; send: ReturnType }; -let gameEvents: GameExtensionRegistry; -let roomEvents: RoomExtensionRegistry; -let sessionEvents: SessionExtensionRegistry; -let registries: EventRegistries; - -const makeService = () => new ProtobufService(mockSocket, registries); - -beforeEach(() => { - mockSocket = { - isOpen: vi.fn().mockReturnValue(true), - send: vi.fn(), - }; - - // Per-test registries inject empty handlers; tests that exercise dispatch - // push their own mock entries. This is what the old `(GameEvents as any).length = 0` - // hack approximated, now expressed as constructor injection. - gameEvents = []; - roomEvents = []; - sessionEvents = []; - registries = { game: gameEvents, room: roomEvents, session: sessionEvents }; -}); - -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>; - - describe('resetCommands', () => { - it('resets cmdId and pendingCommands', () => { - const service = makeService(); - service.sendSessionCommand(sessionExt, vi.fn()); - expect((service as ProtobufInternal).cmdId).toBe(1); - service.resetCommands(); - expect((service as ProtobufInternal).cmdId).toBe(0); - expect((service as ProtobufInternal).pendingCommands).toEqual(new Map()); - }); - }); - - describe('sendCommand', () => { - it('increments cmdId and stores callback', () => { - const service = makeService(); - const cb = vi.fn(); - 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 = makeService(); - mockSocket.isOpen.mockReturnValue(true); - service.sendCommand(create(CommandContainerSchema), vi.fn()); - expect(mockSocket.send).toHaveBeenCalled(); - }); - - it('does not register callback or increment cmdId when transport is closed', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const cb = vi.fn(); - 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); - }); - - it('returns true when command is sent', () => { - const service = makeService(); - const result = service.sendCommand(create(CommandContainerSchema), vi.fn()); - expect(result).toBe(true); - }); - - it('returns false when transport is closed', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const result = service.sendCommand(create(CommandContainerSchema), vi.fn()); - expect(result).toBe(false); - }); - }); - - describe('send*Command when transport is closed', () => { - it('calls onError when sendSessionCommand is dropped', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const onError = vi.fn(); - service.sendSessionCommand(sessionExt, {}, { onError }); - expect(onError).toHaveBeenCalledWith(-1, expect.any(Object)); - }); - - it('calls onError when sendRoomCommand is dropped', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const onError = vi.fn(); - service.sendRoomCommand(42, roomExt, {}, { onError }); - expect(onError).toHaveBeenCalledWith(-1, expect.any(Object)); - }); - - it('calls onError when sendGameCommand is dropped', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const onError = vi.fn(); - service.sendGameCommand(7, gameExt, {}, { onError }); - expect(onError).toHaveBeenCalledWith(-1, expect.any(Object)); - }); - - it('calls onError when sendModeratorCommand is dropped', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const onError = vi.fn(); - service.sendModeratorCommand(moderatorExt, {}, { onError }); - expect(onError).toHaveBeenCalledWith(-1, expect.any(Object)); - }); - - it('calls onError when sendAdminCommand is dropped', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - const onError = vi.fn(); - service.sendAdminCommand(adminExt, {}, { onError }); - expect(onError).toHaveBeenCalledWith(-1, expect.any(Object)); - }); - - it('does not throw when command is dropped with no options', () => { - const service = makeService(); - mockSocket.isOpen.mockReturnValue(false); - expect(() => service.sendSessionCommand(sessionExt, {})).not.toThrow(); - }); - }); - - describe('sendSessionCommand', () => { - it('stores callback and increments cmdId', () => { - const service = makeService(); - 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 = makeService(); - const cb = vi.fn(); - service.sendSessionCommand(sessionExt, {}, { onResponse: cb }); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); - - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); - }); - - it('does not throw when no callback is provided and pending command is triggered', () => { - const service = makeService(); - service.sendSessionCommand(sessionExt, {}); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); - }); - }); - - describe('sendRoomCommand', () => { - it('stores callback and increments cmdId', () => { - const service = makeService(); - 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 = makeService(); - const cb = vi.fn(); - service.sendRoomCommand(42, roomExt, {}, { onResponse: cb }); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); - - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); - }); - - it('does not throw when no callback is provided and pending command is triggered', () => { - const service = makeService(); - service.sendRoomCommand(42, roomExt, {}); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); - }); - }); - - describe('sendGameCommand', () => { - it('stores callback and increments cmdId', () => { - const service = makeService(); - 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 = makeService(); - const cb = vi.fn(); - service.sendGameCommand(7, gameExt, {}, { onResponse: cb }); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); - - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); - }); - - it('does not throw when no callback is provided and pending command is triggered', () => { - const service = makeService(); - service.sendGameCommand(7, gameExt, {}); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); - }); - }); - - describe('sendModeratorCommand', () => { - it('stores callback and increments cmdId', () => { - const service = makeService(); - service.sendModeratorCommand(moderatorExt, {}); - expect((service as ProtobufInternal).cmdId).toBe(1); - }); - - it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = makeService(); - const cb = vi.fn(); - service.sendModeratorCommand(moderatorExt, {}, { onResponse: cb }); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); - - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); - }); - - it('does not throw when no callback is provided and pending command is triggered', () => { - const service = makeService(); - service.sendModeratorCommand(moderatorExt, {}); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); - }); - }); - - describe('sendAdminCommand', () => { - it('stores callback and increments cmdId', () => { - const service = makeService(); - service.sendAdminCommand(adminExt, {}); - expect((service as ProtobufInternal).cmdId).toBe(1); - }); - - it('invokes onResponse with raw response when the pending command is triggered', () => { - const service = makeService(); - const cb = vi.fn(); - service.sendAdminCommand(adminExt, {}, { onResponse: cb }); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - storedCb(create(ResponseSchema)); - - expect(cb).toHaveBeenCalledWith(create(ResponseSchema)); - }); - - it('does not throw when no callback is provided and pending command is triggered', () => { - const service = makeService(); - service.sendAdminCommand(adminExt, {}); - - const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; - expect(() => storedCb(create(ResponseSchema))).not.toThrow(); - }); - }); - - describe('handleMessageEvent', () => { - it('routes RESPONSE message to processServerResponse', () => { - const service = makeService(); - const cb = vi.fn(); - (service as ProtobufInternal).cmdId = 1; - (service as ProtobufInternal).pendingCommands.set(1, cb); - - vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.RESPONSE, - response: create(ResponseSchema, { cmdId: BigInt(1) }), - }) - ); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(cb).toHaveBeenCalledWith(expect.objectContaining({ cmdId: BigInt(1) })); - expect((service as ProtobufInternal).pendingCommands.get(1)).toBeUndefined(); - }); - - it('routes ROOM_EVENT message', () => { - const service = makeService(); - const processRoomEvent = vi.spyOn(service as ProtobufInternal, 'processRoomEvent'); - - vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.ROOM_EVENT, - }) - ); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(processRoomEvent).toHaveBeenCalled(); - }); - - it('routes SESSION_EVENT message', () => { - const service = makeService(); - const processSessionEvent = vi.spyOn(service as ProtobufInternal, 'processSessionEvent'); - - vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.SESSION_EVENT, - }) - ); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(processSessionEvent).toHaveBeenCalled(); - }); - - it('routes GAME_EVENT_CONTAINER message', () => { - const service = makeService(); - const processGameEvent = vi.spyOn(service as ProtobufInternal, 'processGameEvent'); - - vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER, - }) - ); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(processGameEvent).toHaveBeenCalled(); - }); - - it('warns on unknown message types (default case)', () => { - const service = makeService(); - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - vi.mocked(fromBinary).mockReturnValue( - create(ServerMessageSchema, { - messageType: 999, - }) - ); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); - }); - - it('does nothing when decoded message is null', () => { - const service = makeService(); - vi.mocked(fromBinary).mockReturnValue(null!); - expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); - }); - - it('catches and logs decode errors', () => { - const service = makeService(); - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - vi.mocked(fromBinary).mockImplementation(() => { - throw new Error('decode error'); - }); - expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); - }); - }); - - describe('processGameEvent', () => { - it('returns early when container has no eventList', () => { - const service = makeService(); - 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 handler = vi.fn(); - const mockExt = {} as GenExtension; - const payload = { someData: 1 }; - - (gameEvents as any).push([mockExt, handler]); - const service = makeService(); - vi.mocked(hasExtension).mockReturnValue(true); - vi.mocked(getExtension).mockReturnValue(payload); - - (service as ProtobufInternal).processGameEvent({ - gameId: 42, - eventList: [{ playerId: 5 }], - }, {}); - - expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 })); - }); - - it('defaults gameId and playerId to -1 when undefined', () => { - const handler = vi.fn(); - const mockExt = {} as GenExtension; - const payload = { someData: 1 }; - - (gameEvents as any).push([mockExt, handler]); - const service = makeService(); - vi.mocked(hasExtension).mockReturnValue(true); - vi.mocked(getExtension).mockReturnValue(payload); - - (service as ProtobufInternal).processGameEvent({ - gameId: undefined, - eventList: [{ playerId: undefined }], - }); - - expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: -1, playerId: -1 })); - }); - }); - - describe('processServerResponse', () => { - it('returns early when response is undefined', () => { - const service = makeService(); - (service as ProtobufInternal).pendingCommands.set(1, vi.fn()); - (service as ProtobufInternal).processServerResponse(undefined); - expect((service as ProtobufInternal).pendingCommands.size).toBe(1); - }); - }); - - describe('processRoomEvent', () => { - it('returns early when event is undefined', () => { - const service = makeService(); - 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 handler = vi.fn(); - const mockExt = {} as GenExtension; - const payload = { roomData: 1 }; - - (roomEvents as any).push([mockExt, handler]); - const service = makeService(); - vi.mocked(hasExtension).mockReturnValue(true); - vi.mocked(getExtension).mockReturnValue(payload); - - const event = { roomId: 10 }; - (service as ProtobufInternal).processRoomEvent(event); - - expect(handler).toHaveBeenCalledWith(payload, event); - }); - }); - - describe('processSessionEvent', () => { - it('returns early when event is undefined', () => { - const service = makeService(); - 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 handler = vi.fn(); - const mockExt = {} as GenExtension; - const payload = { sessionData: 1 }; - - (sessionEvents as any).push([mockExt, handler]); - const service = makeService(); - vi.mocked(hasExtension).mockReturnValue(true); - vi.mocked(getExtension).mockReturnValue(payload); - - (service as ProtobufInternal).processSessionEvent({ sessionId: 7 }); - - expect(handler).toHaveBeenCalledWith(payload, undefined); - }); - }); - -}); - -describe('ProtobufService protobuf round-trip (real @bufbuild/protobuf)', () => { - it('CommandContainer round-trips cmdId through toBinary → fromBinary', async () => { - const { create, toBinary, fromBinary: realFromBinary } = - await vi.importActual('@bufbuild/protobuf'); - const { CommandContainerSchema } = - await vi.importActual('@app/generated'); - - const original = create(CommandContainerSchema, { cmdId: BigInt(42) }); - const bytes = toBinary(CommandContainerSchema, original); - const decoded = realFromBinary(CommandContainerSchema, bytes); - - expect(decoded.cmdId).toBe(BigInt(42)); - }); - - it('ServerMessage RESPONSE round-trips with cmdId and responseCode', async () => { - const { create, toBinary, fromBinary: realFromBinary } = - await vi.importActual('@bufbuild/protobuf'); - const { ServerMessageSchema, ServerMessage_MessageType, ResponseSchema, Response_ResponseCode } = - await vi.importActual('@app/generated'); - - const response = create(ResponseSchema, { - cmdId: BigInt(7), - responseCode: Response_ResponseCode.RespOk, - }); - const msg = create(ServerMessageSchema, { - messageType: ServerMessage_MessageType.RESPONSE, - response, - }); - - const bytes = toBinary(ServerMessageSchema, msg); - const decoded = realFromBinary(ServerMessageSchema, bytes); - - expect(decoded.messageType).toBe(ServerMessage_MessageType.RESPONSE); - expect(decoded.response?.cmdId).toBe(BigInt(7)); - expect(decoded.response?.responseCode).toBe(Response_ResponseCode.RespOk); - }); - - it('SessionCommand with extension round-trips through CommandContainer', async () => { - const { create, toBinary, fromBinary: realFromBinary, setExtension, getExtension: realGetExtension } = - await vi.importActual('@bufbuild/protobuf'); - const { - CommandContainerSchema, SessionCommandSchema, - Command_Ping_ext, Command_PingSchema, - } = await vi.importActual('@app/generated'); - - const pingCmd = create(Command_PingSchema, {}); - const sesCmd = create(SessionCommandSchema, {}); - setExtension(sesCmd, Command_Ping_ext, pingCmd); - - const container = create(CommandContainerSchema, { - cmdId: BigInt(1), - sessionCommand: [sesCmd], - }); - - const bytes = toBinary(CommandContainerSchema, container); - const decoded = realFromBinary(CommandContainerSchema, bytes); - - expect(decoded.cmdId).toBe(BigInt(1)); - expect(decoded.sessionCommand).toHaveLength(1); - - const decodedPing = realGetExtension(decoded.sessionCommand[0], Command_Ping_ext); - expect(decodedPing).toBeDefined(); - }); -}); diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts deleted file mode 100644 index be5e24fa6..000000000 --- a/webclient/src/websocket/services/ProtobufService.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { create, fromBinary, hasExtension, getExtension, setExtension, toBinary } from '@bufbuild/protobuf'; -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; - -import { - CommandContainerSchema, - GameCommandSchema, - SessionCommandSchema, - RoomCommandSchema, - ModeratorCommandSchema, - AdminCommandSchema, - ServerMessageSchema, - ServerMessage_MessageType, - type Response, - type CommandContainer, - type GameCommand, - type SessionCommand, - type RoomCommand, - type ModeratorCommand, - type AdminCommand, - type ServerMessage, - type GameEventContainer, - type SessionEvent, - type RoomEvent, -} from '@app/generated'; - -import type { GameExtensionRegistry } from '../events/game'; -import type { RoomExtensionRegistry } from '../events/room'; -import type { SessionExtensionRegistry } from '../events/session'; -import type { GameEventMeta } from '../types/WebSocketConfig'; -import { type CommandOptions, handleResponse } from './command-options'; - -export interface SocketTransport { - send(data: Uint8Array): void; - isOpen(): boolean; -} - -export interface EventRegistries { - game: GameExtensionRegistry; - room: RoomExtensionRegistry; - session: SessionExtensionRegistry; -} - -export class ProtobufService { - private cmdId = 0; - private pendingCommands = new Map void>(); - - constructor( - private transport: SocketTransport, - private events: EventRegistries, - ) {} - - public resetCommands() { - this.cmdId = 0; - this.pendingCommands.clear(); - } - - public sendGameCommand( - gameId: number, - ext: GenExtension, - value: V, - options?: CommandOptions - ): void { - const gameCmd = create(GameCommandSchema); - setExtension(gameCmd, ext, value); - const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); - this.dispatchCommand(ext.typeName, cmd, options); - } - - public sendRoomCommand( - roomId: number, - ext: GenExtension, - value: V, - options?: CommandOptions - ): void { - const roomCmd = create(RoomCommandSchema); - setExtension(roomCmd, ext, value); - const cmd = create(CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); - this.dispatchCommand(ext.typeName, cmd, options); - } - - public sendSessionCommand( - ext: GenExtension, - value: V, - options?: CommandOptions - ): void { - const sesCmd = create(SessionCommandSchema); - setExtension(sesCmd, ext, value); - const cmd = create(CommandContainerSchema, { sessionCommand: [sesCmd] }); - this.dispatchCommand(ext.typeName, cmd, options); - } - - public sendModeratorCommand( - ext: GenExtension, - value: V, - options?: CommandOptions - ): void { - const modCmd = create(ModeratorCommandSchema); - setExtension(modCmd, ext, value); - const cmd = create(CommandContainerSchema, { moderatorCommand: [modCmd] }); - this.dispatchCommand(ext.typeName, cmd, options); - } - - public sendAdminCommand( - ext: GenExtension, - value: V, - options?: CommandOptions - ): void { - const adminCmd = create(AdminCommandSchema); - setExtension(adminCmd, ext, value); - const cmd = create(CommandContainerSchema, { adminCommand: [adminCmd] }); - this.dispatchCommand(ext.typeName, cmd, options); - } - - private dispatchCommand(typeName: string, cmd: CommandContainer, options?: CommandOptions): void { - const sent = this.sendCommand(cmd, raw => { - if (options) { - handleResponse(typeName, raw, options); - } - }); - - if (!sent) { - options?.onError?.(-1, {} as Response); - } - } - - public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void): boolean { - if (!this.transport.isOpen()) { - return false; - } - - this.cmdId++; - cmd.cmdId = BigInt(this.cmdId); - this.pendingCommands.set(this.cmdId, callback); - this.transport.send(toBinary(CommandContainerSchema, cmd)); - return true; - } - - public handleMessageEvent({ data }: MessageEvent): void { - try { - const uint8msg = new Uint8Array(data); - const msg: ServerMessage = fromBinary(ServerMessageSchema, uint8msg); - - if (msg) { - switch (msg.messageType) { - case ServerMessage_MessageType.RESPONSE: - this.processServerResponse(msg.response); - break; - case ServerMessage_MessageType.ROOM_EVENT: - this.processRoomEvent(msg.roomEvent); - break; - case ServerMessage_MessageType.SESSION_EVENT: - this.processSessionEvent(msg.sessionEvent); - break; - case ServerMessage_MessageType.GAME_EVENT_CONTAINER: - this.processGameEvent(msg.gameEventContainer); - break; - default: - console.warn('Unknown message type:', msg); - break; - } - } - } catch (err) { - console.error('Processing failed:', err); - } - } - - private processServerResponse(response: Response | undefined) { - if (!response) { - return; - } - const cmdId = Number(response.cmdId); - - if (this.pendingCommands.has(cmdId)) { - this.pendingCommands.get(cmdId)!(response); - this.pendingCommands.delete(cmdId); - } - } - - private processRoomEvent(event: RoomEvent | undefined) { - if (!event) { - return; - } - for (const [ext, handler] of this.events.room) { - if (hasExtension(event, ext)) { - handler(getExtension(event, ext), event); - return; - } - } - } - - private processSessionEvent(event: SessionEvent | undefined) { - if (!event) { - return; - } - for (const [ext, handler] of this.events.session) { - if (hasExtension(event, ext)) { - handler(getExtension(event, ext), undefined); - return; - } - } - } - - private processGameEvent(container: GameEventContainer | undefined): void { - if (!container?.eventList?.length) { - return; - } - - const { gameId, context, secondsElapsed, forcedByJudge } = container; - - for (const event of container.eventList) { - const meta: GameEventMeta = { - gameId: gameId ?? -1, - playerId: event.playerId ?? -1, - context, - secondsElapsed: secondsElapsed ?? 0, - forcedByJudge: forcedByJudge ?? 0, - }; - - for (const [ext, handler] of this.events.game) { - if (hasExtension(event, ext)) { - handler(getExtension(event, ext), meta); - break; - } - } - } - } -} diff --git a/webclient/src/websocket/services/WebSocketService.spec.ts b/webclient/src/websocket/services/WebSocketService.spec.ts deleted file mode 100644 index 17f92f0fb..000000000 --- a/webclient/src/websocket/services/WebSocketService.spec.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { installMockWebSocket } from '../__mocks__/helpers'; -import { withMockLocation } from '../../__test-utils__'; -import { Mock } from 'vitest'; - -vi.mock('../config', () => ({ - CLIENT_OPTIONS: { keepalive: 1000 }, -})); - -import { WebSocketService } from './WebSocketService'; -import type { WebSocketServiceConfig, ReconnectConfig } from './WebSocketService'; -import { KeepAliveService } from './KeepAliveService'; -import { StatusEnum } from '../types/StatusEnum'; - -type WebSocketInternal = WebSocketService & { - keepAliveService: KeepAliveService; -}; - -let MockWS: Mock; -let mockInstance: ReturnType['mockInstance']; -let restoreWebSocket: ReturnType['restore']; -let mockConfig: WebSocketServiceConfig; -let mockOnConnectionFailed: Mock; -let mockOnStatusChange: Mock; -let locationRestores: Array<() => void>; - -beforeEach(() => { - vi.useFakeTimers(); - - const installed = installMockWebSocket(); - MockWS = installed.MockWS; - mockInstance = installed.mockInstance; - restoreWebSocket = installed.restore; - - mockOnConnectionFailed = vi.fn(); - mockOnStatusChange = vi.fn(); - - mockConfig = { - keepAliveFn: vi.fn(), - onConnectionFailed: mockOnConnectionFailed, - onStatusChange: mockOnStatusChange, - }; - - locationRestores = []; -}); - -afterEach(() => { - while (locationRestores.length > 0) { - locationRestores.pop()!(); - } - restoreWebSocket(); - vi.useRealTimers(); -}); - -describe('WebSocketService', () => { - function createConnectedService() { - const service = new WebSocketService(mockConfig); - service.connect({ host: 'h', port: '1' }, 'ws'); - return service; - } - - describe('constructor', () => { - it('subscribes disconnected$ from KeepAliveService', () => { - const service = new WebSocketService(mockConfig); - expect(service).toBeDefined(); - }); - - it('calls disconnect and updateStatus when keepAlive disconnected$ fires', () => { - const service = new WebSocketService(mockConfig); - service.connect({ host: 'localhost', port: '8080' }, 'ws'); - // trigger keepAliveService.disconnected$ - (service as WebSocketInternal).keepAliveService.disconnected$.next(); - expect(mockInstance.close).toHaveBeenCalled(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection timeout'); - }); - }); - - describe('connect', () => { - it('creates a WebSocket with wss protocol by default', () => { - const service = new WebSocketService(mockConfig); - locationRestores.push(withMockLocation({ hostname: 'example.com' })); - service.connect({ host: 'example.com', port: '8080' }); - expect(MockWS).toHaveBeenCalledWith('wss://example.com:8080'); - }); - - it('switches to ws protocol when hostname is localhost', () => { - const service = new WebSocketService(mockConfig); - locationRestores.push(withMockLocation({ hostname: 'localhost' })); - service.connect({ host: 'somehost', port: '1234' }); - expect(MockWS).toHaveBeenCalledWith('ws://somehost:1234'); - }); - - it('sets binaryType to arraybuffer', () => { - createConnectedService(); - expect(mockInstance.binaryType).toBe('arraybuffer'); - }); - - it('fires socket.close after keepalive timeout', () => { - createConnectedService(); - vi.advanceTimersByTime(1000); - expect(mockInstance.close).toHaveBeenCalled(); - }); - }); - - describe('socket event handlers (onopen)', () => { - it('clears the connection timeout when socket opens', () => { - const clearSpy = vi.spyOn(globalThis, 'clearTimeout'); - createConnectedService(); - mockInstance.onopen(); - expect(clearSpy).toHaveBeenCalled(); - }); - - it('calls onStatusChange CONNECTED on open', () => { - createConnectedService(); - mockInstance.onopen(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); - }); - - it('starts the ping loop with the keepalive interval', () => { - const service = new WebSocketService(mockConfig); - const startSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'startPingLoop'); - service.connect({ host: 'h', port: '1' }, 'ws'); - mockInstance.onopen(); - expect(startSpy).toHaveBeenCalledWith(1000, expect.any(Function)); - }); - - it('ping loop callback calls keepAliveFn', () => { - const service = new WebSocketService(mockConfig); - const startSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'startPingLoop'); - service.connect({ host: 'h', port: '1' }, 'ws'); - mockInstance.onopen(); - const pingCb = startSpy.mock.calls[0][1] as (done: Function) => void; - const done = vi.fn(); - pingCb(done); - expect(mockConfig.keepAliveFn).toHaveBeenCalledWith(done); - }); - }); - - describe('socket event handlers (onclose)', () => { - it('calls onStatusChange DISCONNECTED on close when not already DISCONNECTED', () => { - createConnectedService(); - mockInstance.onclose(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); - }); - - it('does not overwrite status if already DISCONNECTED', () => { - createConnectedService(); - mockInstance.onerror(); - mockInstance.onclose(); - expect(mockOnStatusChange).not.toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); - }); - - it('ends the ping loop on close', () => { - const service = new WebSocketService(mockConfig); - const endSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'endPingLoop'); - service.connect({ host: 'h', port: '1' }, 'ws'); - mockInstance.onclose(); - expect(endSpy).toHaveBeenCalled(); - }); - }); - - describe('socket event handlers (onerror)', () => { - it('calls onStatusChange DISCONNECTED on error', () => { - createConnectedService(); - mockInstance.onerror(); - expect(mockOnStatusChange).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Failed'); - }); - - it('calls onConnectionFailed on error', () => { - createConnectedService(); - mockInstance.onerror(); - expect(mockOnConnectionFailed).toHaveBeenCalled(); - }); - }); - - describe('socket event handlers (onmessage)', () => { - it('emits on message$ subject', () => { - const service = createConnectedService(); - const handler = vi.fn(); - service.message$.subscribe(handler); - const event = { data: new ArrayBuffer(4) } as MessageEvent; - mockInstance.onmessage(event); - expect(handler).toHaveBeenCalledWith(event); - }); - }); - - describe('disconnect', () => { - it('closes the socket', () => { - const service = createConnectedService(); - service.disconnect(); - expect(mockInstance.close).toHaveBeenCalled(); - }); - }); - - describe('send', () => { - it('delegates to socket.send', () => { - const service = createConnectedService(); - const data = new Uint8Array([1, 2, 3]); - service.send(data); - expect(mockInstance.send).toHaveBeenCalledWith(data); - }); - - it('does not throw when socket is undefined (before connect)', () => { - const service = new WebSocketService(mockConfig); - const data = new Uint8Array([1, 2, 3]); - expect(() => service.send(data)).not.toThrow(); - }); - - it('skips send when readyState is not OPEN', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const service = createConnectedService(); - // CONNECTING - mockInstance.readyState = 0; - const data = new Uint8Array([1, 2, 3]); - service.send(data); - expect(mockInstance.send).not.toHaveBeenCalled(); - expect(warnSpy).toHaveBeenCalledWith( - '[WebSocketService] send() skipped: socket not OPEN', - 0, - ); - warnSpy.mockRestore(); - }); - }); - - describe('checkReadyState', () => { - it('returns true when readyState matches', () => { - const service = createConnectedService(); - mockInstance.readyState = WebSocket.OPEN; - expect(service.checkReadyState(WebSocket.OPEN)).toBe(true); - }); - - it('returns false when readyState does not match', () => { - const service = createConnectedService(); - // CLOSED - mockInstance.readyState = 3; - expect(service.checkReadyState(WebSocket.OPEN)).toBe(false); - }); - - it('returns false when socket is null', () => { - const service = new WebSocketService(mockConfig); - // no connect called, socket is undefined - expect(service.checkReadyState(WebSocket.OPEN)).toBe(false); - }); - }); - - describe('connect (re-entry)', () => { - it('closes the prior socket when connect is called twice', () => { - const service = new WebSocketService(mockConfig); - service.connect({ host: 'h', port: '1' }, 'ws'); - const firstInstance = mockInstance; - service.connect({ host: 'h', port: '2' }, 'ws'); - expect(firstInstance.close).toHaveBeenCalled(); - }); - }); - - describe('reconnect', () => { - const reconnect: ReconnectConfig = { - maxAttempts: 3, - baseDelayMs: 100, - maxDelayMs: 1000, - }; - - function createReconnectService() { - const service = new WebSocketService({ ...mockConfig, reconnect }); - service.connect({ host: 'h', port: '1' }, 'ws'); - mockInstance.onopen(); - return service; - } - - it('emits RECONNECTING (not DISCONNECTED) on unexpected close when configured', () => { - createReconnectService(); - mockOnStatusChange.mockClear(); - mockInstance.onclose(); - const statuses = mockOnStatusChange.mock.calls.map(c => c[0]); - expect(statuses).toContain(StatusEnum.RECONNECTING); - expect(statuses).not.toContain(StatusEnum.DISCONNECTED); - }); - - it('does not reconnect after explicit disconnect()', () => { - const service = createReconnectService(); - mockOnStatusChange.mockClear(); - service.disconnect(); - // onclose fires with `intentionalDisconnect` already set; reconnect is - // suppressed and the status settles on DISCONNECTED. - mockInstance.onclose(); - const statuses = mockOnStatusChange.mock.calls.map(c => c[0]); - expect(statuses).not.toContain(StatusEnum.RECONNECTING); - expect(statuses).toContain(StatusEnum.DISCONNECTED); - }); - - it('gives up after maxAttempts and emits DISCONNECTED', () => { - const { instances } = installMockWebSocket(); - const service = new WebSocketService({ ...mockConfig, reconnect }); - service.connect({ host: 'h', port: '1' }, 'ws'); - // Flip hasEverOpened so reconnect is eligible, then drop. - instances[0].onopen(); - mockOnStatusChange.mockClear(); - - // Walk through maxAttempts failed reconnects (each socket never opens, - // so the attempt counter keeps climbing). One final close after the - // counter hits max emits DISCONNECTED. - for (let i = 0; i <= reconnect.maxAttempts; i += 1) { - instances[i].onclose(); - vi.advanceTimersByTime(reconnect.maxDelayMs); - } - - const statuses = mockOnStatusChange.mock.calls.map(c => c[0]); - expect(statuses).toContain(StatusEnum.DISCONNECTED); - void service; - }); - - it('resets attempt counter on successful open', () => { - createReconnectService(); - mockOnStatusChange.mockClear(); - // simulate a drop, advance timer to trigger reconnect attempt - mockInstance.onclose(); - vi.advanceTimersByTime(reconnect.maxDelayMs); - // manually open the new socket (simulated) - mockInstance.onopen(); - mockOnStatusChange.mockClear(); - // another drop — the description should start the attempt counter from 1 again - mockInstance.onclose(); - const firstReconnect = mockOnStatusChange.mock.calls.find(c => c[0] === StatusEnum.RECONNECTING); - expect(firstReconnect?.[1]).toMatch(/attempt 1\//); - }); - }); - -}); diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts deleted file mode 100644 index 585abc7ca..000000000 --- a/webclient/src/websocket/services/WebSocketService.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Observable, Subject } from 'rxjs'; - -import { StatusEnum } from '../types/StatusEnum'; -import { KeepAliveService } from './KeepAliveService'; -import { CLIENT_OPTIONS } from '../config'; -import type { ConnectTarget } from '../types/WebClientConfig'; -import { buildWebSocketUrl } from '../utils/buildWebSocketUrl'; - -export interface ReconnectConfig { - maxAttempts: number; - baseDelayMs: number; - maxDelayMs: number; -} - -export interface WebSocketServiceConfig { - keepAliveFn: (pingReceived: () => void) => void; - onStatusChange: (status: StatusEnum, description: string) => void; - onConnectionFailed: () => void; - /** Opt-in automatic reconnect on unexpected socket close. */ - reconnect?: ReconnectConfig; -} - -export class WebSocketService { - private socket: WebSocket | null = null; - - private config: WebSocketServiceConfig; - private keepAliveService: KeepAliveService; - private hasReportedError = false; - - private readonly messageSubject = new Subject(); - public readonly message$: Observable = this.messageSubject.asObservable(); - - private keepalive: number = CLIENT_OPTIONS.keepalive; - - private lastTarget: ConnectTarget | null = null; - private lastProtocol: string | null = null; - - private intentionalDisconnect = false; - /** - * True while `connect()` is cycling a prior socket out to bring a fresh one up. - * Suppresses the `DISCONNECTED` status emission the orphan socket's onclose - * would otherwise fire — that would clobber the `connectionAttempted()` we - * just dispatched at the WebClient layer. - */ - private retiringForReconnect = false; - private reconnectAttempts = 0; - private reconnectTimer: ReturnType | null = null; - /** Flips true on the first successful `onopen`. Gates reconnect — we never retry a connection that never established. */ - private hasEverOpened = false; - - constructor(config: WebSocketServiceConfig) { - this.config = config; - - this.keepAliveService = new KeepAliveService(() => this.checkReadyState(WebSocket.OPEN)); - this.keepAliveService.disconnected$.subscribe(() => { - this.disconnect(); - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection timeout'); - }); - } - - public connect(target: ConnectTarget, protocol: string = 'wss'): void { - if (window.location.hostname === 'localhost') { - protocol = 'ws'; - } - - // Retire any prior socket cleanly. The `retiringForReconnect` flag both - // suppresses reconnect scheduling for the orphan socket AND suppresses the - // DISCONNECTED status emission that would otherwise reset - // `connectionAttemptMade` set by the caller a moment ago. - this.retiringForReconnect = true; - this.clearReconnectTimer(); - this.closeActiveSocket(); - this.retiringForReconnect = false; - - this.lastTarget = target; - this.lastProtocol = protocol; - this.intentionalDisconnect = false; - this.reconnectAttempts = 0; - this.hasEverOpened = false; - this.keepalive = CLIENT_OPTIONS.keepalive; - - const { host, port } = target; - this.socket = this.createWebSocket(buildWebSocketUrl(protocol as 'ws' | 'wss', host, port)); - } - - public disconnect(): void { - this.intentionalDisconnect = true; - this.clearReconnectTimer(); - this.closeActiveSocket(); - } - - public checkReadyState(state: number): boolean { - return this.socket?.readyState === state; - } - - public send(message: Uint8Array): void { - if (!this.socket) { - return; - } - if (this.socket.readyState !== WebSocket.OPEN) { - // Match desktop's TCP-queued semantics conservatively: drop with a warn rather - // than throw. Upstream code treats send as fire-and-forget under these states. - console.warn('[WebSocketService] send() skipped: socket not OPEN', this.socket.readyState); - return; - } - this.socket.send(message as BufferSource); - } - - private createWebSocket(url: string): WebSocket { - const socket = new WebSocket(url); - socket.binaryType = 'arraybuffer'; - - const connectionTimer = setTimeout(() => socket.close(), this.keepalive); - const clearConnectionTimer = (): void => clearTimeout(connectionTimer); - - socket.onopen = () => { - this.hasEverOpened = true; - clearConnectionTimer(); - this.hasReportedError = false; - this.reconnectAttempts = 0; - this.config.onStatusChange(StatusEnum.CONNECTED, 'Connected'); - - this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: () => void) => { - this.config.keepAliveFn(pingReceived); - }); - }; - - socket.onclose = () => { - clearConnectionTimer(); - this.keepAliveService.endPingLoop(); - - if (this.shouldAttemptReconnect()) { - this.scheduleReconnect(); - return; - } - - // Orphan socket retired by a fresh connect() call — the new socket is - // already being wired up; don't fire a DISCONNECTED status that would - // race the just-dispatched connectionAttempted. - if (this.retiringForReconnect) { - this.hasReportedError = false; - return; - } - - // @critical onerror + onclose both fire on failed connects; don't overwrite the richer error status. - // See .github/instructions/webclient.instructions.md#websocket-lifecycle. - if (!this.hasReportedError) { - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Closed'); - } - this.hasReportedError = false; - }; - - socket.onerror = () => { - clearConnectionTimer(); - this.hasReportedError = true; - this.config.onStatusChange(StatusEnum.DISCONNECTED, 'Connection Failed'); - this.config.onConnectionFailed(); - }; - - socket.onmessage = (event: MessageEvent) => { - this.messageSubject.next(event); - }; - - return socket; - } - - private shouldAttemptReconnect(): boolean { - if (this.intentionalDisconnect) { - return false; - } - if (this.retiringForReconnect) { - return false; - } - // Suppress reconnect when the connection never established — onerror already - // reported DISCONNECTED and we don't want to thrash against a dead endpoint. - if (this.hasReportedError) { - return false; - } - // Only retry once we have proof the endpoint was reachable at least once. - // The initial connect path falls through to DISCONNECTED on failure. - if (!this.hasEverOpened) { - return false; - } - const cfg = this.config.reconnect; - if (!cfg || cfg.maxAttempts <= 0) { - return false; - } - return this.reconnectAttempts < cfg.maxAttempts; - } - - private scheduleReconnect(): void { - const cfg = this.config.reconnect!; - const attempt = this.reconnectAttempts; - const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt), cfg.maxDelayMs); - this.reconnectAttempts += 1; - - this.config.onStatusChange( - StatusEnum.RECONNECTING, - `Reconnecting (attempt ${this.reconnectAttempts}/${cfg.maxAttempts})`, - ); - - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null; - if (this.intentionalDisconnect || !this.lastTarget || !this.lastProtocol) { - return; - } - const { host, port } = this.lastTarget; - this.socket = this.createWebSocket( - buildWebSocketUrl(this.lastProtocol as 'ws' | 'wss', host, port), - ); - }, delay); - } - - private clearReconnectTimer(): void { - if (this.reconnectTimer !== null) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - } - - private closeActiveSocket(): void { - if (this.socket) { - this.socket.close(); - this.socket = null; - } - } - -} diff --git a/webclient/src/websocket/services/command-options.spec.ts b/webclient/src/websocket/services/command-options.spec.ts deleted file mode 100644 index a898dfb5e..000000000 --- a/webclient/src/websocket/services/command-options.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -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() }; -}); - -import { create, getExtension } from '@bufbuild/protobuf'; - -import { handleResponse } from './command-options'; - -describe('handleResponse', () => { - it('calls onResponse and returns early when provided', () => { - const onResponse = vi.fn(); - const onSuccess = vi.fn(); - handleResponse('test', create(ResponseSchema, { responseCode: 99 }), { onResponse, onSuccess }); - expect(onResponse).toHaveBeenCalled(); - expect(onSuccess).not.toHaveBeenCalled(); - }); - - it('calls onSuccess when responseCode is RespOk and no responseExt', () => { - const onSuccess = vi.fn(); - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespOk }); - handleResponse('test', raw, { onSuccess }); - expect(onSuccess).toHaveBeenCalledWith(); - }); - - it('calls onSuccess with nested response when responseExt is set', () => { - vi.mocked(getExtension).mockReturnValue({ nested: true }); - const onSuccess = vi.fn(); - const fakeExt = {} as unknown as GenExtension; - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespOk }); - handleResponse('test', raw, { onSuccess, responseExt: fakeExt }); - expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw); - }); - - it('calls onResponseCode handler when code matches', () => { - const specificHandler = vi.fn(); - handleResponse('test', create(ResponseSchema, { responseCode: 5 }), { onResponseCode: { 5: specificHandler } }); - expect(specificHandler).toHaveBeenCalled(); - }); - - it('calls onError when responseCode is not RespOk and no specific handler', () => { - const onError = vi.fn(); - const raw = create(ResponseSchema, { responseCode: 99 }); - handleResponse('test', raw, { onError }); - expect(onError).toHaveBeenCalledWith(99, raw); - }); - - it('logs error to console when no callbacks for non-RespOk response', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - handleResponse('test.Type', create(ResponseSchema, { responseCode: 42 }), {}); - expect(consoleSpy).toHaveBeenCalled(); - consoleSpy.mockRestore(); - }); -}); diff --git a/webclient/src/websocket/services/command-options.ts b/webclient/src/websocket/services/command-options.ts deleted file mode 100644 index 6d1138886..000000000 --- a/webclient/src/websocket/services/command-options.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getExtension } from '@bufbuild/protobuf'; -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Response_ResponseCode, type Response } from '@app/generated'; - -interface CommandOptionsBase { - 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: Response) => void; -} - -export interface CommandOptionsWithoutResponse extends CommandOptionsBase { - responseExt?: undefined; - onSuccess?: () => void; -} - -export type CommandOptions = CommandOptionsWithResponse | CommandOptionsWithoutResponse; - -export function hasResponseExt(options: CommandOptions): options is CommandOptionsWithResponse { - return options.responseExt !== undefined; -} - -export function handleResponse(typeName: string, raw: Response, options: CommandOptions): void { - if (options.onResponse) { - options.onResponse(raw); - return; - } - - const { responseCode } = raw; - - if (responseCode === Response_ResponseCode.RespOk) { - if (hasResponseExt(options)) { - options.onSuccess?.(getExtension(raw, options.responseExt), raw); - } else { - options.onSuccess?.(); - } - return; - } - - if (options.onResponseCode?.[responseCode]) { - options.onResponseCode[responseCode](raw); - return; - } - - if (options.onError) { - options.onError(responseCode, raw); - } else { - console.error(`${typeName} failed with response code: ${responseCode}`); - } -} diff --git a/webclient/src/websocket/types/ConnectOptions.ts b/webclient/src/websocket/types/ConnectOptions.ts deleted file mode 100644 index 5929363b4..000000000 --- a/webclient/src/websocket/types/ConnectOptions.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { ConnectTarget } from './WebClientConfig'; - -export enum WebSocketConnectReason { - LOGIN, - REGISTER, - ACTIVATE_ACCOUNT, - PASSWORD_RESET_REQUEST, - PASSWORD_RESET_CHALLENGE, - PASSWORD_RESET, - TEST_CONNECTION, -} - -interface ConnectTransport extends ConnectTarget { - keepalive?: number; - autojoinrooms?: boolean; - clientid?: string; -} - -export interface LoginConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.LOGIN; - userName: string; - password?: string; - hashedPassword?: string; -} - -export interface RegisterConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.REGISTER; - userName: string; - password: string; - email: string; - country: string; - realName: string; -} - -export interface ActivateConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.ACTIVATE_ACCOUNT; - userName: string; - token: string; - /** Plaintext password carried through so post-activation auto-login can hash it. */ - password?: string; -} - -export interface PasswordResetRequestConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST; - userName: string; -} - -export interface PasswordResetChallengeConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE; - userName: string; - email: string; -} - -export interface PasswordResetConnectOptions extends ConnectTransport { - reason: WebSocketConnectReason.PASSWORD_RESET; - userName: string; - token: string; - newPassword: string; -} - -/** - * Test connection has no proto command -- it just opens and closes a socket to - * verify reachability. - */ -export interface TestConnectionOptions extends ConnectTransport { - reason: WebSocketConnectReason.TEST_CONNECTION; -} - -export type WebSocketConnectOptions = - | LoginConnectOptions - | RegisterConnectOptions - | ActivateConnectOptions - | PasswordResetRequestConnectOptions - | PasswordResetChallengeConnectOptions - | PasswordResetConnectOptions - | TestConnectionOptions; diff --git a/webclient/src/websocket/types/SignalContexts.ts b/webclient/src/websocket/types/SignalContexts.ts deleted file mode 100644 index e66b659a2..000000000 --- a/webclient/src/websocket/types/SignalContexts.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the - * activation dialog can resubmit against the same host/user without re-entering them. - */ -export interface PendingActivationContext { - host: string; - port: string; - userName: string; -} - -/** - * Payload for the LOGIN_SUCCESSFUL signal. Only carries what the UI needs to - * persist into the selected host record (hashedPassword for "remember me"). - */ -export interface LoginSuccessContext { - hashedPassword?: string; -} diff --git a/webclient/src/websocket/types/StatusEnum.ts b/webclient/src/websocket/types/StatusEnum.ts deleted file mode 100644 index 1ae319a27..000000000 --- a/webclient/src/websocket/types/StatusEnum.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum StatusEnum { - DISCONNECTED, - CONNECTING, - CONNECTED, - LOGGING_IN, - LOGGED_IN, - /** Separated from sequential states to reserve room for future connection phases. */ - RECONNECTING = 50, - /** High sentinel value — marks the terminal "tearing down" state that must not collide with future states added above. */ - DISCONNECTING = 99, -} diff --git a/webclient/src/websocket/types/WebClientConfig.ts b/webclient/src/websocket/types/WebClientConfig.ts deleted file mode 100644 index 6ff860009..000000000 --- a/webclient/src/websocket/types/WebClientConfig.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - RegistryEntry, - SessionEvent, - RoomEvent, - GameEvent, -} from '@app/generated'; - -import type { GameEventMeta } from './WebSocketConfig'; -import type { IWebClientResponse } from '.'; - -export interface ConnectTarget { - host: string; - port: string; -} - -export interface WebClientConfig { - response: IWebClientResponse; - - sessionEvents: RegistryEntry[]; - roomEvents: RegistryEntry[]; - gameEvents: RegistryEntry[]; - - keepAliveFn(pingReceived: () => void): void; -} diff --git a/webclient/src/websocket/types/WebClientRequest.ts b/webclient/src/websocket/types/WebClientRequest.ts deleted file mode 100644 index 533346f97..000000000 --- a/webclient/src/websocket/types/WebClientRequest.ts +++ /dev/null @@ -1,167 +0,0 @@ -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, - CreateGameParams, - JoinGameParams, -} from '@app/generated'; - -import type { ConnectTarget } from './WebClientConfig'; -import type { KeyOf } from './WebSocketConfig'; - -// 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; -} - -export interface ISessionRequest { - addToBuddyList(userName: string): void; - removeFromBuddyList(userName: string): void; - addToIgnoreList(userName: string): void; - removeFromIgnoreList(userName: string): void; - changeAccountPassword(oldPassword: string, newPassword: string, hashedNewPassword?: string): void; - changeAccountDetails(passwordCheck: string, realName?: string, email?: string, country?: string): void; - changeAccountImage(image: Uint8Array): void; - sendDirectMessage(userName: string, message: string): void; - getUserInfo(userName: string): void; - getUserGames(userName: string): void; - deckDownload(deckId: number): void; - replayDownload(replayId: number): void; -} - -export interface IRoomsRequest { - joinRoom(roomId: number): void; - leaveRoom(roomId: number): void; - roomSay(roomId: number, message: string): void; - createGame(roomId: number, params: CreateGameParams): void; - joinGame(roomId: number, params: JoinGameParams): void; -} - -export interface IAdminRequest { - adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void; - reloadConfig(): void; - shutdownServer(reason: string, minutes: number): void; - updateServerMessage(): void; -} - -export interface IModeratorRequest { - banFromServer( - minutes: number, - userName?: string, - address?: string, - reason?: string, - visibleReason?: string, - clientid?: string, - removeMessages?: number - ): void; - forceActivateUser(usernameToActivate: string, moderatorName: string): void; - getAdminNotes(userName: string): void; - getBanHistory(userName: string): void; - getWarnHistory(userName: string): void; - getWarnList(modName: string, userName: string, userClientid: string): void; - grantReplayAccess(replayId: number, moderatorName: string): void; - updateAdminNotes(userName: string, notes: string): 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: 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: GameCommand): void; - nextTurn(gameId: number): void; - setActivePhase(gameId: number, params: SetActivePhaseParams): void; - reverseTurn(gameId: number): void; - moveCard(gameId: number, params: MoveCardParams): void; - flipCard(gameId: number, params: FlipCardParams): void; - attachCard(gameId: number, params: AttachCardParams): void; - createToken(gameId: number, params: CreateTokenParams): void; - setCardAttr(gameId: number, params: SetCardAttrParams): void; - setCardCounter(gameId: number, params: SetCardCounterParams): void; - incCardCounter(gameId: number, params: IncCardCounterParams): void; - drawCards(gameId: number, params: DrawCardsParams): void; - undoDraw(gameId: number): void; - createArrow(gameId: number, params: CreateArrowParams): void; - deleteArrow(gameId: number, params: DeleteArrowParams): void; - createCounter(gameId: number, params: CreateCounterParams): void; - setCounter(gameId: number, params: SetCounterParams): void; - incCounter(gameId: number, params: IncCounterParams): void; - delCounter(gameId: number, params: DelCounterParams): void; - shuffle(gameId: number, params: ShuffleParams): void; - dumpZone(gameId: number, params: DumpZoneParams): void; - revealCards(gameId: number, params: RevealCardsParams): void; - changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void; - deckSelect(gameId: number, params: DeckSelectParams): void; - setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void; - setSideboardLock(gameId: number, params: SetSideboardLockParams): void; - mulligan(gameId: number, params: MulliganParams): void; - rollDie(gameId: number, params: RollDieParams): void; -} - -export interface IWebClientRequest< - A extends AuthRequestMap = AuthRequestMap, -> { - authentication: IAuthenticationRequest; - session: ISessionRequest; - rooms: IRoomsRequest; - game: IGameRequest; - admin: IAdminRequest; - moderator: IModeratorRequest; -} diff --git a/webclient/src/websocket/types/WebClientResponse.ts b/webclient/src/websocket/types/WebClientResponse.ts deleted file mode 100644 index 57638ba8d..000000000 --- a/webclient/src/websocket/types/WebClientResponse.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { - Response_GetGamesOfUser, - Response_DeckList, - Response_DeckDownload, - Response_ReplayDownload, - Response_WarnList, - Event_RoomSay, - Event_GameJoined, - Event_GameStateChanged, - Event_MoveCard, - Event_FlipCard, - Event_DestroyCard, - Event_AttachCard, - Event_CreateToken, - Event_SetCardAttr, - Event_SetCardCounter, - Event_CreateArrow, - Event_DeleteArrow, - Event_CreateCounter, - Event_SetCounter, - Event_DelCounter, - Event_DrawCards, - Event_RevealCards, - Event_Shuffle, - Event_RollDie, - Event_DumpZone, - Event_ChangeZoneProperties, - Event_NotifyUser, - Event_PlayerPropertiesChanged, - Event_ServerShutdown, - Event_UserMessage, - RoomEventMap, - ServerInfo_User, - ServerInfo_Room, - ServerInfo_Game, - ServerInfo_PlayerProperties, - ServerInfo_Ban, - ServerInfo_ChatMessage, - ServerInfo_Warning, - ServerInfo_DeckStorage_TreeItem, - ServerInfo_ReplayMatch, -} from '@app/generated'; - -import type { StatusEnum } from './StatusEnum'; -import type { LoginSuccessContext, PendingActivationContext } from './SignalContexts'; -import type { - KeyOf, - WebSocketRoomResponseOverrides, -} from './WebSocketConfig'; - -export interface ISessionResponse { - initialized(): void; - connectionAttempted(): void; - clearStore(): void; - loginSuccessful(options: LoginSuccessContext): void; - loginFailed(): void; - connectionFailed(): void; - testConnectionSuccessful(supportsHashedPassword: boolean): void; - testConnectionFailed(): void; - updateBuddyList(buddyList: ServerInfo_User[]): void; - addToBuddyList(user: ServerInfo_User): void; - removeFromBuddyList(userName: string): void; - updateIgnoreList(ignoreList: ServerInfo_User[]): void; - addToIgnoreList(user: ServerInfo_User): void; - removeFromIgnoreList(userName: string): void; - updateInfo(name: string, version: string): void; - updateStatus(state: StatusEnum, description: string): void; - updateUser(user: ServerInfo_User): void; - updateUsers(users: ServerInfo_User[]): void; - userJoined(user: ServerInfo_User): void; - userLeft(userName: string): void; - serverMessage(message: string): void; - accountAwaitingActivation(options: PendingActivationContext): void; - accountActivationSuccess(): void; - accountActivationFailed(): void; - registrationRequiresEmail(): void; - registrationSuccess(): void; - registrationFailed(reason: string, endTime?: number): void; - registrationEmailError(error: string): void; - registrationPasswordError(error: string): void; - registrationUserNameError(error: string): void; - resetPasswordChallenge(): void; - resetPassword(): void; - resetPasswordSuccess(): void; - resetPasswordFailed(): void; - accountPasswordChange(): void; - accountEditChanged(realName?: string, email?: string, country?: string): void; - accountImageChanged(avatarBmp: Uint8Array): 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: 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: ServerInfo_ReplayMatch[]): void; - replayAdded(matchInfo: ServerInfo_ReplayMatch): void; - replayModifyMatch(gameId: number, doNotHide: boolean): void; - replayDeleteMatch(gameId: number): void; - replayDownloaded(replayId: number, response: Response_ReplayDownload): void; -} - -export interface IRoomResponse { - clearStore(): void; - joinRoom(roomInfo: ServerInfo_Room): void; - leaveRoom(roomId: number): void; - updateRooms(rooms: ServerInfo_Room[]): void; - updateGames(roomId: number, gameList: ServerInfo_Game[]): void; - addMessage(roomId: number, message: T[KeyOf]): void; - userJoined(roomId: number, user: ServerInfo_User): void; - userLeft(roomId: number, name: string): void; - removeMessages(roomId: number, name: string, amount: number): void; - gameCreated(roomId: number): void; - joinedGame(roomId: number, gameId: number): void; - setJoinGamePending(pending: boolean): void; - setJoinGameError(code: number, message: string): void; -} - -export interface IGameResponse { - clearStore(): 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: ServerInfo_PlayerProperties): void; - gameClosed(gameId: number): void; - gameHostChanged(gameId: number, hostId: number): void; - kicked(gameId: number): void; - gameSay(gameId: number, playerId: number, message: string, timeReceived: number): void; - cardMoved(gameId: number, playerId: number, data: Event_MoveCard): void; - cardFlipped(gameId: number, playerId: number, data: Event_FlipCard): void; - cardDestroyed(gameId: number, playerId: number, data: Event_DestroyCard): void; - 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: Event_DumpZone): void; - zonePropertiesChanged(gameId: number, playerId: number, data: Event_ChangeZoneProperties): void; -} - -export interface IAdminResponse { - adjustMod(userName: string, shouldBeMod: boolean, shouldBeJudge: boolean): void; - reloadConfig(): void; - shutdownServer(): void; - updateServerMessage(): void; -} - -export interface IModeratorResponse { - banFromServer(userName: string): 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; - getAdminNotes(userName: string, notes: string): void; - updateAdminNotes(userName: string, notes: string): void; -} - -export interface IWebClientResponse< - R extends RoomEventMap = WebSocketRoomResponseOverrides, -> { - session: ISessionResponse; - room: IRoomResponse; - game: IGameResponse; - admin: IAdminResponse; - moderator: IModeratorResponse; -} diff --git a/webclient/src/websocket/types/WebSocketConfig.ts b/webclient/src/websocket/types/WebSocketConfig.ts deleted file mode 100644 index bf3240dab..000000000 --- a/webclient/src/websocket/types/WebSocketConfig.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { - GameEventContext, - Event_RoomSay, - RoomEventMap, -} from '@app/generated'; - -// `KeyOf` resolves to `'Response_Login'`. -export type KeyOf = { [K in keyof Map]: Map[K] extends V ? K : never }[keyof Map]; - -export interface GameEventMeta { - gameId: number; - playerId: number; - context: GameEventContext | null; - secondsElapsed: number; - forcedByJudge: number; -} - -export interface WebSocketRoomResponseOverrides extends RoomEventMap { - Event_RoomSay: Event_RoomSay & { timeReceived: number }; -} diff --git a/webclient/src/websocket/types/index.ts b/webclient/src/websocket/types/index.ts deleted file mode 100644 index 3e3c127cc..000000000 --- a/webclient/src/websocket/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as WebsocketTypes from './namespace'; diff --git a/webclient/src/websocket/types/namespace.ts b/webclient/src/websocket/types/namespace.ts deleted file mode 100644 index 84ae11770..000000000 --- a/webclient/src/websocket/types/namespace.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type { - ISessionResponse, - IRoomResponse, - IGameResponse, - IAdminResponse, - IModeratorResponse, - IWebClientResponse, -} from './WebClientResponse'; - -export type { - AuthRequestMap, - IAuthenticationRequest, - ISessionRequest, - IRoomsRequest, - IGameRequest, - IAdminRequest, - IModeratorRequest, - IWebClientRequest, -} from './WebClientRequest'; - -export * from './WebClientConfig'; -export * from './WebSocketConfig'; -export * from './StatusEnum'; -export * from './ConnectOptions'; -export * from './SignalContexts'; diff --git a/webclient/src/websocket/utils/buildWebSocketUrl.spec.ts b/webclient/src/websocket/utils/buildWebSocketUrl.spec.ts deleted file mode 100644 index 69d1f9224..000000000 --- a/webclient/src/websocket/utils/buildWebSocketUrl.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { buildWebSocketUrl } from './buildWebSocketUrl'; - -describe('buildWebSocketUrl', () => { - it('includes the port for direct (pathless) endpoints', () => { - expect(buildWebSocketUrl('wss', 'mtg.chickatrice.net', '443')).toBe( - 'wss://mtg.chickatrice.net:443', - ); - }); - - it('drops the port for nginx-proxied hosts that bake a path into the host string', () => { - expect(buildWebSocketUrl('wss', 'server.cockatrice.us/servatrice', '4748')).toBe( - 'wss://server.cockatrice.us/servatrice', - ); - }); - - it('preserves multi-segment proxy paths', () => { - expect(buildWebSocketUrl('wss', 'example.com/foo/bar', '443')).toBe( - 'wss://example.com/foo/bar', - ); - }); - - it('accepts a numeric port for direct endpoints', () => { - expect(buildWebSocketUrl('ws', 'localhost', 4748)).toBe('ws://localhost:4748'); - }); - - it('honors the ws protocol even for nginx-proxied hosts', () => { - expect(buildWebSocketUrl('ws', 'localhost/servatrice', 4748)).toBe( - 'ws://localhost/servatrice', - ); - }); -}); diff --git a/webclient/src/websocket/utils/buildWebSocketUrl.ts b/webclient/src/websocket/utils/buildWebSocketUrl.ts deleted file mode 100644 index 6de30f6d8..000000000 --- a/webclient/src/websocket/utils/buildWebSocketUrl.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Known hosts fall into two shapes: -// 1. Direct endpoint: `mtg.chickatrice.net` + `4748` → ws://mtg.chickatrice.net:4748 -// 2. Reverse-proxied endpoint with an nginx route baked into the host: -// `server.cockatrice.us/servatrice` + `4748` → wss://server.cockatrice.us/servatrice -// -// For shape (2) the `port` field is a dev-convenience value for direct -// connections — on the public TLS endpoint the path is routed via nginx on the -// default 443, NOT on `:4748`. A naive `${protocol}://${host}:${port}` produces -// `wss://server.cockatrice.us/servatrice:4748` which browsers parse as -// host=server.cockatrice.us, port=default, path=/servatrice:4748 — bypassing -// the proxy and failing the WS upgrade. -export function buildWebSocketUrl( - protocol: 'ws' | 'wss', - host: string, - port: string | number, -): string { - if (host.includes('/')) { - return `${protocol}://${host}`; - } - return `${protocol}://${host}:${port}`; -} diff --git a/webclient/src/websocket/utils/connectionState.spec.ts b/webclient/src/websocket/utils/connectionState.spec.ts deleted file mode 100644 index a01086268..000000000 --- a/webclient/src/websocket/utils/connectionState.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { setPendingOptions, consumePendingOptions } from './connectionState'; -import type { WebSocketConnectOptions } from '../types/ConnectOptions'; - -const opts = (over: Partial = {}): WebSocketConnectOptions => ({ - type: 'login', - host: 'h', - port: '1', - userName: 'u', - ...over, -} as unknown as WebSocketConnectOptions); - -describe('connectionState', () => { - beforeEach(() => { - // Drain any value lingering from prior tests. - consumePendingOptions(); - }); - - it('returns null when nothing has been set', () => { - expect(consumePendingOptions()).toBeNull(); - }); - - it('round-trips a value through set → consume', () => { - const value = opts({ host: 'a' }); - setPendingOptions(value); - expect(consumePendingOptions()).toBe(value); - }); - - it('consume is one-shot — second call returns null', () => { - setPendingOptions(opts()); - expect(consumePendingOptions()).not.toBeNull(); - expect(consumePendingOptions()).toBeNull(); - }); - - it('a second set replaces the prior value (no queue semantics)', () => { - const first = opts({ host: 'a' }); - const second = opts({ host: 'b' }); - setPendingOptions(first); - setPendingOptions(second); - expect(consumePendingOptions()).toBe(second); - expect(consumePendingOptions()).toBeNull(); - }); -}); diff --git a/webclient/src/websocket/utils/connectionState.ts b/webclient/src/websocket/utils/connectionState.ts deleted file mode 100644 index 02d0d7002..000000000 --- a/webclient/src/websocket/utils/connectionState.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { WebSocketConnectOptions } from '../types/ConnectOptions'; - -let pendingOptions: WebSocketConnectOptions | null = null; - -export function setPendingOptions(options: WebSocketConnectOptions) { - pendingOptions = options; -} - -export function consumePendingOptions(): WebSocketConnectOptions | null { - const opts = pendingOptions; - pendingOptions = null; - return opts; -} diff --git a/webclient/src/websocket/utils/guid.util.spec.ts b/webclient/src/websocket/utils/guid.util.spec.ts deleted file mode 100644 index d5554b371..000000000 --- a/webclient/src/websocket/utils/guid.util.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { guid } from './guid.util'; - -describe('guid', () => { - it('returns a string', () => { - expect(typeof guid()).toBe('string'); - }); - - it('matches UUID v4 pattern', () => { - const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; - expect(guid()).toMatch(uuidPattern); - }); - - it('returns deterministic value when crypto.getRandomValues is mocked', () => { - const spy = vi.spyOn(crypto, 'getRandomValues').mockImplementation((buf: any) => { - for (let i = 0; i < buf.length; i++) { - buf[i] = 0x1234; - } - return buf; - }); - const result = guid(); - expect(result).toBe(guid()); - spy.mockRestore(); - }); -}); diff --git a/webclient/src/websocket/utils/guid.util.ts b/webclient/src/websocket/utils/guid.util.ts deleted file mode 100644 index ae365c66c..000000000 --- a/webclient/src/websocket/utils/guid.util.ts +++ /dev/null @@ -1,15 +0,0 @@ -function s4(buf: Uint16Array, idx: number): string { - // Mask to 16 bits then OR 0x10000 so the leading nibble is always present - // (guarantees 4 hex digits without padding logic). - const v = (buf[idx] & 0xffff) | 0x10000; - return v.toString(16).substring(1); -} - -export function guid(): string { - const buf = new Uint16Array(8); - crypto.getRandomValues(buf); - return ( - s4(buf, 0) + s4(buf, 1) + '-' + s4(buf, 2) + '-' + s4(buf, 3) + '-' + - s4(buf, 4) + '-' + s4(buf, 5) + s4(buf, 6) + s4(buf, 7) - ); -} diff --git a/webclient/src/websocket/utils/index.ts b/webclient/src/websocket/utils/index.ts deleted file mode 100644 index 3821f61c1..000000000 --- a/webclient/src/websocket/utils/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './buildWebSocketUrl'; -export * from './guid.util'; -export * from './sanitizeHtml.util'; -export * from './passwordHasher'; diff --git a/webclient/src/websocket/utils/passwordHasher.spec.ts b/webclient/src/websocket/utils/passwordHasher.spec.ts deleted file mode 100644 index 61840c68c..000000000 --- a/webclient/src/websocket/utils/passwordHasher.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -vi.mock('../../generated/proto/event_server_identification_pb', async (importOriginal) => ({ - ...(await importOriginal()), - Event_ServerIdentification_ServerOptions: { SupportsPasswordHash: 1 }, -})); - -import { hashPassword, generateSalt, passwordSaltSupported } from './passwordHasher'; - -describe('hashPassword', () => { - it('returns a string starting with the salt', () => { - const result = hashPassword('mysalt', 'mypassword'); - expect(result.startsWith('mysalt')).toBe(true); - }); - - it('returns the same value for the same inputs (deterministic)', () => { - expect(hashPassword('salt', 'pass')).toBe(hashPassword('salt', 'pass')); - }); - - it('returns different values for different salts', () => { - expect(hashPassword('salt1', 'pass')).not.toBe(hashPassword('salt2', 'pass')); - }); - - it('returns different values for different passwords', () => { - expect(hashPassword('salt', 'pass1')).not.toBe(hashPassword('salt', 'pass2')); - }); -}); - -describe('generateSalt', () => { - it('returns a string of 16 characters', () => { - expect(generateSalt()).toHaveLength(16); - }); - - it('only contains alphanumeric characters', () => { - expect(generateSalt()).toMatch(/^[A-Za-z0-9]{16}$/); - }); - - it('returns different values on successive calls (not constant)', () => { - const salts = new Set(Array.from({ length: 10 }, () => generateSalt())); - expect(salts.size).toBeGreaterThan(1); - }); -}); - -describe('passwordSaltSupported', () => { - it('returns false for NoOptions (0)', () => { - expect(passwordSaltSupported(0)).toBeFalsy(); - }); - - it('returns true when the SupportsPasswordHash bit (1) is set', () => { - expect(passwordSaltSupported(1)).toBeTruthy(); - }); - - it('returns false when an unrelated bit is set (2)', () => { - expect(passwordSaltSupported(2)).toBeFalsy(); - }); - - it('returns true when bit 0 is set alongside other bits (3)', () => { - expect(passwordSaltSupported(3)).toBeTruthy(); - }); -}); diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts deleted file mode 100644 index baf1d3b3d..000000000 --- a/webclient/src/websocket/utils/passwordHasher.ts +++ /dev/null @@ -1,34 +0,0 @@ -import sha512 from 'crypto-js/sha512'; -import Base64 from 'crypto-js/enc-base64'; -import { Event_ServerIdentification_ServerOptions } from '@app/generated'; - -const HASH_ROUNDS = 1_000; -const SALT_LENGTH = 16; - -export const hashPassword = (salt: string, password: string): string => { - let hashedPassword = salt + password; - for (let i = 0; i < HASH_ROUNDS; i++) { - hashedPassword = sha512(hashedPassword); - } - - return salt + Base64.stringify(hashedPassword); -}; - -export const generateSalt = (): string => { - const characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - const bytes = new Uint8Array(SALT_LENGTH); - crypto.getRandomValues(bytes); - - let salt = ''; - for (let i = 0; i < SALT_LENGTH; i++) { - salt += characters.charAt(bytes[i] % characters.length); - } - - return salt; -}; - -export const passwordSaltSupported = (serverOptions: number): boolean => { - // Servatrice ServerOptions is a bitmask. See .github/instructions/webclient.instructions.md#protocol-quirks. - return (serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash) !== 0; -}; diff --git a/webclient/src/websocket/utils/sanitizeHtml.util.spec.ts b/webclient/src/websocket/utils/sanitizeHtml.util.spec.ts deleted file mode 100644 index 90a861928..000000000 --- a/webclient/src/websocket/utils/sanitizeHtml.util.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { sanitizeHtml } from './sanitizeHtml.util'; - -describe('sanitizeHtml', () => { - it('passes through plain text unchanged', () => { - expect(sanitizeHtml('hello world')).toBe('hello world'); - }); - - it('allows
tag', () => { - expect(sanitizeHtml('line1
line2')).toBe('line1
line2'); - }); - - it('allows tag', () => { - expect(sanitizeHtml('bold')).toBe('bold'); - }); - - it('allows tag', () => { - expect(sanitizeHtml('')).toBe(''); - }); - - it('allows
tag', () => { - expect(sanitizeHtml('
x
')).toBe('
x
'); - }); - - it('allows tag with color attribute', () => { - expect(sanitizeHtml('text')).toBe('text'); - }); - - it('strips disallowed tag ')).toBe(''); - }); - - it('strips disallowed tag
', () => { - expect(sanitizeHtml('
content
')).toBe('content'); - }); - - it('strips disallowed attribute onclick from ', () => { - expect(sanitizeHtml('hi')).toBe('hi'); - }); - - it('adds target=_blank and rel=noopener noreferrer to
tags', () => { - const result = sanitizeHtml('link'); - expect(result).toContain('target="_blank"'); - expect(result).toContain('rel="noopener noreferrer"'); - }); - - it('allows href attribute on ', () => { - const result = sanitizeHtml('link'); - expect(result).toContain('href="https://example.com"'); - }); - - it('strips disallowed schemes like javascript:', () => { - const result = sanitizeHtml('xss'); - expect(result).not.toContain('javascript:'); - }); - - it('preserves src and alt on img tags', () => { - const result = sanitizeHtml('test'); - expect(result).toContain('src="http://example.com/img.png"'); - expect(result).toContain('alt="test"'); - }); - - it('strips javascript: scheme from img src', () => { - const result = sanitizeHtml(''); - expect(result).not.toContain('src="javascript:'); - }); - - it('strips ftp: scheme from img src (scheme-hardening vs desktop)', () => { - const result = sanitizeHtml(''); - expect(result).not.toContain('ftp://'); - }); - - it('preserves https: scheme on img src', () => { - const result = sanitizeHtml(''); - expect(result).toContain('src="https://example.com/img.png"'); - }); - - it('strips onerror from img while keeping safe src', () => { - const result = sanitizeHtml(''); - expect(result).not.toContain('onerror'); - expect(result).toContain('src="http://example.com/img.png"'); - }); -}); diff --git a/webclient/src/websocket/utils/sanitizeHtml.util.ts b/webclient/src/websocket/utils/sanitizeHtml.util.ts deleted file mode 100644 index ab5911daa..000000000 --- a/webclient/src/websocket/utils/sanitizeHtml.util.ts +++ /dev/null @@ -1,25 +0,0 @@ -import DOMPurify from 'dompurify'; - -DOMPurify.addHook('afterSanitizeAttributes', (node) => { - if (node.tagName === 'A') { - node.setAttribute('target', '_blank'); - node.setAttribute('rel', 'noopener noreferrer'); - } -}); - -export function sanitizeHtml(msg: string): string { - // Desktop Cockatrice renders MOTD via Qt QTextBrowser with no sanitization; - // web client hardens via a DOMPurify tag/attr allowlist and restricts URIs - // to https/http (ftp is effectively dead in modern browsers and would only - // broaden the attack surface for a hostile server). - return DOMPurify.sanitize(msg, { - ALLOWED_TAGS: ['br', 'a', 'img', 'center', 'b', 'font'], - ALLOWED_ATTR: ['href', 'color', 'rel', 'target', 'src', 'alt'], - // Load-bearing: DOMPurify applies ALLOWED_URI_REGEXP to *every* attribute - // it isn't explicitly told is URI-safe. `color` values like "red" don't - // match the regex and would be stripped. This whitelists `color` out of - // URI validation. Removing it breaks the sanitizer test. - ADD_URI_SAFE_ATTR: ['color'], - ALLOWED_URI_REGEXP: /^https?:/i, - }); -} diff --git a/webclient/tsconfig.json b/webclient/tsconfig.json index 43c7c79e0..17d5b7bb3 100644 --- a/webclient/tsconfig.json +++ b/webclient/tsconfig.json @@ -35,9 +35,9 @@ "@app/store": ["./src/store/index.ts"], "@app/types": ["./src/types/index.ts"], "@app/utils": ["./src/utils/index.ts"], - "@app/websocket": ["./src/websocket/index.ts"], - "@app/websocket/types": ["./src/websocket/types/index.ts"], - "@app/generated": ["./src/generated/index.ts"] + "@app/websocket": ["./node_modules/sockatrice/dist/index.d.ts"], + "@app/websocket/types": ["./node_modules/sockatrice/dist/types/index.d.ts"], + "@app/generated": ["./node_modules/sockatrice/dist/generated/index.d.ts"] } }, "include": [ diff --git a/webclient/vite.config.ts b/webclient/vite.config.ts index f4b060360..6e0800892 100644 --- a/webclient/vite.config.ts +++ b/webclient/vite.config.ts @@ -21,9 +21,9 @@ export default defineConfig({ '@app/store': srcPath('store/index.ts'), '@app/types': srcPath('types/index.ts'), '@app/utils': srcPath('utils/index.ts'), - '@app/websocket/types': srcPath('websocket/types/index.ts'), - '@app/websocket': srcPath('websocket/index.ts'), - '@app/generated': srcPath('generated/index.ts'), + '@app/websocket/types': 'sockatrice/types', + '@app/websocket': 'sockatrice', + '@app/generated': 'sockatrice/generated', }, }, optimizeDeps: { @@ -47,6 +47,9 @@ export default defineConfig({ '@testing-library/react', '@testing-library/jest-dom/vitest', '@bufbuild/protobuf', + 'sockatrice', + 'sockatrice/types', + 'sockatrice/generated', ], }, publicDir: 'public', @@ -62,6 +65,11 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', + server: { + deps: { + inline: ['sockatrice'], + }, + }, setupFiles: ['./src/setupTests.ts'], include: ['src/**/*.spec.{ts,tsx}'], exclude: ['node_modules', 'build', 'integration', 'coverage'], @@ -75,7 +83,6 @@ export default defineConfig({ reportsDirectory: './coverage/testing', include: ['src/**/*.{ts,tsx}'], exclude: [ - 'src/generated/**', 'src/**/*.spec.{ts,tsx}', 'src/**/__mocks__/**', 'src/setupTests.ts',