refactor typescript wiring

This commit is contained in:
seavor 2026-04-15 15:46:17 -05:00
parent cea9ae62d8
commit c62c336a11
286 changed files with 2999 additions and 3053 deletions

View file

@ -1,33 +1,13 @@
import {
Game,
ProtoInit,
SortDirection,
StatusEnum,
UserSortField,
WebSocketConnectOptions,
} from 'types';
import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb';
import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb';
import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb';
import type { Response_WarnList } from 'generated/proto/response_warn_list_pb';
import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb';
import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb';
import type { Response_DeckList } from 'generated/proto/response_deck_list_pb';
import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb';
import { App, Data, Enriched } from '@app/types';
import type { MessageInitShape } from '@bufbuild/protobuf';
import { create } from '@bufbuild/protobuf';
import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb';
import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb';
import { ServerInfo_ReplayMatchSchema } from 'generated/proto/serverinfo_replay_match_pb';
import { ServerInfo_ChatMessageSchema } from 'generated/proto/serverinfo_chat_message_pb';
import { ServerInfo_BanSchema } from 'generated/proto/serverinfo_ban_pb';
import { ServerInfo_WarningSchema } from 'generated/proto/serverinfo_warning_pb';
import { Response_WarnListSchema } from 'generated/proto/response_warn_list_pb';
import { ServerInfo_DeckStorage_TreeItemSchema, ServerInfo_DeckStorage_FolderSchema } from 'generated/proto/serverinfo_deckstorage_pb';
import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb';
import { ServerState } from '../server.interfaces';
export function makeUser(overrides: ProtoInit<ServerInfo_User> = {}): ServerInfo_User {
return create(ServerInfo_UserSchema, {
export function makeUser(
overrides: MessageInitShape<typeof Data.ServerInfo_UserSchema> = {}
): Data.ServerInfo_User {
return create(Data.ServerInfo_UserSchema, {
name: 'TestUser',
accountageSecs: 0n,
privlevel: '',
@ -36,8 +16,10 @@ export function makeUser(overrides: ProtoInit<ServerInfo_User> = {}): ServerInfo
});
}
export function makeLogItem(overrides: ProtoInit<ServerInfo_ChatMessage> = {}): ServerInfo_ChatMessage {
return create(ServerInfo_ChatMessageSchema, {
export function makeLogItem(
overrides: MessageInitShape<typeof Data.ServerInfo_ChatMessageSchema> = {}
): Data.ServerInfo_ChatMessage {
return create(Data.ServerInfo_ChatMessageSchema, {
message: '',
senderId: '',
senderIp: '',
@ -50,8 +32,10 @@ export function makeLogItem(overrides: ProtoInit<ServerInfo_ChatMessage> = {}):
});
}
export function makeBanHistoryItem(overrides: ProtoInit<ServerInfo_Ban> = {}): ServerInfo_Ban {
return create(ServerInfo_BanSchema, {
export function makeBanHistoryItem(
overrides: MessageInitShape<typeof Data.ServerInfo_BanSchema> = {}
): Data.ServerInfo_Ban {
return create(Data.ServerInfo_BanSchema, {
adminId: '',
adminName: '',
banTime: '',
@ -62,8 +46,10 @@ export function makeBanHistoryItem(overrides: ProtoInit<ServerInfo_Ban> = {}): S
});
}
export function makeWarnHistoryItem(overrides: ProtoInit<ServerInfo_Warning> = {}): ServerInfo_Warning {
return create(ServerInfo_WarningSchema, {
export function makeWarnHistoryItem(
overrides: MessageInitShape<typeof Data.ServerInfo_WarningSchema> = {}
): Data.ServerInfo_Warning {
return create(Data.ServerInfo_WarningSchema, {
userName: '',
adminName: '',
reason: '',
@ -72,8 +58,10 @@ export function makeWarnHistoryItem(overrides: ProtoInit<ServerInfo_Warning> = {
});
}
export function makeWarnListItem(overrides: ProtoInit<Response_WarnList> = {}): Response_WarnList {
return create(Response_WarnListSchema, {
export function makeWarnListItem(
overrides: MessageInitShape<typeof Data.Response_WarnListSchema> = {}
): Data.Response_WarnList {
return create(Data.Response_WarnListSchema, {
warning: [],
userName: '',
userClientid: '',
@ -81,23 +69,29 @@ export function makeWarnListItem(overrides: ProtoInit<Response_WarnList> = {}):
});
}
export function makeDeckTreeItem(overrides: ProtoInit<ServerInfo_DeckStorage_TreeItem> = {}): ServerInfo_DeckStorage_TreeItem {
return create(ServerInfo_DeckStorage_TreeItemSchema, {
export function makeDeckTreeItem(
overrides: MessageInitShape<typeof Data.ServerInfo_DeckStorage_TreeItemSchema> = {},
): Data.ServerInfo_DeckStorage_TreeItem {
return create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 1,
name: 'item',
...overrides,
});
}
export function makeDeckList(overrides: ProtoInit<Response_DeckList> = {}): Response_DeckList {
return create(Response_DeckListSchema, {
root: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }),
export function makeDeckList(
overrides: MessageInitShape<typeof Data.Response_DeckListSchema> = {}
): Data.Response_DeckList {
return create(Data.Response_DeckListSchema, {
root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }),
...overrides,
});
}
export function makeReplayMatch(overrides: ProtoInit<ServerInfo_ReplayMatch> = {}): ServerInfo_ReplayMatch {
return create(ServerInfo_ReplayMatchSchema, {
export function makeReplayMatch(
overrides: MessageInitShape<typeof Data.ServerInfo_ReplayMatchSchema> = {}
): Data.ServerInfo_ReplayMatch {
return create(Data.ServerInfo_ReplayMatchSchema, {
gameId: 1,
roomName: 'Test Room',
timeStarted: 0,
@ -110,16 +104,26 @@ export function makeReplayMatch(overrides: ProtoInit<ServerInfo_ReplayMatch> = {
});
}
export function makeGame(overrides: Partial<Game> = {}): Game {
return { ...create(ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides };
export function makeGame(overrides: Partial<Enriched.Game> = {}): Enriched.Game {
return { ...create(Data.ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides };
}
export function makeConnectOptions(overrides: Partial<WebSocketConnectOptions> = {}): WebSocketConnectOptions {
export function makeLoginSuccessContext(
overrides: Partial<Enriched.LoginSuccessContext> = {}
): Enriched.LoginSuccessContext {
return {
hashedPassword: 'hash',
...overrides,
};
}
export function makePendingActivationContext(
overrides: Partial<Enriched.PendingActivationContext> = {}
): Enriched.PendingActivationContext {
return {
host: 'localhost',
port: '4747',
userName: 'user',
password: 'pass',
...overrides,
};
}
@ -131,7 +135,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
ignoreList: [],
status: {
connectionAttemptMade: false,
state: StatusEnum.DISCONNECTED,
state: App.StatusEnum.DISCONNECTED,
description: null,
},
info: {
@ -147,8 +151,8 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
user: null,
users: [],
sortUsersBy: {
field: UserSortField.NAME,
order: SortDirection.ASC,
field: App.UserSortField.NAME,
order: App.SortDirection.ASC,
},
messages: {},
userInfo: {},

View file

@ -1,16 +1,14 @@
import { Actions } from './server.actions';
import { App, Data } from '@app/types';
import { Types } from './server.types';
import { create } from '@bufbuild/protobuf';
import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb';
import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb';
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
import {
makeBanHistoryItem,
makeConnectOptions,
makeLoginSuccessContext,
makePendingActivationContext,
makeDeckList,
makeDeckTreeItem,
makeReplayMatch,
makeGame,
makeUser,
makeWarnHistoryItem,
makeWarnListItem,
@ -30,7 +28,7 @@ describe('Actions', () => {
});
it('loginSuccessful', () => {
const options = makeConnectOptions();
const options = makeLoginSuccessContext();
expect(Actions.loginSuccessful(options)).toEqual({ type: Types.LOGIN_SUCCESSFUL, options });
});
@ -38,10 +36,6 @@ describe('Actions', () => {
expect(Actions.loginFailed()).toEqual({ type: Types.LOGIN_FAILED });
});
it('connectionClosed', () => {
expect(Actions.connectionClosed(3)).toEqual({ type: Types.CONNECTION_CLOSED, reason: 3 });
});
it('connectionFailed', () => {
expect(Actions.connectionFailed()).toEqual({ type: Types.CONNECTION_FAILED });
});
@ -92,7 +86,7 @@ describe('Actions', () => {
});
it('updateStatus', () => {
const status = { state: 2, description: 'connected' };
const status = { state: App.StatusEnum.CONNECTED, description: 'connected' };
expect(Actions.updateStatus(status)).toEqual({ type: Types.UPDATE_STATUS, status });
});
@ -116,7 +110,7 @@ describe('Actions', () => {
});
it('viewLogs', () => {
const logs = [{ targetType: 'room' }] as any[];
const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })];
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
});
@ -153,7 +147,7 @@ describe('Actions', () => {
});
it('accountAwaitingActivation', () => {
const options = makeConnectOptions();
const options = makePendingActivationContext();
expect(Actions.accountAwaitingActivation(options)).toEqual({ type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
});
@ -222,17 +216,17 @@ describe('Actions', () => {
});
it('notifyUser', () => {
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
});
it('serverShutdown', () => {
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
});
it('userMessage', () => {
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
});
@ -360,9 +354,8 @@ describe('Actions', () => {
});
it('gamesOfUser', () => {
const games = [makeGame({ gameId: 1 })];
const gametypeMap = { 1: 'Standard' };
expect(Actions.gamesOfUser('alice', games, gametypeMap)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap });
const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] });
expect(Actions.gamesOfUser('alice', response)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', response });
});
it('clearRegistrationErrors', () => {

View file

@ -1,16 +1,4 @@
import {
GametypeMap, WebSocketConnectOptions
} from 'types';
import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb';
import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb';
import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb';
import type { Response_WarnList } from 'generated/proto/response_warn_list_pb';
import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb';
import type { Response_DeckList } from 'generated/proto/response_deck_list_pb';
import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb';
import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb';
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
import { Data, Enriched } from '@app/types';
import { ServerStateStatus } from './server.interfaces';
import { Types } from './server.types';
@ -24,17 +12,13 @@ export const Actions = {
connectionAttempted: () => ({
type: Types.CONNECTION_ATTEMPTED
}),
loginSuccessful: (options: WebSocketConnectOptions) => ({
loginSuccessful: (options: Enriched.LoginSuccessContext) => ({
type: Types.LOGIN_SUCCESSFUL,
options
}),
loginFailed: () => ({
type: Types.LOGIN_FAILED,
}),
connectionClosed: (reason: number) => ({
type: Types.CONNECTION_CLOSED,
reason
}),
connectionFailed: () => ({
type: Types.CONNECTION_FAILED,
}),
@ -48,11 +32,11 @@ export const Actions = {
type: Types.SERVER_MESSAGE,
message
}),
updateBuddyList: (buddyList: ServerInfo_User[]) => ({
updateBuddyList: (buddyList: Data.ServerInfo_User[]) => ({
type: Types.UPDATE_BUDDY_LIST,
buddyList
}),
addToBuddyList: (user: ServerInfo_User) => ({
addToBuddyList: (user: Data.ServerInfo_User) => ({
type: Types.ADD_TO_BUDDY_LIST,
user
}),
@ -60,11 +44,11 @@ export const Actions = {
type: Types.REMOVE_FROM_BUDDY_LIST,
userName
}),
updateIgnoreList: (ignoreList: ServerInfo_User[]) => ({
updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => ({
type: Types.UPDATE_IGNORE_LIST,
ignoreList
}),
addToIgnoreList: (user: ServerInfo_User) => ({
addToIgnoreList: (user: Data.ServerInfo_User) => ({
type: Types.ADD_TO_IGNORE_LIST,
user
}),
@ -76,19 +60,19 @@ export const Actions = {
type: Types.UPDATE_INFO,
info
}),
updateStatus: (status: ServerStateStatus) => ({
updateStatus: (status: Pick<ServerStateStatus, 'state' | 'description'>) => ({
type: Types.UPDATE_STATUS,
status
}),
updateUser: (user: ServerInfo_User) => ({
updateUser: (user: Data.ServerInfo_User) => ({
type: Types.UPDATE_USER,
user
}),
updateUsers: (users: ServerInfo_User[]) => ({
updateUsers: (users: Data.ServerInfo_User[]) => ({
type: Types.UPDATE_USERS,
users
}),
userJoined: (user: ServerInfo_User) => ({
userJoined: (user: Data.ServerInfo_User) => ({
type: Types.USER_JOINED,
user
}),
@ -96,7 +80,7 @@ export const Actions = {
type: Types.USER_LEFT,
name
}),
viewLogs: (logs: ServerInfo_ChatMessage[]) => ({
viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => ({
type: Types.VIEW_LOGS,
logs
}),
@ -129,7 +113,7 @@ export const Actions = {
clearRegistrationErrors: () => ({
type: Types.CLEAR_REGISTRATION_ERRORS,
}),
accountAwaitingActivation: (options: WebSocketConnectOptions) => ({
accountAwaitingActivation: (options: Enriched.PendingActivationContext) => ({
type: Types.ACCOUNT_AWAITING_ACTIVATION,
options
}),
@ -169,27 +153,27 @@ export const Actions = {
accountPasswordChange: () => ({
type: Types.ACCOUNT_PASSWORD_CHANGE,
}),
accountEditChanged: (user: Partial<ServerInfo_User>) => ({
accountEditChanged: (user: Partial<Data.ServerInfo_User>) => ({
type: Types.ACCOUNT_EDIT_CHANGED,
user,
}),
accountImageChanged: (user: Partial<ServerInfo_User>) => ({
accountImageChanged: (user: Partial<Data.ServerInfo_User>) => ({
type: Types.ACCOUNT_IMAGE_CHANGED,
user,
}),
getUserInfo: (userInfo: ServerInfo_User) => ({
getUserInfo: (userInfo: Data.ServerInfo_User) => ({
type: Types.GET_USER_INFO,
userInfo,
}),
notifyUser: (notification: NotifyUserData) => ({
notifyUser: (notification: Data.Event_NotifyUser) => ({
type: Types.NOTIFY_USER,
notification,
}),
serverShutdown: (data: ServerShutdownData) => ({
serverShutdown: (data: Data.Event_ServerShutdown) => ({
type: Types.SERVER_SHUTDOWN,
data,
}),
userMessage: (messageData: UserMessageData) => ({
userMessage: (messageData: Data.Event_UserMessage) => ({
type: Types.USER_MESSAGE,
messageData,
}),
@ -207,17 +191,17 @@ export const Actions = {
type: Types.BAN_FROM_SERVER,
userName,
}),
banHistory: (userName: string, banHistory: ServerInfo_Ban[]) => ({
banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => ({
type: Types.BAN_HISTORY,
userName,
banHistory,
}),
warnHistory: (userName: string, warnHistory: ServerInfo_Warning[]) => ({
warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => ({
type: Types.WARN_HISTORY,
userName,
warnHistory,
}),
warnListOptions: (warnList: Response_WarnList[]) => ({
warnListOptions: (warnList: Data.Response_WarnList[]) => ({
type: Types.WARN_LIST_OPTIONS,
warnList,
}),
@ -245,17 +229,17 @@ export const Actions = {
userName,
notes,
}),
replayList: (matchList: ServerInfo_ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }),
replayAdded: (matchInfo: ServerInfo_ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }),
replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }),
replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }),
replayModifyMatch: (gameId: number, doNotHide: boolean) => ({ type: Types.REPLAY_MODIFY_MATCH, gameId, doNotHide }),
replayDeleteMatch: (gameId: number) => ({ type: Types.REPLAY_DELETE_MATCH, gameId }),
backendDecks: (deckList: Response_DeckList) => ({ type: Types.BACKEND_DECKS, deckList }),
backendDecks: (deckList: Data.Response_DeckList) => ({ type: Types.BACKEND_DECKS, deckList }),
deckNewDir: (path: string, dirName: string) => ({ type: Types.DECK_NEW_DIR, path, dirName }),
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
deckUpload: (path: string, treeItem: ServerInfo_DeckStorage_TreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) =>
({ type: Types.GAMES_OF_USER, userName, games, gametypeMap }),
gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) =>
({ type: Types.GAMES_OF_USER, userName, response }),
}
export type ServerAction = ReturnType<typeof Actions[keyof typeof Actions]>;

View file

@ -1,26 +1,22 @@
vi.mock('store', () => ({ store: { dispatch: vi.fn() } }));
vi.mock('..', () => ({ store: { dispatch: vi.fn() } }));
import { store } from 'store';
import { store } from '..';
import { Actions } from './server.actions';
import { Dispatch } from './server.dispatch';
import { App, Data } from '@app/types';
import { create } from '@bufbuild/protobuf';
import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb';
import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb';
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
import {
makeBanHistoryItem,
makeConnectOptions,
makeLoginSuccessContext,
makePendingActivationContext,
makeDeckList,
makeDeckTreeItem,
makeGame,
makeReplayMatch,
makeUser,
makeWarnHistoryItem,
makeWarnListItem,
} from './__mocks__/server-fixtures';
beforeEach(() => vi.clearAllMocks());
describe('Dispatch', () => {
it('initialized dispatches Actions.initialized()', () => {
Dispatch.initialized();
@ -38,7 +34,7 @@ describe('Dispatch', () => {
});
it('loginSuccessful dispatches Actions.loginSuccessful()', () => {
const options = makeConnectOptions();
const options = makeLoginSuccessContext();
Dispatch.loginSuccessful(options);
expect(store.dispatch).toHaveBeenCalledWith(Actions.loginSuccessful(options));
});
@ -48,11 +44,6 @@ describe('Dispatch', () => {
expect(store.dispatch).toHaveBeenCalledWith(Actions.loginFailed());
});
it('connectionClosed dispatches Actions.connectionClosed()', () => {
Dispatch.connectionClosed(3);
expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionClosed(3));
});
it('connectionFailed dispatches Actions.connectionFailed()', () => {
Dispatch.connectionFailed();
expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionFailed());
@ -108,8 +99,8 @@ describe('Dispatch', () => {
});
it('updateStatus dispatches Actions.updateStatus({ state, description })', () => {
Dispatch.updateStatus(2, 'ok');
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: 2, description: 'ok' }));
Dispatch.updateStatus(App.StatusEnum.CONNECTED, 'ok');
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: App.StatusEnum.CONNECTED, description: 'ok' }));
});
it('updateUser dispatches Actions.updateUser()', () => {
@ -136,7 +127,7 @@ describe('Dispatch', () => {
});
it('viewLogs dispatches Actions.viewLogs()', () => {
const logs = [{ targetType: 'room' }] as any[];
const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })];
Dispatch.viewLogs(logs);
expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
});
@ -187,7 +178,7 @@ describe('Dispatch', () => {
});
it('accountAwaitingActivation dispatches correctly', () => {
const options = makeConnectOptions();
const options = makePendingActivationContext();
Dispatch.accountAwaitingActivation(options);
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountAwaitingActivation(options));
});
@ -266,19 +257,19 @@ describe('Dispatch', () => {
});
it('notifyUser dispatches correctly', () => {
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
Dispatch.notifyUser(notification);
expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
});
it('serverShutdown dispatches correctly', () => {
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
Dispatch.serverShutdown(data);
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
});
it('userMessage dispatches correctly', () => {
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
Dispatch.userMessage(messageData);
expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
});
@ -391,10 +382,9 @@ describe('Dispatch', () => {
});
it('gamesOfUser dispatches correctly', () => {
const games = [makeGame({ gameId: 1 })];
const gametypeMap = { 1: 'Standard' };
Dispatch.gamesOfUser('alice', games, gametypeMap);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games, gametypeMap));
const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] });
Dispatch.gamesOfUser('alice', response);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', response));
});
it('clearRegistrationErrors dispatches correctly', () => {

View file

@ -1,18 +1,6 @@
import { Actions } from './server.actions';
import { store } from 'store';
import {
GametypeMap, WebSocketConnectOptions
} from 'types';
import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb';
import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb';
import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb';
import type { Response_WarnList } from 'generated/proto/response_warn_list_pb';
import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb';
import type { Response_DeckList } from 'generated/proto/response_deck_list_pb';
import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb';
import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb';
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
import { store } from '..';
import { App, Data, Enriched } from '@app/types';
export const Dispatch = {
initialized: () => {
@ -24,15 +12,12 @@ export const Dispatch = {
connectionAttempted: () => {
store.dispatch(Actions.connectionAttempted());
},
loginSuccessful: (options: WebSocketConnectOptions) => {
loginSuccessful: (options: Enriched.LoginSuccessContext) => {
store.dispatch(Actions.loginSuccessful(options));
},
loginFailed: () => {
store.dispatch(Actions.loginFailed());
},
connectionClosed: (reason: number) => {
store.dispatch(Actions.connectionClosed(reason));
},
connectionFailed: () => {
store.dispatch(Actions.connectionFailed());
},
@ -42,19 +27,19 @@ export const Dispatch = {
testConnectionFailed: () => {
store.dispatch(Actions.testConnectionFailed());
},
updateBuddyList: (buddyList: ServerInfo_User[]) => {
updateBuddyList: (buddyList: Data.ServerInfo_User[]) => {
store.dispatch(Actions.updateBuddyList(buddyList));
},
addToBuddyList: (user: ServerInfo_User) => {
addToBuddyList: (user: Data.ServerInfo_User) => {
store.dispatch(Actions.addToBuddyList(user));
},
removeFromBuddyList: (userName: string) => {
store.dispatch(Actions.removeFromBuddyList(userName));
},
updateIgnoreList: (ignoreList: ServerInfo_User[]) => {
updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => {
store.dispatch(Actions.updateIgnoreList(ignoreList));
},
addToIgnoreList: (user: ServerInfo_User) => {
addToIgnoreList: (user: Data.ServerInfo_User) => {
store.dispatch(Actions.addToIgnoreList(user));
},
removeFromIgnoreList: (userName: string) => {
@ -66,25 +51,25 @@ export const Dispatch = {
version
}));
},
updateStatus: (state: number, description: string) => {
updateStatus: (state: App.StatusEnum, description: string) => {
store.dispatch(Actions.updateStatus({
state,
description
}));
},
updateUser: (user: ServerInfo_User) => {
updateUser: (user: Data.ServerInfo_User) => {
store.dispatch(Actions.updateUser(user));
},
updateUsers: (users: ServerInfo_User[]) => {
updateUsers: (users: Data.ServerInfo_User[]) => {
store.dispatch(Actions.updateUsers(users));
},
userJoined: (user: ServerInfo_User) => {
userJoined: (user: Data.ServerInfo_User) => {
store.dispatch(Actions.userJoined(user));
},
userLeft: (name: string) => {
store.dispatch(Actions.userLeft(name));
},
viewLogs: (logs: ServerInfo_ChatMessage[]) => {
viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => {
store.dispatch(Actions.viewLogs(logs));
},
clearLogs: () => {
@ -114,7 +99,7 @@ export const Dispatch = {
registrationUserNameError: (error: string) => {
store.dispatch(Actions.registrationUserNameError(error));
},
accountAwaitingActivation: (options: WebSocketConnectOptions) => {
accountAwaitingActivation: (options: Enriched.PendingActivationContext) => {
store.dispatch(Actions.accountAwaitingActivation(options));
},
accountActivationSuccess: () => {
@ -150,22 +135,22 @@ export const Dispatch = {
accountPasswordChange: () => {
store.dispatch(Actions.accountPasswordChange());
},
accountEditChanged: (user: Partial<ServerInfo_User>) => {
accountEditChanged: (user: Partial<Data.ServerInfo_User>) => {
store.dispatch(Actions.accountEditChanged(user));
},
accountImageChanged: (user: Partial<ServerInfo_User>) => {
accountImageChanged: (user: Partial<Data.ServerInfo_User>) => {
store.dispatch(Actions.accountImageChanged(user));
},
getUserInfo: (userInfo: ServerInfo_User) => {
getUserInfo: (userInfo: Data.ServerInfo_User) => {
store.dispatch(Actions.getUserInfo(userInfo));
},
notifyUser: (notification: NotifyUserData) => {
notifyUser: (notification: Data.Event_NotifyUser) => {
store.dispatch(Actions.notifyUser(notification))
},
serverShutdown: (data: ServerShutdownData) => {
serverShutdown: (data: Data.Event_ServerShutdown) => {
store.dispatch(Actions.serverShutdown(data))
},
userMessage: (messageData: UserMessageData) => {
userMessage: (messageData: Data.Event_UserMessage) => {
store.dispatch(Actions.userMessage(messageData))
},
addToList: (list: string, userName: string) => {
@ -177,13 +162,13 @@ export const Dispatch = {
banFromServer: (userName: string) => {
store.dispatch(Actions.banFromServer(userName));
},
banHistory: (userName: string, banHistory: ServerInfo_Ban[]) => {
banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => {
store.dispatch(Actions.banHistory(userName, banHistory))
},
warnHistory: (userName: string, warnHistory: ServerInfo_Warning[]) => {
warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => {
store.dispatch(Actions.warnHistory(userName, warnHistory))
},
warnListOptions: (warnList: Response_WarnList[]) => {
warnListOptions: (warnList: Data.Response_WarnList[]) => {
store.dispatch(Actions.warnListOptions(warnList))
},
warnUser: (userName: string) => {
@ -201,10 +186,10 @@ export const Dispatch = {
updateAdminNotes: (userName: string, notes: string) => {
store.dispatch(Actions.updateAdminNotes(userName, notes));
},
replayList: (matchList: ServerInfo_ReplayMatch[]) => {
replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => {
store.dispatch(Actions.replayList(matchList));
},
replayAdded: (matchInfo: ServerInfo_ReplayMatch) => {
replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => {
store.dispatch(Actions.replayAdded(matchInfo));
},
replayModifyMatch: (gameId: number, doNotHide: boolean) => {
@ -213,7 +198,7 @@ export const Dispatch = {
replayDeleteMatch: (gameId: number) => {
store.dispatch(Actions.replayDeleteMatch(gameId));
},
backendDecks: (deckList: Response_DeckList) => {
backendDecks: (deckList: Data.Response_DeckList) => {
store.dispatch(Actions.backendDecks(deckList));
},
deckNewDir: (path: string, dirName: string) => {
@ -222,13 +207,13 @@ export const Dispatch = {
deckDelDir: (path: string) => {
store.dispatch(Actions.deckDelDir(path));
},
deckUpload: (path: string, treeItem: ServerInfo_DeckStorage_TreeItem) => {
deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => {
store.dispatch(Actions.deckUpload(path, treeItem));
},
deckDelete: (deckId: number) => {
store.dispatch(Actions.deckDelete(deckId));
},
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) => {
store.dispatch(Actions.gamesOfUser(userName, games, gametypeMap));
gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) => {
store.dispatch(Actions.gamesOfUser(userName, response));
},
}

View file

@ -1,105 +1,57 @@
import {
Game, SortBy, UserSortField
} from 'types';
import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb';
import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb';
import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb';
import type { Response_WarnList } from 'generated/proto/response_warn_list_pb';
import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb';
import type { Response_DeckList } from 'generated/proto/response_deck_list_pb';
import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb';
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
export interface ServerConnectParams {
host: string;
port: string;
userName: string;
password: string;
}
export interface ServerRegisterParams {
host: string;
port: string;
userName: string;
password: string;
email: string;
country: string;
realName: string;
}
export interface RequestPasswordSaltParams {
userName: string;
}
export interface ForgotPasswordParams {
userName: string;
}
export interface ForgotPasswordChallengeParams extends ForgotPasswordParams {
email: string;
}
export interface ForgotPasswordResetParams extends ForgotPasswordParams {
token: string;
newPassword: string;
}
export interface AccountActivationParams extends ServerRegisterParams {
token: string;
}
import { App, Data, Enriched } from '@app/types';
export interface ServerState {
initialized: boolean;
buddyList: ServerInfo_User[];
ignoreList: ServerInfo_User[];
buddyList: Data.ServerInfo_User[];
ignoreList: Data.ServerInfo_User[];
info: ServerStateInfo;
status: ServerStateStatus;
logs: ServerStateLogs;
user: ServerInfo_User;
users: ServerInfo_User[];
user: Data.ServerInfo_User | null;
users: Data.ServerInfo_User[];
sortUsersBy: ServerStateSortUsersBy;
messages: {
[userName: string]: UserMessageData[];
[userName: string]: Data.Event_UserMessage[];
}
userInfo: {
[userName: string]: ServerInfo_User;
[userName: string]: Data.ServerInfo_User;
}
notifications: NotifyUserData[];
serverShutdown: ServerShutdownData;
notifications: Data.Event_NotifyUser[];
serverShutdown: Data.Event_ServerShutdown | null;
banUser: string;
banHistory: {
[userName: string]: ServerInfo_Ban[];
[userName: string]: Data.ServerInfo_Ban[];
};
warnHistory: {
[userName: string]: ServerInfo_Warning[];
[userName: string]: Data.ServerInfo_Warning[];
};
warnListOptions: Response_WarnList[];
warnListOptions: Data.Response_WarnList[];
warnUser: string;
adminNotes: { [userName: string]: string };
replays: ServerInfo_ReplayMatch[];
backendDecks: Response_DeckList | null;
gamesOfUser: { [userName: string]: Game[] };
replays: Data.ServerInfo_ReplayMatch[];
backendDecks: Data.Response_DeckList | null;
gamesOfUser: { [userName: string]: Enriched.Game[] };
registrationError: string | null;
}
export interface ServerStateStatus {
connectionAttemptMade: boolean;
description: string;
state: number;
description: string | null;
state: App.StatusEnum;
}
export interface ServerStateInfo {
message: string;
name: string;
version: string;
message: string | null;
name: string | null;
version: string | null;
}
export interface ServerStateLogs {
room: ServerInfo_ChatMessage[];
game: ServerInfo_ChatMessage[];
chat: ServerInfo_ChatMessage[];
room: Data.ServerInfo_ChatMessage[];
game: Data.ServerInfo_ChatMessage[];
chat: Data.ServerInfo_ChatMessage[];
}
export interface ServerStateSortUsersBy extends SortBy {
field: UserSortField
export interface ServerStateSortUsersBy extends App.SortBy {
field: App.UserSortField
}

View file

@ -1,13 +1,10 @@
import { StatusEnum } from 'types';
import { ServerInfo_User_UserLevelFlag as UserLevelFlag } from 'generated/proto/serverinfo_user_pb';
import { App, Data } from '@app/types';
import { create } from '@bufbuild/protobuf';
import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb';
import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb';
import { serverReducer } from './server.reducer';
import { Types } from './server.types';
import {
makeBanHistoryItem,
makeConnectOptions,
makePendingActivationContext,
makeDeckList,
makeDeckTreeItem,
makeGame,
@ -19,6 +16,8 @@ import {
makeWarnListItem,
} from './__mocks__/server-fixtures';
const UserLevelFlag = Data.ServerInfo_User_UserLevelFlag;
// ── Initialisation ───────────────────────────────────────────────────────────
describe('Initialisation', () => {
@ -26,7 +25,7 @@ describe('Initialisation', () => {
const result = serverReducer(undefined, { type: '@@INIT' });
expect(result.initialized).toBe(false);
expect(result.buddyList).toEqual([]);
expect(result.status.state).toBe(StatusEnum.DISCONNECTED);
expect(result.status.state).toBe(App.StatusEnum.DISCONNECTED);
});
it('INITIALIZED → resets to initialState with initialized: true', () => {
@ -38,7 +37,7 @@ describe('Initialisation', () => {
});
it('CLEAR_STORE → resets to initialState but preserves status', () => {
const status = { state: StatusEnum.LOGGED_IN, description: 'logged in' };
const status = { state: App.StatusEnum.LOGGED_IN, description: 'logged in', connectionAttemptMade: true };
const state = makeServerState({ status, banUser: 'someone' });
const result = serverReducer(state, { type: Types.CLEAR_STORE });
expect(result.banUser).toBe('');
@ -57,13 +56,13 @@ describe('Initialisation', () => {
describe('Account & Connection', () => {
it('CONNECTION_ATTEMPTED → sets connectionAttemptMade to true', () => {
const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.DISCONNECTED, description: null } });
const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.DISCONNECTED, description: null } });
const result = serverReducer(state, { type: Types.CONNECTION_ATTEMPTED });
expect(result.status.connectionAttemptMade).toBe(true);
});
it('ACCOUNT_AWAITING_ACTIVATION → returns state unchanged', () => {
const options = makeConnectOptions();
const options = makePendingActivationContext();
const state = makeServerState();
const result = serverReducer(state, { type: Types.ACCOUNT_AWAITING_ACTIVATION, options });
expect(result).toBe(state);
@ -133,11 +132,13 @@ describe('Server Info & Status', () => {
expect(result.info.message).toBe('hi');
});
it('UPDATE_STATUS → replaces state.status entirely', () => {
it('UPDATE_STATUS → merges state and description into status', () => {
const state = makeServerState();
const status = { state: StatusEnum.LOGGED_IN, description: 'ok' };
const result = serverReducer(state, { type: Types.UPDATE_STATUS, status });
expect(result.status).toEqual(status);
const update = { state: App.StatusEnum.LOGGED_IN, description: 'ok' };
const result = serverReducer(state, { type: Types.UPDATE_STATUS, status: update });
expect(result.status.state).toBe(App.StatusEnum.LOGGED_IN);
expect(result.status.description).toBe('ok');
expect(result.status.connectionAttemptMade).toBe(false);
});
});
@ -281,12 +282,12 @@ describe('Messaging', () => {
});
it('USER_MESSAGE → appends to existing messages for that user', () => {
const existingMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' });
const existingMsg = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' });
const state = makeServerState({
user: makeUser({ name: 'Bob' }),
messages: { Alice: [existingMsg] },
});
const newMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' });
const newMsg = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' });
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
expect(result.messages['Alice']).toHaveLength(2);
});
@ -482,11 +483,11 @@ describe('Deck Storage', () => {
});
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'myDecks', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'myDecks', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
});
const item = makeDeckTreeItem({ name: 'new.cod' });
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item });
@ -512,18 +513,20 @@ describe('Deck Storage', () => {
it('DECK_DELETE → removes item by id from tree', () => {
const item = makeDeckTreeItem({ id: 7 });
const state = makeServerState({ backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }) });
const state = makeServerState({
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }),
});
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 });
expect(result.backendDecks.root.items).toHaveLength(0);
});
it('DECK_DELETE → recursively removes item nested inside a subfolder', () => {
const nested = makeDeckTreeItem({ id: 9, name: 'nested.cod' });
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'sub', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [nested] })
const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'sub', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [nested] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
});
const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 9 });
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
@ -544,11 +547,11 @@ describe('Deck Storage', () => {
});
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'parent', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
});
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: 'parent', dirName: 'child' });
const parent = result.backendDecks.root.items.find(i => i.name === 'parent');
@ -563,36 +566,36 @@ describe('Deck Storage', () => {
});
it('DECK_DEL_DIR → removes folder from root by name', () => {
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'myDir', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'myDir', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
});
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' });
expect(result.backendDecks.root.items).toHaveLength(0);
});
it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => {
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'keep', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'keep', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
});
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' });
expect(result.backendDecks.root.items).toHaveLength(1);
});
it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => {
const child = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'child', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const child = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'child', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
const parent = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [child] })
const parent = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: 'parent', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [child] })
});
const state = makeServerState({
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) })
backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) })
});
const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' });
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
@ -603,24 +606,31 @@ describe('Deck Storage', () => {
describe('GAMES_OF_USER', () => {
it('stores normalized games keyed by userName', () => {
const games = [makeGame({ gameId: 5 })];
const response = create(Data.Response_GetGamesOfUserSchema, {
gameList: [create(Data.ServerInfo_GameSchema, { gameId: 5, description: '' })],
roomList: [],
});
const state = makeServerState();
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap: {} });
expect(result.gamesOfUser['alice']).toEqual(games);
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 5 })]);
});
it('overwrites previous games for same user', () => {
const old = [makeGame({ gameId: 1 })];
const fresh = [makeGame({ gameId: 2 })];
const response = create(Data.Response_GetGamesOfUserSchema, {
gameList: [create(Data.ServerInfo_GameSchema, { gameId: 2, description: '' })],
roomList: [],
});
const state = makeServerState({ gamesOfUser: { alice: old } });
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh, gametypeMap: {} });
expect(result.gamesOfUser['alice']).toEqual(fresh);
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 2 })]);
});
it('does not affect other users\' entries', () => {
const bobGames = [makeGame({ gameId: 3 })];
const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [], roomList: [] });
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [], gametypeMap: {} });
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response });
expect(result.gamesOfUser['bob']).toBe(bobGames);
});
});

View file

@ -1,11 +1,7 @@
import { SortDirection, StatusEnum, UserSortField } from 'types';
import { ServerInfo_User_UserLevelFlag } from 'generated/proto/serverinfo_user_pb';
import type { ServerInfo_DeckStorage_Folder, ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb';
import { App, Data } from '@app/types';
import { create } from '@bufbuild/protobuf';
import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb';
import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb';
import { normalizeBannedUserError, normalizeGameObject, normalizeLogs, SortUtil } from '../common';
import { normalizeBannedUserError, normalizeGameObject, normalizeGametypeMap, normalizeLogs, SortUtil } from '../common';
import { ServerAction } from './server.actions';
import { ServerState } from './server.interfaces'
@ -16,17 +12,17 @@ function splitPath(path: string): string[] {
}
function insertAtPath(
folder: ServerInfo_DeckStorage_Folder,
folder: Data.ServerInfo_DeckStorage_Folder,
pathSegments: string[],
item: ServerInfo_DeckStorage_TreeItem,
): ServerInfo_DeckStorage_Folder {
item: Data.ServerInfo_DeckStorage_TreeItem,
): Data.ServerInfo_DeckStorage_Folder {
if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) {
return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, item] });
return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, item] });
}
const [head, ...tail] = pathSegments;
const match = folder.items.find(child => child.name === head && child.folder);
if (match) {
return create(ServerInfo_DeckStorage_FolderSchema, {
return create(Data.ServerInfo_DeckStorage_FolderSchema, {
items: folder.items.map(child =>
child === match
? { ...child, folder: insertAtPath(child.folder!, tail, item) }
@ -34,14 +30,14 @@ function insertAtPath(
),
});
}
const created: ServerInfo_DeckStorage_TreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: head, folder: insertAtPath(create(ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item)
const created: Data.ServerInfo_DeckStorage_TreeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: head, folder: insertAtPath(create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item)
});
return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] });
return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] });
}
function removeById(folder: ServerInfo_DeckStorage_Folder, id: number): ServerInfo_DeckStorage_Folder {
return create(ServerInfo_DeckStorage_FolderSchema, {
function removeById(folder: Data.ServerInfo_DeckStorage_Folder, id: number): Data.ServerInfo_DeckStorage_Folder {
return create(Data.ServerInfo_DeckStorage_FolderSchema, {
items: folder.items
.filter(item => item.id !== id)
.map(item =>
@ -50,17 +46,17 @@ function removeById(folder: ServerInfo_DeckStorage_Folder, id: number): ServerIn
});
}
function removeByPath(folder: ServerInfo_DeckStorage_Folder, pathSegments: string[]): ServerInfo_DeckStorage_Folder {
function removeByPath(folder: Data.ServerInfo_DeckStorage_Folder, pathSegments: string[]): Data.ServerInfo_DeckStorage_Folder {
if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) {
return folder;
}
const [head, ...tail] = pathSegments;
if (tail.length === 0) {
return create(ServerInfo_DeckStorage_FolderSchema, {
return create(Data.ServerInfo_DeckStorage_FolderSchema, {
items: folder.items.filter(item => !(item.name === head && item.folder != null))
});
}
return create(ServerInfo_DeckStorage_FolderSchema, {
return create(Data.ServerInfo_DeckStorage_FolderSchema, {
items: folder.items.map(item =>
item.name === head && item.folder
? { ...item, folder: removeByPath(item.folder, tail) }
@ -76,7 +72,7 @@ const initialState: ServerState = {
status: {
connectionAttemptMade: false,
state: StatusEnum.DISCONNECTED,
state: App.StatusEnum.DISCONNECTED,
description: null
},
info: {
@ -92,8 +88,8 @@ const initialState: ServerState = {
user: null,
users: [],
sortUsersBy: {
field: UserSortField.NAME,
order: SortDirection.ASC
field: App.UserSortField.NAME,
order: App.SortDirection.ASC
},
messages: {},
userInfo: {},
@ -232,11 +228,19 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
}
case Types.UPDATE_STATUS: {
const { status } = action;
return {
const newState = {
...state,
status: { ...status }
status: { ...state.status, ...status }
};
if (status.state === App.StatusEnum.DISCONNECTED) {
return {
...newState,
status: { ...newState.status, connectionAttemptMade: false }
};
}
return newState;
}
case Types.UPDATE_USER:
case Types.ACCOUNT_EDIT_CHANGED:
@ -417,11 +421,11 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
}
let newLevel = user.userLevel;
newLevel = shouldBeMod
? (newLevel | ServerInfo_User_UserLevelFlag.IsModerator)
: (newLevel & ~ServerInfo_User_UserLevelFlag.IsModerator);
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsModerator)
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsModerator);
newLevel = shouldBeJudge
? (newLevel | ServerInfo_User_UserLevelFlag.IsJudge)
: (newLevel & ~ServerInfo_User_UserLevelFlag.IsJudge);
? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsJudge)
: (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsJudge);
return {
...user,
userLevel: newLevel,
@ -455,7 +459,7 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
}
return {
...state,
backendDecks: create(Response_DeckListSchema, {
backendDecks: create(Data.Response_DeckListSchema, {
root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem),
}),
};
@ -466,7 +470,7 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
}
return {
...state,
backendDecks: create(Response_DeckListSchema, {
backendDecks: create(Data.Response_DeckListSchema, {
root: removeById(state.backendDecks.root, action.deckId),
}),
};
@ -475,12 +479,12 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
if (!state.backendDecks?.root) {
return state;
}
const newFolder: ServerInfo_DeckStorage_TreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: action.dirName, folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
const newFolder: Data.ServerInfo_DeckStorage_TreeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, {
id: 0, name: action.dirName, folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] })
});
return {
...state,
backendDecks: create(Response_DeckListSchema, {
backendDecks: create(Data.Response_DeckListSchema, {
root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder),
}),
};
@ -491,14 +495,17 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
}
return {
...state,
backendDecks: create(Response_DeckListSchema, {
backendDecks: create(Data.Response_DeckListSchema, {
root: removeByPath(state.backendDecks.root, splitPath(action.path)),
}),
};
}
case Types.GAMES_OF_USER: {
const { userName, games, gametypeMap } = action;
const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap));
const { userName, response } = action;
const gametypeMap = normalizeGametypeMap(
(response.roomList ?? []).flatMap(room => room.gametypeList ?? [])
);
const normalizedGames = (response.gameList ?? []).map(g => normalizeGameObject(g, gametypeMap));
return {
...state,
gamesOfUser: {
@ -518,7 +525,6 @@ export const serverReducer = (state = initialState, action: ServerAction) => {
// Signal-only action types — no state mutation, explicit for discriminated-union exhaustiveness
case Types.LOGIN_SUCCESSFUL:
case Types.LOGIN_FAILED:
case Types.CONNECTION_CLOSED:
case Types.CONNECTION_FAILED:
case Types.TEST_CONNECTION_SUCCESSFUL:
case Types.TEST_CONNECTION_FAILED:

View file

@ -6,7 +6,7 @@ import {
makeServerState,
makeUser,
} from './__mocks__/server-fixtures';
import { StatusEnum } from 'types';
import { App } from '@app/types';
function rootState(server: ServerState) {
return { server };
@ -34,17 +34,17 @@ describe('Selectors', () => {
});
it('getDescription → returns status.description', () => {
const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.CONNECTED, description: 'ok' } });
const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.CONNECTED, description: 'ok' } });
expect(Selectors.getDescription(rootState(state))).toBe('ok');
});
it('getState → returns status.state', () => {
const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.LOGGED_IN, description: null } });
expect(Selectors.getState(rootState(state))).toBe(StatusEnum.LOGGED_IN);
const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.LOGGED_IN, description: null } });
expect(Selectors.getState(rootState(state))).toBe(App.StatusEnum.LOGGED_IN);
});
it('getConnectionAttemptMade → returns status.connectionAttemptMade', () => {
const state = makeServerState({ status: { connectionAttemptMade: true, state: StatusEnum.DISCONNECTED, description: null } });
const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.DISCONNECTED, description: null } });
expect(Selectors.getConnectionAttemptMade(rootState(state))).toBe(true);
});

View file

@ -4,7 +4,6 @@ export const Types = {
CONNECTION_ATTEMPTED: '[Server] Connection Attempted',
LOGIN_SUCCESSFUL: '[Server] Login Successful',
LOGIN_FAILED: '[Server] Login Failed',
CONNECTION_CLOSED: '[Server] Connection Closed',
CONNECTION_FAILED: '[Server] Connection Failed',
TEST_CONNECTION_SUCCESSFUL: '[Server] Test Connection Successful',
TEST_CONNECTION_FAILED: '[Server] Test Connection Failed',