Fix various issues

This commit is contained in:
seavor 2026-04-12 13:58:51 -05:00
parent 3001925430
commit c3ae4cffd6
21 changed files with 130 additions and 121 deletions

View file

@ -149,6 +149,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
adminNotes: {},
replays: [],
backendDecks: null,
gamesOfUser: {},
...overrides,
};
}

View file

@ -120,7 +120,7 @@ describe('Actions', () => {
});
it('registrationSuccess', () => {
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCES });
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCESS });
});
it('registrationFailed', () => {
@ -203,14 +203,6 @@ describe('Actions', () => {
expect(Actions.accountImageChanged(user)).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, user });
});
it('directMessageSent', () => {
expect(Actions.directMessageSent('Eve', 'hi')).toEqual({
type: Types.DIRECT_MESSAGE_SENT,
userName: 'Eve',
message: 'hi',
});
});
it('getUserInfo', () => {
const userInfo = makeUser({ name: 'Frank' });
expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo });
@ -353,4 +345,9 @@ describe('Actions', () => {
it('deckDelete', () => {
expect(Actions.deckDelete(42)).toEqual({ type: Types.DECK_DELETE, deckId: 42 });
});
it('gamesOfUser', () => {
const games = [{ gameId: 1 }] as any;
expect(Actions.gamesOfUser('alice', games)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games });
});
});

View file

@ -1,4 +1,4 @@
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
import { Types } from './server.types';
export const Actions = {
@ -91,7 +91,7 @@ export const Actions = {
type: Types.REGISTRATION_REQUIRES_EMAIL,
}),
registrationSuccess: () => ({
type: Types.REGISTRATION_SUCCES,
type: Types.REGISTRATION_SUCCESS,
}),
registrationFailed: (error) => ({
type: Types.REGISTRATION_FAILED,
@ -157,11 +157,6 @@ export const Actions = {
type: Types.ACCOUNT_IMAGE_CHANGED,
user,
}),
directMessageSent: (userName, message) => ({
type: Types.DIRECT_MESSAGE_SENT,
userName,
message,
}),
getUserInfo: (userInfo) => ({
type: Types.GET_USER_INFO,
userInfo,
@ -239,4 +234,5 @@ export const Actions = {
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
gamesOfUser: (userName: string, games: Game[]) => ({ type: Types.GAMES_OF_USER, userName, games }),
}

View file

@ -250,11 +250,6 @@ describe('Dispatch', () => {
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountImageChanged(user));
});
it('directMessageSent dispatches correctly', () => {
Dispatch.directMessageSent('Eve', 'hi');
expect(store.dispatch).toHaveBeenCalledWith(Actions.directMessageSent('Eve', 'hi'));
});
it('getUserInfo dispatches correctly', () => {
const userInfo = makeUser({ name: 'Frank' });
Dispatch.getUserInfo(userInfo);
@ -385,4 +380,10 @@ describe('Dispatch', () => {
Dispatch.deckDelete(42);
expect(store.dispatch).toHaveBeenCalledWith(Actions.deckDelete(42));
});
it('gamesOfUser dispatches correctly', () => {
const games = [{ gameId: 1 }] as any;
Dispatch.gamesOfUser('alice', games);
expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games));
});
});

View file

