mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
upgrade packages + improve typing
This commit is contained in:
parent
fd55f4fb7f
commit
19f5eefdd2
138 changed files with 4504 additions and 11015 deletions
|
|
@ -2,7 +2,9 @@ import {
|
|||
BanHistoryItem,
|
||||
DeckList,
|
||||
DeckStorageTreeItem,
|
||||
Game,
|
||||
LogItem,
|
||||
ProtoInit,
|
||||
ReplayMatch,
|
||||
SortDirection,
|
||||
StatusEnum,
|
||||
|
|
@ -12,20 +14,30 @@ import {
|
|||
WarnHistoryItem,
|
||||
WarnListItem,
|
||||
} from 'types';
|
||||
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: Partial<User> = {}): User {
|
||||
return {
|
||||
export function makeUser(overrides: ProtoInit<User> = {}): User {
|
||||
return create(ServerInfo_UserSchema, {
|
||||
name: 'TestUser',
|
||||
accountageSecs: 0n,
|
||||
privlevel: '',
|
||||
userLevel: 0,
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeLogItem(overrides: Partial<LogItem> = {}): LogItem {
|
||||
return {
|
||||
export function makeLogItem(overrides: ProtoInit<LogItem> = {}): LogItem {
|
||||
return create(ServerInfo_ChatMessageSchema, {
|
||||
message: '',
|
||||
senderId: '',
|
||||
senderIp: '',
|
||||
|
|
@ -35,11 +47,11 @@ export function makeLogItem(overrides: Partial<LogItem> = {}): LogItem {
|
|||
targetType: '',
|
||||
time: '',
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeBanHistoryItem(overrides: Partial<BanHistoryItem> = {}): BanHistoryItem {
|
||||
return {
|
||||
export function makeBanHistoryItem(overrides: ProtoInit<BanHistoryItem> = {}): BanHistoryItem {
|
||||
return create(ServerInfo_BanSchema, {
|
||||
adminId: '',
|
||||
adminName: '',
|
||||
banTime: '',
|
||||
|
|
@ -47,47 +59,45 @@ export function makeBanHistoryItem(overrides: Partial<BanHistoryItem> = {}): Ban
|
|||
banReason: '',
|
||||
visibleReason: '',
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeWarnHistoryItem(overrides: Partial<WarnHistoryItem> = {}): WarnHistoryItem {
|
||||
return {
|
||||
export function makeWarnHistoryItem(overrides: ProtoInit<WarnHistoryItem> = {}): WarnHistoryItem {
|
||||
return create(ServerInfo_WarningSchema, {
|
||||
userName: '',
|
||||
adminName: '',
|
||||
reason: '',
|
||||
timeOf: '',
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeWarnListItem(overrides: Partial<WarnListItem> = {}): WarnListItem {
|
||||
return {
|
||||
warning: '',
|
||||
export function makeWarnListItem(overrides: ProtoInit<WarnListItem> = {}): WarnListItem {
|
||||
return create(Response_WarnListSchema, {
|
||||
warning: [],
|
||||
userName: '',
|
||||
userClientid: '',
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeDeckTreeItem(overrides: Partial<DeckStorageTreeItem> = {}): DeckStorageTreeItem {
|
||||
return {
|
||||
export function makeDeckTreeItem(overrides: ProtoInit<DeckStorageTreeItem> = {}): DeckStorageTreeItem {
|
||||
return create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 1,
|
||||
name: 'item',
|
||||
file: { creationTime: 0 },
|
||||
folder: null,
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeDeckList(overrides: Partial<DeckList> = {}): DeckList {
|
||||
return {
|
||||
root: { items: [] },
|
||||
export function makeDeckList(overrides: ProtoInit<DeckList> = {}): DeckList {
|
||||
return create(Response_DeckListSchema, {
|
||||
root: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }),
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeReplayMatch(overrides: Partial<ReplayMatch> = {}): ReplayMatch {
|
||||
return {
|
||||
export function makeReplayMatch(overrides: ProtoInit<ReplayMatch> = {}): ReplayMatch {
|
||||
return create(ServerInfo_ReplayMatchSchema, {
|
||||
gameId: 1,
|
||||
roomName: 'Test Room',
|
||||
timeStarted: 0,
|
||||
|
|
@ -97,7 +107,11 @@ export function makeReplayMatch(overrides: Partial<ReplayMatch> = {}): ReplayMat
|
|||
doNotHide: false,
|
||||
replayList: [],
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function makeGame(overrides: Partial<Game> = {}): Game {
|
||||
return { ...create(ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides };
|
||||
}
|
||||
|
||||
export function makeConnectOptions(overrides: Partial<WebSocketConnectOptions> = {}): WebSocketConnectOptions {
|
||||
|
|
@ -148,6 +162,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
|
|||
replays: [],
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
registrationError: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { Actions } from './server.actions';
|
||||
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,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeReplayMatch,
|
||||
makeGame,
|
||||
makeUser,
|
||||
makeWarnHistoryItem,
|
||||
makeWarnListItem,
|
||||
|
|
@ -107,7 +112,7 @@ describe('Actions', () => {
|
|||
});
|
||||
|
||||
it('viewLogs', () => {
|
||||
const logs = { room: [], game: [], chat: [] };
|
||||
const logs = [{ targetType: 'room' }] as any[];
|
||||
expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs });
|
||||
});
|
||||
|
||||
|
|
@ -124,7 +129,11 @@ describe('Actions', () => {
|
|||
});
|
||||
|
||||
it('registrationFailed', () => {
|
||||
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, error: 'err' });
|
||||
expect(Actions.registrationFailed('err', 999)).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: 999 });
|
||||
});
|
||||
|
||||
it('registrationFailed without endTime', () => {
|
||||
expect(Actions.registrationFailed('err')).toEqual({ type: Types.REGISTRATION_FAILED, reason: 'err', endTime: undefined });
|
||||
});
|
||||
|
||||
it('registrationEmailError', () => {
|
||||
|
|
@ -209,17 +218,17 @@ describe('Actions', () => {
|
|||
});
|
||||
|
||||
it('notifyUser', () => {
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
||||
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||
expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification });
|
||||
});
|
||||
|
||||
it('serverShutdown', () => {
|
||||
const data = { reason: 'maintenance', minutes: 5 };
|
||||
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||
expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data });
|
||||
});
|
||||
|
||||
it('userMessage', () => {
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
||||
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||
expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData });
|
||||
});
|
||||
|
||||
|
|
@ -347,7 +356,8 @@ describe('Actions', () => {
|
|||
});
|
||||
|
||||
it('gamesOfUser', () => {
|
||||
const games = [{ gameId: 1 }] as any;
|
||||
expect(Actions.gamesOfUser('alice', games)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games });
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
import {
|
||||
BanHistoryItem, DeckList, DeckStorageTreeItem, GametypeMap, LogItem, ReplayMatch,
|
||||
User, WebSocketConnectOptions, WarnHistoryItem, WarnListItem
|
||||
} from 'types';
|
||||
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||
import { ServerStateStatus } from './server.interfaces';
|
||||
import { Types } from './server.types';
|
||||
|
||||
export const Actions = {
|
||||
|
|
@ -15,7 +21,7 @@ export const Actions = {
|
|||
loginFailed: () => ({
|
||||
type: Types.LOGIN_FAILED,
|
||||
}),
|
||||
connectionClosed: reason => ({
|
||||
connectionClosed: (reason: number) => ({
|
||||
type: Types.CONNECTION_CLOSED,
|
||||
reason
|
||||
}),
|
||||
|
|
@ -28,59 +34,59 @@ export const Actions = {
|
|||
testConnectionFailed: () => ({
|
||||
type: Types.TEST_CONNECTION_FAILED,
|
||||
}),
|
||||
serverMessage: message => ({
|
||||
serverMessage: (message: string) => ({
|
||||
type: Types.SERVER_MESSAGE,
|
||||
message
|
||||
}),
|
||||
updateBuddyList: buddyList => ({
|
||||
updateBuddyList: (buddyList: User[]) => ({
|
||||
type: Types.UPDATE_BUDDY_LIST,
|
||||
buddyList
|
||||
}),
|
||||
addToBuddyList: user => ({
|
||||
addToBuddyList: (user: User) => ({
|
||||
type: Types.ADD_TO_BUDDY_LIST,
|
||||
user
|
||||
}),
|
||||
removeFromBuddyList: userName => ({
|
||||
removeFromBuddyList: (userName: string) => ({
|
||||
type: Types.REMOVE_FROM_BUDDY_LIST,
|
||||
userName
|
||||
}),
|
||||
updateIgnoreList: ignoreList => ({
|
||||
updateIgnoreList: (ignoreList: User[]) => ({
|
||||
type: Types.UPDATE_IGNORE_LIST,
|
||||
ignoreList
|
||||
}),
|
||||
addToIgnoreList: user => ({
|
||||
addToIgnoreList: (user: User) => ({
|
||||
type: Types.ADD_TO_IGNORE_LIST,
|
||||
user
|
||||
}),
|
||||
removeFromIgnoreList: userName => ({
|
||||
removeFromIgnoreList: (userName: string) => ({
|
||||
type: Types.REMOVE_FROM_IGNORE_LIST,
|
||||
userName
|
||||
}),
|
||||
updateInfo: info => ({
|
||||
updateInfo: (info: { name: string; version: string }) => ({
|
||||
type: Types.UPDATE_INFO,
|
||||
info
|
||||
}),
|
||||
updateStatus: status => ({
|
||||
updateStatus: (status: ServerStateStatus) => ({
|
||||
type: Types.UPDATE_STATUS,
|
||||
status
|
||||
}),
|
||||
updateUser: user => ({
|
||||
updateUser: (user: User) => ({
|
||||
type: Types.UPDATE_USER,
|
||||
user
|
||||
}),
|
||||
updateUsers: users => ({
|
||||
updateUsers: (users: User[]) => ({
|
||||
type: Types.UPDATE_USERS,
|
||||
users
|
||||
}),
|
||||
userJoined: user => ({
|
||||
userJoined: (user: User) => ({
|
||||
type: Types.USER_JOINED,
|
||||
user
|
||||
}),
|
||||
userLeft: name => ({
|
||||
userLeft: (name: string) => ({
|
||||
type: Types.USER_LEFT,
|
||||
name
|
||||
}),
|
||||
viewLogs: logs => ({
|
||||
viewLogs: (logs: LogItem[]) => ({
|
||||
type: Types.VIEW_LOGS,
|
||||
logs
|
||||
}),
|
||||
|
|
@ -93,22 +99,26 @@ export const Actions = {
|
|||
registrationSuccess: () => ({
|
||||
type: Types.REGISTRATION_SUCCESS,
|
||||
}),
|
||||
registrationFailed: (error) => ({
|
||||
registrationFailed: (reason: string, endTime?: number) => ({
|
||||
type: Types.REGISTRATION_FAILED,
|
||||
error
|
||||
reason,
|
||||
endTime,
|
||||
}),
|
||||
registrationEmailError: (error) => ({
|
||||
registrationEmailError: (error: string) => ({
|
||||
type: Types.REGISTRATION_EMAIL_ERROR,
|
||||
error
|
||||
}),
|
||||
registrationPasswordError: (error) => ({
|
||||
registrationPasswordError: (error: string) => ({
|
||||
type: Types.REGISTRATION_PASSWORD_ERROR,
|
||||
error
|
||||
}),
|
||||
registrationUserNameError: (error) => ({
|
||||
registrationUserNameError: (error: string) => ({
|
||||
type: Types.REGISTRATION_USERNAME_ERROR,
|
||||
error
|
||||
}),
|
||||
clearRegistrationErrors: () => ({
|
||||
type: Types.CLEAR_REGISTRATION_ERRORS,
|
||||
}),
|
||||
accountAwaitingActivation: (options: WebSocketConnectOptions) => ({
|
||||
type: Types.ACCOUNT_AWAITING_ACTIVATION,
|
||||
options
|
||||
|
|
@ -131,7 +141,7 @@ export const Actions = {
|
|||
resetPasswordSuccess: () => ({
|
||||
type: Types.RESET_PASSWORD_SUCCESS,
|
||||
}),
|
||||
adjustMod: (userName, shouldBeMod, shouldBeJudge) => ({
|
||||
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => ({
|
||||
type: Types.ADJUST_MOD,
|
||||
userName,
|
||||
shouldBeMod,
|
||||
|
|
@ -149,59 +159,59 @@ export const Actions = {
|
|||
accountPasswordChange: () => ({
|
||||
type: Types.ACCOUNT_PASSWORD_CHANGE,
|
||||
}),
|
||||
accountEditChanged: (user) => ({
|
||||
accountEditChanged: (user: Partial<User>) => ({
|
||||
type: Types.ACCOUNT_EDIT_CHANGED,
|
||||
user,
|
||||
}),
|
||||
accountImageChanged: (user) => ({
|
||||
accountImageChanged: (user: Partial<User>) => ({
|
||||
type: Types.ACCOUNT_IMAGE_CHANGED,
|
||||
user,
|
||||
}),
|
||||
getUserInfo: (userInfo) => ({
|
||||
getUserInfo: (userInfo: User) => ({
|
||||
type: Types.GET_USER_INFO,
|
||||
userInfo,
|
||||
}),
|
||||
notifyUser: (notification) => ({
|
||||
notifyUser: (notification: NotifyUserData) => ({
|
||||
type: Types.NOTIFY_USER,
|
||||
notification,
|
||||
}),
|
||||
serverShutdown: (data) => ({
|
||||
serverShutdown: (data: ServerShutdownData) => ({
|
||||
type: Types.SERVER_SHUTDOWN,
|
||||
data,
|
||||
}),
|
||||
userMessage: (messageData) => ({
|
||||
userMessage: (messageData: UserMessageData) => ({
|
||||
type: Types.USER_MESSAGE,
|
||||
messageData,
|
||||
}),
|
||||
addToList: (list, userName) => ({
|
||||
addToList: (list: string, userName: string) => ({
|
||||
type: Types.ADD_TO_LIST,
|
||||
list,
|
||||
userName,
|
||||
}),
|
||||
removeFromList: (list, userName) => ({
|
||||
removeFromList: (list: string, userName: string) => ({
|
||||
type: Types.REMOVE_FROM_LIST,
|
||||
list,
|
||||
userName,
|
||||
}),
|
||||
banFromServer: (userName) => ({
|
||||
banFromServer: (userName: string) => ({
|
||||
type: Types.BAN_FROM_SERVER,
|
||||
userName,
|
||||
}),
|
||||
banHistory: (userName, banHistory) => ({
|
||||
banHistory: (userName: string, banHistory: BanHistoryItem[]) => ({
|
||||
type: Types.BAN_HISTORY,
|
||||
userName,
|
||||
banHistory,
|
||||
}),
|
||||
warnHistory: (userName, warnHistory) => ({
|
||||
warnHistory: (userName: string, warnHistory: WarnHistoryItem[]) => ({
|
||||
type: Types.WARN_HISTORY,
|
||||
userName,
|
||||
warnHistory,
|
||||
}),
|
||||
warnListOptions: (warnList) => ({
|
||||
warnListOptions: (warnList: WarnListItem[]) => ({
|
||||
type: Types.WARN_LIST_OPTIONS,
|
||||
warnList,
|
||||
}),
|
||||
warnUser: (userName) => ({
|
||||
warnUser: (userName: string) => ({
|
||||
type: Types.WARN_USER,
|
||||
userName,
|
||||
}),
|
||||
|
|
@ -234,5 +244,8 @@ 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 }),
|
||||
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) =>
|
||||
({ type: Types.GAMES_OF_USER, userName, games, gametypeMap }),
|
||||
}
|
||||
|
||||
export type ServerAction = ReturnType<typeof Actions[keyof typeof Actions]>;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } }));
|
||||
vi.mock('redux-form', () => ({
|
||||
reset: vi.fn((form) => ({ type: '@@redux-form/RESET', meta: { form } })),
|
||||
}));
|
||||
vi.mock('store', () => ({ store: { dispatch: vi.fn() } }));
|
||||
|
||||
import { store } from 'store/store';
|
||||
import { reset } from 'redux-form';
|
||||
import { store } from 'store';
|
||||
import { Actions } from './server.actions';
|
||||
import { Dispatch } from './server.dispatch';
|
||||
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,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeGame,
|
||||
makeReplayMatch,
|
||||
makeUser,
|
||||
makeWarnHistoryItem,
|
||||
|
|
@ -68,11 +69,10 @@ describe('Dispatch', () => {
|
|||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateBuddyList(list));
|
||||
});
|
||||
|
||||
it('addToBuddyList dispatches reset("addToBuddies") then Actions.addToBuddyList()', () => {
|
||||
it('addToBuddyList dispatches Actions.addToBuddyList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToBuddyList(user);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToBuddies'));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToBuddyList(user));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.addToBuddyList(user));
|
||||
});
|
||||
|
||||
it('removeFromBuddyList dispatches Actions.removeFromBuddyList()', () => {
|
||||
|
|
@ -86,11 +86,10 @@ describe('Dispatch', () => {
|
|||
expect(store.dispatch).toHaveBeenCalledWith(Actions.updateIgnoreList(list));
|
||||
});
|
||||
|
||||
it('addToIgnoreList dispatches reset("addToIgnore") then Actions.addToIgnoreList()', () => {
|
||||
it('addToIgnoreList dispatches Actions.addToIgnoreList()', () => {
|
||||
const user = makeUser();
|
||||
Dispatch.addToIgnoreList(user);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, (reset as vi.Mock)('addToIgnore'));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, Actions.addToIgnoreList(user));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.addToIgnoreList(user));
|
||||
});
|
||||
|
||||
it('removeFromIgnoreList dispatches Actions.removeFromIgnoreList()', () => {
|
||||
|
|
@ -132,7 +131,7 @@ describe('Dispatch', () => {
|
|||
});
|
||||
|
||||
it('viewLogs dispatches Actions.viewLogs()', () => {
|
||||
const logs = { room: [], game: [], chat: [] };
|
||||
const logs = [{ targetType: 'room' }] as any[];
|
||||
Dispatch.viewLogs(logs);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs));
|
||||
});
|
||||
|
|
@ -157,9 +156,14 @@ describe('Dispatch', () => {
|
|||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationSuccess());
|
||||
});
|
||||
|
||||
it('registrationFailed dispatches correctly', () => {
|
||||
Dispatch.registrationFailed('err');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('err'));
|
||||
it('registrationFailed passes reason and endTime to action', () => {
|
||||
Dispatch.registrationFailed('reason', 999);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('reason', 999));
|
||||
});
|
||||
|
||||
it('registrationFailed passes reason only when no endTime', () => {
|
||||
Dispatch.registrationFailed('plain reason');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.registrationFailed('plain reason', undefined));
|
||||
});
|
||||
|
||||
it('registrationEmailError dispatches correctly', () => {
|
||||
|
|
@ -257,19 +261,19 @@ describe('Dispatch', () => {
|
|||
});
|
||||
|
||||
it('notifyUser dispatches correctly', () => {
|
||||
const notification = { type: 1, warningReason: '', customTitle: '', customContent: '' };
|
||||
const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' });
|
||||
Dispatch.notifyUser(notification);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification));
|
||||
});
|
||||
|
||||
it('serverShutdown dispatches correctly', () => {
|
||||
const data = { reason: 'maintenance', minutes: 5 };
|
||||
const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 });
|
||||
Dispatch.serverShutdown(data);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data));
|
||||
});
|
||||
|
||||
it('userMessage dispatches correctly', () => {
|
||||
const messageData = { senderName: 'Alice', receiverName: 'Bob', message: 'hey' };
|
||||
const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' });
|
||||
Dispatch.userMessage(messageData);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData));
|
||||
});
|
||||
|
|
@ -382,8 +386,9 @@ describe('Dispatch', () => {
|
|||
});
|
||||
|
||||
it('gamesOfUser dispatches correctly', () => {
|
||||
const games = [{ gameId: 1 }] as any;
|
||||
Dispatch.gamesOfUser('alice', games);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games));
|
||||
const games = [makeGame({ gameId: 1 })];
|
||||
const gametypeMap = { 1: 'Standard' };
|
||||
Dispatch.gamesOfUser('alice', games, gametypeMap);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games, gametypeMap));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { reset } from 'redux-form';
|
||||
import { Actions } from './server.actions';
|
||||
import { store } from 'store';
|
||||
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||
import {
|
||||
BanHistoryItem, DeckList, DeckStorageTreeItem, GametypeMap, LogItem, ReplayMatch,
|
||||
User, WarnHistoryItem, WarnListItem, WebSocketConnectOptions
|
||||
} from 'types';
|
||||
import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb';
|
||||
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||
|
||||
export const Dispatch = {
|
||||
initialized: () => {
|
||||
|
|
@ -10,13 +14,13 @@ export const Dispatch = {
|
|||
clearStore: () => {
|
||||
store.dispatch(Actions.clearStore());
|
||||
},
|
||||
loginSuccessful: options => {
|
||||
loginSuccessful: (options: WebSocketConnectOptions) => {
|
||||
store.dispatch(Actions.loginSuccessful(options));
|
||||
},
|
||||
loginFailed: () => {
|
||||
store.dispatch(Actions.loginFailed());
|
||||
},
|
||||
connectionClosed: reason => {
|
||||
connectionClosed: (reason: number) => {
|
||||
store.dispatch(Actions.connectionClosed(reason));
|
||||
},
|
||||
connectionFailed: () => {
|
||||
|
|
@ -28,57 +32,55 @@ export const Dispatch = {
|
|||
testConnectionFailed: () => {
|
||||
store.dispatch(Actions.testConnectionFailed());
|
||||
},
|
||||
updateBuddyList: buddyList => {
|
||||
updateBuddyList: (buddyList: User[]) => {
|
||||
store.dispatch(Actions.updateBuddyList(buddyList));
|
||||
},
|
||||
addToBuddyList: user => {
|
||||
store.dispatch(reset('addToBuddies'));
|
||||
addToBuddyList: (user: User) => {
|
||||
store.dispatch(Actions.addToBuddyList(user));
|
||||
},
|
||||
removeFromBuddyList: userName => {
|
||||
removeFromBuddyList: (userName: string) => {
|
||||
store.dispatch(Actions.removeFromBuddyList(userName));
|
||||
},
|
||||
updateIgnoreList: ignoreList => {
|
||||
updateIgnoreList: (ignoreList: User[]) => {
|
||||
store.dispatch(Actions.updateIgnoreList(ignoreList));
|
||||
},
|
||||
addToIgnoreList: user => {
|
||||
store.dispatch(reset('addToIgnore'));
|
||||
addToIgnoreList: (user: User) => {
|
||||
store.dispatch(Actions.addToIgnoreList(user));
|
||||
},
|
||||
removeFromIgnoreList: userName => {
|
||||
removeFromIgnoreList: (userName: string) => {
|
||||
store.dispatch(Actions.removeFromIgnoreList(userName));
|
||||
},
|
||||
updateInfo: (name, version) => {
|
||||
updateInfo: (name: string, version: string) => {
|
||||
store.dispatch(Actions.updateInfo({
|
||||
name,
|
||||
version
|
||||
}));
|
||||
},
|
||||
updateStatus: (state, description) => {
|
||||
updateStatus: (state: number, description: string) => {
|
||||
store.dispatch(Actions.updateStatus({
|
||||
state,
|
||||
description
|
||||
}));
|
||||
},
|
||||
updateUser: user => {
|
||||
updateUser: (user: User) => {
|
||||
store.dispatch(Actions.updateUser(user));
|
||||
},
|
||||
updateUsers: users => {
|
||||
updateUsers: (users: User[]) => {
|
||||
store.dispatch(Actions.updateUsers(users));
|
||||
},
|
||||
userJoined: user => {
|
||||
userJoined: (user: User) => {
|
||||
store.dispatch(Actions.userJoined(user));
|
||||
},
|
||||
userLeft: name => {
|
||||
userLeft: (name: string) => {
|
||||
store.dispatch(Actions.userLeft(name));
|
||||
},
|
||||
viewLogs: name => {
|
||||
store.dispatch(Actions.viewLogs(name));
|
||||
viewLogs: (logs: LogItem[]) => {
|
||||
store.dispatch(Actions.viewLogs(logs));
|
||||
},
|
||||
clearLogs: () => {
|
||||
store.dispatch(Actions.clearLogs());
|
||||
},
|
||||
serverMessage: message => {
|
||||
serverMessage: (message: string) => {
|
||||
store.dispatch(Actions.serverMessage(message));
|
||||
},
|
||||
registrationRequiresEmail: () => {
|
||||
|
|
@ -87,16 +89,19 @@ export const Dispatch = {
|
|||
registrationSuccess: () => {
|
||||
store.dispatch(Actions.registrationSuccess())
|
||||
},
|
||||
registrationFailed: (error) => {
|
||||
store.dispatch(Actions.registrationFailed(error));
|
||||
registrationFailed: (reason: string, endTime?: number) => {
|
||||
store.dispatch(Actions.registrationFailed(reason, endTime));
|
||||
},
|
||||
registrationEmailError: (error) => {
|
||||
clearRegistrationErrors: () => {
|
||||
store.dispatch(Actions.clearRegistrationErrors());
|
||||
},
|
||||
registrationEmailError: (error: string) => {
|
||||
store.dispatch(Actions.registrationEmailError(error));
|
||||
},
|
||||
registrationPasswordError: (error) => {
|
||||
registrationPasswordError: (error: string) => {
|
||||
store.dispatch(Actions.registrationPasswordError(error));
|
||||
},
|
||||
registrationUserNameError: (error) => {
|
||||
registrationUserNameError: (error: string) => {
|
||||
store.dispatch(Actions.registrationUserNameError(error));
|
||||
},
|
||||
accountAwaitingActivation: (options: WebSocketConnectOptions) => {
|
||||
|
|
@ -120,7 +125,7 @@ export const Dispatch = {
|
|||
resetPasswordSuccess: () => {
|
||||
store.dispatch(Actions.resetPasswordSuccess());
|
||||
},
|
||||
adjustMod: (userName, shouldBeMod, shouldBeJudge) => {
|
||||
adjustMod: (userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) => {
|
||||
store.dispatch(Actions.adjustMod(userName, shouldBeMod, shouldBeJudge));
|
||||
},
|
||||
reloadConfig: () => {
|
||||
|
|
@ -135,43 +140,43 @@ export const Dispatch = {
|
|||
accountPasswordChange: () => {
|
||||
store.dispatch(Actions.accountPasswordChange());
|
||||
},
|
||||
accountEditChanged: (user) => {
|
||||
accountEditChanged: (user: Partial<User>) => {
|
||||
store.dispatch(Actions.accountEditChanged(user));
|
||||
},
|
||||
accountImageChanged: (user) => {
|
||||
accountImageChanged: (user: Partial<User>) => {
|
||||
store.dispatch(Actions.accountImageChanged(user));
|
||||
},
|
||||
getUserInfo: (userInfo) => {
|
||||
getUserInfo: (userInfo: User) => {
|
||||
store.dispatch(Actions.getUserInfo(userInfo));
|
||||
},
|
||||
notifyUser: (notification) => {
|
||||
notifyUser: (notification: NotifyUserData) => {
|
||||
store.dispatch(Actions.notifyUser(notification))
|
||||
},
|
||||
serverShutdown: (data) => {
|
||||
serverShutdown: (data: ServerShutdownData) => {
|
||||
store.dispatch(Actions.serverShutdown(data))
|
||||
},
|
||||
userMessage: (messageData) => {
|
||||
userMessage: (messageData: UserMessageData) => {
|
||||
store.dispatch(Actions.userMessage(messageData))
|
||||
},
|
||||
addToList: (list, userName) => {
|
||||
addToList: (list: string, userName: string) => {
|
||||
store.dispatch(Actions.addToList(list, userName))
|
||||
},
|
||||
removeFromList: (list, userName) => {
|
||||
removeFromList: (list: string, userName: string) => {
|
||||
store.dispatch(Actions.removeFromList(list, userName))
|
||||
},
|
||||
banFromServer: (userName) => {
|
||||
banFromServer: (userName: string) => {
|
||||
store.dispatch(Actions.banFromServer(userName));
|
||||
},
|
||||
banHistory: (userName, banHistory) => {
|
||||
banHistory: (userName: string, banHistory: BanHistoryItem[]) => {
|
||||
store.dispatch(Actions.banHistory(userName, banHistory))
|
||||
},
|
||||
warnHistory: (userName, warnHistory) => {
|
||||
warnHistory: (userName: string, warnHistory: WarnHistoryItem[]) => {
|
||||
store.dispatch(Actions.warnHistory(userName, warnHistory))
|
||||
},
|
||||
warnListOptions: (warnList) => {
|
||||
warnListOptions: (warnList: WarnListItem[]) => {
|
||||
store.dispatch(Actions.warnListOptions(warnList))
|
||||
},
|
||||
warnUser: (userName) => {
|
||||
warnUser: (userName: string) => {
|
||||
store.dispatch(Actions.warnUser(userName))
|
||||
},
|
||||
grantReplayAccess: (replayId: number, moderatorName: string) => {
|
||||
|
|
@ -213,7 +218,7 @@ export const Dispatch = {
|
|||
deckDelete: (deckId: number) => {
|
||||
store.dispatch(Actions.deckDelete(deckId));
|
||||
},
|
||||
gamesOfUser: (userName: string, games: Game[]) => {
|
||||
store.dispatch(Actions.gamesOfUser(userName, games));
|
||||
gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) => {
|
||||
store.dispatch(Actions.gamesOfUser(userName, games, gametypeMap));
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export interface ServerState {
|
|||
replays: ReplayMatch[];
|
||||
backendDecks: DeckList | null;
|
||||
gamesOfUser: { [userName: string]: Game[] };
|
||||
registrationError: string | null;
|
||||
}
|
||||
|
||||
export interface ServerStateStatus {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { StatusEnum, UserLevelFlag } from '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 {
|
||||
|
|
@ -6,6 +9,7 @@ import {
|
|||
makeConnectOptions,
|
||||
makeDeckList,
|
||||
makeDeckTreeItem,
|
||||
makeGame,
|
||||
makeLogItem,
|
||||
makeReplayMatch,
|
||||
makeServerState,
|
||||
|
|
@ -71,6 +75,35 @@ describe('Account & Connection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Registration ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Registration', () => {
|
||||
it('REGISTRATION_FAILED → stores normalized error (plain reason)', () => {
|
||||
const state = makeServerState({ registrationError: null });
|
||||
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'Server is disabled', endTime: undefined });
|
||||
expect(result.registrationError).toBe('Server is disabled');
|
||||
});
|
||||
|
||||
it('REGISTRATION_FAILED → normalizes banned error when endTime is given', () => {
|
||||
const state = makeServerState({ registrationError: null });
|
||||
const result = serverReducer(state, { type: Types.REGISTRATION_FAILED, reason: 'bad actor', endTime: Date.now() + 100_000 });
|
||||
expect(result.registrationError).toContain('banned');
|
||||
expect(result.registrationError).toContain('bad actor');
|
||||
});
|
||||
|
||||
it('CLEAR_REGISTRATION_ERRORS → sets registrationError to null', () => {
|
||||
const state = makeServerState({ registrationError: 'some error' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_REGISTRATION_ERRORS });
|
||||
expect(result.registrationError).toBeNull();
|
||||
});
|
||||
|
||||
it('CLEAR_STORE → resets registrationError to null', () => {
|
||||
const state = makeServerState({ registrationError: 'stale error' });
|
||||
const result = serverReducer(state, { type: Types.CLEAR_STORE });
|
||||
expect(result.registrationError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Server Info & Status ──────────────────────────────────────────────────────
|
||||
|
||||
describe('Server Info & Status', () => {
|
||||
|
|
@ -205,11 +238,11 @@ describe('Ignore List', () => {
|
|||
// ── Logs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Logs', () => {
|
||||
it('VIEW_LOGS → replaces logs entirely', () => {
|
||||
const logs = { room: [makeLogItem()], game: [], chat: [] };
|
||||
it('VIEW_LOGS → groups LogItem[] into room/game/chat buckets', () => {
|
||||
const log = makeLogItem({ targetType: 'room' });
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs });
|
||||
expect(result.logs).toEqual(logs);
|
||||
const result = serverReducer(state, { type: Types.VIEW_LOGS, logs: [log] });
|
||||
expect(result.logs.room).toEqual([log]);
|
||||
});
|
||||
|
||||
it('CLEAR_LOGS → resets logs to empty arrays', () => {
|
||||
|
|
@ -241,12 +274,12 @@ describe('Messaging', () => {
|
|||
});
|
||||
|
||||
it('USER_MESSAGE → appends to existing messages for that user', () => {
|
||||
const existingMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'first' };
|
||||
const existingMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' });
|
||||
const state = makeServerState({
|
||||
user: makeUser({ name: 'Bob' }),
|
||||
messages: { Alice: [existingMsg] },
|
||||
});
|
||||
const newMsg = { senderName: 'Alice', receiverName: 'Bob', message: 'second' };
|
||||
const newMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' });
|
||||
const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg });
|
||||
expect(result.messages['Alice']).toHaveLength(2);
|
||||
});
|
||||
|
|
@ -442,8 +475,12 @@ describe('Deck Storage', () => {
|
|||
});
|
||||
|
||||
it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => {
|
||||
const subfolder = { id: 0, name: 'myDecks', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'myDecks', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) })
|
||||
});
|
||||
const item = makeDeckTreeItem({ name: 'new.cod' });
|
||||
const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item });
|
||||
const folder = result.backendDecks.root.items.find(i => i.name === 'myDecks');
|
||||
|
|
@ -468,15 +505,19 @@ describe('Deck Storage', () => {
|
|||
|
||||
it('DECK_DELETE → removes item by id from tree', () => {
|
||||
const item = makeDeckTreeItem({ id: 7 });
|
||||
const state = makeServerState({ backendDecks: { root: { items: [item] } } });
|
||||
const state = makeServerState({ backendDecks: makeDeckList({ root: create(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 = { id: 0, name: 'sub', file: null, folder: { items: [nested] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'sub', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [nested] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(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);
|
||||
});
|
||||
|
|
@ -492,12 +533,16 @@ describe('Deck Storage', () => {
|
|||
const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: '', dirName: 'myDir' });
|
||||
expect(result.backendDecks.root.items).toHaveLength(1);
|
||||
expect(result.backendDecks.root.items[0].name).toBe('myDir');
|
||||
expect(result.backendDecks.root.items[0].folder).toEqual({ items: [] });
|
||||
expect(result.backendDecks.root.items[0].folder.items).toEqual([]);
|
||||
});
|
||||
|
||||
it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => {
|
||||
const subfolder = { id: 0, name: 'parent', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(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');
|
||||
expect(parent.folder.items).toHaveLength(1);
|
||||
|
|
@ -511,23 +556,37 @@ describe('Deck Storage', () => {
|
|||
});
|
||||
|
||||
it('DECK_DEL_DIR → removes folder from root by name', () => {
|
||||
const subfolder = { id: 0, name: 'myDir', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'myDir', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(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 = { id: 0, name: 'keep', file: null, folder: { items: [] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [subfolder] } } });
|
||||
const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'keep', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(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 = { id: 0, name: 'child', file: null, folder: { items: [] } };
|
||||
const parent = { id: 0, name: 'parent', file: null, folder: { items: [child] } };
|
||||
const state = makeServerState({ backendDecks: { root: { items: [parent] } } });
|
||||
const child = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'child', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
const parent = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [child] })
|
||||
});
|
||||
const state = makeServerState({
|
||||
backendDecks: makeDeckList({ root: create(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);
|
||||
});
|
||||
|
|
@ -536,25 +595,25 @@ describe('Deck Storage', () => {
|
|||
// ── GAMES_OF_USER ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('GAMES_OF_USER', () => {
|
||||
it('stores games keyed by userName', () => {
|
||||
const games = [{ gameId: 5, roomId: 1 }] as any;
|
||||
it('stores normalized games keyed by userName', () => {
|
||||
const games = [makeGame({ gameId: 5 })];
|
||||
const state = makeServerState();
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games });
|
||||
expect(result.gamesOfUser['alice']).toBe(games);
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap: {} });
|
||||
expect(result.gamesOfUser['alice']).toEqual(games);
|
||||
});
|
||||
|
||||
it('overwrites previous games for same user', () => {
|
||||
const old = [{ gameId: 1 }] as any;
|
||||
const fresh = [{ gameId: 2 }] as any;
|
||||
const old = [makeGame({ gameId: 1 })];
|
||||
const fresh = [makeGame({ gameId: 2 })];
|
||||
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);
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh, gametypeMap: {} });
|
||||
expect(result.gamesOfUser['alice']).toEqual(fresh);
|
||||
});
|
||||
|
||||
it('does not affect other users\' entries', () => {
|
||||
const bobGames = [{ gameId: 3 }] as any;
|
||||
const bobGames = [makeGame({ gameId: 3 })];
|
||||
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [] });
|
||||
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [], gametypeMap: {} });
|
||||
expect(result.gamesOfUser['bob']).toBe(bobGames);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { DeckStorageFolder, DeckStorageTreeItem, SortDirection, StatusEnum, UserLevelFlag, UserSortField } from '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 { SortUtil } from '../common';
|
||||
import { normalizeBannedUserError, normalizeGameObject, normalizeLogs, SortUtil } from '../common';
|
||||
|
||||
import { ServerAction } from './server.actions';
|
||||
import { ServerState } from './server.interfaces'
|
||||
import { Types } from './server.types';
|
||||
|
||||
|
|
@ -11,31 +15,33 @@ function splitPath(path: string): string[] {
|
|||
|
||||
function insertAtPath(folder: DeckStorageFolder, pathSegments: string[], item: DeckStorageTreeItem): DeckStorageFolder {
|
||||
if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) {
|
||||
return { items: [...folder.items, item] };
|
||||
return create(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 {
|
||||
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||
items: folder.items.map(child =>
|
||||
child === match
|
||||
? { ...child, folder: insertAtPath(child.folder!, tail, item) }
|
||||
: child
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
const created: DeckStorageTreeItem = { id: 0, name: head, file: null, folder: insertAtPath({ items: [] }, tail, item) };
|
||||
return { items: [...folder.items, created] };
|
||||
const created: DeckStorageTreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: head, folder: insertAtPath(create(ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item)
|
||||
});
|
||||
return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] });
|
||||
}
|
||||
|
||||
function removeById(folder: DeckStorageFolder, id: number): DeckStorageFolder {
|
||||
return {
|
||||
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||
items: folder.items
|
||||
.filter(item => item.id !== id)
|
||||
.map(item =>
|
||||
item.folder ? { ...item, folder: removeById(item.folder, id) } : item
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckStorageFolder {
|
||||
|
|
@ -44,15 +50,17 @@ function removeByPath(folder: DeckStorageFolder, pathSegments: string[]): DeckSt
|
|||
}
|
||||
const [head, ...tail] = pathSegments;
|
||||
if (tail.length === 0) {
|
||||
return { items: folder.items.filter(item => !(item.name === head && item.folder !== null)) };
|
||||
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||
items: folder.items.filter(item => !(item.name === head && item.folder != null))
|
||||
});
|
||||
}
|
||||
return {
|
||||
return create(ServerInfo_DeckStorage_FolderSchema, {
|
||||
items: folder.items.map(item =>
|
||||
item.name === head && item.folder
|
||||
? { ...item, folder: removeByPath(item.folder, tail) }
|
||||
: item
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const initialState: ServerState = {
|
||||
|
|
@ -93,9 +101,10 @@ const initialState: ServerState = {
|
|||
replays: [],
|
||||
backendDecks: null,
|
||||
gamesOfUser: {},
|
||||
registrationError: null,
|
||||
};
|
||||
|
||||
export const serverReducer = (state = initialState, action: any) => {
|
||||
export const serverReducer = (state = initialState, action: ServerAction) => {
|
||||
switch (action.type) {
|
||||
case Types.INITIALIZED: {
|
||||
return {
|
||||
|
|
@ -271,7 +280,7 @@ export const serverReducer = (state = initialState, action: any) => {
|
|||
return {
|
||||
...state,
|
||||
logs: {
|
||||
...logs
|
||||
...normalizeLogs(logs)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -424,60 +433,96 @@ export const serverReducer = (state = initialState, action: any) => {
|
|||
return { ...state, backendDecks: action.deckList };
|
||||
}
|
||||
case Types.DECK_UPLOAD: {
|
||||
if (!state.backendDecks) {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: {
|
||||
backendDecks: create(Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_DELETE: {
|
||||
if (!state.backendDecks) {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: {
|
||||
backendDecks: create(Response_DeckListSchema, {
|
||||
root: removeById(state.backendDecks.root, action.deckId),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_NEW_DIR: {
|
||||
if (!state.backendDecks) {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
}
|
||||
const newFolder: DeckStorageTreeItem = { id: 0, name: action.dirName, file: null, folder: { items: [] } };
|
||||
const newFolder: DeckStorageTreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, {
|
||||
id: 0, name: action.dirName, folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] })
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
backendDecks: {
|
||||
backendDecks: create(Response_DeckListSchema, {
|
||||
root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.DECK_DEL_DIR: {
|
||||
if (!state.backendDecks) {
|
||||
if (!state.backendDecks?.root) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
backendDecks: {
|
||||
backendDecks: create(Response_DeckListSchema, {
|
||||
root: removeByPath(state.backendDecks.root, splitPath(action.path)),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
case Types.GAMES_OF_USER: {
|
||||
const { userName, games } = action;
|
||||
const { userName, games, gametypeMap } = action;
|
||||
const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap));
|
||||
return {
|
||||
...state,
|
||||
gamesOfUser: {
|
||||
...state.gamesOfUser,
|
||||
[userName]: games,
|
||||
[userName]: normalizedGames,
|
||||
},
|
||||
};
|
||||
}
|
||||
case Types.REGISTRATION_FAILED: {
|
||||
const error = action.endTime
|
||||
? normalizeBannedUserError(action.reason, action.endTime)
|
||||
: action.reason;
|
||||
return { ...state, registrationError: error };
|
||||
}
|
||||
case Types.CLEAR_REGISTRATION_ERRORS:
|
||||
return { ...state, registrationError: null };
|
||||
// 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:
|
||||
case Types.REGISTRATION_REQUIRES_EMAIL:
|
||||
case Types.REGISTRATION_SUCCESS:
|
||||
case Types.REGISTRATION_EMAIL_ERROR:
|
||||
case Types.REGISTRATION_PASSWORD_ERROR:
|
||||
case Types.REGISTRATION_USERNAME_ERROR:
|
||||
case Types.RESET_PASSWORD_REQUESTED:
|
||||
case Types.RESET_PASSWORD_FAILED:
|
||||
case Types.RESET_PASSWORD_CHALLENGE:
|
||||
case Types.RESET_PASSWORD_SUCCESS:
|
||||
case Types.RELOAD_CONFIG:
|
||||
case Types.SHUTDOWN_SERVER:
|
||||
case Types.UPDATE_SERVER_MESSAGE:
|
||||
case Types.ACCOUNT_PASSWORD_CHANGE:
|
||||
case Types.ADD_TO_LIST:
|
||||
case Types.REMOVE_FROM_LIST:
|
||||
case Types.GRANT_REPLAY_ACCESS:
|
||||
case Types.FORCE_ACTIVATE_USER:
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ export const Selectors = {
|
|||
getIgnoreList: ({ server }: State) => server.ignoreList,
|
||||
getReplays: ({ server }: State) => server.replays,
|
||||
getBackendDecks: ({ server }: State) => server.backendDecks,
|
||||
getRegistrationError: ({ server }: State) => server.registrationError,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const Types = {
|
|||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
||||
CLEAR_REGISTRATION_ERRORS: '[Server] Clear Registration Errors',
|
||||
ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation',
|
||||
ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success',
|
||||
ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed',
|
||||
|
|
@ -70,4 +71,4 @@ export const Types = {
|
|||
DECK_DELETE: '[Server] Deck Delete',
|
||||
// User games
|
||||
GAMES_OF_USER: '[Server] Games Of User',
|
||||
};
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue