From c3ae4cffd6e9182f37920479c27e3d222e08b9a2 Mon Sep 17 00:00:00 2001 From: seavor Date: Sun, 12 Apr 2026 13:58:51 -0500 Subject: [PATCH] Fix various issues --- .../src/forms/RegisterForm/RegisterForm.tsx | 2 +- .../src/store/game/__mocks__/fixtures.ts | 1 + webclient/src/store/game/game.interfaces.ts | 1 + .../store/server/__mocks__/server-fixtures.ts | 1 + .../src/store/server/server.actions.spec.ts | 15 ++--- webclient/src/store/server/server.actions.ts | 10 +--- .../src/store/server/server.dispatch.spec.ts | 11 ++-- webclient/src/store/server/server.dispatch.ts | 8 +-- .../src/store/server/server.interfaces.ts | 3 +- .../src/store/server/server.reducer.spec.ts | 56 +++++++++++++++---- webclient/src/store/server/server.reducer.ts | 18 +++++- webclient/src/store/server/server.types.ts | 5 +- webclient/src/websocket/WebClient.ts | 2 + .../__mocks__/sessionCommandMocks.ts | 1 - .../src/websocket/commands/session/message.ts | 7 +-- .../session/sessionCommands-simple.spec.ts | 5 -- .../events/session/connectionClosed.ts | 6 +- .../persistence/SessionPersistence.spec.ts | 32 ++++++----- .../persistence/SessionPersistence.ts | 20 ++++--- .../services/ProtobufService.spec.ts | 27 +-------- .../src/websocket/services/ProtobufService.ts | 20 +------ 21 files changed, 130 insertions(+), 121 deletions(-) diff --git a/webclient/src/forms/RegisterForm/RegisterForm.tsx b/webclient/src/forms/RegisterForm/RegisterForm.tsx index f5f4d9174..086aec0df 100644 --- a/webclient/src/forms/RegisterForm/RegisterForm.tsx +++ b/webclient/src/forms/RegisterForm/RegisterForm.tsx @@ -38,7 +38,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => { useReduxEffect(() => { openToast() - }, ServerTypes.REGISTRATION_SUCCES); + }, ServerTypes.REGISTRATION_SUCCESS); useReduxEffect(({ error }) => { setEmailError(error); diff --git a/webclient/src/store/game/__mocks__/fixtures.ts b/webclient/src/store/game/__mocks__/fixtures.ts index ca19cdadd..19df4d398 100644 --- a/webclient/src/store/game/__mocks__/fixtures.ts +++ b/webclient/src/store/game/__mocks__/fixtures.ts @@ -100,6 +100,7 @@ export function makeGameEntry(overrides: Partial = {}): GameEntry { localPlayerId: 1, spectator: false, judge: false, + resuming: false, started: false, activePlayerId: 0, activePhase: 0, diff --git a/webclient/src/store/game/game.interfaces.ts b/webclient/src/store/game/game.interfaces.ts index 2e5e28325..c7ee6749b 100644 --- a/webclient/src/store/game/game.interfaces.ts +++ b/webclient/src/store/game/game.interfaces.ts @@ -17,6 +17,7 @@ export interface GameEntry { localPlayerId: number; spectator: boolean; judge: boolean; + resuming: boolean; started: boolean; activePlayerId: number; activePhase: number; diff --git a/webclient/src/store/server/__mocks__/server-fixtures.ts b/webclient/src/store/server/__mocks__/server-fixtures.ts index 6ac04c121..404424f6d 100644 --- a/webclient/src/store/server/__mocks__/server-fixtures.ts +++ b/webclient/src/store/server/__mocks__/server-fixtures.ts @@ -149,6 +149,7 @@ export function makeServerState(overrides: Partial = {}): ServerSta adminNotes: {}, replays: [], backendDecks: null, + gamesOfUser: {}, ...overrides, }; } diff --git a/webclient/src/store/server/server.actions.spec.ts b/webclient/src/store/server/server.actions.spec.ts index 7d61717fb..4d4220854 100644 --- a/webclient/src/store/server/server.actions.spec.ts +++ b/webclient/src/store/server/server.actions.spec.ts @@ -120,7 +120,7 @@ describe('Actions', () => { }); it('registrationSuccess', () => { - expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCES }); + expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCESS }); }); it('registrationFailed', () => { @@ -203,14 +203,6 @@ describe('Actions', () => { expect(Actions.accountImageChanged(user)).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, user }); }); - it('directMessageSent', () => { - expect(Actions.directMessageSent('Eve', 'hi')).toEqual({ - type: Types.DIRECT_MESSAGE_SENT, - userName: 'Eve', - message: 'hi', - }); - }); - it('getUserInfo', () => { const userInfo = makeUser({ name: 'Frank' }); expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo }); @@ -353,4 +345,9 @@ describe('Actions', () => { it('deckDelete', () => { expect(Actions.deckDelete(42)).toEqual({ type: Types.DECK_DELETE, deckId: 42 }); }); + + it('gamesOfUser', () => { + const games = [{ gameId: 1 }] as any; + expect(Actions.gamesOfUser('alice', games)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games }); + }); }); diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index 8af5adb75..e72021889 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -1,4 +1,4 @@ -import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; +import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types'; import { Types } from './server.types'; export const Actions = { @@ -91,7 +91,7 @@ export const Actions = { type: Types.REGISTRATION_REQUIRES_EMAIL, }), registrationSuccess: () => ({ - type: Types.REGISTRATION_SUCCES, + type: Types.REGISTRATION_SUCCESS, }), registrationFailed: (error) => ({ type: Types.REGISTRATION_FAILED, @@ -157,11 +157,6 @@ export const Actions = { type: Types.ACCOUNT_IMAGE_CHANGED, user, }), - directMessageSent: (userName, message) => ({ - type: Types.DIRECT_MESSAGE_SENT, - userName, - message, - }), getUserInfo: (userInfo) => ({ type: Types.GET_USER_INFO, userInfo, @@ -239,4 +234,5 @@ export const Actions = { deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }), deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }), deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }), + gamesOfUser: (userName: string, games: Game[]) => ({ type: Types.GAMES_OF_USER, userName, games }), } diff --git a/webclient/src/store/server/server.dispatch.spec.ts b/webclient/src/store/server/server.dispatch.spec.ts index f6bab848d..56f26966f 100644 --- a/webclient/src/store/server/server.dispatch.spec.ts +++ b/webclient/src/store/server/server.dispatch.spec.ts @@ -250,11 +250,6 @@ describe('Dispatch', () => { expect(store.dispatch).toHaveBeenCalledWith(Actions.accountImageChanged(user)); }); - it('directMessageSent dispatches correctly', () => { - Dispatch.directMessageSent('Eve', 'hi'); - expect(store.dispatch).toHaveBeenCalledWith(Actions.directMessageSent('Eve', 'hi')); - }); - it('getUserInfo dispatches correctly', () => { const userInfo = makeUser({ name: 'Frank' }); Dispatch.getUserInfo(userInfo); @@ -385,4 +380,10 @@ describe('Dispatch', () => { Dispatch.deckDelete(42); expect(store.dispatch).toHaveBeenCalledWith(Actions.deckDelete(42)); }); + + it('gamesOfUser dispatches correctly', () => { + const games = [{ gameId: 1 }] as any; + Dispatch.gamesOfUser('alice', games); + expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games)); + }); }); diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 3d3b6d360..ac747f165 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -1,7 +1,7 @@ import { reset } from 'redux-form'; import { Actions } from './server.actions'; import { store } from 'store'; -import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types'; +import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types'; export const Dispatch = { initialized: () => { @@ -141,9 +141,6 @@ export const Dispatch = { accountImageChanged: (user) => { store.dispatch(Actions.accountImageChanged(user)); }, - directMessageSent: (userName, message) => { - store.dispatch(Actions.directMessageSent(userName, message)); - }, getUserInfo: (userInfo) => { store.dispatch(Actions.getUserInfo(userInfo)); }, @@ -216,4 +213,7 @@ export const Dispatch = { deckDelete: (deckId: number) => { store.dispatch(Actions.deckDelete(deckId)); }, + gamesOfUser: (userName: string, games: Game[]) => { + store.dispatch(Actions.gamesOfUser(userName, games)); + }, } diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index 97e9d99da..b0932d0ea 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -1,5 +1,5 @@ import { - WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem + WarnHistoryItem, BanHistoryItem, DeckList, Game, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem } from 'types'; import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; @@ -72,6 +72,7 @@ export interface ServerState { adminNotes: { [userName: string]: string }; replays: ReplayMatch[]; backendDecks: DeckList | null; + gamesOfUser: { [userName: string]: Game[] }; } export interface ServerStateStatus { diff --git a/webclient/src/store/server/server.reducer.spec.ts b/webclient/src/store/server/server.reducer.spec.ts index e6ba27680..3645c299e 100644 --- a/webclient/src/store/server/server.reducer.spec.ts +++ b/webclient/src/store/server/server.reducer.spec.ts @@ -332,31 +332,39 @@ describe('Moderation', () => { describe('ADJUST_MOD', () => { const baseUserLevel = UserLevelFlag.IsUser | UserLevelFlag.IsRegistered | UserLevelFlag.IsModerator | UserLevelFlag.IsJudge; - it('shouldBeMod=true, shouldBeJudge=true → keeps IsModerator and IsJudge bits', () => { + it('shouldBeMod=true, shouldBeJudge=true → sets both bits, preserves IsUser|IsRegistered', () => { const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] }); const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true }); - // IsModerator(4) | IsJudge(16) - expect(result.users[0].userLevel).toBe(20); + // IsUser(1) | IsRegistered(2) | IsModerator(4) | IsJudge(16) = 23 + expect(result.users[0].userLevel).toBe(23); }); - it('shouldBeMod=true, shouldBeJudge=false → keeps only IsModerator bit', () => { + it('shouldBeMod=true, shouldBeJudge=false → sets IsModerator, clears IsJudge, preserves others', () => { const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] }); const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false }); - // IsModerator(4) - expect(result.users[0].userLevel).toBe(4); + // IsUser(1) | IsRegistered(2) | IsModerator(4) = 7 + expect(result.users[0].userLevel).toBe(7); }); - it('shouldBeMod=false, shouldBeJudge=true → keeps only IsJudge bit', () => { + it('shouldBeMod=false, shouldBeJudge=true → clears IsModerator, sets IsJudge, preserves others', () => { const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] }); const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true }); - // IsJudge(16) - expect(result.users[0].userLevel).toBe(16); + // IsUser(1) | IsRegistered(2) | IsJudge(16) = 19 + expect(result.users[0].userLevel).toBe(19); }); - it('shouldBeMod=false, shouldBeJudge=false → clears both bits', () => { + it('shouldBeMod=false, shouldBeJudge=false → clears both bits, preserves IsUser|IsRegistered', () => { const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] }); const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false }); - expect(result.users[0].userLevel).toBe(0); + // IsUser(1) | IsRegistered(2) = 3 + expect(result.users[0].userLevel).toBe(3); + }); + + it('shouldBeMod=true on IsUser|IsRegistered only → produces 7, not 4', () => { + const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: UserLevelFlag.IsUser | UserLevelFlag.IsRegistered })] }); + const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false }); + // IsUser(1) | IsRegistered(2) | IsModerator(4) = 7 + expect(result.users[0].userLevel).toBe(7); }); it('non-matching users are left unchanged', () => { @@ -524,3 +532,29 @@ describe('Deck Storage', () => { expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0); }); }); + +// ── GAMES_OF_USER ───────────────────────────────────────────────────────────── + +describe('GAMES_OF_USER', () => { + it('stores games keyed by userName', () => { + const games = [{ gameId: 5, roomId: 1 }] as any; + const state = makeServerState(); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games }); + expect(result.gamesOfUser['alice']).toBe(games); + }); + + it('overwrites previous games for same user', () => { + const old = [{ gameId: 1 }] as any; + const fresh = [{ gameId: 2 }] as any; + const state = makeServerState({ gamesOfUser: { alice: old } }); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh }); + expect(result.gamesOfUser['alice']).toBe(fresh); + }); + + it('does not affect other users\' entries', () => { + const bobGames = [{ gameId: 3 }] as any; + const state = makeServerState({ gamesOfUser: { bob: bobGames } }); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [] }); + expect(result.gamesOfUser['bob']).toBe(bobGames); + }); +}); diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index a63418eeb..aaa525977 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -93,6 +93,7 @@ const initialState: ServerState = { adminNotes: {}, replays: [], backendDecks: null, + gamesOfUser: {}, }; export const serverReducer = (state = initialState, action: any) => { @@ -401,11 +402,12 @@ export const serverReducer = (state = initialState, action: any) => { if (user.name !== userName) { return user; } - const judgeFlag = shouldBeJudge ? UserLevelFlag.IsJudge : UserLevelFlag.IsNothing; - const modFlag = shouldBeMod ? UserLevelFlag.IsModerator : UserLevelFlag.IsNothing; + let newLevel = user.userLevel; + newLevel = shouldBeMod ? (newLevel | UserLevelFlag.IsModerator) : (newLevel & ~UserLevelFlag.IsModerator); + newLevel = shouldBeJudge ? (newLevel | UserLevelFlag.IsJudge) : (newLevel & ~UserLevelFlag.IsJudge); return { ...user, - userLevel: user.userLevel & (judgeFlag | modFlag) + userLevel: newLevel, } }) }; @@ -475,6 +477,16 @@ export const serverReducer = (state = initialState, action: any) => { }, }; } + case Types.GAMES_OF_USER: { + const { userName, games } = action; + return { + ...state, + gamesOfUser: { + ...state.gamesOfUser, + [userName]: games, + }, + }; + } default: return state; } diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index fb4249011..ab1dd3f1d 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -23,7 +23,7 @@ export const Types = { VIEW_LOGS: '[Server] View Logs', CLEAR_LOGS: '[Server] Clear Logs', REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email', - REGISTRATION_SUCCES: '[Server] Registration Success', + REGISTRATION_SUCCESS: '[Server] Registration Success', REGISTRATION_FAILED: '[Server] Registration Failed', REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error', REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error', @@ -42,7 +42,6 @@ export const Types = { ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change', ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed', ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed', - DIRECT_MESSAGE_SENT: '[Server] Direct Message Sent', GET_USER_INFO: '[Server] Get User Info', NOTIFY_USER: '[Server] Notify User', SERVER_SHUTDOWN: '[Server] Server Shutdown', @@ -69,4 +68,6 @@ export const Types = { DECK_DEL_DIR: '[Server] Deck Del Dir', DECK_UPLOAD: '[Server] Deck Upload', DECK_DELETE: '[Server] Deck Delete', + // User games + GAMES_OF_USER: '[Server] Games Of User', }; diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index 1dae24d0c..4a471b0e5 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -3,6 +3,7 @@ import { StatusEnum, WebSocketConnectOptions } from 'types'; import { ProtobufService } from './services/ProtobufService'; import { WebSocketService } from './services/WebSocketService'; +import { GameDispatch } from 'store'; import { RoomPersistence, SessionPersistence } from './persistence'; export class WebClient { @@ -79,6 +80,7 @@ export class WebClient { } private clearStores() { + GameDispatch.clearStore(); RoomPersistence.clearStore(); SessionPersistence.clearStore(); } diff --git a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts index 7b28335b7..71f84953c 100644 --- a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts +++ b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts @@ -79,7 +79,6 @@ export function makeSessionPersistenceMock() { accountActivationSuccess: jest.fn(), accountActivationFailed: jest.fn(), updateStatus: jest.fn(), - directMessageSent: jest.fn(), addToList: jest.fn(), removeFromList: jest.fn(), deleteServerDeck: jest.fn(), diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts index 075fc3c4b..b6bde9cac 100644 --- a/webclient/src/websocket/commands/session/message.ts +++ b/webclient/src/websocket/commands/session/message.ts @@ -1,10 +1,5 @@ import { BackendService } from '../../services/BackendService'; -import { SessionPersistence } from '../../persistence'; export function message(userName: string, message: string): void { - BackendService.sendSessionCommand('Command_Message', { userName, message }, { - onSuccess: () => { - SessionPersistence.directMessageSent(userName, message); - }, - }); + BackendService.sendSessionCommand('Command_Message', { userName, message }, {}); } diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index f6af910db..bd9a6830b 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -302,11 +302,6 @@ describe('message', () => { ); }); - it('calls directMessageSent on success', () => { - message('bob', 'hi'); - invokeOnSuccess(); - expect(SessionPersistence.directMessageSent).toHaveBeenCalledWith('bob', 'hi'); - }); }); describe('ping', () => { diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts index 227113059..40f75c013 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -3,7 +3,7 @@ import { ProtoController } from '../../services/ProtoController'; import { updateStatus } from '../../commands/session'; import { ConnectionClosedData } from './interfaces'; -export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): void { +export function connectionClosed({ reason, reasonStr, endTime }: ConnectionClosedData): void { let message: string; // @TODO (5) @@ -19,7 +19,9 @@ export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): v message = 'There are too many concurrent connections from your address'; break; case CloseReason.BANNED: - message = 'You are banned'; + message = endTime > 0 + ? `You are banned until ${new Date(endTime * 1000).toLocaleString()}` + : 'You are banned'; break; case CloseReason.DEMOTED: message = 'You were demoted'; diff --git a/webclient/src/websocket/persistence/SessionPersistence.spec.ts b/webclient/src/websocket/persistence/SessionPersistence.spec.ts index ff83f7684..7a35c58a9 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.spec.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.spec.ts @@ -37,7 +37,6 @@ jest.mock('store', () => ({ accountPasswordChange: jest.fn(), accountEditChanged: jest.fn(), accountImageChanged: jest.fn(), - directMessageSent: jest.fn(), getUserInfo: jest.fn(), notifyUser: jest.fn(), serverShutdown: jest.fn(), @@ -53,6 +52,7 @@ jest.mock('store', () => ({ replayAdded: jest.fn(), replayModifyMatch: jest.fn(), replayDeleteMatch: jest.fn(), + gamesOfUser: jest.fn(), }, GameDispatch: { gameJoined: jest.fn(), @@ -68,6 +68,7 @@ jest.mock('../utils/NormalizeService', () => ({ __esModule: true, default: { normalizeBannedUserError: jest.fn((r: string, t: number) => `banned:${r}:${t}`), + normalizeGameObject: jest.fn(), }, })); @@ -291,28 +292,33 @@ describe('SessionPersistence', () => { expect(ServerDispatch.accountImageChanged).toHaveBeenCalledWith({ avatarBmp: buf }); }); - it('directMessageSent passes userName and message', () => { - SessionPersistence.directMessageSent('bob', 'hi'); - expect(ServerDispatch.directMessageSent).toHaveBeenCalledWith('bob', 'hi'); - }); - it('getUserInfo passes userInfo', () => { const user = { name: 'u' } as any; SessionPersistence.getUserInfo(user); expect(ServerDispatch.getUserInfo).toHaveBeenCalledWith(user); }); - it('getGamesOfUser logs to console', () => { - const spy = jest.spyOn(console, 'log').mockImplementation(() => {}); - SessionPersistence.getGamesOfUser('user1', {}); - expect(spy).toHaveBeenCalled(); - spy.mockRestore(); + it('getGamesOfUser normalizes game list and dispatches gamesOfUser', () => { + const gt = { gameTypeId: 1, description: 'Standard' }; + const room = { gametypeList: [gt] }; + const game = { gameId: 5, roomId: 1, gameTypes: [1], description: 'My Game', started: false }; + SessionPersistence.getGamesOfUser('alice', { roomList: [room], gameList: [game] }); + expect(NormalizeService.normalizeGameObject).toHaveBeenCalledWith(game, { 1: 'Standard' }); + expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', [game]); + }); + + it('getGamesOfUser handles empty response', () => { + SessionPersistence.getGamesOfUser('alice', {}); + expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', []); }); it('gameJoined dispatches via GameDispatch.gameJoined', () => { const gameInfo = { gameId: 10, roomId: 2, description: 'test', started: false }; - SessionPersistence.gameJoined({ gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false } as any); - expect(GameDispatch.gameJoined).toHaveBeenCalledWith(10, expect.objectContaining({ gameId: 10, hostId: 3, localPlayerId: 4 })); + SessionPersistence.gameJoined({ gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false, resuming: true } as any); + expect(GameDispatch.gameJoined).toHaveBeenCalledWith( + 10, + expect.objectContaining({ gameId: 10, hostId: 3, localPlayerId: 4, resuming: true }) + ); }); it('notifyUser passes notification', () => { diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 960a844f6..10fcc0539 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -167,21 +167,26 @@ export class SessionPersistence { ServerDispatch.accountImageChanged({ avatarBmp }); } - static directMessageSent(userName: string, message: string): void { - ServerDispatch.directMessageSent(userName, message); - } - static getUserInfo(userInfo: User) { ServerDispatch.getUserInfo(userInfo); } static getGamesOfUser(userName: string, response: any): void { - // Response_GetGamesOfUser contains a gameList field — log for now until game layer is complete - console.log('getGamesOfUser', userName, response); + const gametypeMap: Record = {}; + (response.roomList || []).forEach((room: any) => { + (room.gametypeList || []).forEach((gt: any) => { + gametypeMap[gt.gameTypeId] = gt.description; + }); + }); + const games = (response.gameList || []).map((game: any) => { + NormalizeService.normalizeGameObject(game, gametypeMap); + return game; + }); + ServerDispatch.gamesOfUser(userName, games); } static gameJoined(gameJoinedData: GameJoinedData): void { - const { gameInfo, hostId, playerId, spectator, judge } = gameJoinedData; + const { gameInfo, hostId, playerId, spectator, judge, resuming } = gameJoinedData; const gameEntry: GameEntry = { gameId: gameInfo.gameId, roomId: gameInfo.roomId, @@ -190,6 +195,7 @@ export class SessionPersistence { localPlayerId: playerId, spectator, judge, + resuming, started: gameInfo.started, activePlayerId: -1, activePhase: -1, diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts index 618f643a5..c03173020 100644 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ b/webclient/src/websocket/services/ProtobufService.spec.ts @@ -10,7 +10,6 @@ jest.mock('../commands/session', () => ({ })); jest.mock('../events', () => ({ - CommonEvents: { '.Event_Common.ext': jest.fn() }, GameEvents: { '.Event_Game.ext': jest.fn() }, RoomEvents: { '.Event_Room.ext': jest.fn() }, SessionEvents: { '.Event_Session.ext': jest.fn() }, @@ -21,7 +20,7 @@ jest.mock('../WebClient'); import { ProtobufService } from './ProtobufService'; import { ProtoController } from './ProtoController'; import { ping as sessionPing } from '../commands/session'; -import { GameEvents, CommonEvents } from '../events'; +import { GameEvents } from '../events'; let mockSocket: any; let mockWebClient: any; @@ -321,17 +320,6 @@ describe('ProtobufService', () => { }); }); - describe('processCommonEvent', () => { - it('delegates to processEvent with CommonEvents', () => { - const service = new ProtobufService(mockWebClient); - const processEvent = jest.spyOn(service as any, 'processEvent'); - const response = { '.Event_Common.ext': { data: 1 } }; - const raw = { extra: true }; - (service as any).processCommonEvent(response, raw); - expect(processEvent).toHaveBeenCalledWith(response, CommonEvents, raw); - }); - }); - describe('processGameEvent', () => { it('returns early when container has no eventList', () => { const service = new ProtobufService(mockWebClient); @@ -354,19 +342,6 @@ describe('ProtobufService', () => { expect(gameEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 })); }); - it('falls back to CommonEvents handler when no GameEvents key matches', () => { - const service = new ProtobufService(mockWebClient); - const commonEventHandler = (CommonEvents as any)['.Event_Common.ext'] as jest.Mock; - const payload = { commonData: 2 }; - (service as any).processGameEvent({ - gameId: 7, - context: null, - secondsElapsed: 0, - forcedByJudge: 0, - eventList: [{ '.Event_Common.ext': payload, playerId: 3 }], - }, {}); - expect(commonEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 7, playerId: 3 })); - }); }); describe('processEvent', () => { diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index 626e65913..1be84d81d 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,4 +1,4 @@ -import { CommonEvents, GameEvents, RoomEvents, SessionEvents } from '../events'; +import { GameEvents, RoomEvents, SessionEvents } from '../events'; import { WebClient } from '../WebClient'; import { SessionCommands } from 'websocket'; import { ProtoController } from './ProtoController'; @@ -119,10 +119,6 @@ export class ProtobufService { } } - private processCommonEvent(response: any, raw: any) { - this.processEvent(response, CommonEvents, raw); - } - private processRoomEvent(response: any, raw: any) { this.processEvent(response, RoomEvents, raw); } @@ -147,25 +143,13 @@ export class ProtobufService { forcedByJudge: forcedByJudge ?? 0, }; - // Try registered game event handlers first, then common event handlers - let handled = false; for (const key of Object.keys(GameEvents)) { const payload = event[key]; if (payload !== undefined && payload !== null) { (GameEvents[key] as Function)(payload, meta); - handled = true; break; } } - if (!handled) { - for (const key of Object.keys(CommonEvents)) { - const payload = event[key]; - if (payload !== undefined && payload !== null) { - (CommonEvents[key] as Function)(payload, meta); - break; - } - } - } } } @@ -173,7 +157,7 @@ export class ProtobufService { for (const event in events) { const payload = response[event]; - if (payload) { + if (payload !== undefined && payload !== null) { events[event](payload, raw); return; }