@ -1,7 +1,7 @@
import { reset } from 'redux-form';
import { Actions } from './server.actions';
import { store } from 'store';
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
export const Dispatch = {
initialized: () => {
@ -141,9 +141,6 @@ export const Dispatch = {
accountImageChanged: (user) => {
store.dispatch(Actions.accountImageChanged(user));
},
directMessageSent: (userName, message) => {
store.dispatch(Actions.directMessageSent(userName, message));
},
getUserInfo: (userInfo) => {
store.dispatch(Actions.getUserInfo(userInfo));
},
@ -216,4 +213,7 @@ export const Dispatch = {
deckDelete: (deckId: number) => {
store.dispatch(Actions.deckDelete(deckId));
},
gamesOfUser: (userName: string, games: Game[]) => {
store.dispatch(Actions.gamesOfUser(userName, games));
},
}

View file

@ -1,5 +1,5 @@
import {
WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem
WarnHistoryItem, BanHistoryItem, DeckList, Game, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem
} from 'types';
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
@ -72,6 +72,7 @@ export interface ServerState {
adminNotes: { [userName: string]: string };
replays: ReplayMatch[];
backendDecks: DeckList | null;
gamesOfUser: { [userName: string]: Game[] };
}
export interface ServerStateStatus {

View file

@ -332,31 +332,39 @@ describe('Moderation', () => {
describe('ADJUST_MOD', () => {
const baseUserLevel = UserLevelFlag.IsUser | UserLevelFlag.IsRegistered | UserLevelFlag.IsModerator | UserLevelFlag.IsJudge;
it('shouldBeMod=true, shouldBeJudge=true → keeps IsModerator and IsJudge bits', () => {
it('shouldBeMod=true, shouldBeJudge=true → sets both bits, preserves IsUser|IsRegistered', () => {
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
// IsModerator(4) | IsJudge(16)
expect(result.users[0].userLevel).toBe(20);
// IsUser(1) | IsRegistered(2) | IsModerator(4) | IsJudge(16) = 23
expect(result.users[0].userLevel).toBe(23);
});
it('shouldBeMod=true, shouldBeJudge=false → keeps only IsModerator bit', () => {
it('shouldBeMod=true, shouldBeJudge=false → sets IsModerator, clears IsJudge, preserves others', () => {
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
// IsModerator(4)
expect(result.users[0].userLevel).toBe(4);
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
expect(result.users[0].userLevel).toBe(7);
});
it('shouldBeMod=false, shouldBeJudge=true → keeps only IsJudge bit', () => {
it('shouldBeMod=false, shouldBeJudge=true → clears IsModerator, sets IsJudge, preserves others', () => {
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
// IsJudge(16)
expect(result.users[0].userLevel).toBe(16);
// IsUser(1) | IsRegistered(2) | IsJudge(16) = 19
expect(result.users[0].userLevel).toBe(19);
});
it('shouldBeMod=false, shouldBeJudge=false → clears both bits', () => {
it('shouldBeMod=false, shouldBeJudge=false → clears both bits, preserves IsUser|IsRegistered', () => {
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
expect(result.users[0].userLevel).toBe(0);
// IsUser(1) | IsRegistered(2) = 3
expect(result.users[0].userLevel).toBe(3);
});
it('shouldBeMod=true on IsUser|IsRegistered only → produces 7, not 4', () => {
const state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: UserLevelFlag.IsUser | UserLevelFlag.IsRegistered })] });
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
expect(result.users[0].userLevel).toBe(7);
});
it('non-matching users are left unchanged', () => {
@ -524,3 +532,29 @@ describe('Deck Storage', () => {
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
});
});
// ── GAMES_OF_USER ─────────────────────────────────────────────────────────────
describe('GAMES_OF_USER', () => {
it('stores games keyed by userName', () => {
const games = [{ gameId: 5, roomId: 1 }] as any;
const state = makeServerState();
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games });
expect(result.gamesOfUser['alice']).toBe(games);
});
it('overwrites previous games for same user', () => {
const old = [{ gameId: 1 }] as any;
const fresh = [{ gameId: 2 }] as any;
const state = makeServerState({ gamesOfUser: { alice: old } });
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh });
expect(result.gamesOfUser['alice']).toBe(fresh);
});
it('does not affect other users\' entries', () => {
const bobGames = [{ gameId: 3 }] as any;
const state = makeServerState({ gamesOfUser: { bob: bobGames } });
const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [] });
expect(result.gamesOfUser['bob']).toBe(bobGames);
});
});

View file

@ -93,6 +93,7 @@ const initialState: ServerState = {
adminNotes: {},
replays: [],
backendDecks: null,
gamesOfUser: {},
};
export const serverReducer = (state = initialState, action: any) => {
@ -401,11 +402,12 @@ export const serverReducer = (state = initialState, action: any) => {
if (user.name !== userName) {
return user;
}
const judgeFlag = shouldBeJudge ? UserLevelFlag.IsJudge : UserLevelFlag.IsNothing;
const modFlag = shouldBeMod ? UserLevelFlag.IsModerator : UserLevelFlag.IsNothing;
let newLevel = user.userLevel;
newLevel = shouldBeMod ? (newLevel | UserLevelFlag.IsModerator) : (newLevel & ~UserLevelFlag.IsModerator);
newLevel = shouldBeJudge ? (newLevel | UserLevelFlag.IsJudge) : (newLevel & ~UserLevelFlag.IsJudge);
return {
...user,
userLevel: user.userLevel & (judgeFlag | modFlag)
userLevel: newLevel,
}
})
};
@ -475,6 +477,16 @@ export const serverReducer = (state = initialState, action: any) => {
},
};
}
case Types.GAMES_OF_USER: {
const { userName, games } = action;
return {
...state,
gamesOfUser: {
...state.gamesOfUser,
[userName]: games,
},
};
}
default:
return state;
}

View file

@ -23,7 +23,7 @@ export const Types = {
VIEW_LOGS: '[Server] View Logs',
CLEAR_LOGS: '[Server] Clear Logs',
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
REGISTRATION_SUCCES: '[Server] Registration Success',
REGISTRATION_SUCCESS: '[Server] Registration Success',
REGISTRATION_FAILED: '[Server] Registration Failed',
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
@ -42,7 +42,6 @@ export const Types = {
ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change',
ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed',
ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed',
DIRECT_MESSAGE_SENT: '[Server] Direct Message Sent',
GET_USER_INFO: '[Server] Get User Info',
NOTIFY_USER: '[Server] Notify User',
SERVER_SHUTDOWN: '[Server] Server Shutdown',
@ -69,4 +68,6 @@ export const Types = {
DECK_DEL_DIR: '[Server] Deck Del Dir',
DECK_UPLOAD: '[Server] Deck Upload',
DECK_DELETE: '[Server] Deck Delete',
// User games
GAMES_OF_USER: '[Server] Games Of User',
};