mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-07-03 20:13:56 -07:00
Fix various issues
This commit is contained in:
parent
3001925430
commit
c3ae4cffd6
21 changed files with 130 additions and 121 deletions
|
|
@ -38,7 +38,7 @@ const RegisterForm = ({ onSubmit }: RegisterFormProps) => {
|
||||||
|
|
||||||
useReduxEffect(() => {
|
useReduxEffect(() => {
|
||||||
openToast()
|
openToast()
|
||||||
}, ServerTypes.REGISTRATION_SUCCES);
|
}, ServerTypes.REGISTRATION_SUCCESS);
|
||||||
|
|
||||||
useReduxEffect(({ error }) => {
|
useReduxEffect(({ error }) => {
|
||||||
setEmailError(error);
|
setEmailError(error);
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ export function makeGameEntry(overrides: Partial<GameEntry> = {}): GameEntry {
|
||||||
localPlayerId: 1,
|
localPlayerId: 1,
|
||||||
spectator: false,
|
spectator: false,
|
||||||
judge: false,
|
judge: false,
|
||||||
|
resuming: false,
|
||||||
started: false,
|
started: false,
|
||||||
activePlayerId: 0,
|
activePlayerId: 0,
|
||||||
activePhase: 0,
|
activePhase: 0,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export interface GameEntry {
|
||||||
localPlayerId: number;
|
localPlayerId: number;
|
||||||
spectator: boolean;
|
spectator: boolean;
|
||||||
judge: boolean;
|
judge: boolean;
|
||||||
|
resuming: boolean;
|
||||||
started: boolean;
|
started: boolean;
|
||||||
activePlayerId: number;
|
activePlayerId: number;
|
||||||
activePhase: number;
|
activePhase: number;
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ export function makeServerState(overrides: Partial<ServerState> = {}): ServerSta
|
||||||
adminNotes: {},
|
adminNotes: {},
|
||||||
replays: [],
|
replays: [],
|
||||||
backendDecks: null,
|
backendDecks: null,
|
||||||
|
gamesOfUser: {},
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationSuccess', () => {
|
it('registrationSuccess', () => {
|
||||||
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCES });
|
expect(Actions.registrationSuccess()).toEqual({ type: Types.REGISTRATION_SUCCESS });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('registrationFailed', () => {
|
it('registrationFailed', () => {
|
||||||
|
|
@ -203,14 +203,6 @@ describe('Actions', () => {
|
||||||
expect(Actions.accountImageChanged(user)).toEqual({ type: Types.ACCOUNT_IMAGE_CHANGED, user });
|
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', () => {
|
it('getUserInfo', () => {
|
||||||
const userInfo = makeUser({ name: 'Frank' });
|
const userInfo = makeUser({ name: 'Frank' });
|
||||||
expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo });
|
expect(Actions.getUserInfo(userInfo)).toEqual({ type: Types.GET_USER_INFO, userInfo });
|
||||||
|
|
@ -353,4 +345,9 @@ describe('Actions', () => {
|
||||||
it('deckDelete', () => {
|
it('deckDelete', () => {
|
||||||
expect(Actions.deckDelete(42)).toEqual({ type: Types.DECK_DELETE, deckId: 42 });
|
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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
|
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||||
import { Types } from './server.types';
|
import { Types } from './server.types';
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
|
|
@ -91,7 +91,7 @@ export const Actions = {
|
||||||
type: Types.REGISTRATION_REQUIRES_EMAIL,
|
type: Types.REGISTRATION_REQUIRES_EMAIL,
|
||||||
}),
|
}),
|
||||||
registrationSuccess: () => ({
|
registrationSuccess: () => ({
|
||||||
type: Types.REGISTRATION_SUCCES,
|
type: Types.REGISTRATION_SUCCESS,
|
||||||
}),
|
}),
|
||||||
registrationFailed: (error) => ({
|
registrationFailed: (error) => ({
|
||||||
type: Types.REGISTRATION_FAILED,
|
type: Types.REGISTRATION_FAILED,
|
||||||
|
|
@ -157,11 +157,6 @@ export const Actions = {
|
||||||
type: Types.ACCOUNT_IMAGE_CHANGED,
|
type: Types.ACCOUNT_IMAGE_CHANGED,
|
||||||
user,
|
user,
|
||||||
}),
|
}),
|
||||||
directMessageSent: (userName, message) => ({
|
|
||||||
type: Types.DIRECT_MESSAGE_SENT,
|
|
||||||
userName,
|
|
||||||
message,
|
|
||||||
}),
|
|
||||||
getUserInfo: (userInfo) => ({
|
getUserInfo: (userInfo) => ({
|
||||||
type: Types.GET_USER_INFO,
|
type: Types.GET_USER_INFO,
|
||||||
userInfo,
|
userInfo,
|
||||||
|
|
@ -239,4 +234,5 @@ export const Actions = {
|
||||||
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }),
|
||||||
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
deckUpload: (path: string, treeItem: DeckStorageTreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }),
|
||||||
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }),
|
||||||
|
gamesOfUser: (userName: string, games: Game[]) => ({ type: Types.GAMES_OF_USER, userName, games }),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,11 +250,6 @@ describe('Dispatch', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.accountImageChanged(user));
|
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', () => {
|
it('getUserInfo dispatches correctly', () => {
|
||||||
const userInfo = makeUser({ name: 'Frank' });
|
const userInfo = makeUser({ name: 'Frank' });
|
||||||
Dispatch.getUserInfo(userInfo);
|
Dispatch.getUserInfo(userInfo);
|
||||||
|
|
@ -385,4 +380,10 @@ describe('Dispatch', () => {
|
||||||
Dispatch.deckDelete(42);
|
Dispatch.deckDelete(42);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(Actions.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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { reset } from 'redux-form';
|
import { reset } from 'redux-form';
|
||||||
import { Actions } from './server.actions';
|
import { Actions } from './server.actions';
|
||||||
import { store } from 'store';
|
import { store } from 'store';
|
||||||
import { DeckList, DeckStorageTreeItem, ReplayMatch, WebSocketConnectOptions } from 'types';
|
import { DeckList, DeckStorageTreeItem, Game, ReplayMatch, WebSocketConnectOptions } from 'types';
|
||||||
|
|
||||||
export const Dispatch = {
|
export const Dispatch = {
|
||||||
initialized: () => {
|
initialized: () => {
|
||||||
|
|
@ -141,9 +141,6 @@ export const Dispatch = {
|
||||||
accountImageChanged: (user) => {
|
accountImageChanged: (user) => {
|
||||||
store.dispatch(Actions.accountImageChanged(user));
|
store.dispatch(Actions.accountImageChanged(user));
|
||||||
},
|
},
|
||||||
directMessageSent: (userName, message) => {
|
|
||||||
store.dispatch(Actions.directMessageSent(userName, message));
|
|
||||||
},
|
|
||||||
getUserInfo: (userInfo) => {
|
getUserInfo: (userInfo) => {
|
||||||
store.dispatch(Actions.getUserInfo(userInfo));
|
store.dispatch(Actions.getUserInfo(userInfo));
|
||||||
},
|
},
|
||||||
|
|
@ -216,4 +213,7 @@ export const Dispatch = {
|
||||||
deckDelete: (deckId: number) => {
|
deckDelete: (deckId: number) => {
|
||||||
store.dispatch(Actions.deckDelete(deckId));
|
store.dispatch(Actions.deckDelete(deckId));
|
||||||
},
|
},
|
||||||
|
gamesOfUser: (userName: string, games: Game[]) => {
|
||||||
|
store.dispatch(Actions.gamesOfUser(userName, games));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {
|
import {
|
||||||
WarnHistoryItem, BanHistoryItem, DeckList, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem
|
WarnHistoryItem, BanHistoryItem, DeckList, Game, LogItem, ReplayMatch, SortBy, User, UserSortField, WebSocketConnectOptions, WarnListItem
|
||||||
} from 'types';
|
} from 'types';
|
||||||
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces';
|
||||||
|
|
||||||
|
|
@ -72,6 +72,7 @@ export interface ServerState {
|
||||||
adminNotes: { [userName: string]: string };
|
adminNotes: { [userName: string]: string };
|
||||||
replays: ReplayMatch[];
|
replays: ReplayMatch[];
|
||||||
backendDecks: DeckList | null;
|
backendDecks: DeckList | null;
|
||||||
|
gamesOfUser: { [userName: string]: Game[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerStateStatus {
|
export interface ServerStateStatus {
|
||||||
|
|
|
||||||
|
|
@ -332,31 +332,39 @@ describe('Moderation', () => {
|
||||||
describe('ADJUST_MOD', () => {
|
describe('ADJUST_MOD', () => {
|
||||||
const baseUserLevel = UserLevelFlag.IsUser | UserLevelFlag.IsRegistered | UserLevelFlag.IsModerator | UserLevelFlag.IsJudge;
|
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 state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
|
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: true });
|
||||||
// IsModerator(4) | IsJudge(16)
|
// IsUser(1) | IsRegistered(2) | IsModerator(4) | IsJudge(16) = 23
|
||||||
expect(result.users[0].userLevel).toBe(20);
|
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 state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: true, shouldBeJudge: false });
|
||||||
// IsModerator(4)
|
// IsUser(1) | IsRegistered(2) | IsModerator(4) = 7
|
||||||
expect(result.users[0].userLevel).toBe(4);
|
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 state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
|
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: true });
|
||||||
// IsJudge(16)
|
// IsUser(1) | IsRegistered(2) | IsJudge(16) = 19
|
||||||
expect(result.users[0].userLevel).toBe(16);
|
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 state = makeServerState({ users: [makeUser({ name: 'Dan', userLevel: baseUserLevel })] });
|
||||||
const result = serverReducer(state, { type: Types.ADJUST_MOD, userName: 'Dan', shouldBeMod: false, shouldBeJudge: false });
|
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', () => {
|
it('non-matching users are left unchanged', () => {
|
||||||
|
|
@ -524,3 +532,29 @@ describe('Deck Storage', () => {
|
||||||
expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ const initialState: ServerState = {
|
||||||
adminNotes: {},
|
adminNotes: {},
|
||||||
replays: [],
|
replays: [],
|
||||||
backendDecks: null,
|
backendDecks: null,
|
||||||
|
gamesOfUser: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serverReducer = (state = initialState, action: any) => {
|
export const serverReducer = (state = initialState, action: any) => {
|
||||||
|
|
@ -401,11 +402,12 @@ export const serverReducer = (state = initialState, action: any) => {
|
||||||
if (user.name !== userName) {
|
if (user.name !== userName) {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
const judgeFlag = shouldBeJudge ? UserLevelFlag.IsJudge : UserLevelFlag.IsNothing;
|
let newLevel = user.userLevel;
|
||||||
const modFlag = shouldBeMod ? UserLevelFlag.IsModerator : UserLevelFlag.IsNothing;
|
newLevel = shouldBeMod ? (newLevel | UserLevelFlag.IsModerator) : (newLevel & ~UserLevelFlag.IsModerator);
|
||||||
|
newLevel = shouldBeJudge ? (newLevel | UserLevelFlag.IsJudge) : (newLevel & ~UserLevelFlag.IsJudge);
|
||||||
return {
|
return {
|
||||||
...user,
|
...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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const Types = {
|
||||||
VIEW_LOGS: '[Server] View Logs',
|
VIEW_LOGS: '[Server] View Logs',
|
||||||
CLEAR_LOGS: '[Server] Clear Logs',
|
CLEAR_LOGS: '[Server] Clear Logs',
|
||||||
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
|
REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email',
|
||||||
REGISTRATION_SUCCES: '[Server] Registration Success',
|
REGISTRATION_SUCCESS: '[Server] Registration Success',
|
||||||
REGISTRATION_FAILED: '[Server] Registration Failed',
|
REGISTRATION_FAILED: '[Server] Registration Failed',
|
||||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||||
|
|
@ -42,7 +42,6 @@ export const Types = {
|
||||||
ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change',
|
ACCOUNT_PASSWORD_CHANGE: '[Server] Account Password Change',
|
||||||
ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed',
|
ACCOUNT_EDIT_CHANGED: '[Server] Account Edit Changed',
|
||||||
ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed',
|
ACCOUNT_IMAGE_CHANGED: '[Server] Account Image Changed',
|
||||||
DIRECT_MESSAGE_SENT: '[Server] Direct Message Sent',
|
|
||||||
GET_USER_INFO: '[Server] Get User Info',
|
GET_USER_INFO: '[Server] Get User Info',
|
||||||
NOTIFY_USER: '[Server] Notify User',
|
NOTIFY_USER: '[Server] Notify User',
|
||||||
SERVER_SHUTDOWN: '[Server] Server Shutdown',
|
SERVER_SHUTDOWN: '[Server] Server Shutdown',
|
||||||
|
|
@ -69,4 +68,6 @@ export const Types = {
|
||||||
DECK_DEL_DIR: '[Server] Deck Del Dir',
|
DECK_DEL_DIR: '[Server] Deck Del Dir',
|
||||||
DECK_UPLOAD: '[Server] Deck Upload',
|
DECK_UPLOAD: '[Server] Deck Upload',
|
||||||
DECK_DELETE: '[Server] Deck Delete',
|
DECK_DELETE: '[Server] Deck Delete',
|
||||||
|
// User games
|
||||||
|
GAMES_OF_USER: '[Server] Games Of User',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { StatusEnum, WebSocketConnectOptions } from 'types';
|
||||||
import { ProtobufService } from './services/ProtobufService';
|
import { ProtobufService } from './services/ProtobufService';
|
||||||
import { WebSocketService } from './services/WebSocketService';
|
import { WebSocketService } from './services/WebSocketService';
|
||||||
|
|
||||||
|
import { GameDispatch } from 'store';
|
||||||
import { RoomPersistence, SessionPersistence } from './persistence';
|
import { RoomPersistence, SessionPersistence } from './persistence';
|
||||||
|
|
||||||
export class WebClient {
|
export class WebClient {
|
||||||
|
|
@ -79,6 +80,7 @@ export class WebClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearStores() {
|
private clearStores() {
|
||||||
|
GameDispatch.clearStore();
|
||||||
RoomPersistence.clearStore();
|
RoomPersistence.clearStore();
|
||||||
SessionPersistence.clearStore();
|
SessionPersistence.clearStore();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ export function makeSessionPersistenceMock() {
|
||||||
accountActivationSuccess: jest.fn(),
|
accountActivationSuccess: jest.fn(),
|
||||||
accountActivationFailed: jest.fn(),
|
accountActivationFailed: jest.fn(),
|
||||||
updateStatus: jest.fn(),
|
updateStatus: jest.fn(),
|
||||||
directMessageSent: jest.fn(),
|
|
||||||
addToList: jest.fn(),
|
addToList: jest.fn(),
|
||||||
removeFromList: jest.fn(),
|
removeFromList: jest.fn(),
|
||||||
deleteServerDeck: jest.fn(),
|
deleteServerDeck: jest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import { BackendService } from '../../services/BackendService';
|
import { BackendService } from '../../services/BackendService';
|
||||||
import { SessionPersistence } from '../../persistence';
|
|
||||||
|
|
||||||
export function message(userName: string, message: string): void {
|
export function message(userName: string, message: string): void {
|
||||||
BackendService.sendSessionCommand('Command_Message', { userName, message }, {
|
BackendService.sendSessionCommand('Command_Message', { userName, message }, {});
|
||||||
onSuccess: () => {
|
|
||||||
SessionPersistence.directMessageSent(userName, message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,11 +302,6 @@ describe('message', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls directMessageSent on success', () => {
|
|
||||||
message('bob', 'hi');
|
|
||||||
invokeOnSuccess();
|
|
||||||
expect(SessionPersistence.directMessageSent).toHaveBeenCalledWith('bob', 'hi');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ping', () => {
|
describe('ping', () => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { ProtoController } from '../../services/ProtoController';
|
||||||
import { updateStatus } from '../../commands/session';
|
import { updateStatus } from '../../commands/session';
|
||||||
import { ConnectionClosedData } from './interfaces';
|
import { ConnectionClosedData } from './interfaces';
|
||||||
|
|
||||||
export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): void {
|
export function connectionClosed({ reason, reasonStr, endTime }: ConnectionClosedData): void {
|
||||||
let message: string;
|
let message: string;
|
||||||
|
|
||||||
// @TODO (5)
|
// @TODO (5)
|
||||||
|
|
@ -19,7 +19,9 @@ export function connectionClosed({ reason, reasonStr }: ConnectionClosedData): v
|
||||||
message = 'There are too many concurrent connections from your address';
|
message = 'There are too many concurrent connections from your address';
|
||||||
break;
|
break;
|
||||||
case CloseReason.BANNED:
|
case CloseReason.BANNED:
|
||||||
message = 'You are banned';
|
message = endTime > 0
|
||||||
|
? `You are banned until ${new Date(endTime * 1000).toLocaleString()}`
|
||||||
|
: 'You are banned';
|
||||||
break;
|
break;
|
||||||
case CloseReason.DEMOTED:
|
case CloseReason.DEMOTED:
|
||||||
message = 'You were demoted';
|
message = 'You were demoted';
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ jest.mock('store', () => ({
|
||||||
accountPasswordChange: jest.fn(),
|
accountPasswordChange: jest.fn(),
|
||||||
accountEditChanged: jest.fn(),
|
accountEditChanged: jest.fn(),
|
||||||
accountImageChanged: jest.fn(),
|
accountImageChanged: jest.fn(),
|
||||||
directMessageSent: jest.fn(),
|
|
||||||
getUserInfo: jest.fn(),
|
getUserInfo: jest.fn(),
|
||||||
notifyUser: jest.fn(),
|
notifyUser: jest.fn(),
|
||||||
serverShutdown: jest.fn(),
|
serverShutdown: jest.fn(),
|
||||||
|
|
@ -53,6 +52,7 @@ jest.mock('store', () => ({
|
||||||
replayAdded: jest.fn(),
|
replayAdded: jest.fn(),
|
||||||
replayModifyMatch: jest.fn(),
|
replayModifyMatch: jest.fn(),
|
||||||
replayDeleteMatch: jest.fn(),
|
replayDeleteMatch: jest.fn(),
|
||||||
|
gamesOfUser: jest.fn(),
|
||||||
},
|
},
|
||||||
GameDispatch: {
|
GameDispatch: {
|
||||||
gameJoined: jest.fn(),
|
gameJoined: jest.fn(),
|
||||||
|
|
@ -68,6 +68,7 @@ jest.mock('../utils/NormalizeService', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: {
|
default: {
|
||||||
normalizeBannedUserError: jest.fn((r: string, t: number) => `banned:${r}:${t}`),
|
normalizeBannedUserError: jest.fn((r: string, t: number) => `banned:${r}:${t}`),
|
||||||
|
normalizeGameObject: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -291,28 +292,33 @@ describe('SessionPersistence', () => {
|
||||||
expect(ServerDispatch.accountImageChanged).toHaveBeenCalledWith({ avatarBmp: buf });
|
expect(ServerDispatch.accountImageChanged).toHaveBeenCalledWith({ avatarBmp: buf });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('directMessageSent passes userName and message', () => {
|
|
||||||
SessionPersistence.directMessageSent('bob', 'hi');
|
|
||||||
expect(ServerDispatch.directMessageSent).toHaveBeenCalledWith('bob', 'hi');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getUserInfo passes userInfo', () => {
|
it('getUserInfo passes userInfo', () => {
|
||||||
const user = { name: 'u' } as any;
|
const user = { name: 'u' } as any;
|
||||||
SessionPersistence.getUserInfo(user);
|
SessionPersistence.getUserInfo(user);
|
||||||
expect(ServerDispatch.getUserInfo).toHaveBeenCalledWith(user);
|
expect(ServerDispatch.getUserInfo).toHaveBeenCalledWith(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getGamesOfUser logs to console', () => {
|
it('getGamesOfUser normalizes game list and dispatches gamesOfUser', () => {
|
||||||
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
const gt = { gameTypeId: 1, description: 'Standard' };
|
||||||
SessionPersistence.getGamesOfUser('user1', {});
|
const room = { gametypeList: [gt] };
|
||||||
expect(spy).toHaveBeenCalled();
|
const game = { gameId: 5, roomId: 1, gameTypes: [1], description: 'My Game', started: false };
|
||||||
spy.mockRestore();
|
SessionPersistence.getGamesOfUser('alice', { roomList: [room], gameList: [game] });
|
||||||
|
expect(NormalizeService.normalizeGameObject).toHaveBeenCalledWith(game, { 1: 'Standard' });
|
||||||
|
expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', [game]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getGamesOfUser handles empty response', () => {
|
||||||
|
SessionPersistence.getGamesOfUser('alice', {});
|
||||||
|
expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gameJoined dispatches via GameDispatch.gameJoined', () => {
|
it('gameJoined dispatches via GameDispatch.gameJoined', () => {
|
||||||
const gameInfo = { gameId: 10, roomId: 2, description: 'test', started: false };
|
const gameInfo = { gameId: 10, roomId: 2, description: 'test', started: false };
|
||||||
SessionPersistence.gameJoined({ gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false } as any);
|
SessionPersistence.gameJoined({ gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false, resuming: true } as any);
|
||||||
expect(GameDispatch.gameJoined).toHaveBeenCalledWith(10, expect.objectContaining({ gameId: 10, hostId: 3, localPlayerId: 4 }));
|
expect(GameDispatch.gameJoined).toHaveBeenCalledWith(
|
||||||
|
10,
|
||||||
|
expect.objectContaining({ gameId: 10, hostId: 3, localPlayerId: 4, resuming: true })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('notifyUser passes notification', () => {
|
it('notifyUser passes notification', () => {
|
||||||
|
|
|
||||||
|
|
@ -167,21 +167,26 @@ export class SessionPersistence {
|
||||||
ServerDispatch.accountImageChanged({ avatarBmp });
|
ServerDispatch.accountImageChanged({ avatarBmp });
|
||||||
}
|
}
|
||||||
|
|
||||||
static directMessageSent(userName: string, message: string): void {
|
|
||||||
ServerDispatch.directMessageSent(userName, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUserInfo(userInfo: User) {
|
static getUserInfo(userInfo: User) {
|
||||||
ServerDispatch.getUserInfo(userInfo);
|
ServerDispatch.getUserInfo(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getGamesOfUser(userName: string, response: any): void {
|
static getGamesOfUser(userName: string, response: any): void {
|
||||||
// Response_GetGamesOfUser contains a gameList field — log for now until game layer is complete
|
const gametypeMap: Record<number, string> = {};
|
||||||
console.log('getGamesOfUser', userName, response);
|
(response.roomList || []).forEach((room: any) => {
|
||||||
|
(room.gametypeList || []).forEach((gt: any) => {
|
||||||
|
gametypeMap[gt.gameTypeId] = gt.description;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const games = (response.gameList || []).map((game: any) => {
|
||||||
|
NormalizeService.normalizeGameObject(game, gametypeMap);
|
||||||
|
return game;
|
||||||
|
});
|
||||||
|
ServerDispatch.gamesOfUser(userName, games);
|
||||||
}
|
}
|
||||||
|
|
||||||
static gameJoined(gameJoinedData: GameJoinedData): void {
|
static gameJoined(gameJoinedData: GameJoinedData): void {
|
||||||
const { gameInfo, hostId, playerId, spectator, judge } = gameJoinedData;
|
const { gameInfo, hostId, playerId, spectator, judge, resuming } = gameJoinedData;
|
||||||
const gameEntry: GameEntry = {
|
const gameEntry: GameEntry = {
|
||||||
gameId: gameInfo.gameId,
|
gameId: gameInfo.gameId,
|
||||||
roomId: gameInfo.roomId,
|
roomId: gameInfo.roomId,
|
||||||
|
|
@ -190,6 +195,7 @@ export class SessionPersistence {
|
||||||
localPlayerId: playerId,
|
localPlayerId: playerId,
|
||||||
spectator,
|
spectator,
|
||||||
judge,
|
judge,
|
||||||
|
resuming,
|
||||||
started: gameInfo.started,
|
started: gameInfo.started,
|
||||||
activePlayerId: -1,
|
activePlayerId: -1,
|
||||||
activePhase: -1,
|
activePhase: -1,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ jest.mock('../commands/session', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../events', () => ({
|
jest.mock('../events', () => ({
|
||||||
CommonEvents: { '.Event_Common.ext': jest.fn() },
|
|
||||||
GameEvents: { '.Event_Game.ext': jest.fn() },
|
GameEvents: { '.Event_Game.ext': jest.fn() },
|
||||||
RoomEvents: { '.Event_Room.ext': jest.fn() },
|
RoomEvents: { '.Event_Room.ext': jest.fn() },
|
||||||
SessionEvents: { '.Event_Session.ext': jest.fn() },
|
SessionEvents: { '.Event_Session.ext': jest.fn() },
|
||||||
|
|
@ -21,7 +20,7 @@ jest.mock('../WebClient');
|
||||||
import { ProtobufService } from './ProtobufService';
|
import { ProtobufService } from './ProtobufService';
|
||||||
import { ProtoController } from './ProtoController';
|
import { ProtoController } from './ProtoController';
|
||||||
import { ping as sessionPing } from '../commands/session';
|
import { ping as sessionPing } from '../commands/session';
|
||||||
import { GameEvents, CommonEvents } from '../events';
|
import { GameEvents } from '../events';
|
||||||
|
|
||||||
let mockSocket: any;
|
let mockSocket: any;
|
||||||
let mockWebClient: any;
|
let mockWebClient: any;
|
||||||
|
|
@ -321,17 +320,6 @@ describe('ProtobufService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('processCommonEvent', () => {
|
|
||||||
it('delegates to processEvent with CommonEvents', () => {
|
|
||||||
const service = new ProtobufService(mockWebClient);
|
|
||||||
const processEvent = jest.spyOn(service as any, 'processEvent');
|
|
||||||
const response = { '.Event_Common.ext': { data: 1 } };
|
|
||||||
const raw = { extra: true };
|
|
||||||
(service as any).processCommonEvent(response, raw);
|
|
||||||
expect(processEvent).toHaveBeenCalledWith(response, CommonEvents, raw);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('processGameEvent', () => {
|
describe('processGameEvent', () => {
|
||||||
it('returns early when container has no eventList', () => {
|
it('returns early when container has no eventList', () => {
|
||||||
const service = new ProtobufService(mockWebClient);
|
const service = new ProtobufService(mockWebClient);
|
||||||
|
|
@ -354,19 +342,6 @@ describe('ProtobufService', () => {
|
||||||
expect(gameEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 }));
|
expect(gameEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to CommonEvents handler when no GameEvents key matches', () => {
|
|
||||||
const service = new ProtobufService(mockWebClient);
|
|
||||||
const commonEventHandler = (CommonEvents as any)['.Event_Common.ext'] as jest.Mock;
|
|
||||||
const payload = { commonData: 2 };
|
|
||||||
(service as any).processGameEvent({
|
|
||||||
gameId: 7,
|
|
||||||
context: null,
|
|
||||||
secondsElapsed: 0,
|
|
||||||
forcedByJudge: 0,
|
|
||||||
eventList: [{ '.Event_Common.ext': payload, playerId: 3 }],
|
|
||||||
}, {});
|
|
||||||
expect(commonEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 7, playerId: 3 }));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('processEvent', () => {
|
describe('processEvent', () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CommonEvents, GameEvents, RoomEvents, SessionEvents } from '../events';
|
import { GameEvents, RoomEvents, SessionEvents } from '../events';
|
||||||
import { WebClient } from '../WebClient';
|
import { WebClient } from '../WebClient';
|
||||||
import { SessionCommands } from 'websocket';
|
import { SessionCommands } from 'websocket';
|
||||||
import { ProtoController } from './ProtoController';
|
import { ProtoController } from './ProtoController';
|
||||||
|
|
@ -119,10 +119,6 @@ export class ProtobufService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processCommonEvent(response: any, raw: any) {
|
|
||||||
this.processEvent(response, CommonEvents, raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
private processRoomEvent(response: any, raw: any) {
|
private processRoomEvent(response: any, raw: any) {
|
||||||
this.processEvent(response, RoomEvents, raw);
|
this.processEvent(response, RoomEvents, raw);
|
||||||
}
|
}
|
||||||
|
|
@ -147,25 +143,13 @@ export class ProtobufService {
|
||||||
forcedByJudge: forcedByJudge ?? 0,
|
forcedByJudge: forcedByJudge ?? 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try registered game event handlers first, then common event handlers
|
|
||||||
let handled = false;
|
|
||||||
for (const key of Object.keys(GameEvents)) {
|
for (const key of Object.keys(GameEvents)) {
|
||||||
const payload = event[key];
|
const payload = event[key];
|
||||||
if (payload !== undefined && payload !== null) {
|
if (payload !== undefined && payload !== null) {
|
||||||
(GameEvents[key] as Function)(payload, meta);
|
(GameEvents[key] as Function)(payload, meta);
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!handled) {
|
|
||||||
for (const key of Object.keys(CommonEvents)) {
|
|
||||||
const payload = event[key];
|
|
||||||
if (payload !== undefined && payload !== null) {
|
|
||||||
(CommonEvents[key] as Function)(payload, meta);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +157,7 @@ export class ProtobufService {
|
||||||
for (const event in events) {
|
for (const event in events) {
|
||||||
const payload = response[event];
|
const payload = response[event];
|
||||||
|
|
||||||
if (payload) {
|
if (payload !== undefined && payload !== null) {
|
||||||
events[event](payload, raw);
|
events[event](payload, raw);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue